feat(async+events): change notifications on the asynchronous surface#99
Draft
jeswr wants to merge 4 commits into
Draft
feat(async+events): change notifications on the asynchronous surface#99jeswr wants to merge 4 commits into
jeswr wants to merge 4 commits into
Conversation
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>
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Completes the asynchronous surface with change notifications, mirroring the sync-side proposals in #93/#94:
AsyncDatasetWrapper.on(listener)/off(listener)— subscribe anIAsyncDatasetChangeListener(new exported type) to every mutation performed through the wrapper: directadd/deletecalls, mutator methods of mapping classes projected out of the wrapper, and mutations of anAsyncWrappingSet. Async-specific semantics:has()lookup before the mutation — performed only while at least one listener is subscribed, so the unobserved fast path is unchanged.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'sWrappingSet.on/off: the same(listener, subject, predicate)-keyedWeakMapadapter registry, sooffworks across the fresh instancesAsyncSetFrom.subjectPredicatereturns 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 typedDatasetEventsError.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:IAsyncTermWrapperConstructortypes its dataset parameter as theAsyncDatasetCoreinterface from@rdfjs/types(as feat(async): AsyncDatasetWrapper query helpers #97 does) rather than the concrete class from@jeswr/async-dataset(as feat(async): AsyncTermWrapper and asynchronous mapping namespaces #98 did), because feat(async): AsyncDatasetWrapper query helpers #97's query helpers hand theAsyncDatasetWrapperitself to mapping-class constructors, and the wrapper implements the interface but is not an instance of the class.AsyncTermWrapperandAsyncWrappingSettype-only imports follow suit; no behaviour change.AsyncTermWrapper-basedAsyncParent/AsyncChild(a superset of feat(async): AsyncDatasetWrapper query helpers #97's hand-rolled ones);asyncDatasetFromRdfkeeps feat(async): AsyncTermWrapper and asynchronous mapping namespaces #98's lazy-thunk source while reusingdatasetFromRdf;typedoc.json/mod.tstake the union.Once #97 and #98 land, this PR rebases down to the one feature commit.
Relation to #73 / #93 / #94
AsyncNotifyingDatasetCoreconcern,AsyncWrappingSet'son/off/adapter registry, and the async event tests (ported intoasync_wrapping_set_events.test.ts/async_dataset_events.test.ts). Two deliberate deviations from the omnibus:AsyncDatasetWrapperitself rather than in a separateAsyncNotifyingDatasetCorewrapper class: the foundation async dataset deliberately stays event-free, every wrapper-driven mutation already flows throughAsyncDatasetWrapper.add/delete, andAsyncWrappingSetreaches the notifying surface throughAsyncTermWrapper.dataset— so interception at the wrapper delivers the same behaviour with much less machinery.ChangeEventtype export:IAsyncDatasetChangeListenerandAsyncWrappingSetListenerinline the"add" | "delete"union (as feat(events): WrappingSet.on/off with mapped values #93'sWrappingSetListenerdoes). If this PR exported its ownChangeEventalongside feat(events): change notifications on DatasetWrapper (on/off) #94's, the two star re-exports inmod.tswould make the name ambiguous and silently drop it from the public surface.src/errors/DatasetEventsError.tsoverlaps with feat(events): WrappingSet.on/off with mapped values #93: it is feat(events): WrappingSet.on/off with mapped values #93's class with thedatasetparameter widened toDatasetCore | AsyncDatasetCoreand 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'sdataset_events.test.tson 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'swrapping_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,offacross fresh instances / unknown listeners / per-(subject, predicate) detachment, re-subscription replacement, and theDatasetEventsErrorpath.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) andnpm audit --omit=dev --audit-level=moderateall pass locally on Node 24.