Files
Sylvain Duchesne 259d7fe58d Settle what interchangeability means, and ask for what is missing
Opens the brief that holds this binding to `@activitypods/refine-providers`,
with which it must be interchangeable, and settles its six questions. The
decisive finding of the comparison: the reference asks its server for almost
none of what a Refine resource needs — filters, sorting, pagination and
totals are computed in memory there too — so most of the gap is this
package's own work rather than the polyfill's.

What the engagement is now asked for, in order: a shape-typed write and
removal that can be awaited outside React, a call that closes a session, a
display name, and a write-ownership predicate that would let an interface
hide a control instead of letting it fail.

Two corrections worth their own mention, both found by review rather than
by reading further:

- a non-guarantee about how strongly the upstream authenticates was being
  allowed to dictate the SHAPE of this binding, which had `login` and
  `logout` published as having no surface. Opening a wallet is the act the
  target will have, so the auth provider is wired to it and the missing
  closing call is a request, not an absence of analogue;
- what is internal to the emulation must not be known in this repository at
  all. Restating it here would duplicate a claim this package cannot verify
  and would teach an application a distinction that disappears at migration.
2026-10-01 19:54:45 +02:00

15 KiB

type, summary, against
type summary against
usage Binds a Refine application's resources to NextGraph documents — one shape per resource, one document per entity — through this engagement's shape reads, its shape-typed insert and its inbox @ng-eventually/polyfill@1.0.0-dev.15 (tag polyfill/v1.0.0-dev.15, commit ec3900077bfd090f5388e52b47a8132b2fc68d16)

usage_refine-providers-eventually — @ng-eventually/refine-providers

This package is a set of Refine providers — dataProvider, authProvider, liveProvider — that an application declares once and then drives through Refine's own hooks (useTable, useForm, useShow). It is a library, not an application: it declares this engagement a peer, so the application remains the one place the package is named.

It carries no compensation for the current state of NextGraph. Capability isolation, identity, inboxes, sharing, broker state: all of that is this engagement's. What this package does is translate — a Refine resource is one SHEX shape, a Refine id is the NURI of one document, and one document holds one business entity.

The pinned version is 1.0.0-dev.15, through the tag polyfill/v1.0.0-dev.15 that carries it.

Consumed surface

Bootstrap (the application's, before it renders; this package only reads what it settled) — configure, init, ensureIdentity.

Reading. getObjects(shapeType, config) is the whole of the read path, on two scopes and no other:

  • graphs: "" for a resource listing. There is no call that lists the documents of a store, so the per-document capability filter IS the listing: what comes back is what this identity holds of that shape, which is exactly what a listing should contain — the documents it created and the documents whose key it was given, with no gesture of its own to tell them apart.
  • graphs: [<nuri>] for one record, or several.

Refine's filters and sorters are applied in memory, over what the read returns — the engagement gives where and orderBy no published semantics, and the one guarantee it attaches to them is that an orderBy makes the result an array rather than a Set. Passing them through and claiming they carry a filter would be relying on something nothing promises.

docs.sparqlQuery, anchored, is called before a named read when the document may be a public one this identity has never read — see ## Frictions.

Writing. storeRegistry.createEntityDoc(store?) with privateStore() / protectedStore() / publicStore() to place it, then insertObject(shapeType, object) with the object carrying its @graph and an empty @id. docs.sparqlUpdate for modification and removal, for want of anything else — see ## Needs.

Reacting. useShapeQuery(shapeType, scope) for a resource channel, through getSnapshot / subscribe / refetch — it is an observable rather than a hook, which is what makes it reachable from a provider. subscribeDocs + docChangeType for a channel over named ids.

Sharing (outside Refine's CRUD, reached through the provider's custom or through hooks this package exports) — inbox.share, inbox.postToDocument, inbox.readForDocument, inbox.readSyncedForDocument, storeRegistry.openDocumentInbox.

Types — Nuri, NuriLike, Scope, RdfOrmConfig, ShapeType, BaseType, ShapeQuery, ShapeObservable, PrincipalId.

useShape is NOT consumed: it is a React hook, and a DataProvider is not a component.

Constraints

A read under graphs: "" must keep answering what this identity holds, and nothing else. This is the one mechanism that makes a resource listing both correct and complete, since nothing lists a store. If it ever widened to what the wallet physically carries, every application built on this package would show every identity's records with nothing signalling it.

useShapeQuery must stay an observable, and its data must keep carrying the shape's own objects (@id, @graph, the shape's predicates). Both are load-bearing: the first is what a non-React provider can subscribe to, the second is what lets an application move between this package and @ng-org/refine-providers without rewriting a single page — the records must have the same shape on both sides.

getObjects's collection kind must stay predictable from the config — an array with orderBy, a Set otherwise. Refine needs a stable order and a count.

insertObject must keep returning the object with its @id filled in. Refine's create resolves with the created record; there is nothing else to build it from.

One document per entity must remain the recommended placement, and access must stay per document. A Refine id is a document's NURI; if access were granted at any other granularity, id would stop being the unit that can be read, shared and listed.

The application must inject ormSubscription, getObjects, insertObject and initNg into configure. All four are optional fields of EventuallyConfig, and this package reaches none of the ORM on its own — it is a library, and ## Non-guarantees states that one wiring no @ng-org/* of its own cannot reach the injected write. An application that omits them gets a binding whose reads and writes have no engine. useShape must also carry a value, being required, though this package never calls it: a function that throws says the most.

And it must call THIS package's initNg, never the SDK's. ## Scope is explicit about the consequence — « Calling @ng-org/orm's initNg directly leaves ORM writes ungoverned », and ## Change policy repeats that such an application « still works and is still unguarded ». That call decides which ng the ORM reaches the engine through, and the write guard rides on the substituted one. This is the single most consequential line of the bootstrap and the easiest to get wrong, because the wrong version works: the sibling binding calls the SDK's initNg today (src/auth-provider.ts), and a binding ported from it inherits an unguarded write path where every other guarantee still appears to hold.

The application must hand this package the session. docs.docCreate, sparqlQuery and sparqlUpdate all take a sessionId first, and the engagement gives a library no way to obtain one: a session is never built, no call takes one, getSession was removed, and the session arrives only in the callback passed to this package's init. So the application catches it there and hands it over — the same shape the sibling already has, where authProvider.getSession() is what the data provider calls. A library that assumed it could ask for one would have no first argument to pass.

Relayed to the application, which owns its URL: ?ng-id= must survive its router. Refine's syncWithLocation rewrites the query string, and a dropped parameter is a silent mis-identification rather than an error.

Frictions

Modifying and removing a record have no published path that is both shape-typed and awaitable. ## Non-guarantees routes removal through the LIVE set a shape query handed back, and that set is reachable only through useShape — a React hook, which a DataProvider cannot call. Worse for this shape of caller: a mutation of a live set is batched into a call NextGraph does not await, so a refusal surfaces as an unhandled rejection in the console rather than at the line that made the change. Refine's update and deleteOne must resolve with the record or reject; neither is possible on that channel. What is left is projecting the shape into SPARQL and calling docs.sparqlUpdate — re-implementing in this package what NextGraph's ORM already does, which is exactly the kind of compensation this package exists not to carry.

Reading one named record costs either two calls or a short-lived observable. getObjects spends no round-trip asking a public store for its key, so a reference received from elsewhere reads empty through it. ## Guarantees names the two calls that do ask — an anchored docs.sparqlQuery, always, and useShapeQuery for a document named in scope.graphs since 1.0.0-dev.9, typed and reactive — so a getOne is either a priming query followed by getObjects, or a useShapeQuery subscribed and torn down for one read. Neither is wrong; both pay for the asymmetry between the one-shot read and the reactive one.

Reading one record changes what the next listing contains. ## Guarantees says of a document read by name that « the key lands in what you hold, which all three re-read on ». A listing under graphs: "" is defined as what this identity holds, so opening one public document by its reference — a link followed into a show page — puts it in every subsequent listing of that shape, with no gesture from the application and no way to take it back. The acquisition is the engagement's intended behaviour; what is unsettled is that it makes a Refine resource grow by being browsed.

A named read can stay pending for the life of the session. ## Guarantees keeps a named document that never synced in isPending deliberately — « a barrier that was never crossed is not authority to call anything empty » — and ## Non-guarantees adds the shape query the engine refuses, which never finishes loading either. A DataProvider method must return a promise that settles; built on an observable that may never answer, getOne hangs and the interface shows a spinner with no end. The reference binding cannot do this: its reads are HTTP and always settle, success or failure.

Naming several documents fails as a whole. One unreachable document in graphs means nothing is answered, so a getMany over a heterogeneous list — some created here, some received, some public — cannot return the records that ARE readable. A Refine table that resolves relations in one call cannot use it.

A listing has no count and no reachable pagination. getObjects's config is Omit<RdfOrmConfig, "pageSize" | "maxActivePages">, so the only way to a total is to read everything this identity holds of the shape and measure it. Refine paginates from a total, so every page costs the whole collection. useShape carries nextPage / previousPage; the one-shot read does not.

ShapeObservable and refetch() are both open migration points, by this engagement's own account — the first on the deletion list, coupled to the shared wallet, the second with no recorded counterpart and the only published way out of a read stuck pending. The live provider is written on both.

Needs

A shape-typed write and a shape-typed removal, awaitable, callable outside React. The analogue of insertObject for modifying and removing — same posture: the object carries its @graph, the call rejects when refused, and the caller awaits it.

First, a silence to break rather than a call to add. ## Surface says insertObject fills in an @id « where one was left empty », and says nothing about an object that arrives carrying one that already exists in its document — merge, duplicate subject, or refusal. If it merges, the modification path already exists and this need is half answered; if it duplicates, an application that tries it corrupts a document and learns so later. Either way the answer belongs in the engagement, and it is cheaper to state than to implement.

That aside, this is the need that decides the shape of this package's dataProvider, and the bar is set outside this pair: the binding is held to being interchangeable with @activitypods/refine-providers, which serves update and deleteOne as ordinary awaited calls. Every other gap this leaf records has a caller-side answer — filters, sorting, pagination and totals are computed in memory there too, a getMany reads one record at a time and keeps what answered, and a reference received from elsewhere is reached through useShapeQuery. These two have none: Refine's update and deleteOne must resolve with the record or reject, and no published call can do either.

A meaning for "delete" that a Refine interface can present. ## Non-guarantees is categorical: there is no object removal call and no document deletion « at any level of NextGraph »; what a removal takes away is the predicates the shape declares on that subject, while the subject, the document and every other triple survive. So even once the write above exists, deleteOne cannot make a record go away: the document stays readable, answers isSuccess with nothing of the shape in it, and the key already handed to someone else keeps resolving. A bookmarked show page renders an empty record rather than a missing one, where the reference binding's DELETE makes the next read a 404. What is needed is not only the call but a published answer to what deletion means here — whether an emptied document is meant to read as absent, and through which observation.

A call that closes the session. In the target, an open wallet is the authenticated state and closing it is the sign-out, so a binding wires Refine's logout to that act — the shape survives migration. No published call performs it: ## Guarantees names "changing identity or clearing it" as an event that happens, setCurrentUser was removed on purpose, and awaiting ensureIdentity() again shows no barrier. The consequence is concrete and it is a loss of protection rather than an inconvenience: a Logout button can be rendered, it cannot sign anybody out, and the next visitor re-enters the previous identity in one click from a prefilled barrier. The reference binding clears its token and check() then fails.

A display name, or something able to carry one. This is an ASK TO WIDEN the scope, not a gap: ## Scope excludes display names explicitly. The reason to ask anyway is that the two halves of the current answer leave a caller with no conforming option — ## Non-guarantees forbids rendering the identifier as a readable name, and nothing published offers a name to render instead. Refine puts getIdentity().name in its own layout by default, and the reference binding reads a name and an avatar off the person's profile, so an interface that showed a name on one binding can show nothing lawful on this one. Anything observable would settle it: a name alongside the identifier, or a published place to keep one. Until then this package returns the identifier in that field, deliberately and against the letter of the non-guarantee, because the alternative is an empty layout.

A predicate for write ownership — a comfort, not a blocker. ## Guarantees says only a document's owner writes to it, and insertObject refuses a document this identity does not own, but no published call answers whether it owns one. A listing here is everything accessible of a shape, so it legitimately mixes the two, and the default path is to let the person act and surface the refusal — which needs the awaitable write above and nothing else. What the predicate would buy is an interface that hides a control rather than letting it fail; it is worth asking for and it blocks nothing. It is not the read-side question this surface declines to answer: on the read side the answer already arrives as the absence of the record from what a read returns. It is the write side, where ## Guarantees already expects a caller to know what it owns — « write into documents you own, and treat a document you merely hold the key for as read-only » — while publishing no call that says which is which.