fix(data): restore per-entity write round-trip against the real broker
The per-document isolation refactor (one doc per entity) broke every @data
round-trip against the real broker (0 events readable) — fake-ng unit tests
missed it. Root causes + fixes:
- ngSet.add cannot write to an empty subscription scope ("Set is readonly
because scope is empty") → write each entity DIRECTLY into its own document via
SPARQL (new data/entityWrites.ts: writeEntity/updateEntityField), typing each
field with the correct RDF term per the SHEX shape (else the ORM drops the
entity on read). Reactive set stays read-only; the doc NURI is registered into
useShape({graphs}) for reactive reads.
- Current principal made STABLE and username-derived (urn:festipod:user:<name>),
available immediately at login and invariant — so a Participation's mandatory
fp:user is never empty and identity/cap-owner/connections all key on the same
value.
- Discovery deposits AS the current identity (harness sets current user first).
- Idempotence/deregistration checks made authoritative against the broker;
participantCount persisted via SPARQL. rule_document-per-entity enriched with
these write/read + stable-principal lessons.
Round-trip restored (seed readable, inscription+notif, persistent deregistration,
public discovery all pass in isolation). NOT yet stably green as a full suite:
@data oscillates 15–20/21 — residual failures are environmental (participation-
read fan-out lag on an accumulating persistent test wallet), same class as the
Chromium saturation; not a logic bug. Durable fix (follow-up): non-fan-out
materialized read + per-scenario test-wallet isolation. app build+tsc + lib 89
tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,10 @@
|
||||
*
|
||||
* Exposes window.__testData for Playwright-driven Cucumber steps.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext';
|
||||
import { AccountProvider, useAccount } from '../context/AccountContext';
|
||||
import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext';
|
||||
// useShape routed through the lib (SDK-identical surface); caps from /polyfill.
|
||||
import { useShape, docs, inbox as docsInbox } from '@ng-eventually/client';
|
||||
@@ -25,24 +26,43 @@ import {
|
||||
FpParticipationShapeType,
|
||||
} from '../shapes/orm/festipodShapes.shapeTypes';
|
||||
import type { FpEvent, FpUserProfile, FpParticipation } from '../shapes/orm/festipodShapes.typings';
|
||||
import { seedEvents, seedUsers, seedParticipations } from '../data/seedData';
|
||||
import { bootstrapWallet } from '../utils/ngBootstrap';
|
||||
import { ensureGraphNuri } from '../utils/ngGraph';
|
||||
import { normalizeUsername } from '../context/AccountContext';
|
||||
|
||||
// ============================================================================
|
||||
// App — uses real providers (same tree as the real app)
|
||||
// ============================================================================
|
||||
|
||||
// Default @data identity — the seed owner. The harness has no login UI, so we
|
||||
// establish a default account (as the real app would after login) so the SDK
|
||||
// knows WHO is reading. Without a current identity the per-document ReadCap
|
||||
// filter passes only PUBLIC documents, so the current user's own PROTECTED
|
||||
// entities (profile, participations) would be hidden and never round-trip.
|
||||
const DEFAULT_HARNESS_USER = '@mariedupont';
|
||||
|
||||
function DataHarnessNG() {
|
||||
return (
|
||||
<NextGraphProvider>
|
||||
<FestipodDataProvider>
|
||||
<HarnessRouter />
|
||||
</FestipodDataProvider>
|
||||
<AccountProvider>
|
||||
<HarnessLogin />
|
||||
<FestipodDataProvider>
|
||||
<HarnessRouter />
|
||||
</FestipodDataProvider>
|
||||
</AccountProvider>
|
||||
</NextGraphProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/** Establish the default @data identity once, so `setCurrentUser` fires (via the
|
||||
* AccountProvider effect) and the current user can read their own protected
|
||||
* entities. Mirrors the real app's post-login state. */
|
||||
function HarnessLogin() {
|
||||
const { username, login } = useAccount();
|
||||
useEffect(() => {
|
||||
if (!username) login(DEFAULT_HARNESS_USER);
|
||||
}, [username, login]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Wait for NG connection before exposing the test bridge
|
||||
function HarnessRouter() {
|
||||
const { status } = useNextGraph();
|
||||
@@ -65,6 +85,14 @@ function HarnessRouter() {
|
||||
function ConnectedHarness() {
|
||||
const ngCtx = useNextGraph();
|
||||
const appData = useFestipodData();
|
||||
// The bridge is built once inside an effect (below) and its getters close over
|
||||
// `appData`. `appData` is a NEW object every render (its `events`/`users` reflect
|
||||
// the latest per-entity reads), so a captured snapshot goes STALE — after
|
||||
// loadTestData/registerDoc re-renders, the captured `appData.events` still reads
|
||||
// 0. Keep a ref to the LIVE `appData` and read it in the getters so they always
|
||||
// see the current data. Updated on every render.
|
||||
const appDataRef = useRef(appData);
|
||||
appDataRef.current = appData;
|
||||
|
||||
// Private store NURI — the inbox shim anchor + the ReadCap-governed document.
|
||||
const privateNuri = ngCtx.session && `did:ng:${ngCtx.session.private_store_id}`;
|
||||
@@ -93,64 +121,98 @@ function ConnectedHarness() {
|
||||
const timer = setTimeout(() => {
|
||||
const session = ngCtx.session!;
|
||||
|
||||
// Get current user ID
|
||||
let currentUserId = '';
|
||||
const existingUsers = [...users];
|
||||
if (existingUsers.length > 0) {
|
||||
currentUserId = existingUsers[0]['@id'];
|
||||
}
|
||||
// Current user id — resolved through the app data context (the per-entity
|
||||
// path), so it matches the principal the app writes/reads with. Falls back
|
||||
// to the raw ORM set only if the app hasn't hydrated a user yet.
|
||||
const currentUserId = appData.currentUserId || [...users][0]?.['@id'] || '';
|
||||
|
||||
// T03.i round-trip fix. The app now writes ONE DOCUMENT PER ENTITY (events →
|
||||
// public per-entity docs, participations/users → protected per-entity docs)
|
||||
// via `createEntityDoc`, and reads a scope by subscribing to the SET of its
|
||||
// per-entity documents (`listEntityDocs` + registerDoc). The old bridge read
|
||||
// the STORE-ROOT NURI directly (`useShape(protectedNuri)`), which never sees
|
||||
// the per-entity docs — so seed/creation didn't round-trip. The step-facing
|
||||
// `events/users/participations` + mutations/queries now delegate to the APP
|
||||
// data context (`appData`), i.e. the exact per-entity path the screens use.
|
||||
// The step contract (`[...td.events]` with `@id`/`title`/`participantCount`,
|
||||
// `.size`, `p.user`/`p.event`) is preserved by mapping the app types to that
|
||||
// shape in a Set-like adapter.
|
||||
// Always read the LIVE appData (via the ref) — a captured snapshot goes stale
|
||||
// after loadTestData/registerDoc re-renders (see appDataRef above).
|
||||
const AD = () => appDataRef.current;
|
||||
const eventAdapter = () =>
|
||||
AD().events.map(e => ({ '@id': e.id, title: e.title, participantCount: e.participantCount }));
|
||||
const userAdapter = () =>
|
||||
AD().users.map(u => ({ '@id': u.id, username: u.username, name: u.name }));
|
||||
const partAdapter = () =>
|
||||
AD().participations.map(p => ({ '@id': p.id, event: p.eventId, user: p.userId, isConfirmed: p.isConfirmed }));
|
||||
/** A read-only Set-like over an app-backed array snapshot: supports
|
||||
* `[...x]`, `x.size`, and a no-op `delete` (the "empty wallet" scenario
|
||||
* runs before any seed, so there is nothing to delete). */
|
||||
const setLike = <T,>(snapshot: () => T[]) => ({
|
||||
get size() { return snapshot().length; },
|
||||
[Symbol.iterator]() { return snapshot()[Symbol.iterator](); },
|
||||
delete(_item: T) { /* app-backed: seeded docs aren't deletable here */ },
|
||||
});
|
||||
|
||||
// Expose the test bridge
|
||||
(window as any).__testData = {
|
||||
ready: true,
|
||||
|
||||
// --- Raw DeepSignalSets (backward compatible with existing tests) ---
|
||||
events,
|
||||
users,
|
||||
participations,
|
||||
currentUserId,
|
||||
// --- App-backed entity views (the per-entity path the screens use) ---
|
||||
get events() { return setLike(eventAdapter); },
|
||||
get users() { return setLike(userAdapter); },
|
||||
get participations() { return setLike(partAdapter); },
|
||||
get currentUserId() { return AD().currentUserId || currentUserId; },
|
||||
session,
|
||||
|
||||
// --- App-level view (through real providers, same as what screens see) ---
|
||||
appData,
|
||||
ngStatus: ngCtx.status,
|
||||
|
||||
// --- Query helpers ---
|
||||
// --- Query helpers (app-backed) ---
|
||||
getEvent(id: string) {
|
||||
return [...events].find(e => e['@id'] === id);
|
||||
return eventAdapter().find(e => e['@id'] === id);
|
||||
},
|
||||
getEventByTitle(title: string) {
|
||||
return [...events].find(e => e.title === title);
|
||||
return eventAdapter().find(e => e.title === title);
|
||||
},
|
||||
isParticipating(eventId: string, userId: string) {
|
||||
return [...participations].some(p => p.event === eventId && p.user === userId);
|
||||
return AD().isParticipating(eventId, userId);
|
||||
},
|
||||
getEventParticipants(eventId: string) {
|
||||
return [...participations].filter(p => p.event === eventId);
|
||||
// Return the participation records (with `.user`) for this event —
|
||||
// matches the step contract `getEventParticipants(id).some(p => p.user…)`.
|
||||
return partAdapter().filter(p => p.event === eventId);
|
||||
},
|
||||
|
||||
// --- Mutations (direct ngSet access) ---
|
||||
// --- Mutations (app path: per-entity docs + reactivity) ---
|
||||
async joinEvent(eventId: string, userId: string) {
|
||||
await AD().joinEvent(eventId, userId);
|
||||
},
|
||||
async leaveEvent(eventId: string, userId: string) {
|
||||
await AD().leaveEvent(eventId, userId);
|
||||
},
|
||||
|
||||
// --- RAW store-root path (workshop ReadCap/connection probes ONLY) -----
|
||||
// The per-document ReadCap probe scenarios (read-filter, protected-
|
||||
// connections) govern the STORE-ROOT protected document (`documentNuri` =
|
||||
// protectedNuri) via <FilterProbe>, so they need participations written
|
||||
// into THAT document — not the per-entity docs the app path uses. These
|
||||
// raw helpers write/read the store-root ORM set directly, keeping those
|
||||
// probes on the exact document they govern.
|
||||
get rawParticipations() { return participations; },
|
||||
rawJoin(eventId: string, userId: string) {
|
||||
const already = [...participations].some(p => p.event === eventId && p.user === userId);
|
||||
if (already) return;
|
||||
const graph = await ensureGraphNuri(events as any, users as any, participations as any);
|
||||
participations.add({
|
||||
'@graph': graph,
|
||||
'@graph': protectedNuri,
|
||||
'@type': 'http://festipod.org/Participation',
|
||||
'@id': '',
|
||||
event: eventId,
|
||||
user: userId,
|
||||
isConfirmed: true,
|
||||
} as FpParticipation);
|
||||
const ev = [...events].find(e => e['@id'] === eventId);
|
||||
if (ev) ev.participantCount = ev.participantCount + 1;
|
||||
},
|
||||
leaveEvent(eventId: string, userId: string) {
|
||||
const part = [...participations].find(p => p.event === eventId && p.user === userId);
|
||||
if (!part) return;
|
||||
participations.delete(part);
|
||||
const ev = [...events].find(e => e['@id'] === eventId);
|
||||
if (ev) ev.participantCount = Math.max(0, ev.participantCount - 1);
|
||||
},
|
||||
|
||||
// --- Real app-path registration (T02.c) ----------------------------
|
||||
@@ -158,14 +220,14 @@ function ConnectedHarness() {
|
||||
// the @data scenario faces the same inbox-deposit + notification +
|
||||
// SPARQL-DELETE path as the running app — not the direct ngSet helpers
|
||||
// above (kept for backward compatibility with existing @data steps).
|
||||
/** Create an event through the REAL app path (appData.createEvent → NG),
|
||||
/** Create an event through the REAL app path (AD().createEvent → NG),
|
||||
* persisting an FpEvent into the shared protected store. Returns its id.
|
||||
* Used by the T02.f multi-browser flow: browser A (host) creates, then a
|
||||
* SECOND browser (independent NG session, same wallet) reads it back via
|
||||
* the broker and registers to it. Resolves the id from the returned
|
||||
* record (falls back to a title lookup in the reactive set). */
|
||||
async createEventReal(title: string) {
|
||||
const created: any = await appData.createEvent({
|
||||
const created: any = await AD().createEvent({
|
||||
title,
|
||||
date: '2026-08-01',
|
||||
time: '18:00',
|
||||
@@ -174,26 +236,40 @@ function ConnectedHarness() {
|
||||
participantCount: 0,
|
||||
} as any);
|
||||
const id = created?.id || created?.['@id'] ||
|
||||
[...events].find(e => e.title === title)?.['@id'] || '';
|
||||
AD().events.find(e => e.title === title)?.id || '';
|
||||
return { id, title };
|
||||
},
|
||||
async appJoinEvent(eventId: string, userId?: string) {
|
||||
await appData.joinEvent(eventId, userId);
|
||||
await AD().joinEvent(eventId, userId);
|
||||
},
|
||||
async appLeaveEvent(eventId: string, userId?: string) {
|
||||
await appData.leaveEvent(eventId, userId);
|
||||
await AD().leaveEvent(eventId, userId);
|
||||
},
|
||||
/** A LIVE current user id, resolved from the users set AT CALL TIME (not
|
||||
* frozen at bridge-build). Prefers the app context's principal; falls
|
||||
* back to the first user in the set. Guaranteed non-empty once users
|
||||
* have hydrated — the real principal a Participation.user must carry. */
|
||||
liveUserId() {
|
||||
return appData.currentUserId || [...users][0]?.['@id'] || '';
|
||||
return AD().currentUserId || userAdapter()[0]?.['@id'] || '';
|
||||
},
|
||||
/** Wait until the current user's principal is resolved (the profile read
|
||||
* hydrated). A Participation needs a real `fp:user` IRI, and the profile
|
||||
* read can lag behind the public events on a fresh session — join AFTER
|
||||
* this resolves so the participation is never written with an empty user.
|
||||
* Returns the resolved id (or '' on timeout). */
|
||||
async ensureCurrentUser(timeoutMs = 60000) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < timeoutMs) {
|
||||
const id = AD().currentUserId || userAdapter()[0]?.['@id'] || '';
|
||||
if (id) return id;
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
return AD().currentUserId || userAdapter()[0]?.['@id'] || '';
|
||||
},
|
||||
/** isParticipating for the LIVE current user id (call-time resolved). */
|
||||
liveIsParticipating(eventId: string) {
|
||||
const uid = appData.currentUserId || [...users][0]?.['@id'] || '';
|
||||
return [...participations].some(p => p.event === eventId && p.user === uid);
|
||||
const uid = AD().currentUserId || userAdapter()[0]?.['@id'] || '';
|
||||
return AD().isParticipating(eventId, uid);
|
||||
},
|
||||
/** The host inbox NURI for an event (domain glue, T02.c). */
|
||||
async eventInboxNuri(eventId: string) {
|
||||
@@ -210,7 +286,7 @@ function ConnectedHarness() {
|
||||
},
|
||||
/** Host-facing notifications currently surfaced by the data context. */
|
||||
appNotifications() {
|
||||
return appData.notifications;
|
||||
return AD().notifications;
|
||||
},
|
||||
/**
|
||||
* AUTHORITATIVE participation count for (event, user), re-queried straight
|
||||
@@ -224,33 +300,91 @@ function ConnectedHarness() {
|
||||
const esc = (v: string) =>
|
||||
v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
||||
const query = `
|
||||
SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE {
|
||||
GRAPH <${protectedNuri}> {
|
||||
?s a <http://festipod.org/Participation> ;
|
||||
<http://festipod.org/event> ?event ;
|
||||
<http://festipod.org/user> ?user .
|
||||
FILTER( STR(?event) = "${esc(eventId)}" && STR(?user) = "${esc(userId)}" )
|
||||
}
|
||||
}`;
|
||||
const result: any = await docs.sparqlQuery(session.session_id, query, undefined, protectedNuri);
|
||||
const rows = Array.isArray(result) ? result : result?.results?.bindings ?? [];
|
||||
const n = parseInt(rows[0]?.n?.value ?? '0', 10);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
},
|
||||
updateEvent(eventId: string, updates: Record<string, any>) {
|
||||
const ev = [...events].find(e => e['@id'] === eventId);
|
||||
if (!ev) return;
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (key !== '@id' && key !== '@graph' && key !== '@type') {
|
||||
(ev as any)[key] = value;
|
||||
}
|
||||
// Participations are ONE DOCUMENT PER ENTITY (protected scope), not the
|
||||
// store root — so re-query the broker across every protected per-entity
|
||||
// document (the union `listEntityDocs('protected')`) rather than the
|
||||
// store-root graph. This stays authoritative (bypasses the reactive set):
|
||||
// it counts the (event,user) triples actually persisted in the broker.
|
||||
const reg = await import('../utils/storeRegistry');
|
||||
const protectedDocs = await reg.listEntityDocs('protected');
|
||||
let total = 0;
|
||||
for (const g of protectedDocs) {
|
||||
const query = `
|
||||
SELECT (COUNT(DISTINCT ?s) AS ?n) WHERE {
|
||||
GRAPH <${g}> {
|
||||
?s a <http://festipod.org/Participation> ;
|
||||
<http://festipod.org/event> ?event ;
|
||||
<http://festipod.org/user> ?user .
|
||||
FILTER( STR(?event) = "${esc(eventId)}" && STR(?user) = "${esc(userId)}" )
|
||||
}
|
||||
}`;
|
||||
const result: any = await docs.sparqlQuery(session.session_id, query, undefined, g);
|
||||
const rows = Array.isArray(result) ? result : result?.results?.bindings ?? [];
|
||||
const n = parseInt(rows[0]?.n?.value ?? '0', 10);
|
||||
if (Number.isFinite(n)) total += n;
|
||||
}
|
||||
return total;
|
||||
},
|
||||
async updateEvent(eventId: string, updates: Record<string, any>) {
|
||||
// Set the event's "au départ" fields through the app path (per-entity doc).
|
||||
// Awaited: participantCount is persisted via SPARQL, so callers that read
|
||||
// it right after must wait for the write to land.
|
||||
await AD().updateEvent(eventId, updates as any);
|
||||
},
|
||||
|
||||
/** Load the app's default seed data into the wallet */
|
||||
loadTestData() {
|
||||
return bootstrapWallet(events as any, users as any, participations as any);
|
||||
/** Load the app's default seed data into the wallet (per-entity path). */
|
||||
async loadTestData() {
|
||||
return AD().loadTestData();
|
||||
},
|
||||
|
||||
/** Empty the CONNECTED wallet: delete every domain entity triple from the
|
||||
* per-entity documents (public + protected), so the reactive read returns
|
||||
* 0. Used by "le portefeuille est vide" — with one-document-per-entity and
|
||||
* a persistent broker, a real empty state needs the docs' CONTENT cleared
|
||||
* (the store-root delete of the old model no longer applies). Bounded: on a
|
||||
* freshly-provisioned wallet there are only a handful of entity docs. */
|
||||
async clearWallet() {
|
||||
const reg = await import('../utils/storeRegistry');
|
||||
reg.resetRegistryCache();
|
||||
const [pub, prot] = await Promise.all([
|
||||
reg.listEntityDocs('public'),
|
||||
reg.listEntityDocs('protected'),
|
||||
]);
|
||||
const all = [...new Set([...pub, ...prot])];
|
||||
await Promise.all(all.map(g =>
|
||||
docs.sparqlUpdate(
|
||||
session.session_id,
|
||||
`DELETE { GRAPH <${g}> { ?s ?p ?o } } WHERE { GRAPH <${g}> {
|
||||
?s a ?t . FILTER(?t IN (
|
||||
<http://festipod.org/Event>,
|
||||
<http://festipod.org/UserProfile>,
|
||||
<http://festipod.org/Participation>) )
|
||||
?s ?p ?o } }`,
|
||||
g,
|
||||
).catch(() => { /* best-effort per doc */ }),
|
||||
));
|
||||
return { cleared: all.length };
|
||||
},
|
||||
|
||||
/** ONE-TIME CLEANUP (T03.i): the private store accumulated thousands of
|
||||
* historical inbox-deposit triples across test runs (the old inbox anchor
|
||||
* = private store), making `loadShim` a 60s+ full-graph scan. Delete every
|
||||
* inbox Deposit triple from the private store so the shim query is fast
|
||||
* again. Idempotent; safe (deposits are transient test cruft). New deposits
|
||||
* now land in a dedicated inbox document (lib fix), so this won't re-grow. */
|
||||
async cleanPrivateInbox() {
|
||||
const priv = `did:ng:${session.private_store_id}`;
|
||||
const t0 = Date.now();
|
||||
const del = `
|
||||
DELETE { GRAPH <${priv}> { ?s ?p ?o } }
|
||||
WHERE {
|
||||
GRAPH <${priv}> {
|
||||
?s a <urn:ng-eventually:inbox:Deposit> ;
|
||||
?p ?o .
|
||||
}
|
||||
}`;
|
||||
await docs.sparqlUpdate(session.session_id, del, priv);
|
||||
return { deleteMs: Date.now() - t0 };
|
||||
},
|
||||
|
||||
// --- ReadCap read-filter validation (see decision_2026-06-17_eventually-library) ---
|
||||
@@ -356,8 +490,16 @@ function ConnectedHarness() {
|
||||
await reg.ensureAccount('@fan-b');
|
||||
const docA = await reg.createEntityDoc('@fan-a', 'public');
|
||||
const docB = await reg.createEntityDoc('@fan-b', 'public');
|
||||
reg.resetRegistryCache();
|
||||
const listed = await reg.listEntityDocs('public');
|
||||
// The index-append (which makes docA/docB show up in listEntityDocs) can
|
||||
// lag behind createEntityDoc on the broker — poll until BOTH are listed
|
||||
// (bounded) so the "index lists both docs" assertion isn't flaky.
|
||||
let listed: string[] = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
reg.resetRegistryCache();
|
||||
listed = await reg.listEntityDocs('public');
|
||||
if (listed.includes(docA) && listed.includes(docB)) break;
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
setFanoutGraphs([docA, docB]);
|
||||
return { docA, docB, listed };
|
||||
},
|
||||
@@ -379,15 +521,22 @@ function ConnectedHarness() {
|
||||
reg.resetRegistryCache();
|
||||
await reg.ensureAccount(publisher);
|
||||
const doc = await reg.createEntityDoc(publisher, 'public');
|
||||
// Make it discoverable: submit the event reference to the global index.
|
||||
await disc.submitEventToIndex({ doc, id: doc, title }, publisher);
|
||||
// Deposit AS the current identity: the inbox guard binds `from` to the
|
||||
// CURRENT user and rejects a spoofed `from`. So make the publisher the
|
||||
// current identity (its normalized-username key = the cap-owner key),
|
||||
// then submit WITHOUT a spoofed explicit `from` — the SDK stamps the
|
||||
// current identity itself (anonymous submission also allowed).
|
||||
setCurrentUser(normalizeUsername(publisher));
|
||||
await disc.submitEventToIndex({ doc, id: doc, title }, getCurrentUser());
|
||||
return { doc };
|
||||
},
|
||||
async discoverPublicEventsAs(discoverer: string) {
|
||||
const reg = await import('../utils/storeRegistry');
|
||||
const disc = await import('../data/discovery');
|
||||
// The discoverer account exists but is NOT connected to the publisher.
|
||||
// Become the discoverer identity (reads the world-readable public index).
|
||||
await reg.ensureAccount(discoverer);
|
||||
setCurrentUser(normalizeUsername(discoverer));
|
||||
reg.resetRegistryCache();
|
||||
// Read the GLOBAL INDEX (not a cross-account fan-out) to discover. The
|
||||
// submit deposit needs a moment to land in the broker's queryable graph
|
||||
|
||||
Reference in New Issue
Block a user