Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 224 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,50 +177,249 @@ RDF/JS Wrapper uses the interfaces described in the [RDF/JS](https://rdf.js.org/

### Named Graphs

The `namedGraph` function creates a `DatasetCore` view over a single named graph, projecting its contents into the default graph. This lets you use any existing `TermWrapper` or `DatasetWrapper` classes unchanged, scoped to a specific graph.
The `GraphScopedDataset` class is a `DatasetWrapper` that exposes one or more named graphs of an underlying dataset projected onto the default graph. Existing `TermWrapper` and `DatasetWrapper` subclasses can be reused unchanged against quads that live in named graphs.

The recommended entry point is `DatasetWrapper.scoped`, which constructs the projection for you from a parent wrapper:

```javascript
import { namedGraph, DatasetWrapper } from "@rdfjs/wrapper"
import { DatasetWrapper, GraphScopedDataset } from "@rdfjs/wrapper"

// Given a dataset with quads in a named graph:
// <ex:person1> <ex:name> "Alice" <ex:graph1> .
// <ex:person2> <ex:name> "Bob" <ex:graph1> .
// <ex:person1> <ex:name> "Charlie" . (default graph)
class People extends GraphScopedDataset {
get all() {
return this.subjectsOf("https://example.org/name", Person)
}
}

const graphView = namedGraph(DataFactory.namedNode("https://example.org/graph1"), dataset, DataFactory)
class Workspace extends DatasetWrapper {
people(graphIri) {
// Read from and write to the same named graph.
return this.scoped(graphIri, [graphIri], People)
}
}
```

// graphView behaves as a DatasetCore containing only default graph quads:
// <ex:person1> <ex:name> "Alice" .
// <ex:person2> <ex:name> "Bob" .
Given the following RDF:

// Wrap it with your existing classes:
class People extends DatasetWrapper {
```turtle
PREFIX ex: <https://example.org/>

GRAPH ex:graph1 {
ex:person1 ex:name "Alice" .
ex:person2 ex:name "Bob" .
}

ex:person1 ex:name "Charlie" . # default graph
```

```javascript
const ws = new Workspace(dataset, DataFactory, datasetFactory)
const team = ws.people("https://example.org/graph1")

for (const p of team.all) {
console.log(p.name)
}
// outputs "Alice", "Bob" (Charlie is excluded — different graph)
```

Writes through the view are mapped back into the configured `writeGraph`:

```javascript
team.add(DataFactory.quad(s, p, o))
// stored in the underlying dataset as:
// DataFactory.quad(s, p, o, DataFactory.namedNode("https://example.org/graph1"))
```

`writeGraph` and `readGraphs` need not be the same. Passing `undefined` for `readGraphs` reads from every graph (default and named) and deduplicates triples across them — useful for read-only union views:

```javascript
class ReadOnlyUnion extends GraphScopedDataset { /* ... */ }
const union = ws.scoped("https://example.org/scratch", undefined, ReadOnlyUnion)
```

Any attempt to use a non-default graph on the projected view throws a `NamedGraphError` (for `add` / `delete` / `has`) or a `TermTypeError` (for `match`):

```javascript
// These all throw:
team.add(DataFactory.quad(s, p, o, DataFactory.namedNode("https://other.org/g"))) // NamedGraphError
team.match(undefined, undefined, undefined, DataFactory.namedNode("https://other.org/g")) // TermTypeError
```


### Change notifications

Every `DatasetWrapper` exposes `on(listener)` / `off(listener)` so consumers can react to additions and removals on the underlying dataset:

```javascript
const ds = new People(dataset, DataFactory, datasetFactory)

const listener = (event, quad) => {
// event is "add" or "delete"
console.log(event, quad.subject.value, quad.predicate.value, quad.object.value)
}
ds.on(listener)
// ...
ds.off(listener)
```

Notifications fire for **every** quad-level mutation, regardless of how it was triggered:

- direct `dataset.add(quad)` / `dataset.delete(quad)`
- a setter on a `TermWrapper` (`person.name = "..."`)
- mutations through a `WrappingSet` returned by `SetFrom`
- mutations through an `RdfList`
- writes made through a `GraphScopedDataset` view (the listener attached to the scoped view receives default-graph quads; the listener attached to the underlying dataset receives the rewritten named-graph quads)

Setters that *change* a value emit a `delete` for the previous quad followed by an `add` for the new quad. Clearing an optional value emits only `delete`. Setting from `undefined` emits only `add`.

#### Set-level notifications

`WrappingSet` (the type returned by `SetFrom.subjectPredicate`) also exposes `on` / `off`. The listener receives the mutation type and the **mapped JavaScript value** for that set's subject + predicate, so callers do not need to filter dataset-wide events themselves:

```javascript
import { SetFrom, TermAs, TermFrom, TermWrapper } from "@rdfjs/wrapper"

class Person extends TermWrapper {
get children() {
return SetFrom.subjectPredicate(this, "https://example.org/hasChild", TermAs.instance(Person), TermFrom.instance)
}
}

const alice = new Person("https://example.org/alice", dataset, DataFactory)

alice.children.on((event, child) => console.log(event, child.value))

alice.children.add(bob) // logs: add, https://example.org/bob
alice.children.delete(bob) // logs: delete, https://example.org/bob
```

The set is a **live view**: iterating `alice.children` always reflects the current state of the dataset, including additions made by other code paths.

`WrappingSet.off(listener)` is keyed by `(listener, subject, predicate)` rather than by instance, so it works correctly even when called on a fresh `WrappingSet` returned by a subsequent property access:

```javascript
alice.children.on(listener)
alice.children.off(listener) // detaches the listener attached above
```


### Async API

The library ships a parallel asynchronous surface for use with RDF/JS-shaped datasets that are themselves asynchronous (or that you want to expose through promises). Every type, class and mapping has a sync sibling and an async sibling; the names are prefixed with `Async`.

The shape of the async dataset interface mirrors RDF/JS `DatasetCore` with one deliberate exception: `match` returns another `AsyncDatasetCore` synchronously (the matched view is materialised lazily on iteration), while every other read/write returns a `Promise`. Iteration is exposed via `Symbol.asyncIterator`; there is no synchronous `Symbol.iterator`.

| Sync | Async |
| ----------------------------------- | ------------------------------------------- |
| `DatasetCore` | `AsyncDatasetCore` |
| `NotifyingDatasetCore` | `AsyncNotifyingDatasetCore` |
| `NotifyingDatasetCoreWrapper` | `AsyncNotifyingDatasetCoreWrapper` |
| `DatasetWrapper` | `AsyncDatasetWrapper` |
| `TermWrapper` | `AsyncTermWrapper` |
| `WrappingSet` | `AsyncWrappingSet` |
| `RequiredFrom` / `OptionalFrom` | `AsyncRequiredFrom` / `AsyncOptionalFrom` |
| `RequiredAs` / `OptionalAs` | `AsyncRequiredAs` / `AsyncOptionalAs` |
| `SetFrom` | `AsyncSetFrom` |
| `TermAs` / `LiteralAs` | `AsyncTermAs` / `AsyncLiteralAs` |
| `LiteralFrom` / `NamedNodeFrom` / `BlankNodeFrom` / `TermFrom` | _reused as-is_ (pure functions) |

#### Defining async wrappers

JavaScript property setters cannot be `async`, so write-mappings on async wrappers are exposed as `setX(value)` methods that return a `Promise`. Read-mappings are normal getters that return a `Promise`; set-mappings return an `AsyncWrappingSet`.

```javascript
import {
AsyncLiteralAs, AsyncOptionalAs, AsyncOptionalFrom,
AsyncRequiredAs, AsyncRequiredFrom,
AsyncSetFrom, AsyncTermAs,
AsyncTermWrapper,
LiteralFrom, TermFrom,
} from "@rdfjs/wrapper"

class AsyncPerson extends AsyncTermWrapper {
get name() {
return AsyncRequiredFrom.subjectPredicate(this, "https://example.org/name", AsyncLiteralAs.string)
}
setName(value) {
return AsyncRequiredAs.object(this, "https://example.org/name", value, LiteralFrom.string)
}

get nickname() {
return AsyncOptionalFrom.subjectPredicate(this, "https://example.org/nickname", AsyncLiteralAs.string)
}
setNickname(value) {
return AsyncOptionalAs.object(this, "https://example.org/nickname", value, LiteralFrom.string)
}

get children() {
return AsyncSetFrom.subjectPredicate(this, "https://example.org/hasChild", AsyncTermAs.instance(AsyncPerson), TermFrom.instance)
}
}
```

Usage:

```javascript
const alice = new AsyncPerson("https://example.org/alice", asyncDataset, DataFactory)

console.log(await alice.name) // "Alice"
await alice.setName("Alicia")
console.log(await alice.name) // "Alicia"

for await (const child of alice.children) {
console.log(await child.name)
}
```

#### Async dataset wrappers

`AsyncDatasetWrapper` is the async counterpart of `DatasetWrapper`. The same `subjectsOf` / `objectsOf` / `instancesOf` / `matchSubjectsOf` / `matchObjectsOf` helpers are available, but they return `AsyncIterable` so callers iterate with `for await`:

```javascript
import { AsyncDatasetWrapper } from "@rdfjs/wrapper"

class People extends AsyncDatasetWrapper {
get all() {
return this.subjectsOf("https://example.org/name", Person)
return this.subjectsOf("https://example.org/name", AsyncPerson)
}
}

const people = new People(graphView, DataFactory)
for (const person of people.all) {
console.log(person.name)
const people = new People(asyncDataset, DataFactory, asyncDatasetFactory)
for await (const person of people.all) {
console.log(await person.name)
}
// outputs "Alice", "Bob" (Charlie is excluded — different graph)

console.log(await people.size) // resolves to a number
```

Writes through the view are mapped back to the named graph in the underlying dataset:
#### Bridging a synchronous dataset

`AsyncNotifyingDatasetCoreWrapper` accepts either an `AsyncDatasetCore` or any synchronous `DatasetCore` (e.g. an n3 `Store`), so existing sync stores can be exposed through the async pipeline without re-implementation:

```javascript
// Adding a quad through the view stores it in the named graph
graphView.add(DataFactory.quad(s, p, o))
// Equivalent to: dataset.add(DataFactory.quad(s, p, o, DataFactory.namedNode("https://example.org/graph1")))
import { AsyncNotifyingDatasetCoreWrapper } from "@rdfjs/wrapper"
import { Store } from "n3"

const store = new Store()
const asyncDataset = new AsyncNotifyingDatasetCoreWrapper(store)

await asyncDataset.add(quad)
console.log(await asyncDataset.size)
```

Any attempt to use a non-default graph on the returned `DatasetCore` throws a `NamedGraphError`:
You can also implement `AsyncNotifyingDatasetCoreFactory` to plug in a genuinely asynchronous backing store (database, remote SPARQL endpoint, etc.).

#### Async change notifications

`AsyncDatasetWrapper` and `AsyncWrappingSet` expose the same `on` / `off` shape as their sync siblings. Listeners may be `async` - the dispatcher awaits a returned promise before invoking the next listener:

```javascript
// These all throw NamedGraphError:
graphView.add(DataFactory.quad(s, p, o, DataFactory.namedNode("https://other.org/g")))
graphView.match(undefined, undefined, undefined, DataFactory.namedNode("https://other.org/g"))
asyncDataset.on(async (event, quad) => {
await sendToAuditLog(event, quad)
})

alice.children.on(async (event, child) => {
console.log(event, await child.name)
})
```


Expand Down
Loading
Loading