Files

8.8 KiB

ng-helpers

Helpers built on top of NextGraph. The first — and for now the only — one is @ng-helpers/indexing: an index that an application can point at and read.

Why this is not in the polyfill

NextGraph has no indexing concept, at any level, and will not grow one. This is not scaffolding standing in for something that will arrive upstream; it is a construction above NextGraph, and it stays in its own repository so that the two are never confused.

So the dependency runs one way and only one way: this repo depends on @ng-eventually/polyfill, never the reverse.

That boundary is held by one file. src/polyfill-adapter.ts is the only place that imports the polyfill at runtime; everything else is written against NextGraphPort (src/port.ts), a four-operation interface describing what this layer needs. Only entries listed in that package's published contract_polyfill-surface are used — nothing is reached for inside it.

What an index is

An ordinary document. Nothing marks it as an index. What makes it one is that it declares a field, and that an application references its NURI in its own source.

  • Any user may create one, and its creator owns it.
  • It lives in the creator's public store, so any reader can open it from the reference alone.
  • It declares, on its own subject, the field it indexes by — a predicate, since the objects are RDF. An index "by a date" is simply an index whose field is a date predicate: there is no separate kind of index. Entries come out in chronological order because ISO-8601 sorts as a string.
  • Each entry is a subject keyed by the indexed object's NURI, carrying that object's value for the field.

Contributing is a deposit, not a write. Handing an index a reference means depositing into the index document's inbox — inbox.postToDocument, which is exactly "reach this document's owner" and is open to anyone. Nobody but the owner ever writes the index.

What travels is a bare reference. Nothing else. It states no claim and gives no instruction: no operation, no index reference (the inbox address already identifies the index), no copy of the indexed value. Whoever processes it opens the object itself and takes what the object says. This is the shape NextGraph already uses upstream, where a SocialQueryRequest carries a reference to an RDF definition and the recipient composes its own update; a payload carrying an operation would be a licence for anyone to rewrite someone else's document.

Reading needs nothing new. An application that knows the NURI calls readUnion([indexNuri]) and gets the entries as subjects, or runs an anchored SELECT. There is no read helper here, and that absence is deliberate: one would only teach a shape a caller has to unlearn.

What this package does, and what it deliberately does not

Two acts — create(field) and add(index, object) — and its whole job is to hide that an index is implemented by an inbox. An application names documents and never an address.

Nothing here makes an entry of what an index receives. No resolving a reference, no opening the object, no writing an entry, no reading a document or an inbox, no going through a store to find one's own indexes, nothing watching. That work belongs to the layer that processes an index's inbox, which is where it now lives — and this package has no call that does it, aims it, or asks about it. A reference handed to an index becomes an entry without anything on this surface being involved.

That is why indexing(sessionId) is not a promise and reaches nothing: a handle is a session id and two acts.

An index still only ever grows, and this package still cannot express a removal. src/sparql.ts builds exactly one statement — an anchored INSERT DATA — and there is no delete builder. What enforces that is not a pattern over source: test/adapter.test.ts runs the real adapter on test/fake-polyfill.ts, an in-memory polyfill whose SPARQL is executed by an engine that understands one statement and refuses everything else. A removal is therefore not detected, it is unrunnable: DELETE WHERE … fails on the first keyword, a second statement smuggled after the closing brace fails on the trailing text, a keyword hidden inside a literal stays inside the literal because a parser tokenises where a regex only matches, and splitting the keyword across concatenated strings buys nothing, since it is one string by the time it arrives. The regex over src/ in test/units.test.ts stays as a cheap tripwire that names the file early; it is not the proof.

The lesson that came out of the removed half is worth keeping, because it was learned the expensive way: "nothing removes" is a claim about the write path, and an invariant about what a reader can see has to be checked on the read path too. Three rounds of adversarial review broke the guarantee five times and not one break involved deleting anything — every one was on the read path (an entry requiring exactly one value read as absent when a race gave it two; the same rule on the declared field took every entry with it; one non-NURI subject threw out of the whole listing; a field named constructor read back an inherited function). It belongs with whoever holds the read path now.

Open questions

Deliberately not settled.

  • What an entry holds besides the object reference and the field value, and how several index kinds would coexist, are both untouched.
  • Nothing marks a document as an index except the field it declares. That is enough to recognise one you already hold a reference to, and not enough to discover one.

The questions about what becomes of a reference — an object carrying nothing for the field, or several values; whether an already-indexed object is ever re-read; what a raced entry keeps; whether a processed deposit is ever retired — are no longer this package's to answer, and moved with the code that answered them.

Layout

Path What it is
src/index.ts Everything published: one function, two acts, two IRIs
src/indexing.ts The two acts, bound to one identity
src/port.ts What this layer needs from NextGraph, and nothing more
src/polyfill-adapter.ts The only runtime import of @ng-eventually/polyfill
src/nuri.ts What a reference is, checked before it goes anywhere
src/sparql.ts The one statement this package writes — no deletion exists
src/vocabulary.ts The two IRIs an index document is written with
test/fake-nextgraph.ts An in-memory NextGraph behind NextGraphPort, enforcing the polyfill's published guarantees
test/fake-polyfill.ts An in-memory @ng-eventually/polyfill that executes the SPARQL — an engine that can only add
test/adapter.test.ts Runs the real adapter on it: behaviour, every query emitted, and every method driven

Depends on

@ng-eventually/polyfill, declared as a peer dependency: an application using this package supplies it, so exactly one copy of it exists in that application. That is a requirement of the polyfill itself, which keeps its state in the package — two copies mean two subscription registries and two current identities, and nothing detects it.

For this repository's own tests and typecheck it is also a devDependency by local path (link:../ng-eventually-js/packages/polyfill), which expects that repository to sit beside this one. A dev dependency is not installed by a consumer, so this local path never reaches one. ng-e2e-helpers is a devDependency by local path on the same expectation.

link:, not file:, and pnpm makes that a real difference. pnpm COPIES a file: directory into its virtual store, and a copy is cut off from the sibling checkout's own node_modules — the polyfill's optional peers (@ng-org/shex-orm, @ng-org/alien-deepsignals) stop resolving and the typecheck fails on them. link: symlinks the sibling package where it lives, so it keeps its own dependencies and an edit made there is the one this repository tests against.

Running it

pnpm install
bunx tsc --noEmit -p tsconfig.json
bun test

pnpm installs; bun runs. pnpm-lock.yaml is the committed lockfile and pnpm install is the only install path — the same package manager the sibling ng-eventually-js uses. bun stays the test runner and bunx tsc the typechecker; neither reads a lockfile, so nothing about that changed.

bun install does not work in this repository (checked with bun 1.3.9): bun resolves a mandatory peer dependency against the npm registry whatever local path provides it, and @ng-eventually/polyfill is published to no registry, so the install stops on GET https://registry.npmjs.org/@ng-eventually%2fpolyfill - 404. That is why no bun.lock is kept here: bun cannot regenerate one, so the file that was here could only rot. bun test itself is unaffected — it is only the installer that cannot express this.