Skip to content

feat(async): AsyncTermWrapper and asynchronous mapping namespaces - #98

Draft
jeswr wants to merge 1 commit into
rdfjs:mainfrom
jeswr:feat/async-term-wrapper
Draft

feat(async): AsyncTermWrapper and asynchronous mapping namespaces#98
jeswr wants to merge 1 commit into
rdfjs:mainfrom
jeswr:feat/async-term-wrapper

Conversation

@jeswr

@jeswr jeswr commented Jul 6, 2026

Copy link
Copy Markdown
Member

What this does

Adds an asynchronous counterpart of the term-wrapping surface, for datasets that cannot be accessed synchronously — for example datasets backed by disk or by remote storage. This is the substrate for interacting with remote datasets (#10) with the same mapping-class idiom the synchronous surface uses.

New public API, exported from mod.ts under an // Async surface banner:

  • AsyncTermWrapper — mirrors TermWrapper, but is bound to an AsyncDatasetCore (from the new @jeswr/async-dataset dependency, see below). Term-identity members (termType, value, equals, …) stay synchronous because they never touch the dataset; everything that reads or writes the dataset goes through the async mapping namespaces and returns promises. Because JavaScript property setters cannot return values, write mappings on async models are conventionally setX(value) methods returning Promise<void> (documented on the class).
  • Async mapping namespaces mirroring their synchronous siblings one-to-one: AsyncRequiredFrom, AsyncRequiredAs, AsyncOptionalFrom, AsyncOptionalAs, AsyncSetFrom, AsyncLiteralAs (the seven primitive mappers: bigint, boolean, date, langString, number, string, symbol) and AsyncTermAs (instance, is, term). Notable async-specific behaviours:
    • AsyncRequiredFrom.subjectPredicate pulls at most two quads from the (potentially lazy) async match view — the first iterator step must yield and the second must complete — instead of materializing the whole match.
    • AsyncOptionalAs.object materializes the existing matches before deleting any of them, so a lazy match view is never consumed while the dataset underneath it is being mutated.
    • Graph semantics are exactly those of the sync mappings (match on subject + predicate, no graph filter).
  • AsyncWrappingSet<T> — the live, asynchronously iterable set returned by AsyncSetFrom.subjectPredicate. Same member shape as the sync Set view with Promise-returning methods and for await iteration. (Exported because, unlike sync SetFrom which can declare Set<T>, there is no built-in asynchronous set type to hide it behind.)
  • IAsyncTermAsValueMapping, IAsyncTermFromValueMapping, IAsyncTermWrapperConstructor — async counterparts of the mapping types. IAsyncTermAsValueMapping may return a value or a promise; IAsyncTermFromValueMapping is intentionally synchronous (term creation is pure), so the existing LiteralFrom/NamedNodeFrom/BlankNodeFrom/TermFrom mappers plug into the async surface unchanged.

The dataset dependency

The async dataset itself is not implemented here: this PR adds @jeswr/async-dataset (github: jeswr/async-dataset#feat/async-dataset) as the library's first runtime dependency. It provides AsyncDatasetCore, a Promise/async-iterable implementation of the proposed RDF/JS AsyncDatasetCore interface (jeswr/types#2) over any synchronous DatasetCore — passed directly, as a promise, or as a lazy thunk that is only invoked on first use (which is what makes remote/disk-backed sources practical). Making that a visible, deliberate decision rather than baking a dataset implementation into this library keeps the wrapper focused on mapping.

Errors

The async mappings throw typed errors from day one: CardinalityError (required mapping found no value / more than one value; carries term, predicate and a found: "none" | "multiple" discriminator) and MappingArgumentError (an undefined termAs/termFrom mapper). These two files are byte-identical to the copies proposed in #89 (which retrofits them onto the sync mappings), and the mod.ts export lines are the same two lines in the same place, so the two branches merge cleanly in either order; if #89 is rejected, the classes still stand on their own here.

Relation to #61 / #73 / #74

Supersedes exactly the async/mapping slice of #73: src/async/AsyncTermWrapper.ts, src/async/mapping/* (AsyncLiteralAs, AsyncOptionalAs, AsyncOptionalFrom, AsyncRequiredAs, AsyncRequiredFrom, AsyncSetFrom, AsyncTermAs), src/async/type/IAsync*.ts, src/async/AsyncWrappingSet.ts (minus its change-event members) and the async_term_wrapper.test.ts + AsyncParent/AsyncChild test models. It deliberately does not take on #73's AsyncDatasetCore/AsyncNotifyingDatasetCore (replaced by the @jeswr/async-dataset dependency), AsyncDatasetWrapper, or the change-notification (on/off) surface — events are a separate concern (#93/#94 for the sync side). No overlap with #61 or #74.

Tests

test/unit/async_term_wrapper.test.ts mirrors term_wrapper.test.ts across the same Turtle fixture — value, term, object, arity, set and recursion mappings, plus the wrapper's RDF/JS term surface, direct AsyncLiteralAs/AsyncTermAs guard tests, CardinalityError property assertions and MappingArgumentError coverage of every async entry point. The n3 store is exposed through a lazily resolved AsyncDatasetCore (thunk source), so the tests genuinely exercise asynchronous resolution. 176 tests, 0 failures; all new code is at 100% line coverage except the three defensive guards in AsyncOptionalAs that are equally uncovered in sync OptionalAs. npx tsc, npm test, npx typedoc (same 6-warning baseline as main) and npm audit --omit=dev all pass.

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).

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>
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