Skip to content

feat(async+events): change notifications on the asynchronous surface#99

Draft
jeswr wants to merge 4 commits into
rdfjs:mainfrom
jeswr:feat/async-dataset-events
Draft

feat(async+events): change notifications on the asynchronous surface#99
jeswr wants to merge 4 commits into
rdfjs:mainfrom
jeswr:feat/async-dataset-events

Conversation

@jeswr

@jeswr jeswr commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What this does

Completes the asynchronous surface with change notifications, mirroring the sync-side proposals in #93/#94:

  • AsyncDatasetWrapper.on(listener) / off(listener) — subscribe an IAsyncDatasetChangeListener (new exported type) to every mutation performed through the wrapper: direct add/delete calls, mutator methods of mapping classes projected out of the wrapper, and mutations of an AsyncWrappingSet. Async-specific semantics:
    • Listeners may be asynchronous: they are invoked in subscription order, a returned promise is awaited before the next listener, and the promise returned by the mutation that triggered the notifications only resolves once every listener has settled.
    • Only effective changes notify (per the sync semantics of feat(events): change notifications on DatasetWrapper (on/off) #94): adding a quad the dataset already contains, or deleting an absent one, emits nothing. Because the underlying dataset can only be queried asynchronously, effectiveness is determined with a has() lookup before the mutation — performed only while at least one listener is subscribed, so the unobserved fast path is unchanged.
    • Only mutations performed through the wrapper (or objects bound to it) are observed; mutating the wrapped dataset directly is not. This matches the analogous caveat on the sync side.
  • AsyncWrappingSet.on(listener) / off(listener) — set-level subscriptions delivering the kind of mutation and the mapped JavaScript value, exactly like feat(events): WrappingSet.on/off with mapped values #93's WrappingSet.on/off: the same (listener, subject, predicate)-keyed WeakMap adapter registry, so off works across the fresh instances AsyncSetFrom.subjectPredicate returns on every property access, and re-attaching the same listener replaces the previous subscription. Datasets that do not emit change events are rejected with a typed DatasetEventsError.

Stacked on #97 and #98

This is the third slice of the async surface and requires both #97 (AsyncDatasetWrapper) and #98 (AsyncTermWrapper/AsyncWrappingSet/async mappings): only the last commit (feat(async+events): change notifications on the asynchronous surface) is proposed here; the first two commits are #97 and #98 unchanged, plus one reconciliation merge commit that makes the two branches compile together:

Once #97 and #98 land, this PR rebases down to the one feature commit.

Relation to #73 / #93 / #94

  • Supersedes the async notification slice of Omnibus #73 (Omnibus): the AsyncNotifyingDatasetCore concern, AsyncWrappingSet's on/off/adapter registry, and the async event tests (ported into async_wrapping_set_events.test.ts / async_dataset_events.test.ts). Two deliberate deviations from the omnibus:
    • Notification is implemented in AsyncDatasetWrapper itself rather than in a separate AsyncNotifyingDatasetCore wrapper class: the foundation async dataset deliberately stays event-free, every wrapper-driven mutation already flows through AsyncDatasetWrapper.add/delete, and AsyncWrappingSet reaches the notifying surface through AsyncTermWrapper.dataset — so interception at the wrapper delivers the same behaviour with much less machinery.
    • Omnibus #73 notified every mutation; this PR follows feat(events): change notifications on DatasetWrapper (on/off) #94's effective-change semantics instead.
  • No ChangeEvent type export: IAsyncDatasetChangeListener and AsyncWrappingSetListener inline the "add" | "delete" union (as feat(events): WrappingSet.on/off with mapped values #93's WrappingSetListener does). If this PR exported its own ChangeEvent alongside feat(events): change notifications on DatasetWrapper (on/off) #94's, the two star re-exports in mod.ts would make the name ambiguous and silently drop it from the public surface.
  • src/errors/DatasetEventsError.ts overlaps with feat(events): WrappingSet.on/off with mapped values #93: it is feat(events): WrappingSet.on/off with mapped values #93's class with the dataset parameter widened to DatasetCore | AsyncDatasetCore and the remark retargeted at the async surface. Whichever PR lands second resolves to the widened variant.

No issue is closed by this PR.

Tests

  • test/unit/async_dataset_events.test.ts — mirrors feat(events): change notifications on DatasetWrapper (on/off) #94's dataset_events.test.ts on the async fixtures: direct add/delete notifications, effective-change filtering, listener bookkeeping (off, double-subscribe, multiple listeners, unknown-listener off), wrapper-driven events for required/optional/typed properties and set mutations, plus async-specific coverage: an async listener is awaited before the mutation resolves, listeners are dispatched sequentially, and direct mutations of the wrapped dataset do not notify.
  • test/unit/async_wrapping_set_events.test.ts — mirrors feat(events): WrappingSet.on/off with mapped values #93's wrapping_set_events.test.ts: mapped-value add/delete/clear emission (term and literal mappings), subject/predicate filtering, direct dataset mutations, no event on ineffective adds, multiple independent listeners, live re-iteration inside an awaited async listener, off across fresh instances / unknown listeners / per-(subject, predicate) detachment, re-subscription replacement, and the DatasetEventsError path.

npx tsc, npm test (222 tests, 0 fail; new code at 100% line/branch coverage), npx typedoc (same 6-warning baseline as the base branch) and npm audit --omit=dev --audit-level=moderate all pass locally on Node 24.

Review timing: This draft was prepared with Claude; I (@jeswr) will personally review it before it progresses. I'm currently batching a lot of work in flight, so expect active review Wednesday-Friday (8-10 July).

jeswr and others added 4 commits July 6, 2026 04:26
Adds an asynchronous counterpart of DatasetWrapper for datasets whose
contents cannot be read synchronously (disk- or remote-backed stores):

- src/async/AsyncDatasetWrapper.ts implements the proposed RDF/JS
  AsyncDatasetCore interface by delegation (Promise-based size/add/
  delete/has, for-await iteration, lazy match views) and provides the
  same protected query helpers as DatasetWrapper - subjectsOf,
  objectsOf, instancesOf, matchSubjectsOf, matchObjectsOf - returning
  AsyncIterable<T> built from an IAsyncTermWrapperConstructor.
- src/async/type/IAsyncTermWrapperConstructor.ts mirrors
  ITermWrapperConstructor for the asynchronous surface.
- Synchronous DatasetCore sources (instance, promise or lazy thunk)
  are adapted automatically via the @jeswr/async-dataset foundation
  package, so existing sync stores (e.g. an n3 Store) work unchanged.
- Ports the async_dataset_wrapper.test.ts slice with AsyncParent/
  AsyncChild/AsyncParentDataset fixture models.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an asynchronous counterpart of the term-wrapping surface, for
datasets that cannot be accessed synchronously (e.g. disk-backed or
remote stores):

- AsyncTermWrapper, bound to an AsyncDatasetCore from the
  @jeswr/async-dataset foundation package, with the same RDF/JS term
  surface as TermWrapper. Term-identity members stay synchronous;
  everything that touches the dataset returns promises.
- Async mapping namespaces mirroring their synchronous siblings:
  AsyncRequiredFrom, AsyncRequiredAs, AsyncOptionalFrom,
  AsyncOptionalAs, AsyncSetFrom, AsyncLiteralAs and AsyncTermAs.
- AsyncWrappingSet, the live asynchronously iterable set returned by
  AsyncSetFrom.subjectPredicate.
- IAsyncTermAsValueMapping, IAsyncTermFromValueMapping and
  IAsyncTermWrapperConstructor types. Synchronous *From mappers plug
  into the async surface unchanged, since term creation is pure.
- CardinalityError and MappingArgumentError (byte-identical to the
  copies proposed in rdfjs#89) thrown by the async mappings for arity
  violations and undefined mapper arguments.

Tests mirror term_wrapper.test.ts over an n3 store exposed through a
lazily resolved AsyncDatasetCore. All new mapping code is at 100%
coverage except the defensive guards also uncovered in OptionalAs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles the two async slices (rdfjs#97 + rdfjs#98) so change notifications can
build on both:

- IAsyncTermWrapperConstructor: single definition typing the dataset
  parameter as the AsyncDatasetCore interface from @rdfjs/types (as rdfjs#97
  does) rather than the concrete class from @jeswr/async-dataset, so an
  AsyncDatasetWrapper (which implements the interface but is not an
  instance of the class) can be handed to mapping-class constructors by
  the query helpers.
- AsyncTermWrapper/AsyncWrappingSet: type-only imports of
  AsyncDatasetCore switched to the @rdfjs/types interface accordingly;
  no behaviour change.
- Test models: keep the AsyncTermWrapper-based AsyncParent/AsyncChild
  from rdfjs#98 (a superset of the hand-rolled models from rdfjs#97, which only
  needed hasString).
- asyncDatasetFromRdf: union of both versions - reuses datasetFromRdf
  and hands the store to the async dataset as a lazy thunk.
- typedoc.json/mod.ts: union of the link mappings and export lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the async counterpart of the change-notification surface proposed
for the sync side in rdfjs#93/rdfjs#94:

- AsyncDatasetWrapper.on/off subscribe IAsyncDatasetChangeListener
  callbacks to effective mutations performed through the wrapper.
  Listeners may be asynchronous: they are invoked in subscription order,
  a returned promise is awaited before the next listener, and the
  mutation's promise only resolves once every listener has settled.
  Effectiveness is determined with a has() lookup, performed only while
  at least one listener is subscribed.
- AsyncWrappingSet.on/off deliver set-level notifications with the
  mapped JavaScript value, keyed by (listener, subject, predicate) in a
  WeakMap registry so the fresh instance returned by every property
  access is interchangeable with the one on() was called on.
  Datasets that do not notify are rejected with DatasetEventsError.
- Ports the async notification tests from the rdfjs#73 omnibus and mirrors
  the sync event test suites (rdfjs#93/rdfjs#94) onto the async fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeswr jeswr mentioned this pull request Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant