43aadbeb45
98 annotations posées à côté des déclarations, et un test qui les exige sur la surface publiée. Elles portent trois choses : le niveau qui répond, la référence amont, et la catégorie parmi les cinq. La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que j'avais classé « notre invention » en raisonnant depuis l'absence, alors que c'est le meilleur alignement disponible. Les références citent un SYMBOLE, jamais une ligne : trois citations du document avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source — un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos noms, plus un argument jamais transmis. La sémantique survit à la migration, les sites d'appel non, et la nuance disparaissait sous une étiquette trop flatteuse. Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à une invention qui prétendrait citer une référence — vérifié en cassant les trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé dans index.ts se faisait analyser comme une annotation. La classification couvre l'interne qui prétend ressembler à la cible — tout emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine portait sur une fonction non exportée ; n'être pas publié n'a protégé personne. Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la seconde nommée dans la note plutôt que lissée.
100 lines
6.1 KiB
TypeScript
100 lines
6.1 KiB
TypeScript
/**
|
|
* Lifecycle re-exports — SDK-shaped forwarders so the app imports `init` /
|
|
* `initNg` from `@ng-eventually/polyfill` rather than from `@ng-org/*`. They
|
|
* delegate to the REAL functions injected at `configure()`.
|
|
*
|
|
* ── `init` is not a bare passthrough, and that is the point ────────────────
|
|
* The real `init()` hands the page to the broker as its FIRST statement — a top-level
|
|
* document is navigated to NextGraph's login, carrying `window.location.href` AS IT FINDS
|
|
* IT. It knows nothing of `?ng-id=`, and it does not come back: everything the application
|
|
* would have run after that line runs in a document that no longer exists. So an
|
|
* application that signed in on the next line never signed in at all — the barrier never
|
|
* showed, the identifier never reached the URL that crossed, and a first-time user landed
|
|
* on a login with no wallet and no error anywhere.
|
|
*
|
|
* The obvious remedy — "call the gate first" — is an ordering rule written in prose, which
|
|
* every consumer gets to get wrong once; and it deadlocks besides, because signing in used
|
|
* to include waiting for the session `init()` is what opens. So the invariant is carried
|
|
* HERE, by composition: this forwarder settles the identity, then delegates. A caller
|
|
* cannot get the order wrong because a caller no longer takes part in it
|
|
* (`shared-wallet/access-gate.ts`, {@link settleIdentity}).
|
|
*/
|
|
|
|
import { getConfig, rememberHandOver } from "../shared-wallet/bootstrap";
|
|
import { settleIdentity } from "../shared-wallet/access-gate";
|
|
import { captureSession, expectSession } from "../shared-wallet/session";
|
|
|
|
/**
|
|
* Forwards to the real `@ng-org/web` `init`, once the identifier is in the address bar.
|
|
*
|
|
* Awaits {@link settleIdentity} — the session-free half of signing in — and NOT
|
|
* `ensureIdentity()`, which also awaits the connection work, which awaits `getSession()`,
|
|
* which resolves only from the session `init()` has not opened yet. That wait is the
|
|
* deadlock, and avoiding it is what the split in `access-gate.ts` is for.
|
|
*
|
|
* A settling failure REJECTS rather than delegating. No shared wallet configured, or no DOM
|
|
* to ask on, means there is no identifier to hand over — and handing the page to the broker
|
|
* anyway IS the defect, a navigation the user cannot come back from. It fails at the call
|
|
* the application made, where the cause is.
|
|
*
|
|
* The "not injected" error stays SYNCHRONOUS: it is a wiring mistake rather than a runtime
|
|
* one, and it threw synchronously before this forwarder had anything to await.
|
|
*
|
|
* ── It also LISTENS on the way through, and that is the one argument it touches ──
|
|
* The real `init` delivers the session by calling its callback with
|
|
* `{ status: "loggedin", session }` — once, and it is the only channel that ever produces
|
|
* one (`@ng-org/web` `dist/ngweb.js:113-137`, VERIFIED). Until 2026-08-12 every application
|
|
* had to catch that event itself and hand the library a thunk unwrapping it, which is a
|
|
* shape the target never asks anyone to build and which two consumers in a row got wrong.
|
|
*
|
|
* So the callback in position 0 is WRAPPED: the wrapper reads the event, keeps the session
|
|
* (`shared-wallet/session.ts`), and then calls the caller's callback with that same event,
|
|
* unchanged and un-narrowed. Nothing else about the call moves — the remaining arguments and
|
|
* the return value pass straight through, and the caller's callback still sees exactly what
|
|
* the real `init` sent it. It is the one place a wrapper can be, because it is the one place
|
|
* that knows both what the caller asked and what the SDK will answer.
|
|
*
|
|
* A caller that passes NO callback is the same act with nobody listening — upstream accepts
|
|
* it (`callback: Function | null`, and the call site is guarded). The wrapper still goes in,
|
|
* so the package gets its session either way, and calls nothing afterwards.
|
|
*/
|
|
// @provenance init kind=passthrough level=2 ref=sdk/js/web/src/index.ts:init — arguments and return pass straight through; the callback in position 0 is wrapped to keep the session it carries
|
|
export function init(...args: any[]): any {
|
|
const f = getConfig().init;
|
|
if (!f) throw new Error("[ng-eventually] init() not injected — pass it to configure()");
|
|
const [callback, ...rest] = args;
|
|
const listen = (event: unknown): unknown => {
|
|
captureSession(event);
|
|
return typeof callback === "function" ? callback(event) : undefined;
|
|
};
|
|
const delegate = (): unknown => f(listen, ...rest);
|
|
// Two notes taken on the way in, both about what only this call can know.
|
|
//
|
|
// The hand-over, first: this delegation is the whole of it, and a person who reaches the
|
|
// broker without a wallet comes back to THIS document with it already spent. The barrier
|
|
// revives there and needs something to hand the page over with — this, exactly as it
|
|
// would have run the first time (`shared-wallet/access-gate.ts`).
|
|
rememberHandOver((): void => {
|
|
// Un-awaited, and it has to be: the promise this call returned was answered on the way
|
|
// out, so a rejection here has no caller to reach. Reported rather than dropped as an
|
|
// unhandled rejection — the page is on its way to the broker either way.
|
|
void Promise.resolve(delegate()).catch((failure: unknown) => {
|
|
console.error("[ng-eventually] the hand-over to the broker failed", failure);
|
|
});
|
|
});
|
|
// And that it RAN. The session arrives through this call and through no other, so
|
|
// "`init()` has not been called" is the same statement as "no session can ever arrive" —
|
|
// which is what lets `ensureIdentity()` refuse an impossible wait instead of hanging on it
|
|
// (`shared-wallet/session.ts`, `shared-wallet/access-gate.ts`).
|
|
expectSession();
|
|
return settleIdentity().then(delegate);
|
|
}
|
|
|
|
/** Forwards to the real `@ng-org/orm` `initNg` (ORM signals). */
|
|
// @provenance initNg kind=passthrough level=3 ref=sdk/js/orm/src/core.ts:initNg — upstream's own exported name for `initNgSignals`
|
|
export function initNg(...args: any[]): any {
|
|
const f = getConfig().initNg;
|
|
if (!f) throw new Error("[ng-eventually] initNg() not injected — pass it to configure()");
|
|
return f(...args);
|
|
}
|