Files
Module-uo/client/src/lib/useShardFeatures.js
wtclaude 28f4b9afe2 feat(client): the whole client half (phase 3, slice 3)
The 35 files behind twelve public pages, seven admin views, two player views
and three core-page extensions, ported onto `window.__rg`. Every one of them
imports exactly the seven kit members plus `lib/format.js`, which is the
finding §2.7.1 predicted and this confirms.

`client/src/core.js` is the port mechanism, and unlike the server's it is a
plain read: `window.__rg` is published before any module chunk evaluates, so
there is no gap to defer around and a ported component keeps its ordinary
import shape. `client/src/api.js` rebuilds the UO namespaces over the request
primitive — same URLs, because §1.2 freezes the API surface.

SPA paths changed and API paths did not. `/site/shard` is `/uo/shard`, and the
admin paths lost their now-redundant `shard-` prefixes (`/admin/uo/ops`), a
clean break being the only moment that is free.

`shim/rg.js` becomes the single reader of the global, so the "core did not
publish its dependencies" message is reachable from whichever module the
bundler happens to touch first rather than from whichever one is imported
first — a guarantee that used to last until someone sorted the imports.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 18:00:25 -05:00

96 lines
3.4 KiB
JavaScript

import { useEffect, useState } from 'react'
import api from '../api.js'
// Which shard surfaces the current viewer may reach, from
// GET /public/shard/features. Admins configure this per feature (Admin → Shard
// Visibility), so the nav can't be a static list any more.
//
// This is PRESENTATION only. The gate is server-side: a disabled feature 404s
// and an out-of-rung one 403s whether or not the link is rendered. So while the
// answer is still in flight we return `null` and callers show their default set
// — better a link that briefly 403s than a nav that flickers in on every load.
//
// Cached module-level: the answer is per-viewer but stable for a session, and
// every consumer would otherwise refetch it on mount.
let cached = null
let inFlight = null
export function resetShardFeatures() {
cached = null
inFlight = null
}
export function useShardFeatures() {
const [features, setFeatures] = useState(cached)
useEffect(() => {
if (cached) return undefined
let alive = true
inFlight =
inFlight ||
api.shard
.features()
.then((data) => {
cached = {
level: data.level,
set: new Set(data.features || []),
// Not a visibility flag and deliberately carried alongside them: it
// is the same per-viewer, once-a-session answer from the same
// endpoint, and GameAccounts asking for it separately would be a
// second round-trip for a field already on the wire.
gameAccountSignup: Boolean(data.gameAccountSignup),
}
return cached
})
.catch(() => {
// A failed lookup must not blank the nav — fall back to "show
// everything" and let the server do the gating.
cached = null
inFlight = null
return null
})
inFlight.then((result) => {
if (alive) setFeatures(result)
})
return () => {
alive = false
}
}, [])
return features
}
// Convenience: true when `name` is visible, or when we don't know yet.
export function canSee(features, name) {
return !features || features.set.has(name)
}
// The same answer in the shape core's generic feature seam takes: a Set-like of
// the flags this viewer may see, or null while we do not know yet
// (core's modules/featureGate.js). This module registers it as the provider for
// the `uo` namespace in entry.jsx, and the nine shard-gated rows in the public
// header are ours to gate as of slice 3.
//
// It used to be core that registered this hook, under owner id `core`, so that
// the seam was exercised from the day it was built. That prediction held exactly
// — this slice deleted a registration and a file rather than rewriting a header.
export function useShardFlags() {
const features = useShardFeatures()
return features ? features.set : null
}
/**
* Does this site offer game-account creation right now?
*
* `null` while unknown, which callers must treat as "not yet" rather than "no":
* the form it guards would 403 anyway, and flashing it in and out is worse than
* arriving a beat late. Unlike the visibility flags above this one fails CLOSED
* on a lookup error — showing a create-account form on a shard that refuses them
* is a dead end the player cannot tell from a bug, whereas a hidden nav row has
* another way round.
*/
export function useGameAccountSignup() {
const features = useShardFeatures()
return features ? features.gameAccountSignup : null
}