@volter/world-console 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -611,6 +611,10 @@ function WorldView({ name, token, twin, tab, arg, sub, admin, snapshot, parentTo
611
611
 
612
612
  const current: Tab = (TABS as readonly string[]).includes(tab) ? (tab as Tab) : 'overview';
613
613
  const vendors = twins.state === 'ok' ? twins.data.twins.map((t) => t.twin) : [];
614
+ // an address naming no vendor of this World (mistyped, or guessed) says so, never shows as a vendor that stores nothing
615
+ if (twin && twins.state === 'ok' && !vendors.includes(twin)) {
616
+ return <main className="center"><Empty title={`${name} has no ${twin}`} action={<a href={`${BASE}/${name}`} onClick={(e) => { e.preventDefault(); go(name.split('/')); }}>Open {name}</a>}><p>No vendor of this World is called {twin}. Its changesets are under Changes › History.</p></Empty></main>;
617
+ }
614
618
  const chosen = current === 'vendors' && arg ? arg : undefined;
615
619
  const retry = (): void => { setScope('pending'); setAttempt((a) => a + 1); };
616
620
  // ALL SCREENS is a canvas app's own page: the whole window, the World's pages a click away (Figma's, tldraw's)
@@ -1091,6 +1095,10 @@ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn,
1091
1095
  const diff = useDoor<Diff>(token, `/-/${name}/diff`, every);
1092
1096
  const sets = useDoor<{ changesets: ChangesetView[] }>(token, `/-/${name}/changesets`, every);
1093
1097
  const rules = useDoor<{ changesets: ChangesetRules }>(token, `/-/${name}/rules`, every);
1098
+ // the sites this World's twins answer, each read at `<hostname>.<this World's origin>` (only on a World's own local origin)
1099
+ const sites = useDoor<{ sites: Array<{ twin: string; host: string }> }>(token, `/-/${name}/sites`, every);
1100
+ const siteAt = (host: string): string | null => (/^[^.]+--[^.]+\.localhost$/.test(window.location.hostname) ? `${window.location.protocol}//${host}.${window.location.host}/` : null);
1101
+ const siteLinks = sites.state === 'ok' ? sites.data.sites.flatMap((x) => { const at = siteAt(x.host); return at ? [{ ...x, at }] : []; }) : [];
1094
1102
  const [picked, setPicked] = useState<string | null>(null);
1095
1103
  const to = (parts: string[]) => (e?: React.MouseEvent): void => { e?.preventDefault(); go([...name.split('/'), '~', ...parts]); };
1096
1104
  const vendors = twins.state === 'ok' ? twins.data.twins.map((t) => t.twin) : [];
@@ -1196,6 +1204,8 @@ function Workspace({ name, token, view, sub, arg, win, who, snapshot, onReturn,
1196
1204
  {place((view === 'events' && !arg) || view === 'trace', <Icon name="activity" size={16} />, 'Events', 'events', to(['events']))}
1197
1205
  <p className="side-group">Records</p>
1198
1206
  {vendors.map((v) => <React.Fragment key={v}>{place(view === 'vendor' && arg === v, <Mark vendor={v} />, vendorName(v), `vendors/${v}`, to(['vendors', v]), held(v) ? String(held(v)) : undefined)}</React.Fragment>)}
1207
+ {siteLinks.length ? <p className="side-group">Sites</p> : null}
1208
+ {siteLinks.map((x) => <a key={x.host} href={x.at} target="_blank" rel="noreferrer" title={`${x.host} as ${vendorName(x.twin)} serves it in this World`}><Icon name="globe" size={16} /><span>{x.host}</span><Icon name="external-link" size={14} /></a>)}
1199
1209
  <p className="side-group">Remote</p>
1200
1210
  {place(false, <Icon name="cloud" size={16} />, there ? 'origin' : 'Name a remote…', 'remote', to(['remote']), waiting ? `↑${waiting}` : undefined)}
1201
1211
  </nav>
@@ -1791,12 +1801,19 @@ const changesetChanges = (c: ChangesetView): Array<{ twin: string; entry: Entry
1791
1801
  * here), or a push receipt `confirmed` (the remote performed it). Every action performed is Deployed, some is Partly
1792
1802
  * deployed; a refused or failed one is said as a failure; otherwise the push decides: every change replayed at the remote
1793
1803
  * is Pushed (the remote World, never a vendor), else Not sent. */
1794
- function sentOf(c: ChangesetView): { label: string; tone: 'done' | 'good' | 'warn' | 'bad' | 'plain'; performed: number; failed: number; total: number } {
1795
- const total = c.actions.length;
1804
+ /** Whether an action is the World's own (a door, a screen, the gap, the clock or a seed made it): no deploy offers it to
1805
+ * the vendor (the kernel's settlingEntries), so it is never counted as waiting to be performed. */
1806
+ const worldOwn = (a: ChangesetView['actions'][number]): boolean => {
1807
+ const input = ((a.action as { input?: Record<string, unknown> }).input ?? {});
1808
+ return !(typeof input.operationId === 'string' || (input.graphql !== undefined && input.graphql !== null));
1809
+ };
1810
+ function sentOf(c: ChangesetView): { label: string; tone: 'done' | 'good' | 'warn' | 'bad' | 'plain'; performed: number; failed: number; total: number; own: number } {
1811
+ const own = c.actions.filter(worldOwn).length;
1812
+ const total = c.actions.length - own;
1796
1813
  const pushed = c.applied?.receipts ?? []; const deployed = c.deployed ?? [];
1797
1814
  const performed = new Set([...deployed.filter((r) => r.status === 'deployed'), ...pushed.filter((r) => r.status === 'confirmed')].map((r) => r.actionId)).size;
1798
1815
  const failed = new Set([...deployed.filter((r) => r.status === 'refused' || r.status === 'failed'), ...pushed.filter((r) => r.status === 'failed')].map((r) => r.actionId)).size;
1799
- const n = { performed, failed, total };
1816
+ const n = { performed, failed, total, own };
1800
1817
  if (total > 0 && performed >= total) return { label: 'Deployed', tone: 'done', ...n };
1801
1818
  if (performed > 0) return { label: `Partly deployed: ${performed} of ${total}`, tone: 'warn', ...n };
1802
1819
  if (failed > 0) return { label: `${failed} refused or failed`, tone: 'bad', ...n };
@@ -1899,7 +1916,7 @@ function ChangesetPage({ name, token, signsAs, c, canWrite, rooted, refused, onC
1899
1916
  <h4>Where it went</h4>
1900
1917
  <div className="block where">
1901
1918
  {sent.performed > 0 ? (
1902
- <p className="pad">{sent.performed >= sent.total ? 'Deployed' : 'Partly deployed'}: {sent.performed} of {sent.total} change{sent.total === 1 ? '' : 's'} performed at {listed([...new Set([...here.map((r) => r.service), ...receipts.filter((r) => r.status === 'confirmed').map((r) => r.service)].map(vendorName))])}{here.length ? ` (${here.length} from here` : ' ('}{here.length && byRemote ? ', ' : ''}{byRemote ? `${byRemote} by the remote` : ''}).</p>
1919
+ <p className="pad">{sent.performed >= sent.total ? 'Deployed' : 'Partly deployed'}: {sent.performed} of {sent.total} change{sent.total === 1 ? '' : 's'}{sent.own ? ` (and ${sent.own} of the World's own, which stay in it)` : ''} performed at {listed([...new Set([...here.map((r) => r.service), ...receipts.filter((r) => r.status === 'confirmed').map((r) => r.service)].map(vendorName))])}{here.length ? ` (${here.length} from here` : ' ('}{here.length && byRemote ? ', ' : ''}{byRemote ? `${byRemote} by the remote` : ''}).</p>
1903
1920
  ) : sent.label === 'Pushed' ? (
1904
1921
  <p className="pad">Pushed to the remote{there ? <> <code>{there}</code></> : null}{c.applied ? <> · <At iso={c.applied.at} /></> : null}: every change was replayed there. No vendor has performed any of it.</p>
1905
1922
  ) : sent.failed === 0 ? (
@@ -2123,7 +2140,9 @@ function usePushPull(name: string, token: string, tick: number, refused: (r: Res
2123
2140
  };
2124
2141
  const there = origin.state === 'ok' && origin.data.origin ? originAddress(origin.data.origin) : null;
2125
2142
  const changes = diff.state === 'ok' ? diff.data.changes.length : null;
2126
- const waiting = sets.state === 'ok' ? unpushed(sets.data.changesets).sort((a, b) => a.createdAt.localeCompare(b.createdAt)) : [];
2143
+ // what waits to be pushed: this World's own changesets, and only when it has a remote to push them to (a shared World
2144
+ // holds what others pushed, which is theirs)
2145
+ const waiting = sets.state === 'ok' && there ? unpushed(sets.data.changesets.filter((c) => !c.pushedBy)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)) : [];
2127
2146
  const push = async (): Promise<void> => {
2128
2147
  const b = await send<{ pushed: Array<{ changeset: string; outcome: string; receipts?: Array<{ status: string }>; refusal?: string[] }> }>('Push', 'origin/push', { method: 'POST' });
2129
2148
  if (!b) return;