TS ORM SDK

Reactive ORM library for NextGraph: use (typed) objects that automatically sync to NextGraph’s encrypted, local-first storage.

For a walk-through you can see the expense-tracker example apps for JSON documents or typed graph documents.

Note that there are two variants of the SDK:

  • The RDF ORM for working with RDF (graph) data (good for interoperability, cross-document data, evolving schemas)
  • The discrete ORM for working with discrete (single-document), JSON-based CRDTs: Automerge & YJS currently supported (you need to enforce the schema in the code yourself)

The SDK is reactive. Modifications to your received “plain old TypeScript objects” are instantly synced with the database and other devices.
Vice versa, when the data is modified on a different device, that is reflected in your TS object and your frontend rerenders the data.
We offer frontend framework support for React, Vue, and Svelte (5 and 4) but you can use the SDK without a frontend framework as well.

Table of Contents


Example Apps

Before writing your own app, you are strongly advised to look at the example apps below, where you can find walkthroughs for different framework and crdt-specific walkthroughs.

The app looks the same in all implementations. You can see that the useShape() and useDiscrete() frontend hooks that retrieve the data share the same syntax across all frameworks.

Installation

npm install @ng-org/orm @ng-org/web

For schema generation for the RDF ORM, also install:

npm install --save-dev @ng-org/shex-orm

Initializing NextGraph in Your App

Before using the ORM, initialize NextGraph in your app entry point:

import { ng, init } from "@ng-org/web";
import { initNg } from "@ng-org/orm";

// Call init as early as possible when your app loads.
// At the first call, it will redirect the user to login with their wallet.
// In that case, there is no need to render the rest of the app.
// When the wallet is opened, your app will start in an iframe.
// The call to init then connects the interface to the engine.
await init(
  async (event) => {
    // The ORM needs to have access to ng,
    // the interface to the engine running in WASM.
    initNg(ng, event.session);
  },
  true,
  [],
);

RDF (Graph) ORM

The ORM is designed to make working with RDF as “normal” as possible. You get an object as you are used to it and when you change properties, they are automatically persisted and synced with other devices. Conversely, modifications coming from other devices update the ORM objects too and your frontend components refresh.

About RDF

RDF (Resource Description Framework) is a standard to describe data. Rather than organizing data as tables (e.g. SQL) or trees (e.g. JSON), RDF represents data as a non-hierarchical, unstructured set of triples (a graph aka network).

Each triple consists of a subject (about which you are describing something), a predicate (the property of the relationship, e.g. the first name), and an object (the value of that property or a reference). Triples belong to documents, also called graphs in the context of RDF. Together with a graph, triples become quads. Subjects, predicates and graphs are all IRIs (the generalization of URLs). There are many specifications for describing data, to aid application interoperability.

A NextGraph-related IRI is called NURI: “NextGraph URI”. Auto-generated subjects (the @id) and graphs are such NURIs. Every NextGraph document ID is a graph NURI.

RDF’s flexible and schema-less design aids in schema-evolution, interoperability, and data relationships. You are advised to take a moment to get yourself familiar with RDF if you are new to it.

To work with RDF in applications and bring structure to it, we need to define schemas to query the data below.

Creating an RDF Document

First, you need a document to store and get your data. With the document ID (NURI), you can then create ORM objects.

// Create a new NextGraph document
const docNuri = await ng.doc_create(
  session_id,
  "Graph",
  "data:graph",
  "store",
  undefined,
);

const APPLICATION_CLASS_IRI = "did:ng:z:MyApplication";

// Add a class to the document so we can find it again.
await ng.sparql_update(
  session_id,
  `INSERT DATA { GRAPH <${documentId}> {<${documentId}> a <${APPLICATION_CLASS_IRI}> } }`,
  documentId,
);

To later find your document NURI, you make a sparql query:

const ret = await ng.sparql_query(
  session_id,
  `SELECT ?storeId WHERE { GRAPH ?storeId { ?s a <${APPLICATION_CLASS_IRI}> } }`,
  undefined,
  undefined,
);
const documentId = ret?.results.bindings?.[0]?.storeId?.value;

Defining a Schema

In order to work with typed data, you need to define a SHEX schema. The schema defines the properties that the orm objects have and how they map to RDF.

You create those schemas with the help of @ng-org/shex-orm, as documented here.

When you followed the steps there, you will have generated so-called ShapeTypes, one for each schema. ShapeTypes contain the typescript type definitions as well as the schemas. Whenever you call a method to retrieve ORM data, you pass it the ShapeType. The details are described below.

Using and Modifying RDF ORM Objects

To retrieve your data, you need to create an RdfOrmSubscription or use a higher-level function. The RdfOrmSubscription.getOrCreate(shapeType, conf) function receives a ShapeType and config (scope, ordering, pagination, …) and loads the data and keeps it in sync.

The data that you will receive is a reactive (DeepSignal) set or array. If you specified no ordering, it will be a set, otherwise a read-only array. To sets, you are allowed to add and remove items. Because order is managed by the subscription, you are not allowed to make modifications affecting adds, moves, or removes.

There are multiple ways to create a subscription and get the data (you will see examples for them in the next sections):

Frontend Framework Integration: useShape()

The SDK offers useShape(ShapeType, config) hooks that let you load and interact with data inside of components. Implementations are available for Svelte 5, Svelte 4, Vue, and React.

The hooks create a 2-way binding between the engine and the frontend. You can modify the data returned by the hook like any other object. Changes are immediately reflected in the database. When data used inside a component changed, the component rerenders (thanks to the useDeepSignal hooks). When the component unmounts, the subscription is closed.

The returned data object is identical to the subscription’s .signalObject.
The second parameter, the config has the same type as the config you pass to RdfOrmSubscription.getOrCreate(shapeType, conf).

You can find more detailed descriptions of the parameters and return types in the inline-comments or the reference of the respective useShape implementation.

The following example loads the expenses with the subject IRI (@id) <s1 IRI> and <s2 IRI> in the documents did:ng:o:g1 and did:ng:o:g2.

const {
  data, // The `subscription.signalObject`, once loaded.
  promise, // Resolves and returns `data`, once loaded.
  subscription, // The underlying RdfOrmSubscription managing the data.
  isLoading, // True while the data is loading (usually very short).
} = useShape(ExpenseShapeType, {
  graphs: ["did:ng:o:g1", "did:ng:o:g2"],
  subjects: ["<s1 IRI>", "<s2 IRI>"],
  orderBy: [{ price: "asc" }], // One or more properties to order by.
  pageSize: undefined, // No pagination.
  maxActivePages: 0, // In case of pagination, how many to hold loaded at once (0 = no limit).
});
// When orderBy is `undefined`, the returned data has type:
// `DeepSignal<Set<Expense>>`
// Otherwise: `DeepSignal<ReadonlyArray<Expense>>`.

// Now you can use the data in your component
// and modify it, to persist it and trigger a refresh.

Using @id as key attribute for child components

In general, you are encouraged to use ORM object’s @id properties as unique key when you render child components. Each object in a set or array returned by useShape or useDiscrete includes such an @id property. When you add a new object, a globally unique one will be auto-generated.

In the RDF ORM, the @id is the RDF subject IRI. You are allowed (but not encouraged) to set your own @id. If you want to use it as key for rendering child components, ensure that it is unique within your scope.

Scopes for Retrieving Data

The RDF ORM lets you retrieve data across different documents using the graphs parameter in the config, as you can see in the example above.

If you want to query across all datasets, use the following Nuri: "did:ng:i" or simply use "".

When you specify one or more subject IRIs in the config, only those subject will be considered for your request (those will be queried across all graphs specified). Because not all objects with the specified subject IRIs might match the shape you provided, some returned objects might be missing from the subject IRIs of your request.

Relationships

To reference external objects, you can use their @id (the RDF subject IRI).

// Note that jackIri is the `@id` (subject IRI) of an object that describes Jack.
const jackIri = ...;

casey.friends.add(jackIri);

// When the child object is a nested object that you do not have in memory,
// you can establish the link by adding an object that contains the `@id` property only.
shoppingExpense.category.add({ "@id": "<Subject IRI of expense category>" });

// If the property has cardinality 1, set it like this:
dog.owner = jackIri;

// Resolve the relationship
const jack = people.find((p) => p["@id"] === dog.owner);

Note that when you delete a nested object from a parent, only the linkage to it is removed. The nested object itself (its quads) are not deleted.

Note that it is highly recommended to keep subject IRIs globally unique. This is not a requirement by RDF and there are certain use cases where it makes sense but generally, you are discouraged to do so. When you create a new object, you are not required to specify the subject IRI (leave the @id property undefined or ""). In that case, the subscription generates a unique one. The @id is generated while you attach a new object to the subscription’s data so you can use it immediately after that.

Ordering

With the RDF ORM, you can specify an orderBy property in the config objects passed to getObjects(), useShape(), or RdfOrmSubscription.getOrCreate().

When orderBy is set, the returned data is not a set but an array.

const contactsSubscription = RdfOrmSubscription.getOrCreate(ContactShape, {
  graphs: [contactDocNuri],
  orderBy: [
    // The key is the property name defined in the schema,
    // the value "asc" for ascending order or "desc" for descending order.
    { lastName: "asc" },
    // You can add secondary orderBy values in the array.
    { firstName: "asc" },
    { birthDate: "desc" },
  ],
});
await contactsSubscription.readyPromise;

const contacts: DeepSignal<ReadOnlyArray<Contact>> =
  contactsSubscription.signalObject;

console.log(
  "I have the following contacts in my document, ordered by last name, first name and birth date:",
);
for (contact of contacts) {
  console.log(contact);
}

Note that you cannot add, move, or remove items in the returned array. This logic is maintained internally. You can however change the items themselves. If you want to add an item, you can call insertObject() instead which will make the item appear in the array (unless in simple pagination mode, see below). Use removeObject() for removing an object. If you want to modify the position, just modify the properties that the data is ordered by and it will update itself.

If two objects have the same oderBy value, the ordering will be decided by their @graph and secondly their @id, sorted alphabetically.

Pagination / Infinite Scroll

As your dataset grows, loading all items of a ShapeType becomes computationally expensive. For that case, you are advised to use pagination. As a prerequisit, you must specify an ordering as described above.

Currently we do not support “classical pagination” by page numbers. Similar to tanstack’s useInfiniteQuery, you can only load the next page (and depending on the mode, the previous page).

When pagination is configured, you can call nextPage() and previousPage() on your RdfOrmSubscription object or the object returned by useShape().

There are two parameters, to configure pagination:

  • pageSize (mandatory): The number of items to load at once
  • maxActivePages (optional): If specified, the number of pages to keep loaded

From that, two modes of pagination arise:

  • “cumulative pagination”: Once an item was loaded, it remains loaded. maxActivePages is not set and you can only call nextPage(). On a nextPage() call, the data array is extended to include the new elements. When an item within the loaded range becomes valid, it will appear at the correct position. When you implement an infinite scroll and expect a lot of data to be iterated though, keeping all items loaded might become computationally heavy. In that cases, you can use “simple pagination” instead.
    In the RdfOrmSubscription class, the property mode will be set to "orderedPaginatedCumulative" under this configuration.

  • ”simple pagination”: The initial data you will see is an array with as many items as was set in pageSize. When you call nextPage() and previousPage(), previously loaded items in the array are removed.

    You must set maxActivePages to a value greater than 0. If you set it to greater than 1, requesting the next page will not immediately remove the existing items in the array. Instead, items will be removed only when the loaded items exceed maxActivePages Ă— pageSize. Because the items are ordered that means that when nextPage() is called, the first pageSize items are removed from the array; when previousPage() is called, the last pageSize items are removed. When you implement infinite scroll, you are recommended to set higher values for maxActivePages so that not all items are replaced at the same time.

    Note that if an item becomes invalid, it will be removed from the loaded items. If however an item becomes valid that would fit in the current window by its ordering, it will not appear. Your page can shrink but not grow in size. As long as its ordering changes within the page bounds, it remains and changes positions. In the RdfOrmSubscription class, the property mode will be set to "orderedPaginatedSimple" under this configuration.

Simple example in React:

const { data, nextPage, previousPage } = useShape(ExpenseShapeType, {
    graphs: [docNuri],
    orderBy: {dateOfPurchase: "desc"}
    pageSize: 15,
    maxActivePages: 4,
});

return (
    <div>
        <label>Expenses</label>
        <div>
            {data?.map((expense) =>
                <Expense key={expense['@id']} expense={expense} />
            )}
        </div>

        <button onClick={previousPage}>Load previous</button>
        <button onClick={nextPage}>Load more</button>
    </div>
);

Some Remarks on Pagination

nextPage() and previousPage() are not async function. And there is no direct way to know if the data has loaded or not. Since you are in a local-first context though, the delay for loading new items is negligible.

When there are no (more) loadable items, calling nextPage() and previousPage() has no effect.

When an item is deleted or becomes invalid, it is removed from the page without a new item being loaded. That means, the array containing the items can even become empty.

In the simple pagination mode, when items become valid or invalid that are below the currently loaded items (previously loaded but removed after nextPage() call), that does not affect the loaded items. If there are a lot of changes, it can happen though that nextPage() and previousPage() load “the wrong” elements. That means that there might be items skipped between the previously and the newly loaded page.

When the order of an item changes to the last position of the page (or in case of orderedPaginatedSimple also the first), it will disappear. It is not deleted but untracked because it can’t be checked if it actually moved to just the position at the end/beginning of the page or beyond that.

Filtering

When you specified a shape but only want to query a certain subset of items, you can specify the where config, to filter by one or more values.

const colleaguesInParisOrBerlinSubscription = RdfOrmSubscription.getOrCreate(ContactShape, {
    graphs: [contactDocNuri],
    where: {
        affiliation: "colleague"
        location: {
            city: ["Paris", "Berlin"]
        }
    }
});

Internally, this is equivalent to modifying the SHEX shape of ContactShape, marking the affiliation and city predicate as EXTRA, and setting the allowed literal values "colleague" and "city", respectively. That means that a colleague that is based in Paris but has the affiliation "friend" too, will be loaded as well.

The SHEX equivalent after applying the where filter:

ex:ContactShape EXTRA ex:affiliation {
    ex:affiliation [ "colleague" ] * ;
    # ... rest of shape
}
ex:LocationShape EXTRA ex:city {
    ex:city [ "Paris" "Berlin" ] ;
    # ... rest of shape
}

The RdfOrmSubscription Class

In many cases, it is enough to use insertObject(), getObjects(), and deleteObject() or the useShape() hook inside of a component. You can however establish a subscriptions outside of frontend components using the RdfOrmSubscription class directly using RdfOrmSubscription.getOrCreate() which returns an instance of the class. Once the subscription is fully established, its .readyPromise resolves and the .signalObject contains the 2-way bound data (before that, signalObject is an empty object).

If a subscription with the same document or scope (and no pagination) exists already, a reference to that object is returned. Otherwise, a new one is created. This pooling is especially useful when more than one frontend component subscribes to the same data and scope by calling useShape() or useDiscrete(). This reduces load and the data updates even quicker.

Subscriptions are open until .close() is called on all references of this object. The useShape hook calls .close() on their reference when their component unmounts. For data that you use frequently throughout the lifetime of your application, you can create a globally available subscription. You can then use useDeepSignal on the signalObject of the subscription.

Example:

const dogSubscription = RdfOrmSubscription.getOrCreate(DogShape, {
  graphs: [docNuri],
});
await dogSubscription.readyPromise;

const dogSet: DeepSignal<Set<Dog>> = dogSubscription.signalObject;

dogs.add({
  // Required: The document NURI. May be set to `""` for nested objects (will be inherited from parent object then).
  "@graph": docNuri,
  "@type": "did:ng:z:Dog", // Required: RDF type
  "@id": "", // Empty string = auto-generate subject IRI
  name: "Mr Puppy",
  age: 2,
  toys: new Set(["ball", "rope"]),
});

// When you know that only one element is in the set, you can call `.first()` to get it.
const aDog = dogs.first();
aDog.age += 1;
aDog.toy.add("bone");

// Utility to find objects in sets:
const sameDog = dogs.getBy(aDog["@graph"], aDog["@id"]);
// sameDog === aDog.

// Attention: This deletes all triples in dog's document where the subject is that of `aDog`.
// Not only the triples with predicates that are available in the loaded data.
dogs.delete(aDog);

“Disappearing” Objects

It might happen that an object is modified in a way that makes it invalid for the ShapeType it was loaded in. Apart from external modifications, this can happen when the schema specified cardinality constraints that are not expressible in TypeScript, e.g. less than 10 and greater than 5. When that happens, the object disappears, i.e. is removed from the loaded data. The underlying triples are not gone though.

Discrete (JSON-based) ORM

Creating an Automerge or YJS Document

First, you need a document to store and get your data. With the document ID (NURI), you can then create ORM objects.

// Create a new NextGraph document
const docNuri = await ng.doc_create(
  session_id,
  "YMap", // Or "Automerge",
  "data:map", // Or "data:json" in case of Automerge
  "store",
  undefined,
);

const APPLICATION_CLASS_IRI = "did:ng:z:MyApplicationWithYjs";

// Add a class to the RDF part of the document so we can find it again.
// Note: Every type of document can additionally store RDF data.
await ng.sparql_update(
  session_id,
  `INSERT DATA { GRAPH <${documentId}> {<${documentId}> a <${APPLICATION_CLASS_IRI}> } }`,
  documentId,
);

To find your document NURI, you make a sparql query:

const ret = await ng.sparql_query(
  session_id,
  `SELECT ?storeId WHERE { GRAPH ?storeId { ?s a <${APPLICATION_CLASS_IRI}> } }`,
  undefined,
  undefined,
);
let documentId = ret?.results.bindings?.[0]?.storeId?.value;

The DiscreteOrmSubscription Class

You can establish subscriptions outside of frontend components using the DiscreteOrmSubscription class. DiscreteOrmSubscriptions are scoped to one document. Once a subscription is established, its .readyPromise resolves and the .signalObject contains the 2-way bound data (before this, signalObject is an empty object or array).

You can create a new subscription using DiscreteOrmSubscription.getOrCreate(). If a subscription with the same document or scope exists already, a reference to that object is returned. Otherwise, a new one is created. This pooling is especially useful when more than one frontend component subscribes to the same data and scope by calling useDiscrete(). This reduces load and the data is available instantly.

Subscriptions are open until .close() is called on all references of this object. The useDiscrete hook calls .close() on their reference when their component unmounts. For data that you use frequently throughout the lifetime of your application, you can create a globally available subscription. You can then use useDeepSignal on the signalObject of the subscription.

Using @id as Unique Object Identifier in Arrays

In root arrays and in arrays of root objects, each object in the array has a unique @id property. If you attach a new object, the @id will be auto-generated. You cannot choose the @id yourself.

You can use the @id property as a unique value as the key attribute in your frontend framework, for rendering arrays. Note that when you add a new array, at first, a temporary ID is set which is then replaced with a permanent one assigned by the engine asynchronously. Once assigned by the engine, the @id property is globally unique and stable. So it can also be useful to refer to objects in arrays of different locations (rather than by index).

Transactions

You can start transactions with RDF and Discrete ORM subscriptions using .beginTransaction() and .commitTransaction() that both classes provide. This will delay the persistence until .commitTransaction() is called. Transactions do not affect updates to the frontend and incoming updates from the engine / other devices. When more than one reference to a subscription exists, the transaction affects all of them.

Note that even in non-transaction mode, changes are batched and only committed after the current task finished. The changes are sent to the engine in a microtask.

Reactive Objects: The DeepSignal<> Type

Data returned by the ORM is of type DeepSignal<T>. It behaves like plain objects of type T but with some extras. Under the hood, the object is proxied. The proxy tracks modifications and will immediately update the frontend and propagate the changes to the engine.

In your code however, you do not have to to wrap your type definitions in DeepSignal<>. Nevertheless, it can be instructive for TypeScript to show you the additional utilities that DeepSignal objects expose. Also, it might keep you aware that modifications you make to those objects are persisted and that they update the frontend. The utilities that DeepSignal objects include are:

  • For sets (with the RDF ORM), you have the following extra features:
    • iterator helper methods (e.g. map(), filter(), reduce(), any(), …)
    • first() to get one element from the set — useful if you know that there is only one.
    • getBy(graphNuri: string, subjectIri: string), to find objects by their graph (document) NURI and subject IRI.
    • NOTE: When assigning a set to DeepSignal<Set>, TypeScript will warn you. You can safely ignore this by writing (parent.children = new Set() as DeepSignal<Set<any>>). Internally, the set is automatically converted but this is not expressible in TypeScript.
  • For all objects: RAW_KEY which gives you the non-proxied object without tracking value access and without triggering updates upon modifications. Tracking value access is used in the frontend so it knows on what changes to refresh. Modifying the raw object is not reactive. This is an advanced feature with limited use cases (for example when you want to clone the object). Modifying the raw object can cause the object to get out of sync.

Signal Objects in Frontend Frameworks

Note that you can use the reactive signal object of an orm subscription (e.g. myOrmSubscription.signalObject) in components too. For that, you need to use useDeepSignal(signalObject) from the package @ng-org/alien-deepsignals/svelte|vue|react. This can be useful to keep a connection open over the lifetime of a component and to avoid the delay when creating new subscriptions.


Reference

Classes

DiscreteOrmSubscription

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:45

Class for managing RDF-based ORM subscriptions with the engine.

You have two options on how to interact with the ORM:

Type Parameters

T

T = DiscreteRoot

Properties

documentId

readonly documentId: string

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:50

The document ID (NURI) of the subscribed document.

isReady

isReady: boolean

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:224

Returns true if the subscription is fully established and the data is available in signalObject

Accessors

inTransaction
Get Signature

get inTransaction(): boolean

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:216

True, if a transaction is running.

Returns

boolean

readyPromise
Get Signature

get readyPromise(): Promise<DeepSignal<T>>

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:220

Await to ensure that the subscription is established and the data arrived. Resolves to signalObject.

Returns

Promise<DeepSignal<T>>

signalObject
Get Signature

get signalObject(): DeepSignal<T> | undefined

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:115

The signalObject containing all data of the document (once subscription is established). The object behaves like a regular object or array with a couple of additions:

  • Modifications are immediately propagated back to the database.
  • Database changes are immediately reflected in the object.
  • Watch for object changes using watchDeepSignal.
  • Objects in arrays receive a unique @id property.
Returns

DeepSignal<T> | undefined

Methods

beginTransaction()

beginTransaction(): void

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:332

Begins a transaction that batches changes to be committed to the database. This is useful for performance reasons.

Note that this does not disable reactivity of the signalObject. Modifications keep being rendered instantly.

Returns

void

close()

close(): void

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:236

Stop the subscription.

If there is more than one subscription with the document ID, the orm subscription won’t close yet.

Additionally, the closing of the subscription is delayed by a couple hundred milliseconds so that when frontend frameworks unmount and soon mount a component again with the same document ID, we reuse the same orm subscription.

Returns

void

commitTransaction()

commitTransaction(): Promise<void>

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:341

Commits a transactions sending all modifications made during the transaction (started with beginTransaction) to the database.

Returns

Promise<void>

Throws

if no transaction is open.

getOrCreate()

static getOrCreate<T>(documentId): DiscreteOrmSubscription<T>

Defined in: sdk/js/orm/src/connector/DiscreteOrmSubscription.ts:197

Returns an OrmSubscription which subscribes to the given document in a 2-way binding.

You find the document data in the signalObject, once readyPromise resolves. This is a DeepSignal object or array, depending on your CRDT document (e.g. YArray vs YMap). The signalObject behaves like a regular set to the outside but has a couple of additional features:

  • Modifications are propagated back to the document. Note that multiple immediate modifications in the same task, e.g. obj[0] = "foo"; obj[1] = "bar" are batched together and sent in a subsequent microtask.
  • External document changes are immediately reflected in the object.
  • Watch for object changes using watchDeepSignal.

You can use transactions, to prevent excessive calls to the engine with beginTransaction and commitTransaction.

In many cases, you are advised to use a hook for your favorite framework under @ng-org/orm/react|vue|svelte instead of calling getOrCreate directly.

Call `close, to close the subscription.

Note: If another call to getOrCreate was previously made and close was not called on it (or only shortly after), it will return the same OrmSubscription (pooling).

Type Parameters
T

T

Parameters
documentId

string

The document ID (NURI) of the CRDT

Returns

DiscreteOrmSubscription<T>

Example
// We assume you have created a CRDT document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
//     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
//     "store",
//     undefined
// );
const subscription = DiscreteOrmSubscription.getOrCreate(documentId);
// Wait for data.
await subscription.readyPromise;

const document = subscription.signalObject;
if (!document.expenses) {
  document.expenses = [];
}
document.expenses.push({
  name: "New Expense name",
  description: "Expense description",
});

// Await promise to run the below code in a new task.
// That will have push the changes to the database.
await Promise.resolve();

// Here, the expense modifications have been committed
// (unless you had previously called subscription.beginTransaction()).
// The data is available in subscriptions running on a different device too.

subscription.close();

// If you create a new subscription with the same document within a couple of 100ms,
// The subscription hasn't been closed and the old one is returned so that the data
// is available instantly. This is especially useful in the context of unmounting and remounting frontend frameworks.
const subscription2 = DiscreteOrmSubscription.getOrCreate(documentId);

subscription2.signalObject.expenses.push({
  name: "Second expense",
  description: "Second description",
});

subscription2.close();

RdfOrmSubscription

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:54

Class for managing RDF-based ORM subscriptions with the engine.

You have two options on how to interact with the ORM:

Type Parameters

ST

ST extends ShapeType<any>

CONF

CONF extends RdfOrmConfig<T>

T

T extends BaseType = ST extends ShapeType<infer T_> ? T_ : never

SUBSCRIPTION_DATA

SUBSCRIPTION_DATA = SubscriptionData<T, CONF>

Properties

mode

readonly mode: "unordered" | "orderedPaginatedCumulative" | "orderedPaginatedSimple" | "orderedUnpaginated"

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:83

The ordering mode which depends on the passed subscription’s OrderByConfig.

  • unordered: The root object is a set (no orderBy config set)
  • orderedUnpaginated: orderBy is set but pageSize not -> signalObject is an array of all items matching the shape, scope, and where config..
  • orderedPaginatedSimple: orderBy, pageSize, and maxActivePages are set -> signalObject is an array but only contains the items of the loaded pages. Pages will be removed from signalObject when more pages are loaded than maxActivePages allows. You can call nextPage and previousPage to navigate.
  • orderedPaginatedCumulative: orderBy and pageSize is set but maxActivePages not -> signalObject is an array but only contains all loaded pages so far. You can call nextPage but not previousPage.
scope

readonly scope: Scope

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:69

The Scope of the subscription.

shapeType

readonly shapeType: ShapeType<T>

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:67

The shape type that is subscribed to.

Accessors

inTransaction
Get Signature

get inTransaction(): boolean

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:365

True, if a transaction is active.

Returns

boolean

isReady
Get Signature

get isReady(): boolean

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:374

Returns

boolean

readyPromise
Get Signature

get readyPromise(): Promise<SUBSCRIPTION_DATA>

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:369

Await to ensure that the subscription is established and the data arrived.

Returns

Promise<SUBSCRIPTION_DATA>

signalObject
Get Signature

get signalObject(): SUBSCRIPTION_DATA

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:107

The signalObject containing all data matching the shape and scope (once subscription is established). Depending on the orderBy config, the object is a set or an array. See mode for more details.

This object is a reactive DeepSignal object. To the outside behaves like a regular object but has a couple of additional features:

  • Modifications are immediately propagated back to the database.
  • Database changes are immediately reflected in the object.
  • .getBy(graphIri, subjectIri) utility for quicker access to objects in sets.
  • .first() utility to get the first element added to the set.
  • the iterator utilities, e.g. .map(), .filter(), …
  • Watch for object changes using watchDeepSignal.
  • Use can use them in effect and computed.
  • When used in the frontend with useShape(), modifications trigger rerenders.
Returns

SUBSCRIPTION_DATA

Methods

addChangeListener()

addChangeListener(listener): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:407

Parameters
listener

OrmChangeListener<T>

Returns

void

beginTransaction()

beginTransaction(): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:642

Begins a transaction that batches changes to be committed to the database. This is useful for performance reasons.

Note that this does not disable reactivity of the signalObject. Modifications keep being rendered. If in need, use structuredClone on the raw object instead.

If already in a transaction, this has no effect.

Returns

void

close()

close(): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:388

Stop the subscription.

If there is more than one subscription with the same shape type and scope, and no pagination, the orm subscription will persist.

Additionally, the closing of the subscription is delayed by a couple hundred milliseconds so that when frontend frameworks unmount and soon mount a component again with the same shape type and scope, we reuse the same orm subscription.

Returns

void

commitTransaction()

commitTransaction(): Promise<void>

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:650

Commits a transactions sending all modifications made during the transaction (started with beginTransaction) to the database.

Returns

Promise<void>

nextPage()

nextPage(): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:683

Loads the next page of items. If options.maxActivePages is set and the number of items exceeds the allowed one (pageSize Ă— maxActivePages), the left-most items will be removed from the signalObject array. If no more elements are there to be loaded, nothing happens.

Only available when orderBy and pageSize were set in the options of getOrCreate.

Returns

void

previousPage()

previousPage(): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:699

Loads the previous page of items. This only has an effect if there were items previously loaded and dropped because more items were loaded than allowed (configured through pageSize Ă— maxActivePages).

Calling this function will have the effect that the right-most items are dropped.

Only available when orderBy, pageSize, and maxActivePages were set in the options of getOrCreate.

Returns

void

removeChangeListener()

removeChangeListener(listener): void

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:410

Parameters
listener

OrmChangeListener<T>

Returns

void

getOrCreate()

static getOrCreate<ST, T, CONF>(shapeType, conf): RdfOrmSubscriptionFor<ST, CONF, T>

Defined in: sdk/js/orm/src/connector/RdfOrmSubscription.ts:316

Returns an RdfOrmSubscription which subscribes to the given ShapeType and Scope in a 2-way binding.

You find the data and objects matching the shape and scope in the signalObject once readyPromise resolves.

The signalObject is either an array if you specify an orderBy in the options or a set otherwise.

To the outside, the signalObject behaves like a regular Set or Array but it has a couple of additional properties:

  • Modifications are propagated back to the database. Note that multiple immediate modifications in the same task, e.g. obj[0] = "foo"; obj[1] = "bar" are batched together and sent in a subsequent microtask.
  • Database changes are immediately reflected in the object.
  • .getBy(graphIri, subjectIri) utility for quicker access to objects in set.
  • .first() utility to get the first element added to the set (useful when you know that it is only one).
  • The iterator utilities, e.g. .map(), .filter(), …
  • Use the object with alien-deepsignal functions like effect, computed, or watchDeepSignal.

You can (and should) use transactions, to prevent excessive calls to the database with beginTransaction and commitTransaction.

In many cases, you are advised to use a hook for your favorite framework under @ng-org/orm/react|vue|svelte instead of calling getOrCreate directly.

Call close, to close the subscription.

Note: If another call to getOrCreate was previously made and close was not called on it (or only shortly after), it will return the same RdfOrmSubscription.

Type Parameters
ST

ST extends ShapeType<any>

T

T extends BaseType

CONF

CONF extends RdfOrmConfig<T>

Parameters
shapeType

ShapeType<T>

The ShapeType

conf

CONF

The RdfOrmConfig.

Returns

RdfOrmSubscriptionFor<ST, CONF, T>

Example
// We assume you have created a graph document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     "Graph",
//     "data:graph",
//     "store",
//     undefined
// );
const subscription = RdfOrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [documentId]});
// Wait for data.
await subscription.readyPromise;

const expense = subscription.signalObject.first()
expense.name = "updated name";
expense.description = "updated description";

// Await promise to run the below code in a new task.
// That will push the changes to the database.
await Promise.resolve();

// Here, the expense modifications have been have been committed
// (unless you had previously called subscription.beginTransaction()).
// The data is available in subscriptions running on a different device too.

subscription.close();
// If you create a new subscription with the same document within a couple of 100ms,
// The subscription hasn't been closed and the old one is returned so that the data
// is available instantly. This is especially useful in the context of frontend frameworks.
const subscription2 = RdfOrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [documentId]});

subscription2.signalObject.add({
   "@graph": documentId,
   "@id": "", // Leave empty to auto-assign one.
   name": "A new expense",
   description: "A new description"
});

subscription2.close()

Interfaces

DiscreteObject

Defined in: sdk/js/orm/src/types.ts:78

An allowed object in the CRDT.

Indexable

[key: string]: DiscreteType


DiscreteRootObject

Defined in: sdk/js/orm/src/types.ts:104

The root object for reading and modifying the CRDT as a plain object.

Indexable

[key: string]: string | number | boolean | DiscreteObject | DiscreteRootArray

Type Aliases

DeepSignal

DeepSignal<T> = T extends Function ? T : T extends string | number | boolean ? T : T extends DeepSignalObjectProps<any> | DeepSignalObjectProps<any>[] ? T : T extends infer I[] ? DeepSignal<I>[] : T extends Set<infer S> ? DeepSignalSet<S> : T extends object ? DeepSignalObject<T> : T

Defined in: sdk/js/alien-deepsignals/src/types.ts:260

The object returned by the deepSignal function. It is decorated with utility functions for sets, see DeepSignalSetProps and a __raw__ prop to get the underlying non-reactive object.

Type Parameters

T

T


DeepSignalObject

DeepSignalObject<T> = { [K in keyof T]: DeepSignal<T[K]> }

Defined in: sdk/js/alien-deepsignals/src/types.ts:274

Type Parameters

T

T extends object


DeepSignalSet

DeepSignalSet<T> = DeepSignalSet_<T> & DeepSignalSetProps<T> & DeepSignalObjectProps<T>

Defined in: sdk/js/alien-deepsignals/src/types.ts:225

Type alias for DeepSignal<Set<T>> and reactive Set wrapper that accepts raw or proxied entries. Additionally it is decorated with DeepSignalSetProps and iterator utilities like .map(), .filter(), .some(), …

Note that you can assign plain Sets to properties with type DeepSignalSet, however Typescript will give you a warning. That is a limitation of TypeScript’s capability. Internally, the object will be converted to a DeepSignalSet. You can instruct TypeScript to ignore this with parent.children = new Set() as DeepSignal<Set<any>>.

Type Parameters

T

T


DiscreteCrdt

DiscreteCrdt = "YMap" | "YArray" | "Automerge"

Defined in: sdk/js/orm/src/types.ts:121

The supported discrete (JSON) CRDTs. Automerge and YMap require objects as roots. YArray requires an array as root.


DiscreteRoot

DiscreteRoot = DiscreteRootArray | DiscreteRootObject

Defined in: sdk/js/orm/src/types.ts:114

A discrete document’s root object, either an array or an object.


DiscreteRootArray

DiscreteRootArray = (DiscreteArray | string | number | boolean | DiscreteObject & object)[]

Defined in: sdk/js/orm/src/types.ts:93

The root array for reading and modifying the CRDT as a plain object.


DiscreteType

DiscreteType = DiscreteArray | DiscreteObject | string | number | boolean

Defined in: sdk/js/orm/src/types.ts:83

An allowed type in the CRDT.


OrderByConfig

OrderByConfig<T> = NonEmptyArray<OrderByConfigObject<T>> | OrderByConfigObject<T>

Defined in: sdk/js/orm/src/utilTypes.ts:90

Defines how results are sorted. Must contain a single property with the key being the property to sort by and the value being "asc", "desc". The property to sort by must have a cardinality of exactly 1.

May be used as single object or array of objects if you want secondary ordering.

Type Parameters

T

T extends BaseType

Example

{
  orderBy: [
    { firstName: "asc"},
    { lastName: "asc"},
    { birthDate: "desc"}
  ],
  ...
}

RdfOrmConfig

RdfOrmConfig<T, PS, OB> = Scope & object

Defined in: sdk/js/orm/src/utilTypes.ts:126

Options for creating an RdfOrmSubscription.

Type Declaration

maxActivePages?

optional maxActivePages: PS extends undefined ? never : number

The number of pages after which loading the next page will discard the first one of the current window. Leave undefined or set to 0, for no page disposal. Note that once items are outside of the current window, they are not tracked and therefore creations and invalidations do not cause “page shifts” - the first item in the window remains stable.

Requires pageSize to be set.

orderBy?

optional orderBy: OB

Property / Properties to sort data by.

pageSize?

optional pageSize: OB extends undefined ? never : PS

If set to a value greater than 0, pagination is activated with the here specified size. Use nextPage() or previousPage(), to load the next / previous items.

Requires orderBy to be set.

where?

optional where: WhereConfig<T>

Properties or nested properties to filter by.

Example
{
   "name": ["Jon Doe", "Jane Doe"],
   "birthPlace": {
      "city": "Berlin"
   }
}

Note that when you specify a property value, this is equivalent to marking this property as EXTRA in the SHEX definition. The equivalent SHEX expression for the above is:

ex:PersonShape EXTRA ex:name {
    ex:name [ "Jon Doe" "Jane Doe" ] ;
    # ... rest of shape
}
ex:PlaceShape EXTRA ex:city {
    ex:city [ "Berlin" ] ;
    # ... rest of shape
}

Type Parameters

T

T extends BaseType

PS

PS = number | undefined

OB

OB = OrderByConfig<T> | undefined


Scope

Scope = object

Defined in: sdk/js/orm/src/types.ts:27

The scope of a shape request. Part of the RdfOrmConfig. In most cases, it is recommended to use a narrow scope for performance. You can filter results by subjects and graphs. Only objects in that scope will be returned.

Example

// Contains all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
const expenses: DeepSignal<Set<Expense>> = useShape(ExpenseShape, {
  graphs: ["<graph1 NURI>", "<graph2 NURI>"],
  subjects: ["<subject1 IRI>", "<subject2 IRI>"],
});

Properties

graphs

graphs: string[] | string

Defined in: sdk/js/orm/src/types.ts:34

The graphs to filter for. If an array is provided, the union of all graphs is considered.

  • Set value to ["did:ng:i"] or [""] for whole dataset.
  • Setting value to [] or leaving it undefined, no objects are returned.
subjects?

optional subjects: string[]

Defined in: sdk/js/orm/src/types.ts:39

Subjects to filter for. Set to [] or leave it undefined for no filtering.


SubscriptionData

SubscriptionData<T, CONF> = undefined extends CONF["orderBy"] ? DeepSignalSet<T> : DeepSignal<ReadOnlyArray<T>>

Defined in: sdk/js/orm/src/utilTypes.ts:191

The data type of signal objects depending on the OrmConfig.

Type Parameters

T

T extends BaseType

CONF

CONF extends RdfOrmConfig<any>


WhereConfig

WhereConfig<T> = { [P in LiteralProps<T>]?: T[P] extends Set<infer S> | undefined ? S | NonEmptyArray<Exclude<S, undefined>> : T[P] | NonEmptyArray<Exclude<T[P], undefined>> } & { [P in ObjectProps<T>]?: T[P] extends Set<infer S extends BaseType> ? IsUnion<S> extends true ? never : WhereConfig<S> : IsUnion<T[P]> extends true ? never : WhereConfig<T[P]> }

Defined in: sdk/js/orm/src/utilTypes.ts:43

Used in RdfOrmConfig.

Type Parameters

T

T extends BaseType

Variables

effect()

const effect: (fn) => () => void = alienEffect

Defined in: sdk/js/alien-deepsignals/src/core.ts:108

Re-export of alien-signals effect function.

Callback reruns on every signal modification that is used within its callback.

Parameters

fn

() => void

Returns

(): void

Returns

void


ngSession

const ngSession: Promise<{ ng: __module; session: Session; }>

Defined in: sdk/js/orm/src/connector/initNg.ts:16

Resolves to the NG session and the ng implementation.

Functions

getObjects()

getObjects<T>(shapeType, config): Promise<T[] | Set<T>>

Defined in: sdk/js/orm/src/connector/getObjects.ts:24

Utility for retrieving objects once without establishing a two-way subscription.

Type Parameters

T

T extends BaseType

Parameters

shapeType

ShapeType<T>

The shape type of the objects to be retrieved.

config

Omit<RdfOrmConfig<T>, "pageSize" | "maxActivePages">

Returns

Promise<T[] | Set<T>>

A set of all objects matching the shape and scope


getRaw()

getRaw<T>(value): any

Defined in: sdk/js/alien-deepsignals/src/deepSignal.ts:1466

Get the original, raw value of a deep signal.

Type Parameters

T

T extends object

Parameters

value

T | DeepSignal<T>

Returns

any


initNg()

initNg(ngImpl, session): void

Defined in: sdk/js/orm/src/connector/initNg.ts:51

Initialize the ORM by passing the ng implementation and session.

This is the first thing you need to do before using the ORM.

Parameters

ngImpl

__module

The NextGraph API, e.g. exported from @ng-org/web.

session

Session

The established NextGraph session.

Returns

void

Example

import { ng, init } from "@ng-org/web";
import { initNg as initNgSignals, Session } from "@ng-org/orm";
let session: Session;

// Call as early as possible as it will redirect to the auth page.
await init(
  async (event: any) => {
    session = event.session;
    session!.ng ??= ng;

    // Call initNgSignals
    initNgSignals(ng, session);
  },
  true,
  [],
);

insertObject()

insertObject<T>(shapeType, object): Promise<T>

Defined in: sdk/js/orm/src/connector/insertObject.ts:26

Utility for adding ORM-typed objects to the database without the need for subscribing to documents using an RdfOrmSubscription.

Type Parameters

T

T extends BaseType

Parameters

shapeType

ShapeType<T>

The shape type of the objects to be inserted.

object

T

The object to be inserted. The @graphmust be set. It is recommended to set @id to "" or leave it undefined, to auto-generate a unique NURI.

Returns

Promise<T>

the inserted object. If @id was set to "" or undefined, it’s now set to an auto-generated NURI. This is true for nested objects as well. For nested objects, the @graph, if unset, will be set to the parent’s @graph.


reactUseDiscrete()

reactUseDiscrete<T, DocId>(documentId): UseDiscreteResult<T, DocId>

Defined in: sdk/js/orm/src/frontendAdapters/react/useDiscrete.ts:118

Hook to subscribe to an existing discrete (JSON) CRDT document. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT document.

Establishes a 2-way binding: Modifications to the object are immediately committed; changes coming from the engine (or other components) cause an immediate rerender.

In comparison to reactUseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T extends DiscreteRoot = DiscreteRoot

DocId

DocId extends DocumentId = DocumentId

Parameters

documentId

DocId

The NURI of the CRDT document.

Returns

UseDiscreteResult<T, DocId>

An object that contains as doc the reactive DeepSignal object or undefined if documentId is undefined.

Example

// We assume you have created a CRDT document already, as below.
// const documentId = await ng.doc_create(
//     session_id,
//     crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
//     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
//     "store",
//     undefined
// );

function Expenses({ documentId }: { documentId: string }) {
  const { doc } = useDiscrete(documentId);

  // If the CRDT document is still empty, we need to initialize it.
  if (doc && !doc.expenses) {
    doc.expenses = [];
  }
  const expenses = doc?.expenses;

  const createExpense = useCallback(() => {
    // Note that we use *expense["@id"]* as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed or added from the array.
    // The `@id` is a NURI with the schema `<documentId>:d:<object-specific id>`.
    // Since the `@id` is generated in the engine, the object is
    // *preliminarily given a mock id* which will be replaced immediately.
    expenses.push({
      title: "New expense",
      date: new Date().toISOString(),
    });
  }, [expenses]);

  // Still loading?
  if (!doc) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={() => createExpense()}>+ Add expense</button>
      <div>
        {expenses.length === 0 ? (
          <p>No expenses yet.</p>
        ) : (
          expenses.map((expense) => (
            <ExpenseCard key={expense["@id"]} expense={expense} />
          ))
        )}
      </div>
    </div>
  );
}

In the ExpenseCard component:

function ExpenseCard({expense}: {expense: Expense}) {
   return (
       <input
           value={expense.title}
           onChange={(e) => {
               expense.title = e.target.value; // Changes trigger rerender.
           }}
       />
       <div>
           <p>Date</p>
           <p>{expense.doc}
       </div
   );
}

reactUseShape()

reactUseShape<ST, CONF, T, SUBSCRIPTION_DATA>(shapeType, conf): WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

Defined in: sdk/js/orm/src/frontendAdapters/react/useShape.ts:98

Hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Establishes a 2-way binding with the returned data: Modifications to the data are automatically committed, changes coming from the engine (or other components) cause an immediate rerender.

Type Parameters

ST

ST extends ShapeType<T>

CONF

CONF extends RdfOrmConfig<T>

T

T extends BaseType = ST extends ShapeType<T_> ? T_ : never

SUBSCRIPTION_DATA

SUBSCRIPTION_DATA = SubscriptionData<T, CONF>

Parameters

shapeType

ST

The ShapeType that the items should conform to (generated by the shex-orm tool).

conf

The config that can be a document nuri, RdfOrmConfig or undefined.

string | CONF | undefined

Returns

WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

A UseShapeResult with the orm objects or an empty set, if still loading.
If the conf is explicitly set to undefined, the returned data is undefined and isLoading is false.

Example

function Expenses() {
    const { data: expenses, isLoading }: DeepSignal<Set<Expense>> = useShape(
        ExpenseShapeType,
        {
            graphs: ["<document NURI>"]
            orderBy: {dateOrPurchase: "desc"}
        }
    );

    const createExpense = useCallback(
        () => {
            expenses.add({
                "@graph": `<document NURI>`,
                "@type": "did:ng:z:Expense",
                "@id": "", // Assigns ID automatically, if set to "".
                title: "New expense",
                dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
            });
        },
        [expenses]
    );

    return (
        <div>
            <button
                onClick={() => createExpense({})}
            >
                + Add expense
            </button>
            <div>
                {isLoading && (
                    <p>
                        Loading...
                    </p>
                )}
                {expenses && expenses.size === 0 && (
                    <p>
                        No expenses yet.
                    </p>
                )}
                {expenses && expenses.size > 0 && (
                    [...expenses].map((expense) => (
                        // You can modify the expense's properties in the ExpenseCard component
                        // which will instantly trigger a rerender.
                        <ExpenseCard
                            key={expense["@id"]}
                            expense={expense}
                        />
                    ))
                )}
            </div>
        </div>
    );
}

removeObject()

removeObject(graphNuri, subjectIri): Promise<void>

Defined in: sdk/js/orm/src/connector/removeObject.ts:26

Utility for removing all data (quads) for a given document and subject.

Essentially this runs the following SPARQL query:
DELETE WHERE { GRAPH <${graphNuri}> { <${subjectIri}> ?p ?o . } }

Note that this call has the same effect to calling delete() on an object in an RDF ORM Subscription.

Parameters

graphNuri

string

The Nuri of the document to remove data in.

subjectIri

string

The IRI of the subject to delete all data for.

Returns

Promise<void>


svelte4UseDiscrete()

svelte4UseDiscrete<T>(documentIdOrPromise): UseDiscreteResult<T>

Defined in: sdk/js/orm/src/frontendAdapters/svelte4/useDiscrete.svelte.ts:102

Svelte 4 hook to subscribe to discrete (JSON) CRDT documents. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

In comparison to svelte4UseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

Parameters

documentIdOrPromise

The NURI of the CRDT document or a promise to that.

string | Promise<string> | undefined

Returns

UseDiscreteResult<T>

The store of the reactive JSON object of the CRDT document or undefined.

Example

<script lang="ts">

    // We assume you have created a CRDT document already, as below.
    // const documentId = await ng.doc_create(
    //     session_id,
    //     crdt, // "Automerge" | "YMap" | "YArray"
    //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
    //     "store",
    //     undefined

    const { doc, isLoading } = useDiscrete(documentIdPromise);

    // If the CRDT document is still empty, we need to initialize it.
    $: if (doc && !doc.expenses) {
        doc.expenses = [];
    }

    // Call doc.expenses.push({title: "Example title"}), to add new elements.

    // Note that we use expense["@id"] NURI as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed from the array.
    // Since the `@id` is generated in the backend, the object is preliminarily
    // given a mock ID which will be replaced immediately
</script>

<section>
    <div>
        {#if isLoading}
            Loading...
        {:else if doc.expenses.length === 0}
        <p>
            Nothing tracked yet - log your first purchase to kick things off.
        </p>
        {:else}
        {#each doc.expenses as expense, index (expense['@id']) }
            <ExpenseCard
            expense={expense}
            />
        {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

    let {
        expense = $bindable(),
    }: { expense: Expense; } = $props();
</script>

<div>
    <input
        value={expense.title ?? ""}
        oninput={(event) => {expense.title = event.currentTarget?.value ?? ""}}
        placeholder="Expense title"
    />
</div>

svelte4UseShape()

svelte4UseShape<ST, CONF, T, SUBSCRIPTION_DATA>(shape, conf): WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

Defined in: sdk/js/orm/src/frontendAdapters/svelte4/useShape.svelte.ts:100

Svelte 4 hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Returns a DeepSignalSet store containing the objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

Type Parameters

ST

ST extends ShapeType<T>

CONF

CONF extends RdfOrmConfig<T>

T

T extends BaseType = ST extends ShapeType<T_> ? T_ : never

SUBSCRIPTION_DATA

SUBSCRIPTION_DATA = SubscriptionData<T, CONF>

Parameters

shape

ST

conf

The RdfOrmConfig or a graph string.

string | CONF | undefined

Returns

WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
    // Gets all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
    const { data: expenses, isLoading } = useShape(ExpenseShape,
        {graphs: ["<g1 NURI>", "<g2 NURI>"],
        subjects: ["<s1 NURI>", "<s2 NURI>"]});
    // expenses has type `: DeepSignal<Set<Expense>>`

    // Call expenses.add({"@graph": "<g1 or g2 NURI>", "@id": "", title: "Example title"}), to add new elements.
    // Leave `@id` an empty string to auto-generate a subject IRI (adjust your scope accordingly).
</script>

<section>
    <div>
        {# if isLoading}
        <p>
            Loading...
        </p>
        {:else if expenses.size === 0}
        <p>
            No expense yet.
        </p>
        {:else}
        {#each expensesSorted as expense, index (expense['@id']) }
            <ExpenseCard
                expense={expense}
            />
        {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:


  let {
    expense = $bindable(),
  }: { expense: Expense; } = $props();
</script>

<div>
  <input
    bind:value={expense.title}
    placeholder="Expense title"
  />
</div>

svelteUseDiscrete()

svelteUseDiscrete<T, DocIdOrPromise>(documentIdOrPromise): UseDiscreteResult<T, DocIdOrPromise>

Defined in: sdk/js/orm/src/frontendAdapters/svelte/useDiscrete.svelte.ts:106

Svelte 5 hook to subscribe to existing discrete (JSON) CRDT documents. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

In comparison to svelteUseShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

DocIdOrPromise

DocIdOrPromise extends string | Promise<string> | undefined = string | Promise<string> | undefined

Parameters

documentIdOrPromise

DocIdOrPromise

The NURI of the CRDT document or a promise to that.

Returns

UseDiscreteResult<T, DocIdOrPromise>

The reactive JSON object of the CRDT document.

Example

<script lang="ts">
    // We assume you have created a CRDT document already, as below.
    // const documentId = await ng.doc_create(
    //     session_id,
    //     crdt, // "Automerge" | "YMap" | "YArray"
    //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
    //     "store",
    //     undefined,
    // );

    const { doc } = useDiscrete(documentIdPromise);

    $effect(() => {
        // If the CRDT document is still empty, we need to initialize it.
        if (doc && !doc.expenses) {
            doc.expenses = [];
        }
    });

    const createExpense = () => {
        // Note that we use *expense["@id"]* as a key in the expense list.
        // Every object added to a CRDT array gets a stable `@id` property assigned
        // which you can use for referencing objects in arrays even as
        // preceding objects are removed or added from the array.
        // The `@id` is an NURI with the schema `<documentId>:d:<object-specific id>`.
        // Since the `@id` is generated in the engine, the object is
        // *preliminarily given a mock id* which will be replaced immediately.
        expenses.push({
            title: "New expense",
            date: new Date().toISOString(),
        });
     };

</script>

<section>
    <div>
        <button on:click={() => createExpense({})}/>

        {#if !doc}
            Loading...
        {:else if doc.expenses.length === 0}
            <p>
                Nothing tracked yet - log your first purchase to kick things off.
            </p>
        {:else}
            {#each doc.expenses as expense, index (expense['@id']) }
                <ExpenseCard
                    expense={expense}
                />
            {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

    let {
        expense = $bindable(),
    }: { expense: Expense; } = $props();
</script>

<div>
    <input
        bind:value={expense.title}
    />
</div>

svelteUseShape()

svelteUseShape<ST, CONF, T>(shape, conf): WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

Defined in: sdk/js/orm/src/frontendAdapters/svelte/useShape.svelte.ts:103

Svelte 5 hook to subscribe to RDF data in the graph database using a shape, see ShapeType.

Returns a DeepSignalSet that contain the objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

Type Parameters

ST

ST extends ShapeType<T>

CONF

CONF extends RdfOrmConfig<T>

T

T extends BaseType = ST extends ShapeType<T_> ? T_ : never

Parameters

shape

ST

The ShapeType the objects should have (generated by the @ng-org/shex-orm tool).

conf

The RdfOrmConfig or a graph string.

string | CONF | undefined

Returns

WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
    // Gets all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
    const { data: expenses, isLoading } = $derived(
        useShape(ExpenseShapeType,
            {graphs: ["<g1 NURI>", "<g2 NURI>"],
            subjects: ["<s1 NURI>", "<s2 NURI>"]}));
    // `expenses` has type `DeepSignal<Set<Expense>>`

    const createExpense = () => {
        expenses.add({
            "@graph": `<graph NURI>`,
            "@type": "did:ng:z:Expense",
            "@id": "", // Assign ID automatically.
            title: "New expense",
            dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
        });
    };

</script>

<section>
    <div>
        <button on:click={() => createExpense()}>
            + Add expense
        </button>

        {# if isLoading}
            <p>
                Loading...
            </p>
        {:else if expenses.size === 0}
            <p>
                No expense yet.
            </p>
        {:else}
            {#each expenses as expense, index (expense['@id']) }
                <ExpenseCard
                    expense={expense}
                />
            {/each}
        {/if}
    </div>
</section>

In the ExpenseCard component:

<script lang="ts">
let {
    expense,
}: { expense: DeepSignal<Expense>; } = $props();
</script>

<div>
    <input
        bind:value={expense.title}
    />
</div>

vueUseDiscrete()

vueUseDiscrete<T, DOC_ID>(documentId): UseDiscreteResult<T>

Defined in: sdk/js/orm/src/frontendAdapters/vue/useDiscrete.ts:119

Hook to subscribe to an existing discrete (JSON) CRDT document. You can modify the returned object like any other JSON object. Changes are immediately reflected in the CRDT document.

Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the engine (or other components) cause an immediate rerender.

In comparison to useShape, discrete CRDTs are untyped. You can put any JSON data inside and need to validate the schema yourself.

Type Parameters

T

T = DiscreteRoot

DOC_ID

DOC_ID extends string | undefined = string | undefined

Parameters

documentId

MaybeRefOrGetter<DOC_ID>

The NURI of the CRDT document or undefined as MaybeRefOrGetter.

Returns

UseDiscreteResult<T>

An object that contains as data the reactive DeepSignal object or undefined if not loaded yet or documentId is undefined.

Example

<script lang="ts">
  // We assume you have created a CRDT document already, as below.
  // const documentId = await ng.doc_create(
  //     session_id,
  //     crdt, // "Automerge" | "YMap" | "YArray"
  //     crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
  //     "store",
  //     undefined
  // );
  const { doc } = useDiscrete(documentId);

  // If document is new, we need to set up the basic structure.
  effect(() => {
    if (doc.value && !doc.value.expenses) {
      doc.value.expenses = [];
    }
  });

  const createExpense = () => {
    // Note that we use *expense["@id"]* as a key in the expense list.
    // Every object added to a CRDT array gets a stable `@id` property assigned
    // which you can use for referencing objects in arrays even as
    // objects are removed or added from the array.
    // The `@id` is an NURI with the schema `<documentId>:d:<object-specific id>`.
    // Since the `@id` is generated in the engine, the object is
    // *preliminarily given a mock id* which will be replaced immediately.
    doc.value.expenses.push({
      title: "New expense",
      date: new Date().toISOString(),
    });
  };
</script>

<template>
  <div v-if="!doc">Loading...</div>
  <div v-else>
    <p v-if="expenses.length === 0">No expenses yet.</p>
    <template v-else>
      <button @click="{()" ="">createExpense()} > + Add expense</button>
      <ExpenseCard
        v-for="expense in expenses"
        :key="expense['@id']"
        :expense="expense"
      />
    </template>
  </div>
</template>

In the ExpenseCard component:

<script lang="ts">
  const { expense } = defineProps<{
    expense: DeepSignal<Expense>;
  }>();

  // If you modify expense in the component,
  // the changes are immediately propagated to other consuming components
  // And persisted in the database.
</script>

<template>
  <input v-model="expense.title" placeholder="Expense title" />
</template>

vueUseShape()

vueUseShape<ST, CONF, T>(shape, conf): WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

Defined in: sdk/js/orm/src/frontendAdapters/vue/useShape.ts:94

Hook to subscribe to RDF data in the graph database using a shape, see ShapeType. The returned objects are as easy to use as other TypeScript objects.

Returns a DeepSignalSet of objects matching the shape and that are within the scope. Establishes a 2-way binding: Modifications to the object are immediately committed, changes coming from the backend (or other components) cause an immediate rerender.

Type Parameters

ST

ST extends ShapeType<T>

CONF

CONF extends RdfOrmConfig<T>

T

T extends BaseType = ST extends ShapeType<T_> ? T_ : never

Parameters

shape

ST

The ShapeType the objects should have (generated by the shex-orm tool).

conf

The RdfOrmConfig or a graph string.

string | CONF

Returns

WithMaybePagination<UseShapeResult_<ST, CONF, T, SubscriptionData<T, CONF>>, CONF, T>

A DeepSignalSet with the orm objects or an empty set, if still loading.
If the scope is explicitly set to undefined, an empty set is returned which errors if you try to make modifications on it.

Example

<script lang="ts">
  // Contains all expense objects with `@id` <s1 IRI> or <s2 IRI> and `@graph` <g1 NURI> or <g2 NURI>
  const { data: expenses, isLoading }: DeepSignal<Set<Expense>> = useShape(
    ExpenseShapeType,
    { graphs: ["<g1 NURI>", "<g2 NURI>"], subjects: ["<s1 IRI>", "<s2 IRI>"] },
  );

  // Simply call expenses.add({"@graph": "<g1 or g2 NURI>", "@id": "", title: "Example title"}), to add new elements.
  // Leave `@id` an empty string to auto-generate a subject NURI (adjust your scope accordingly).
</script>

<template>
  <div>
    <p v-if="isLoading">No expenses yet.</p>
    <p v-else-if="expenses.size === 0">No expenses yet.</p>
    <template v-else>
      <ExpenseCard
        v-for="expense in expenses"
        :key="expense['@id'])"
        :expense="expense"
      />
    </template>
  </div>
</template>

In the ExpenseCard component:

<script lang="ts">
  const { expense } = defineProps<{
    expense: DeepSignal<Expense>;
  }>();

  // If you modify expense in the component,
  // the changes are immediately propagated to other consuming components.
  // And persisted in the database.
</script>

<template>
  <input v-model="expense.title" placeholder="Expense title" />
</template>

watch()

watch<T>(source, callback, options?): object

Defined in: sdk/js/alien-deepsignals/src/watch.ts:94

Watch for changes to a deepSignal.

Whenever a change is made, callback is called with the patches describing the change and the new value. If you set triggerInstantly, the callback is called on every property change. If not, all changes are aggregated and callback is called in a microtask when the current task finishes, e.g. await is called (meaning it supports batching).

When objects are added to Sets, their synthetic ID (usually @id) becomes part of the patch path. This allows patches to uniquely identify which Set entry is being mutated.

When you do not need need the patches but only want to be called back on object changes that you depend on, you are advised to use effect instead.

Note: If you attach an existing signal object, you won’t see the changes made on the child signal object by watching the root. All you will see is an initial add patch with the signal object as value. Watch the nested object separately.

Type Parameters

T

T extends unknown

Parameters

source

T

callback

WatchPatchCallback<T>

options?

WatchOptions = {}

Returns

object

registerCleanup

registerCleanup: RegisterCleanup

stopListening()

stopListening: () => void

Returns

void

Example

const state = deepSignal(
    { s: new Set() },
    { ...}
);

watch(state, ({ patches }) => {
    console.log(JSON.stringify(patches));
});

state.s.add({ data: "test" });
// Will log:
// [
//   {"path":["s","did:ng:o:123"],"op":"add"},
//   {"path":["s","did:ng:o:123","@id"],"op":"add","value":"did:ng:o:123"},
//   {"path":["s","did:ng:o:123","data"],"op":"add","value":"test"}
// ]

state.s.getById("did:ng:o:123")!.data = "new value"
// Will log:
// [
//   {"path":["s","did:ng:o:123","data"],"op":"add","value":"new value"}
// ]