fix: lire un document par la requête ancrée, readUnion ayant quitté le polyfill

Le contrat polyfill-surface passe de 1.0.0-dev.6 à 1.0.0-dev.15, épinglé
désormais sur le tag polyfill/v1.0.0-dev.15. readUnion et UnionSubject ont
été retirés en dev.8 : l'application e2e lit un document par un SELECT ancré
regroupé par sujet, en ignorant les registres urn:ng-eventually: que la
requête renvoie désormais.

La déclaration usage_ng-helpers retire ce que l'engagement a absorbé
(exemple de forme, initNg, orm_start_graph corrigé en dev.7, comportement
d'un document illisible, lecture d'un document public par sa seule
référence, pin par tag) et garde les demandes encore ouvertes.

contract_indexing-layer 4.0.1 (patch) : la lecture d'un index ne cite plus
readUnion.
This commit is contained in:
Sylvain Duchesne
2026-10-01 16:22:02 +02:00
parent 71667ca14e
commit 8bcea7c5be
12 changed files with 305 additions and 82 deletions
+14 -5
View File
@@ -14,14 +14,23 @@
* ends up typed as a reference.
*/
import type { UnionSubject } from "@ng-eventually/polyfill";
/**
* One subject of a document and everything the document says about it, keyed by
* predicate. Declared here because nothing publishes it: the polyfill withdrew its own
* (`UnionSubject`, with `readUnion`) in `1.0.0-dev.8`, and grouping the rows of an
* anchored query is the application's business -- see `readDocument` in the page.
*/
export interface DocumentSubject {
readonly subject: string;
readonly props: Readonly<Record<string, readonly string[]>>;
}
/**
* One entry, as THIS APPLICATION decodes one out of the index document.
*
* It is declared here and not imported, because `@ng-helpers/indexing` publishes no
* such type and no call that produces one: an index is an ordinary document, so its
* contents are whatever `readUnion` hands back, read with the two published IRIs.
* contents are whatever an anchored query hands back, read with the two published IRIs.
* A subject carrying several values shows up as several rows — the document's own
* truth, and the application's business to make something of.
*/
@@ -120,13 +129,13 @@ export interface IndexingBridge {
rebuildHandle(): Promise<void>;
/**
* The index's entries, ordered by value — decoded BY THIS APPLICATION, out of an
* ordinary `readUnion` of the index document, with nothing from the library but the
* ordinary anchored read of the index document, with nothing from the library but the
* two IRIs it publishes. That the suite can do this at all is the claim under test.
*/
read(index: string): Promise<IndexRow[]>;
/** What a document literally holds, straight off `readUnion` — the write-form probe. */
readRaw(doc: string): Promise<UnionSubject[]>;
/** What a document literally holds, grouped by subject — the write-form probe. */
readRaw(doc: string): Promise<DocumentSubject[]>;
/**
* Run a SPARQL SELECT anchored on a document, through `docs.sparqlQuery`.
*
+51 -13
View File
@@ -15,9 +15,9 @@
* Nothing here makes an entry of a deposit, and nothing here can ask for one: that is
* the business of the layer below, and this page is where that shows or does not.
* Everything else here is the polyfill (`configure`, `ensureIdentity`, `init`, `initNg`,
* `ng`, `readUnion`, `docs`, `useShapeQuery`) -- including reading the index, which is an
* ordinary `readUnion` of an ordinary document. If something here is awkward, it is
* awkward for every consumer, which is the second reason to write it this way.
* `ng`, `docs`, `useShapeQuery`) -- including reading the index, which is an ordinary
* anchored `docs.sparqlQuery` of an ordinary document. If something here is awkward, it
* is awkward for every consumer, which is the second reason to write it this way.
*
* This page WIRES THE ORM, because one thing here has no other door: the store enumeration it
* used to discover a leaked document left the engagement, and what replaces it is a shape
@@ -43,13 +43,11 @@ import {
init,
initNg,
ng,
readUnion,
useShapeQuery,
type Scope,
type ShapeObservable,
type ShapeQuery,
type ShapeType,
type UnionSubject,
} from "@ng-eventually/polyfill";
import { ng as realNg, init as realInit } from "@ng-org/web";
import { RdfOrmSubscription, initNg as ormInitNg } from "@ng-org/orm";
@@ -60,7 +58,13 @@ import { ENTRY_VALUE, INDEX_FIELD, indexing, type Indexing } from "../src/index"
import { indexingOn } from "../src/indexing";
import type { NextGraphPort } from "../src/port";
import { polyfillPort } from "../src/polyfill-adapter";
import type { BrokenInboxOutcome, IndexRow, IndexingBridge, SelectOutcome } from "./bridge";
import type {
BrokenInboxOutcome,
DocumentSubject,
IndexRow,
IndexingBridge,
SelectOutcome,
} from "./bridge";
// bootstrap: the one polyfill-era call, then the SDK-shaped ones
//
@@ -82,7 +86,7 @@ configure({
useShape: () => {
throw new Error(
"[e2e] useShape was never wired: this page has no framework adapter to give it, " +
"and it reads through readUnion and useShapeQuery instead",
"and it reads through docs.sparqlQuery and useShapeQuery instead",
);
},
ormSubscription: RdfOrmSubscription.getOrCreate,
@@ -129,8 +133,8 @@ async function boot(): Promise<void> {
// through it, and this package's own `initNg` is the one to CALL -- not the SDK's,
// which was handed to `configure` and is reached from there. The `ng` it is given is
// this package's too, the one the surface says to call instead of the one `configure`
// was passed. Measured: the argument makes no difference either way, because the call
// substitutes its own -- see `usage_ng-helpers`'s `## Frictions`.
// was passed. The engagement states the argument is discarded either way: the call
// substitutes that same published `ng`.
initNg(ng, session);
// The session id, and nothing else — this application never names a port, and never
// awaits anything here: a handle reaches nothing. A reference this application hands
@@ -325,6 +329,40 @@ function rowsOf(result: unknown): Array<Record<string, string>> {
});
}
/**
* Everything a document holds, grouped by subject -- what an application writes in place
* of the `readUnion` the polyfill withdrew in `1.0.0-dev.8`.
*
* One anchored `SELECT ?s ?p ?o`: the anchor makes the document the query's default graph,
* so the body names no graph and reads that document and nothing else. A document this
* identity cannot read REJECTS, and the rejection is left to reach the caller -- an
* unreadable document is "unknown", never "empty". The polyfill's own registers live
* under `urn:ng-eventually:` in the same graph and come back with the rest; they are the
* polyfill's, not the document's, so they are dropped here.
*/
async function readDocument(doc: string): Promise<DocumentSubject[]> {
const session = await sessionReady;
const result = await docs.sparqlQuery(
session.session_id,
"SELECT ?s ?p ?o WHERE { ?s ?p ?o }",
undefined,
doc,
);
const bySubject = new Map<string, Record<string, string[]>>();
for (const row of rowsOf(result)) {
const { s, p, o } = row;
if (s === undefined || p === undefined || o === undefined) continue;
if (s.startsWith(RESERVED_PREFIX)) continue;
const props = bySubject.get(s) ?? {};
(props[p] ??= []).push(o);
bySubject.set(s, props);
}
return [...bySubject].map(([subject, props]) => ({ subject, props }));
}
/** The polyfill's reserved prefix: what it writes into a document under it is its own. */
const RESERVED_PREFIX = "urn:ng-eventually:";
// the acts
const bridge: IndexingBridge = {
@@ -372,12 +410,12 @@ const bridge: IndexingBridge = {
/**
* The index read BY THIS APPLICATION, with nothing the package publishes but
* `ENTRY_VALUE` — the claim "an index is an ordinary document" performed rather than
* repeated. `readUnion([index])` is the same call this page makes on any other
* repeated. `readDocument(index)` is the same read this page makes of any other
* document; the index's own subject is the one that is not an entry, told apart by
* being the document itself.
*/
async read(index: string): Promise<IndexRow[]> {
const subjects = await readUnion([index]);
const subjects = await readDocument(index);
const rows: IndexRow[] = [];
for (const subject of subjects) {
if (subject.subject === index) continue;
@@ -394,8 +432,8 @@ const bridge: IndexingBridge = {
return rows;
},
async readRaw(doc: string): Promise<UnionSubject[]> {
return readUnion([doc]);
async readRaw(doc: string): Promise<DocumentSubject[]> {
return readDocument(doc);
},
/**
+8 -8
View File
@@ -221,7 +221,7 @@ const { check, journey, finish } = declareSuite({
{
name: "The index answers an ordinary SPARQL query",
checks: [
"readUnion returns both entries and the index's own declaration",
"reading the document returns both entries and the index's own declaration",
"a SELECT for the entry predicate returns both entries, with their values",
"a SELECT of the index's own subject returns the field it declares",
"a stranger's SELECT returns the same entries",
@@ -278,7 +278,7 @@ function step<T>(what: string, ms: number, task: () => Promise<T>): Promise<T> {
* to suit it.
*
* ── It reads the way an application reads ───────────────────────────────────
* `read` is an ordinary `readUnion` of an ordinary document, or an anchored SELECT — the
* `read` is an ordinary anchored read of an ordinary document, or a narrower SELECT — the
* only two ways anyone has of reading an index, the harness included. Nothing here reaches
* for a way to hurry the layer below, because a consumer has none either.
*
@@ -455,9 +455,9 @@ async function main(): Promise<void> {
const declared = self?.props[INDEX_FIELD] ?? [];
check(
"the index document declares the field it indexes by",
declared.length === 1 && declared[0] === PUBLISHED_AT && self?.graph === index,
declared.length === 1 && declared[0] === PUBLISHED_AT,
`subjects=${raw.length} self=${self === undefined ? "(not found)" : "found"} ` +
`graph=${self?.graph} declared=${JSON.stringify(declared)}`,
`declared=${JSON.stringify(declared)}`,
);
},
});
@@ -762,8 +762,8 @@ async function main(): Promise<void> {
// which is the state the question is about — one entry cannot tell a query that
// returns everything from one that returns the first thing it finds.
//
// WHAT IS BEING ASKED. This package documents one way of reading an index
// (`readUnion`) and issues no query of its own, so "an index is an ordinary document
// WHAT IS BEING ASKED. This package documents how to read an index and issues no
// query of its own, so "an index is an ordinary document
// anyone queries normally" has never been anything but plausible. These four checks
// are a measurement of that sentence, not a feature: an empty answer is a RESULT and
// is reported as one, and a rejection is reported apart from it, because "the query
@@ -789,7 +789,7 @@ async function main(): Promise<void> {
PUBLISHED_AT,
);
check(
"readUnion returns both entries and the index's own declaration",
"reading the document returns both entries and the index's own declaration",
held.size === 2 && declares,
`entries=${JSON.stringify([...held.keys()])} declares=${declares}`,
);
@@ -826,7 +826,7 @@ async function main(): Promise<void> {
);
// The reader who matters: an index exists to be read by people who own neither it
// nor anything in it. `readUnion` already answers him (the journey above); whether
// nor anything in it. Reading the whole document already answers him (the journey above); whether
// a query does is a separate question, and it is the one an application asks.
const theirs = await step("Bob querying the index he does not own", BRIDGE_MS, () =>
bob!.frame.evaluate(