53c0e095cf
The data layer is now reached through one pulled, version-pinned engagement (`.project/concepts/data-layer/contract_polyfill-surface.md`, @1ecf511e9d). That copy is the only reference: the provider's sources are never opened, and what the contract does not answer is a gap raised with it, never worked around here. Surface - `@ng-eventually/sdk` -> `@ng-eventually/polyfill`, one entry point. - `configure` loses `getSession`, `normalizeId`, `currentUser`; the session belongs to the package and its own `init` captures it. - Placement is named by scope alone -- a session is one user, so the app no longer passes an identity it had no way to obtain. This removes a constant that made every user collide on one owner's document. - `init(...)` then `await ensureIdentity()`, in that order, as one sequence: React runs child effects first, so the two calls sat in the wrong order and the contract now makes that throw. - `sessionId` relayed as `string | number`, `materialize` -> `read`. A rejection means "unknown", never "absent" Four places treated a caught error as an empty result. The worst wrote a duplicate participation: an unknown count read as zero defeated the idempotence guard of `joinEvent`. Also fixed: a per-document count, a silently dropped notification shown optimistically anyway, and a failed listing that left the owned-event set empty and disabled the materializer for the whole session. Shared identity is not a Festipod notion A browser context is one user. The per-scenario identity plant is deleted at its source and its five sites; what stays is the deployment's wallet file, which the contract requires an application to serve. Documentation The doctrine no longer describes how the data layer works underneath: five leaves whose subject was internals are gone, a dozen more are re-founded on the contract's own words, and two frozen arbitrations about a deleted screen were removed rather than left to mislead a future session. Test harness It can sign in at last: cucumber runs under node, which does not load `.env`, so the harness never received the wallet material and every scenario silently fell back to an empty local mode. A failed sign-in is now loud on both sides. The suite also releases what it opens and exits on its own -- runs were still resident hours after reporting, holding a browser and two servers. Known red: `@data` cannot be measured. The served wallet accumulates and nothing resets it; moving the browser profile aside does not, since the data lives in the wallet file, not the profile.
107 lines
4.7 KiB
TypeScript
107 lines
4.7 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* link-polyfill.ts — Reactive local link for the @ng-eventually/polyfill polyfill.
|
|
*
|
|
* WHY a copy-overlay and not a symlink:
|
|
* The committed prod dependency installs @ng-eventually/polyfill from Gitea (git+https)
|
|
* into pnpm's store WITHOUT its own node_modules/@ng-org → @ng-org/web resolves up to
|
|
* Festipod → ONE @ng-org instance (one verifier). The local polyfill CHECKOUT, however,
|
|
* carries its own node_modules/@ng-org/* (symlinks into the ng-eventually-js monorepo
|
|
* store). Symlinking node_modules/@ng-eventually/polyfill to that checkout puts the
|
|
* checkout's @ng-org in the resolution path → a SECOND @ng-org instance → broken SDK
|
|
* (two verifiers). So we overlay a real directory that contains ONLY the polyfill's
|
|
* source (no node_modules) and keep it in sync by copying — @ng-org still resolves to
|
|
* Festipod, single instance preserved.
|
|
*
|
|
* WHAT IT DOES:
|
|
* 1. Replaces node_modules/@ng-eventually/polyfill (the pnpm store symlink) with a real
|
|
* directory holding the local polyfill's package.json + src (NO node_modules).
|
|
* 2. Asserts the single-instance invariant (same @ng-org/web realpath from Festipod and
|
|
* from the overlay) — aborts if it would break.
|
|
* 3. Watches the local polyfill src and copies each change into the overlay, so
|
|
* `bun --hot` (bun run dev) reloads the edited file live.
|
|
*
|
|
* USAGE (reactive dev):
|
|
* Terminal 1: pnpm run link:polyfill # overlays local source, then watches
|
|
* Terminal 2: bun run dev # portless festipod bun --hot src/index.ts
|
|
* Edit files under packages/polyfill/src → they land in node_modules → bun --hot reloads.
|
|
*
|
|
* pnpm run link:polyfill --once # overlay + verify, no watch (CI / one-shot)
|
|
* Return to the committed git-installed dependency: pnpm install
|
|
*
|
|
* Override the local checkout path with NG_EVENTUALLY_LOCAL=/path/to/packages/polyfill.
|
|
*/
|
|
import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs";
|
|
import { watch } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
|
|
const FESTIPOD = realpathSync(join(import.meta.dir, ".."));
|
|
const LOCAL =
|
|
process.env.NG_EVENTUALLY_LOCAL ??
|
|
"/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill";
|
|
const TARGET = join(FESTIPOD, "node_modules", "@ng-eventually", "polyfill");
|
|
const SRC_LOCAL = join(LOCAL, "src");
|
|
const SRC_TARGET = join(TARGET, "src");
|
|
const ONCE = process.argv.includes("--once");
|
|
|
|
function fail(msg: string): never {
|
|
console.error(`✖ link:polyfill — ${msg}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!existsSync(join(LOCAL, "package.json"))) {
|
|
fail(`local polyfill not found at ${LOCAL} (set NG_EVENTUALLY_LOCAL to override)`);
|
|
}
|
|
|
|
// 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules).
|
|
console.log(`→ overlaying local polyfill: ${LOCAL}`);
|
|
if (existsSync(TARGET) || lstatSync(TARGET, { throwIfNoEntry: false })) {
|
|
rmSync(TARGET, { recursive: true, force: true });
|
|
}
|
|
mkdirSync(TARGET, { recursive: true });
|
|
for (const meta of ["package.json", "tsconfig.json", "README.md"]) {
|
|
const from = join(LOCAL, meta);
|
|
if (existsSync(from)) copyFileSync(from, join(TARGET, meta));
|
|
}
|
|
// Copy src fresh (NEVER a node_modules dir — that is what guarantees single @ng-org instance).
|
|
cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true });
|
|
|
|
// 2. Assert the single-instance invariant.
|
|
const fromFestipod = realpathSync(Bun.resolveSync("@ng-org/web", FESTIPOD));
|
|
const overlayReal = realpathSync(TARGET);
|
|
const fromPolyfill = realpathSync(Bun.resolveSync("@ng-org/web", overlayReal));
|
|
console.log(` @ng-org/web (Festipod): ${fromFestipod}`);
|
|
console.log(` @ng-org/web (overlay) : ${fromPolyfill}`);
|
|
if (fromFestipod !== fromPolyfill) {
|
|
fail(
|
|
"single-instance invariant BROKEN — @ng-org/web resolves to two different realpaths.\n" +
|
|
" The overlay must not contain its own node_modules/@ng-org. Aborting.",
|
|
);
|
|
}
|
|
console.log("✓ single @ng-org/web instance preserved");
|
|
|
|
if (ONCE) {
|
|
console.log("✓ overlay ready (--once, not watching)");
|
|
process.exit(0);
|
|
}
|
|
|
|
// 3. Watch and copy on change so `bun --hot` sees live edits.
|
|
console.log(`👀 watching ${SRC_LOCAL} → ${SRC_TARGET} (Ctrl-C to stop)`);
|
|
watch(SRC_LOCAL, { recursive: true }, (_event, filename) => {
|
|
if (!filename) return;
|
|
const from = join(SRC_LOCAL, filename);
|
|
const to = join(SRC_TARGET, filename);
|
|
try {
|
|
if (existsSync(from)) {
|
|
mkdirSync(dirname(to), { recursive: true });
|
|
copyFileSync(from, to);
|
|
console.log(` ↻ ${filename}`);
|
|
} else if (existsSync(to)) {
|
|
rmSync(to, { force: true });
|
|
console.log(` ✗ ${filename} (removed)`);
|
|
}
|
|
} catch (err) {
|
|
console.error(` ! failed to sync ${filename}:`, err);
|
|
}
|
|
});
|