refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph

Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont
ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever
l'ambiguïté à chaque phrase — et le contrat publié, lu par une application,
était le pire endroit pour laisser traîner ça.

packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill,
contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts →
e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md.

Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes,
y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est
fait occurrence par occurrence, pas par substitution.

Le contrat énonce désormais son identité en une phrase : « This package is a
polyfill of NextGraph's SDK. »
This commit is contained in:
Sylvain Duchesne
2026-08-10 17:14:25 +02:00
parent 49b046268e
commit 737729c9ce
88 changed files with 122 additions and 106 deletions
+153
View File
@@ -0,0 +1,153 @@
/**
* Low-level document + SPARQL primitives.
*
* These call the real injected `ng` (`getConfig().ng`) directly — never the
* public `ng` proxy (`makeNg`). This is a validated hard constraint, not a style
* choice: the public `ng` is a JS `Proxy` over `@ng-org/web`'s iframe-RPC proxy,
* and layering our Proxy on top breaks `doc_create`'s `postMessage` marshaling
* with **`DataCloneError: function ... could not be cloned`** — the footgun this
* rule exists to prevent. Reaching the real `ng` held in the config avoids the
* double-proxy. Do not import from `./ng-proxy`.
*
* Signatures mirror the real `@ng-org/web` `ng` surface (verified against the
* app's storeRegistry usage), so this is a drop-in for those raw calls.
*/
import { getCaps, getConfig } from "../shared-wallet/bootstrap";
import { logAccess, enabled as accessLogEnabled } from "../shared-wallet/access-log";
import { isNuri, toNuri } from "../model/nuri";
import { assertMayReach, assertMayWrite } from "../emulated-verifier/reach";
import { fetchReadCap } from "../emulated-verifier/public-store";
import type { Nuri, NuriLike } from "../model/types";
// The low common point for ALL document access: every read in the SDK routes
// through `sparqlQuery`, every write through `sparqlUpdate` (+ container creation
// through `docCreate`) — each ultimately calling the real injected `ng` here. The
// access log is therefore instrumented HERE so no access path escapes it. Callers
// pass a semantic `label` (readDoc|readUnion|listMyEntityDocs|writeEntity|deposit
// |…); it is a lib-internal probe param, NOT forwarded to the real `ng` (the docs
// primitives forward the exact SDK signature — see test/docs.test.ts). When the
// log is OFF (default) the extra param is inert and costs one boolean read.
/** Count rows in a raw SPARQL SELECT result, tolerant of the possible shapes. */
function rowCount(result: unknown): number {
if (!result) return 0;
if (Array.isArray(result)) return result.length;
const anyRes = result as { results?: { bindings?: unknown[] } };
return anyRes.results?.bindings?.length ?? 0;
}
/**
* Create one document → its NURI.
*
* Mirrors `ng.doc_create(session_id, crdt, cls, dest, store_repo?)`. For a graph
* document in the (shared) private store: `docCreate(sid, "Graph", "data:graph",
* "store")` (store_repo left undefined → private store).
*/
export async function docCreate(
sessionId: string,
crdt: string,
cls: string,
dest: string,
store?: unknown,
): Promise<Nuri> {
const { ng } = getConfig();
const nuri = await ng.doc_create(sessionId, crdt, cls, dest, store);
// The BROKER boundary. `ng` is a permissive property bag (`NgLike`), so what
// comes back is `any` and this function's `Promise<Nuri>` would otherwise be an
// unchecked promise — every typed NURI downstream rests on it. Validate once,
// here, rather than let a non-reference propagate as a document.
if (typeof nuri !== "string" || !isNuri(nuri)) {
throw new Error(
`[ng-eventually] docCreate: the broker returned something that is not a NextGraph reference: ${JSON.stringify(nuri)}`,
);
}
// **Creating a document gives you its cap.** Upstream that is not a courtesy but
// the mechanism: `doc_create` commits `AddRepo { read_cap }` to the store's Store
// branch, so the creator holds it from the first instant. Without this, a caller
// could create a document through this primitive and then be refused reading or
// writing it — which is what the e2e run against the live broker exposed.
//
// `physical.ts`'s counterpart deliberately does NOT do this: the shim's own
// documents belong to no virtual user, and `store-registry` files their caps
// itself, where it knows whose they are.
getCaps().mint(nuri);
// A container creation is a WRITE; the NURI only exists after the call.
logAccess("WRITE", nuri, "docCreate");
return nuri;
}
/**
* Run a SPARQL UPDATE (INSERT/DELETE DATA, etc.).
*
* Mirrors `ng.sparql_update(session_id, query, anchor?)`, where `anchor` is the
* document NURI the update is scoped/base'd to (optional).
*/
export async function sparqlUpdate(
sessionId: string,
query: string,
anchorLike?: NuriLike,
label = "sparqlUpdate",
): Promise<void> {
const { ng } = getConfig();
const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlUpdate");
// The boundary, in two questions that are NOT the same one.
//
// Reaching is possession. Writing is OWNERSHIP — upstream the right to write is
// membership of the repo (`verify_permission`, reachable only from `Commit::verify`,
// so on commits and never on reads), and how you came by the READ key changes nothing
// about it. A public store hands its read cap to whoever asks; a cap deposited in your
// inbox is a Link someone gave you. Neither makes you a member.
//
// NO public-store fetch here, unlike the read below: asking the network for a read key
// has no bearing on a write.
if (anchor !== undefined) {
assertMayReach(anchor, "docs.sparqlUpdate");
await assertMayWrite(anchor, "docs.sparqlUpdate");
}
// `label` is a lib-internal access-log tag, NOT forwarded to `ng`.
logAccess("WRITE", anchor ?? "(no anchor)", label);
return ng.sparql_update(sessionId, query, anchor);
}
/**
* Run a SPARQL SELECT/CONSTRUCT/ASK query → the raw SDK result.
*
* Mirrors `ng.sparql_query(session_id, query, base?, anchor?)`. `base` is the
* query base IRI (usually `undefined`); `anchor` is the document NURI to query.
*/
export async function sparqlQuery(
sessionId: string,
query: string,
base?: string,
anchorLike?: NuriLike,
label = "sparqlQuery",
): Promise<unknown> {
const { ng } = getConfig();
const anchor = anchorLike === undefined ? undefined : toNuri(anchorLike, "docs.sparqlQuery");
// The boundary: an ANCHORED read may only touch what the connected virtual user
// reaches. An anchorless query spans the local union — a different problem (it is
// O(wallet size), and the read path never uses it), not one this guard can bound.
//
// Asking the (emulated) network first, as `ensureRepoOpen` does: a document in a
// public store gives its cap to whoever asks, and this is a door an application can
// reach with nothing but a bare reference. Inert once the cap is held, memoised
// otherwise — see public-store.ts.
if (anchor !== undefined) {
await fetchReadCap(anchor);
assertMayReach(anchor, "docs.sparqlQuery");
}
// `label` is a lib-internal access-log tag, NOT forwarded to `ng`.
const result = await ng.sparql_query(sessionId, query, base, anchor);
// Log AFTER the read so the row count (a strong leak signal: a doc rendering
// rows under an identity that should see nothing) can be appended. Skip the
// rowCount work entirely when the log is off.
if (accessLogEnabled()) {
// `rows` here are raw RDF triple bindings (the SPARQL `?s ?p ?o` result), NOT
// domain objects — one document's entity is spread across several triple rows.
// Spell that out so the log isn't mistaken for an object count (the app-level
// object/shape count is logged separately by useShapeQuery → dataStats).
logAccess("READ", anchor ?? "(no anchor)", label, " → " + rowCount(result) + " triple-rows");
}
return result;
}