@ng-org/orm 0.1.2-alpha.1 → 0.1.2-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -230
- package/dist/connector/applyPatches.d.ts +19 -49
- package/dist/connector/applyPatches.d.ts.map +1 -1
- package/dist/connector/applyPatches.js +57 -58
- package/dist/connector/discrete/discreteOrmSubscriptionHandler.d.ts +168 -0
- package/dist/connector/discrete/discreteOrmSubscriptionHandler.d.ts.map +1 -0
- package/dist/connector/discrete/discreteOrmSubscriptionHandler.js +309 -0
- package/dist/connector/getObjects.d.ts +11 -0
- package/dist/connector/getObjects.d.ts.map +1 -0
- package/dist/connector/getObjects.js +26 -0
- package/dist/connector/initNg.d.ts +33 -2
- package/dist/connector/initNg.d.ts.map +1 -1
- package/dist/connector/initNg.js +30 -0
- package/dist/connector/insertObject.d.ts +9 -0
- package/dist/connector/insertObject.d.ts.map +1 -0
- package/dist/connector/insertObject.js +24 -0
- package/dist/connector/ormSubscriptionHandler.d.ts +167 -0
- package/dist/connector/ormSubscriptionHandler.d.ts.map +1 -0
- package/dist/connector/ormSubscriptionHandler.js +400 -0
- package/dist/connector/utils.d.ts +17 -0
- package/dist/connector/utils.d.ts.map +1 -0
- package/dist/connector/utils.js +68 -0
- package/dist/frontendAdapters/react/index.d.ts +2 -1
- package/dist/frontendAdapters/react/index.d.ts.map +1 -1
- package/dist/frontendAdapters/react/index.js +2 -1
- package/dist/frontendAdapters/react/useDiscrete.d.ts +104 -0
- package/dist/frontendAdapters/react/useDiscrete.d.ts.map +1 -0
- package/dist/frontendAdapters/react/useDiscrete.js +151 -0
- package/dist/frontendAdapters/react/useShape.d.ts +69 -2
- package/dist/frontendAdapters/react/useShape.d.ts.map +1 -1
- package/dist/frontendAdapters/react/useShape.js +89 -8
- package/dist/frontendAdapters/svelte/index.d.ts +2 -1
- package/dist/frontendAdapters/svelte/index.d.ts.map +1 -1
- package/dist/frontendAdapters/svelte/index.js +2 -1
- package/dist/frontendAdapters/svelte/useDiscrete.svelte.d.ts +94 -0
- package/dist/frontendAdapters/svelte/useDiscrete.svelte.d.ts.map +1 -0
- package/dist/frontendAdapters/svelte/useDiscrete.svelte.js +137 -0
- package/dist/frontendAdapters/svelte/useShape.svelte.d.ts +79 -10
- package/dist/frontendAdapters/svelte/useShape.svelte.d.ts.map +1 -1
- package/dist/frontendAdapters/svelte/useShape.svelte.js +88 -9
- package/dist/frontendAdapters/svelte4/index.d.ts +5 -0
- package/dist/frontendAdapters/svelte4/index.d.ts.map +1 -0
- package/dist/{connector/createSignalObjectForShape.js → frontendAdapters/svelte4/index.js} +3 -15
- package/dist/frontendAdapters/svelte4/useDiscrete.svelte.d.ts +84 -0
- package/dist/frontendAdapters/svelte4/useDiscrete.svelte.d.ts.map +1 -0
- package/dist/frontendAdapters/svelte4/useDiscrete.svelte.js +126 -0
- package/dist/frontendAdapters/svelte4/useShape.svelte.d.ts +77 -0
- package/dist/frontendAdapters/svelte4/useShape.svelte.d.ts.map +1 -0
- package/dist/frontendAdapters/svelte4/useShape.svelte.js +91 -0
- package/dist/frontendAdapters/utils.d.ts +2 -0
- package/dist/frontendAdapters/utils.d.ts.map +1 -0
- package/dist/frontendAdapters/utils.js +14 -0
- package/dist/frontendAdapters/vue/index.d.ts +2 -1
- package/dist/frontendAdapters/vue/index.d.ts.map +1 -1
- package/dist/frontendAdapters/vue/index.js +2 -1
- package/dist/frontendAdapters/vue/useDiscrete.d.ts +99 -0
- package/dist/frontendAdapters/vue/useDiscrete.d.ts.map +1 -0
- package/dist/frontendAdapters/vue/useDiscrete.js +123 -0
- package/dist/frontendAdapters/vue/useShape.d.ts +73 -2
- package/dist/frontendAdapters/vue/useShape.d.ts.map +1 -1
- package/dist/frontendAdapters/vue/useShape.js +79 -4
- package/dist/index.d.ts +13 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -6
- package/dist/types.d.ts +63 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -1
- package/package.json +37 -48
- package/dist/connector/applyPatches.test.d.ts +0 -2
- package/dist/connector/applyPatches.test.d.ts.map +0 -1
- package/dist/connector/applyPatches.test.js +0 -772
- package/dist/connector/createSignalObjectForShape.d.ts +0 -14
- package/dist/connector/createSignalObjectForShape.d.ts.map +0 -1
- package/dist/connector/ormConnectionHandler.d.ts +0 -47
- package/dist/connector/ormConnectionHandler.d.ts.map +0 -1
- package/dist/connector/ormConnectionHandler.js +0 -240
- package/src/connector/applyPatches.test.ts +0 -842
- package/src/connector/applyPatches.ts +0 -404
- package/src/connector/createSignalObjectForShape.ts +0 -35
- package/src/connector/initNg.ts +0 -30
- package/src/connector/ormConnectionHandler.ts +0 -300
- package/src/frontendAdapters/react/index.ts +0 -2
- package/src/frontendAdapters/react/useShape.ts +0 -36
- package/src/frontendAdapters/svelte/index.ts +0 -2
- package/src/frontendAdapters/svelte/useShape.svelte.ts +0 -46
- package/src/frontendAdapters/vue/index.ts +0 -2
- package/src/frontendAdapters/vue/useShape.ts +0 -33
- package/src/index.ts +0 -14
- package/src/types.ts +0 -26
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
// Licensed under the Apache License, Version 2.0
|
|
4
|
+
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
|
|
5
|
+
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
|
6
|
+
// at your option. All files in the project carrying such
|
|
7
|
+
// notice may not be copied, modified, or distributed except
|
|
8
|
+
// according to those terms.
|
|
9
|
+
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
10
|
+
import { normalizeScope } from "../types.js";
|
|
11
|
+
import { applyPatchesToDeepSignal } from "./applyPatches.js";
|
|
12
|
+
import { ngSession } from "./initNg.js";
|
|
13
|
+
import { deepSignal, watch as watchDeepSignal, batch, } from "@ng-org/alien-deepsignals";
|
|
14
|
+
import { deepPatchesToWasm } from "./utils.js";
|
|
15
|
+
/**
|
|
16
|
+
* Delay in ms to wait before closing subscription.\
|
|
17
|
+
* Useful when a hook unsubscribes and resubscribes in a short time interval
|
|
18
|
+
* so that no new subscriptions need to be set up.
|
|
19
|
+
*/
|
|
20
|
+
const WAIT_BEFORE_CLOSE = 500;
|
|
21
|
+
/**
|
|
22
|
+
* Class for managing RDF-based ORM subscriptions with the engine.
|
|
23
|
+
*
|
|
24
|
+
* You have two options on how to interact with the ORM:
|
|
25
|
+
* - Use a hook for your favorite framework under `@ng-org/orm/react|vue|svelte`
|
|
26
|
+
* - Call {@link OrmSubscription.getOrCreate} to create a subscription manually
|
|
27
|
+
*
|
|
28
|
+
* For more information about RDF-based ORM subscriptions,
|
|
29
|
+
* see the README and follow the tutorial.
|
|
30
|
+
*/
|
|
31
|
+
export class OrmSubscription {
|
|
32
|
+
/** Global store of all subscriptions. We use that for pooling. */
|
|
33
|
+
static idToEntry = new Map();
|
|
34
|
+
/** The shape type that is subscribed to. */
|
|
35
|
+
shapeType;
|
|
36
|
+
/** The {@link Scope} of the subscription. */
|
|
37
|
+
scope;
|
|
38
|
+
/**
|
|
39
|
+
* The signalObject containing all data matching the shape and scope
|
|
40
|
+
* (once subscription is established).
|
|
41
|
+
* The object is of type {@link DeepSignalSet} which
|
|
42
|
+
* to the outside behaves like a regular set but has a couple of
|
|
43
|
+
* additional features:
|
|
44
|
+
* - Modifications are immediately propagated back to the database.
|
|
45
|
+
* - Database changes are immediately reflected in the object.
|
|
46
|
+
* - `.getBy(graphIri, subjectIri)` utility for quicker access to objects in set.
|
|
47
|
+
* - `.first()` utility to get the first element added to the set.
|
|
48
|
+
* - the iterator utilities, e.g. `.map()`, `.filter()`, ...
|
|
49
|
+
* - Watch for object changes using {@link watchDeepSignal}.
|
|
50
|
+
*/
|
|
51
|
+
signalObject;
|
|
52
|
+
stopSignalListening;
|
|
53
|
+
/** The subscription ID kept as an identifier for communicating with the verifier. */
|
|
54
|
+
subscriptionId;
|
|
55
|
+
/** The number of OrmSubscriptions with the same shape and scope (for pooling). */
|
|
56
|
+
refCount;
|
|
57
|
+
/** Identifier as a combination of shape type and scope. Prevents duplications. */
|
|
58
|
+
identifier;
|
|
59
|
+
/** When true, modifications from the signalObject are not processed. */
|
|
60
|
+
suspendDeepWatcher;
|
|
61
|
+
/** True, if a transaction is running. */
|
|
62
|
+
inTransaction = false;
|
|
63
|
+
/** Aggregation of patches to be sent when in transaction. */
|
|
64
|
+
pendingPatches;
|
|
65
|
+
/** **Await to ensure that the subscription is established and the data arrived.** */
|
|
66
|
+
readyPromise;
|
|
67
|
+
closeOrmSubscription;
|
|
68
|
+
/** Function to call once initial data has been applied. */
|
|
69
|
+
resolveReady;
|
|
70
|
+
// FinalizationRegistry to clean up subscriptions when signal objects are GC'd.
|
|
71
|
+
static cleanupSignalRegistry = typeof FinalizationRegistry === "function"
|
|
72
|
+
? new FinalizationRegistry((connectionId) => {
|
|
73
|
+
console.log("finalization called for", connectionId);
|
|
74
|
+
// Best-effort fallback; look up by id and clean
|
|
75
|
+
const entry = this.idToEntry.get(connectionId);
|
|
76
|
+
console.log("cleaning up connection", connectionId);
|
|
77
|
+
if (!entry)
|
|
78
|
+
return;
|
|
79
|
+
entry.close();
|
|
80
|
+
})
|
|
81
|
+
: null;
|
|
82
|
+
constructor(shapeType, scope) {
|
|
83
|
+
// @ts-expect-error
|
|
84
|
+
window.ormSignalConnections = OrmSubscription.idToEntry;
|
|
85
|
+
// @ts-expect-error
|
|
86
|
+
window.OrmSubscription = OrmSubscription;
|
|
87
|
+
this.shapeType = shapeType;
|
|
88
|
+
const normalizedScope = normalizeScope(scope);
|
|
89
|
+
this.scope = normalizedScope;
|
|
90
|
+
this.refCount = 1;
|
|
91
|
+
this.closeOrmSubscription = () => { };
|
|
92
|
+
this.suspendDeepWatcher = false;
|
|
93
|
+
this.identifier = `${shapeType.shape}|${canonicalScope(normalizedScope)}`;
|
|
94
|
+
this.signalObject = deepSignal(new Set(), {
|
|
95
|
+
propGenerator: this.signalObjectPropGenerator,
|
|
96
|
+
// Don't set syntheticIdPropertyName - let propGenerator handle all ID logic
|
|
97
|
+
readOnlyProps: ["@id", "@graph"],
|
|
98
|
+
});
|
|
99
|
+
// Schedule cleanup of the connection when the signal object is GC'd.
|
|
100
|
+
OrmSubscription.cleanupSignalRegistry?.register(this.signalObject, this.identifier, this.signalObject);
|
|
101
|
+
// Add listener to deep signal object to report changes back to wasm land.
|
|
102
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
|
|
103
|
+
this.stopSignalListening = stopListening;
|
|
104
|
+
// Set promise to be resolved when data arrived from engine.
|
|
105
|
+
this.readyPromise = new Promise((resolve) => {
|
|
106
|
+
this.resolveReady = resolve;
|
|
107
|
+
});
|
|
108
|
+
ngSession.then(async ({ ng, session }) => {
|
|
109
|
+
try {
|
|
110
|
+
this.closeOrmSubscription = await ng.orm_start_graph(normalizedScope.graphs, normalizedScope.subjects, shapeType, session.session_id, this.onBackendMessage);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
console.error(e);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Returns an OrmSubscription which subscribes to the given
|
|
119
|
+
* {@link ShapeType} and {@link Scope} in a 2-way binding.
|
|
120
|
+
*
|
|
121
|
+
* You **find the data** and objects matching the shape and scope
|
|
122
|
+
* in the **`signalObject`** once {@link readyPromise} resolves. This is a {@link DeepSignalSet} which
|
|
123
|
+
* to the outside behaves like a regular set but has a couple of
|
|
124
|
+
* additional features:
|
|
125
|
+
* - Modifications are propagated back to the database.
|
|
126
|
+
* Note that multiple immediate modifications in the same task,
|
|
127
|
+
* e.g. `obj[0] = "foo"; obj[1] = "bar"` are batched together
|
|
128
|
+
* and sent in a subsequent microtask.
|
|
129
|
+
* - Database changes are immediately reflected in the object.
|
|
130
|
+
* - `.getBy(graphIri, subjectIri)` utility for quicker access to objects in set.
|
|
131
|
+
* - `.first()` utility to get the first element added to the set.
|
|
132
|
+
* - the iterator utilities, e.g. `.map()`, `.filter()`, ...
|
|
133
|
+
* - Watch for object changes using {@link watchDeepSignal}.
|
|
134
|
+
*
|
|
135
|
+
* You can use **transactions**, to prevent excessive calls to the database
|
|
136
|
+
* with {@link beginTransaction} and {@link commitTransaction}.
|
|
137
|
+
*
|
|
138
|
+
* In many cases, you are advised to use a hook for your
|
|
139
|
+
* favorite framework under `@ng-org/orm/react|vue|svelte`
|
|
140
|
+
* instead of calling `getOrCreate` directly.
|
|
141
|
+
*
|
|
142
|
+
* Call {@link close}, to close the subscription.
|
|
143
|
+
*
|
|
144
|
+
* Note: If another call to `getOrCreate` was previously made
|
|
145
|
+
* and `close` was not called on it (or only shortly after),
|
|
146
|
+
* it will return the same OrmSubscription.
|
|
147
|
+
*
|
|
148
|
+
* @param shapeType The {@link ShapeType}
|
|
149
|
+
* @param scope The {@link Scope}. If no scope is given, the whole store is considered.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* // We assume you have created a graph document already, as below.
|
|
154
|
+
* // const documentId = await ng.doc_create(
|
|
155
|
+
* // session_id,
|
|
156
|
+
* // "Graph",
|
|
157
|
+
* // "data:graph",
|
|
158
|
+
* // "store",
|
|
159
|
+
* // undefined
|
|
160
|
+
* // );
|
|
161
|
+
* const subscription = OrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [graphIri]});
|
|
162
|
+
* // Wait for data.
|
|
163
|
+
* await subscription.readyPromise;
|
|
164
|
+
*
|
|
165
|
+
* const expense = subscription.signalObject.first()
|
|
166
|
+
* expense.name = "updated name";
|
|
167
|
+
* expense.description = "updated description";
|
|
168
|
+
*
|
|
169
|
+
* // Await promise to run the below code in a new task.
|
|
170
|
+
* // That will push the changes to the database.
|
|
171
|
+
* await Promise.resolve();
|
|
172
|
+
*
|
|
173
|
+
* // Here, the expense modifications have been have been committed
|
|
174
|
+
* // (unless you had previously called subscription.beginTransaction()).
|
|
175
|
+
* // The data is available in subscriptions running on a different device too.
|
|
176
|
+
*
|
|
177
|
+
* subscription.close();
|
|
178
|
+
* // If you create a new subscription with the same document within a couple of 100ms,
|
|
179
|
+
* // The subscription hasn't been closed and the old one is returned so that the data
|
|
180
|
+
* // is available instantly. This is especially useful in the context of frontend frameworks.
|
|
181
|
+
* const subscription2 = OrmSubscription.getOrCreate(ExpenseShapeType, {graphs: [graphIri]});
|
|
182
|
+
*
|
|
183
|
+
* subscription2.signalObject.add({
|
|
184
|
+
* "@graph": graphIri,
|
|
185
|
+
* "@id": "", // Leave empty to auto-assign one.
|
|
186
|
+
* name": "A new expense",
|
|
187
|
+
* description: "A new description"
|
|
188
|
+
* });
|
|
189
|
+
*
|
|
190
|
+
* subscription2.close()
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
static getOrCreate = (shapeType, scope) => {
|
|
194
|
+
const scopeKey = canonicalScope(scope);
|
|
195
|
+
// Unique identifier for a given shape type and scope.
|
|
196
|
+
const identifier = `${shapeType.shape}|${scopeKey}`;
|
|
197
|
+
// If we already have an object for this shape+scope,
|
|
198
|
+
// return it and just increase the reference count.
|
|
199
|
+
// Otherwise, create new one.
|
|
200
|
+
const existingConnection = OrmSubscription.idToEntry.get(identifier);
|
|
201
|
+
if (existingConnection) {
|
|
202
|
+
existingConnection.refCount += 1;
|
|
203
|
+
return existingConnection;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const newConnection = new OrmSubscription(shapeType, scope);
|
|
207
|
+
OrmSubscription.idToEntry.set(identifier, newConnection);
|
|
208
|
+
return newConnection;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Stop the subscription.
|
|
213
|
+
*
|
|
214
|
+
* **If there is more than one subscription with the same shape type and scope
|
|
215
|
+
* the orm subscription will persist.**
|
|
216
|
+
*
|
|
217
|
+
* Additionally, the closing of the subscription is delayed by a couple hundred milliseconds
|
|
218
|
+
* so that when frontend frameworks unmount and soon mount a component again with the same
|
|
219
|
+
* shape type and scope, we reuse the same orm subscription.
|
|
220
|
+
*/
|
|
221
|
+
close = () => {
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
if (this.refCount > 0)
|
|
224
|
+
this.refCount--;
|
|
225
|
+
if (this.refCount === 0) {
|
|
226
|
+
OrmSubscription.idToEntry.delete(this.identifier);
|
|
227
|
+
OrmSubscription.cleanupSignalRegistry?.unregister(this.signalObject);
|
|
228
|
+
this.closeOrmSubscription();
|
|
229
|
+
}
|
|
230
|
+
}, WAIT_BEFORE_CLOSE);
|
|
231
|
+
};
|
|
232
|
+
/** Handle updates (patches) coming from signal object modifications. */
|
|
233
|
+
onSignalObjectUpdate = async ({ patches }) => {
|
|
234
|
+
if (this.suspendDeepWatcher || !patches.length)
|
|
235
|
+
return;
|
|
236
|
+
const ormPatches = deepPatchesToWasm(patches);
|
|
237
|
+
// If in transaction, collect patches immediately (no await before).
|
|
238
|
+
if (this.inTransaction) {
|
|
239
|
+
this.pendingPatches?.push(...ormPatches);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Wait for session and subscription to be initialized.
|
|
243
|
+
const { ng, session } = await ngSession;
|
|
244
|
+
await this.readyPromise;
|
|
245
|
+
ng.graph_orm_update(this.subscriptionId, ormPatches, session.session_id);
|
|
246
|
+
};
|
|
247
|
+
/** Handle messages coming from the engine (initial data or patches). */
|
|
248
|
+
onBackendMessage = (message) => {
|
|
249
|
+
const data = message?.V0;
|
|
250
|
+
if (data?.GraphOrmInitial) {
|
|
251
|
+
this.handleInitialResponse(data.GraphOrmInitial);
|
|
252
|
+
}
|
|
253
|
+
else if (data?.GraphOrmUpdate) {
|
|
254
|
+
this.onBackendUpdate(data.GraphOrmUpdate);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.warn("Received unknown ORM message from engine", message);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
handleInitialResponse = ([initialData, subscriptionId]) => {
|
|
261
|
+
this.subscriptionId = subscriptionId;
|
|
262
|
+
// Assign initial data to empty signal object without triggering watcher at first.
|
|
263
|
+
this.suspendDeepWatcher = true;
|
|
264
|
+
batch(() => {
|
|
265
|
+
// Note: Instead, we await for the connection to be initialized and send patches after. So no need to remove.
|
|
266
|
+
// // Do this in case the there was any (incorrect) data added before initialization.
|
|
267
|
+
// this.signalObject.clear();
|
|
268
|
+
// Convert arrays to sets and apply to signalObject (we only have sets but can only transport arrays).
|
|
269
|
+
for (const newItem of parseOrmInitialObject(initialData)) {
|
|
270
|
+
this.signalObject.add(newItem);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
queueMicrotask(() => {
|
|
274
|
+
this.suspendDeepWatcher = false;
|
|
275
|
+
// Resolve readiness after initial data is committed and watcher armed.
|
|
276
|
+
this.resolveReady();
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
/** Handle incoming patches from the engine */
|
|
280
|
+
onBackendUpdate = (patches) => {
|
|
281
|
+
this.suspendDeepWatcher = true;
|
|
282
|
+
applyPatchesToDeepSignal(this.signalObject, patches, "set");
|
|
283
|
+
// Use queueMicrotask to ensure watcher is re-enabled _after_ batch completes
|
|
284
|
+
queueMicrotask(() => {
|
|
285
|
+
this.suspendDeepWatcher = false;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
/** Function to create random subject NURIs for newly created nested objects. */
|
|
289
|
+
signalObjectPropGenerator = ({ path, object, }) => {
|
|
290
|
+
let graphIri = undefined;
|
|
291
|
+
let subjectIri = undefined;
|
|
292
|
+
// If no @graph is set, add the parent's graph NURI. If there is no parent, throw.
|
|
293
|
+
if (!object["@graph"] || object["@graph"] === "") {
|
|
294
|
+
if (path.length > 1) {
|
|
295
|
+
// The first part of the path is the <graphNuri>|<subjectIri> composition.
|
|
296
|
+
graphIri = path[0].split("|")[0];
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
throw new Error("When adding new root orm objects, you must specify the @graph");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
graphIri = object["@graph"];
|
|
304
|
+
}
|
|
305
|
+
if (object["@id"] && object["@id"] !== "") {
|
|
306
|
+
subjectIri = object["@id"];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Generate 33 random bytes using Web Crypto API
|
|
310
|
+
const b = new Uint8Array(33);
|
|
311
|
+
crypto.getRandomValues(b);
|
|
312
|
+
// Convert to base64url
|
|
313
|
+
const base64url = (bytes) => btoa(String.fromCharCode(...bytes))
|
|
314
|
+
.replace(/\+/g, "-")
|
|
315
|
+
.replace(/\//g, "_")
|
|
316
|
+
.replace(/=+$/, "");
|
|
317
|
+
const randomString = base64url(b);
|
|
318
|
+
// We use the root subject's graph as the basis.
|
|
319
|
+
// TODO: We could use the closest parent's graph instead.
|
|
320
|
+
subjectIri =
|
|
321
|
+
(path[0] ?? graphIri).substring(0, 9 + 44) +
|
|
322
|
+
":q:" +
|
|
323
|
+
randomString;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
extraProps: { "@id": subjectIri, "@graph": graphIri },
|
|
327
|
+
syntheticId: graphIri + "|" + subjectIri,
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Begins a transaction that batches changes to be committed to the database.
|
|
332
|
+
* This is useful for performance reasons.
|
|
333
|
+
*
|
|
334
|
+
* Note that this does not disable reactivity of the `signalObject`.
|
|
335
|
+
* Modifications keep being rendered.
|
|
336
|
+
*/
|
|
337
|
+
beginTransaction = () => {
|
|
338
|
+
this.inTransaction = true;
|
|
339
|
+
this.pendingPatches = [];
|
|
340
|
+
// Use a listener that immediately triggers on object modifications.
|
|
341
|
+
// We don't need the deep-signal's batching (through microtasks) here.
|
|
342
|
+
this.stopSignalListening();
|
|
343
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate, { triggerInstantly: true });
|
|
344
|
+
this.stopSignalListening = stopListening;
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Commits a transactions sending all modifications made during the transaction
|
|
348
|
+
* (started with `beginTransaction`) to the database.
|
|
349
|
+
*/
|
|
350
|
+
commitTransaction = async () => {
|
|
351
|
+
if (!this.inTransaction) {
|
|
352
|
+
throw new Error("No transaction is open. Call `beginTransaction` first.");
|
|
353
|
+
}
|
|
354
|
+
const { ng, session } = await ngSession;
|
|
355
|
+
await this.readyPromise;
|
|
356
|
+
this.inTransaction = false;
|
|
357
|
+
if (this.pendingPatches?.length == 0) {
|
|
358
|
+
// Nothing to send to the engine.
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Send patches to engine.
|
|
362
|
+
await ng.graph_orm_update(this.subscriptionId, this.pendingPatches, session.session_id);
|
|
363
|
+
}
|
|
364
|
+
this.pendingPatches = undefined;
|
|
365
|
+
// Go back to the regular object modification listening where we want batching
|
|
366
|
+
// scheduled in a microtask only triggered after the main task.
|
|
367
|
+
// This way we prevent excessive calls to the engine.
|
|
368
|
+
this.stopSignalListening();
|
|
369
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
|
|
370
|
+
this.stopSignalListening = stopListening;
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const parseOrmInitialObject = (obj) => {
|
|
374
|
+
// Regular arrays become sets.
|
|
375
|
+
if (Array.isArray(obj)) {
|
|
376
|
+
return new Set(obj.map(parseOrmInitialObject));
|
|
377
|
+
}
|
|
378
|
+
else if (obj && typeof obj === "object") {
|
|
379
|
+
if ("@id" in obj) {
|
|
380
|
+
// Regular object.
|
|
381
|
+
for (const key of Object.keys(obj)) {
|
|
382
|
+
obj[key] = parseOrmInitialObject(obj[key]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// Object does not have @id, that means it's a set of objects.
|
|
387
|
+
return new Set(Object.values(obj).map(parseOrmInitialObject));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return obj;
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Creates a string out of the scope in the format
|
|
394
|
+
* `graphIri1,graphIri2|subjectIri1,subjectIri2`
|
|
395
|
+
*/
|
|
396
|
+
function canonicalScope(scope) {
|
|
397
|
+
if (!scope)
|
|
398
|
+
return "";
|
|
399
|
+
return `${(scope.graphs || []).slice().sort().join(",")}|${(scope.subjects || []).slice().sort().join(",")}`;
|
|
400
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DeepPatch } from "@ng-org/alien-deepsignals";
|
|
2
|
+
import { Patch } from "./applyPatches.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Deep clone function with support for Sets and Maps.
|
|
5
|
+
* Function generated by Claude Opus 4.5
|
|
6
|
+
* @ignore
|
|
7
|
+
*/
|
|
8
|
+
export declare const deepClone: <T>(object: T, seen?: WeakMap<WeakKey, any>) => T;
|
|
9
|
+
/**
|
|
10
|
+
* Converts DeepSignal patches to ORM Wasm-compatible patches
|
|
11
|
+
* @param patches DeepSignal patches
|
|
12
|
+
* @returns Patches with stringified path
|
|
13
|
+
*
|
|
14
|
+
* @ignore
|
|
15
|
+
*/
|
|
16
|
+
export declare function deepPatchesToWasm(patches: DeepPatch[]): Patch[];
|
|
17
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/connector/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,4BAAoB,KAAG,CAwD9D,CAAC;AAEF;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,CAO/D"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep clone function with support for Sets and Maps.
|
|
3
|
+
* Function generated by Claude Opus 4.5
|
|
4
|
+
* @ignore
|
|
5
|
+
*/
|
|
6
|
+
export const deepClone = (object, seen = new WeakMap()) => {
|
|
7
|
+
// Handle primitives and null
|
|
8
|
+
if (object === null || typeof object !== "object") {
|
|
9
|
+
return object;
|
|
10
|
+
}
|
|
11
|
+
// Handle circular references
|
|
12
|
+
if (seen.has(object)) {
|
|
13
|
+
return seen.get(object);
|
|
14
|
+
}
|
|
15
|
+
// Handle Date
|
|
16
|
+
if (object instanceof Date) {
|
|
17
|
+
return new Date(object.getTime());
|
|
18
|
+
}
|
|
19
|
+
// Handle Set
|
|
20
|
+
if (object instanceof Set) {
|
|
21
|
+
const clonedSet = new Set();
|
|
22
|
+
seen.set(object, clonedSet);
|
|
23
|
+
for (const item of object) {
|
|
24
|
+
clonedSet.add(deepClone(item, seen));
|
|
25
|
+
}
|
|
26
|
+
return clonedSet;
|
|
27
|
+
}
|
|
28
|
+
// Handle Map
|
|
29
|
+
if (object instanceof Map) {
|
|
30
|
+
const clonedMap = new Map();
|
|
31
|
+
seen.set(object, clonedMap);
|
|
32
|
+
for (const [key, value] of object) {
|
|
33
|
+
clonedMap.set(deepClone(key, seen), deepClone(value, seen));
|
|
34
|
+
}
|
|
35
|
+
return clonedMap;
|
|
36
|
+
}
|
|
37
|
+
// Handle Array
|
|
38
|
+
if (Array.isArray(object)) {
|
|
39
|
+
const clonedArray = [];
|
|
40
|
+
seen.set(object, clonedArray);
|
|
41
|
+
for (const item of object) {
|
|
42
|
+
clonedArray.push(deepClone(item, seen));
|
|
43
|
+
}
|
|
44
|
+
return clonedArray;
|
|
45
|
+
}
|
|
46
|
+
// Handle plain objects
|
|
47
|
+
const clonedObject = {};
|
|
48
|
+
seen.set(object, clonedObject);
|
|
49
|
+
for (const key of Object.keys(object)) {
|
|
50
|
+
clonedObject[key] = deepClone(object[key], seen);
|
|
51
|
+
}
|
|
52
|
+
return clonedObject;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Converts DeepSignal patches to ORM Wasm-compatible patches
|
|
56
|
+
* @param patches DeepSignal patches
|
|
57
|
+
* @returns Patches with stringified path
|
|
58
|
+
*
|
|
59
|
+
* @ignore
|
|
60
|
+
*/
|
|
61
|
+
export function deepPatchesToWasm(patches) {
|
|
62
|
+
return patches.flatMap((patch) => {
|
|
63
|
+
if (patch.op === "add" && patch.type === "set" && !patch.value?.length)
|
|
64
|
+
return [];
|
|
65
|
+
const path = "/" + patch.path.join("/");
|
|
66
|
+
return { ...patch, path };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/frontendAdapters/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/frontendAdapters/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
2
|
+
import { DiscreteRoot } from "../../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Hook to subscribe to an existing discrete (JSON) CRDT document.
|
|
5
|
+
* You can modify the returned object like any other JSON object. Changes are immediately
|
|
6
|
+
* reflected in the CRDT document.
|
|
7
|
+
*
|
|
8
|
+
* Establishes a 2-way binding: Modifications to the object are immediately committed;
|
|
9
|
+
* changes coming from the engine (or other components) cause an immediate rerender.
|
|
10
|
+
*
|
|
11
|
+
* In comparison to {@link reactUseShape}, discrete CRDTs are untyped.
|
|
12
|
+
* You can put any JSON data inside and need to validate the schema yourself.
|
|
13
|
+
*
|
|
14
|
+
* @param documentId The NURI of the CRDT document.
|
|
15
|
+
* @returns An object that contains as `doc` the reactive DeepSignal object or undefined if `documentId` is undefined.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // We assume you have created a CRDT document already, as below.
|
|
20
|
+
* // const documentId = await ng.doc_create(
|
|
21
|
+
* // session_id,
|
|
22
|
+
* // crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
|
|
23
|
+
* // crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
|
|
24
|
+
* // "store",
|
|
25
|
+
* // undefined
|
|
26
|
+
* // );
|
|
27
|
+
*
|
|
28
|
+
* function Expenses({documentId}: {documentId: string}) {
|
|
29
|
+
* const { doc } = useDiscrete(documentId);
|
|
30
|
+
*
|
|
31
|
+
* // If the CRDT document is still empty, we need to initialize it.
|
|
32
|
+
* if (doc && !doc.expenses) {
|
|
33
|
+
* doc.expenses = [];
|
|
34
|
+
* }
|
|
35
|
+
* const expenses = doc?.expenses;
|
|
36
|
+
*
|
|
37
|
+
* const createExpense = useCallback(() => {
|
|
38
|
+
* // Note that we use *expense["@id"]* as a key in the expense list.
|
|
39
|
+
* // Every object added to a CRDT array gets a stable `@id` property assigned
|
|
40
|
+
* // which you can use for referencing objects in arrays even as
|
|
41
|
+
* // objects are removed or added from the array.
|
|
42
|
+
* // The `@id` is a NURI with the schema `<documentId>:d:<object-specific id>`.
|
|
43
|
+
* // Since the `@id` is generated in the engine, the object is
|
|
44
|
+
* // *preliminarily given a mock id* which will be replaced immediately.
|
|
45
|
+
* expenses.push({
|
|
46
|
+
* title: "New expense",
|
|
47
|
+
* date: new Date().toISOString(),
|
|
48
|
+
* });
|
|
49
|
+
* },
|
|
50
|
+
* [expenses]
|
|
51
|
+
* );
|
|
52
|
+
*
|
|
53
|
+
* // Still loading?
|
|
54
|
+
* if (!doc) return <div>Loading...</div>;
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <div>
|
|
58
|
+
* <button
|
|
59
|
+
* onClick={() => createExpense()}
|
|
60
|
+
* >
|
|
61
|
+
* + Add expense
|
|
62
|
+
* </button>
|
|
63
|
+
* <div>
|
|
64
|
+
* {expenses.length === 0 ? (
|
|
65
|
+
* <p>
|
|
66
|
+
* No expenses yet.
|
|
67
|
+
* </p>
|
|
68
|
+
* ) : (
|
|
69
|
+
* expenses.map((expense) => (
|
|
70
|
+
* <ExpenseCard
|
|
71
|
+
* key={expense["@id"]}
|
|
72
|
+
* expense={expense}
|
|
73
|
+
* />
|
|
74
|
+
* ))
|
|
75
|
+
* )}
|
|
76
|
+
* </div>
|
|
77
|
+
* </div>
|
|
78
|
+
* );
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* ---
|
|
83
|
+
* In the ExpenseCard component:
|
|
84
|
+
* ```tsx
|
|
85
|
+
* function ExpenseCard({expense}: {expense: Expense}) {
|
|
86
|
+
* return (
|
|
87
|
+
* <input
|
|
88
|
+
* value={expense.title}
|
|
89
|
+
* onChange={(e) => {
|
|
90
|
+
* expense.title = e.target.value; // Changes trigger rerender.
|
|
91
|
+
* }}
|
|
92
|
+
* />
|
|
93
|
+
* <div>
|
|
94
|
+
* <p>Date</p>
|
|
95
|
+
* <p>{expense.doc}
|
|
96
|
+
* </div
|
|
97
|
+
* );
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export declare function useDiscrete<T extends DiscreteRoot = DiscreteRoot>(documentId: string | undefined): {
|
|
102
|
+
doc: DeepSignal<T> | undefined;
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=useDiscrete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDiscrete.d.ts","sourceRoot":"","sources":["../../../src/frontendAdapters/react/useDiscrete.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAI9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiGG;AAEH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAC7D,UAAU,EAAE,MAAM,GAAG,SAAS;;EAsDjC"}
|