Files
ng-eventually/packages/polyfill/src/surface/lifecycle.ts
T
Sylvain Duchesne 43aadbeb45 docs: chaque symbole dit d'où il vient
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.
2026-08-16 22:53:50 +02:00

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);
}