@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.
Files changed (89) hide show
  1. package/README.md +151 -230
  2. package/dist/connector/applyPatches.d.ts +19 -49
  3. package/dist/connector/applyPatches.d.ts.map +1 -1
  4. package/dist/connector/applyPatches.js +57 -58
  5. package/dist/connector/discrete/discreteOrmSubscriptionHandler.d.ts +168 -0
  6. package/dist/connector/discrete/discreteOrmSubscriptionHandler.d.ts.map +1 -0
  7. package/dist/connector/discrete/discreteOrmSubscriptionHandler.js +309 -0
  8. package/dist/connector/getObjects.d.ts +11 -0
  9. package/dist/connector/getObjects.d.ts.map +1 -0
  10. package/dist/connector/getObjects.js +26 -0
  11. package/dist/connector/initNg.d.ts +33 -2
  12. package/dist/connector/initNg.d.ts.map +1 -1
  13. package/dist/connector/initNg.js +30 -0
  14. package/dist/connector/insertObject.d.ts +9 -0
  15. package/dist/connector/insertObject.d.ts.map +1 -0
  16. package/dist/connector/insertObject.js +24 -0
  17. package/dist/connector/ormSubscriptionHandler.d.ts +167 -0
  18. package/dist/connector/ormSubscriptionHandler.d.ts.map +1 -0
  19. package/dist/connector/ormSubscriptionHandler.js +400 -0
  20. package/dist/connector/utils.d.ts +17 -0
  21. package/dist/connector/utils.d.ts.map +1 -0
  22. package/dist/connector/utils.js +68 -0
  23. package/dist/frontendAdapters/react/index.d.ts +2 -1
  24. package/dist/frontendAdapters/react/index.d.ts.map +1 -1
  25. package/dist/frontendAdapters/react/index.js +2 -1
  26. package/dist/frontendAdapters/react/useDiscrete.d.ts +104 -0
  27. package/dist/frontendAdapters/react/useDiscrete.d.ts.map +1 -0
  28. package/dist/frontendAdapters/react/useDiscrete.js +151 -0
  29. package/dist/frontendAdapters/react/useShape.d.ts +69 -2
  30. package/dist/frontendAdapters/react/useShape.d.ts.map +1 -1
  31. package/dist/frontendAdapters/react/useShape.js +89 -8
  32. package/dist/frontendAdapters/svelte/index.d.ts +2 -1
  33. package/dist/frontendAdapters/svelte/index.d.ts.map +1 -1
  34. package/dist/frontendAdapters/svelte/index.js +2 -1
  35. package/dist/frontendAdapters/svelte/useDiscrete.svelte.d.ts +94 -0
  36. package/dist/frontendAdapters/svelte/useDiscrete.svelte.d.ts.map +1 -0
  37. package/dist/frontendAdapters/svelte/useDiscrete.svelte.js +137 -0
  38. package/dist/frontendAdapters/svelte/useShape.svelte.d.ts +79 -10
  39. package/dist/frontendAdapters/svelte/useShape.svelte.d.ts.map +1 -1
  40. package/dist/frontendAdapters/svelte/useShape.svelte.js +88 -9
  41. package/dist/frontendAdapters/svelte4/index.d.ts +5 -0
  42. package/dist/frontendAdapters/svelte4/index.d.ts.map +1 -0
  43. package/dist/{connector/createSignalObjectForShape.js → frontendAdapters/svelte4/index.js} +3 -15
  44. package/dist/frontendAdapters/svelte4/useDiscrete.svelte.d.ts +84 -0
  45. package/dist/frontendAdapters/svelte4/useDiscrete.svelte.d.ts.map +1 -0
  46. package/dist/frontendAdapters/svelte4/useDiscrete.svelte.js +126 -0
  47. package/dist/frontendAdapters/svelte4/useShape.svelte.d.ts +77 -0
  48. package/dist/frontendAdapters/svelte4/useShape.svelte.d.ts.map +1 -0
  49. package/dist/frontendAdapters/svelte4/useShape.svelte.js +91 -0
  50. package/dist/frontendAdapters/utils.d.ts +2 -0
  51. package/dist/frontendAdapters/utils.d.ts.map +1 -0
  52. package/dist/frontendAdapters/utils.js +14 -0
  53. package/dist/frontendAdapters/vue/index.d.ts +2 -1
  54. package/dist/frontendAdapters/vue/index.d.ts.map +1 -1
  55. package/dist/frontendAdapters/vue/index.js +2 -1
  56. package/dist/frontendAdapters/vue/useDiscrete.d.ts +99 -0
  57. package/dist/frontendAdapters/vue/useDiscrete.d.ts.map +1 -0
  58. package/dist/frontendAdapters/vue/useDiscrete.js +123 -0
  59. package/dist/frontendAdapters/vue/useShape.d.ts +73 -2
  60. package/dist/frontendAdapters/vue/useShape.d.ts.map +1 -1
  61. package/dist/frontendAdapters/vue/useShape.js +79 -4
  62. package/dist/index.d.ts +13 -6
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +21 -6
  65. package/dist/types.d.ts +63 -11
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js +13 -1
  68. package/package.json +37 -48
  69. package/dist/connector/applyPatches.test.d.ts +0 -2
  70. package/dist/connector/applyPatches.test.d.ts.map +0 -1
  71. package/dist/connector/applyPatches.test.js +0 -772
  72. package/dist/connector/createSignalObjectForShape.d.ts +0 -14
  73. package/dist/connector/createSignalObjectForShape.d.ts.map +0 -1
  74. package/dist/connector/ormConnectionHandler.d.ts +0 -47
  75. package/dist/connector/ormConnectionHandler.d.ts.map +0 -1
  76. package/dist/connector/ormConnectionHandler.js +0 -240
  77. package/src/connector/applyPatches.test.ts +0 -842
  78. package/src/connector/applyPatches.ts +0 -404
  79. package/src/connector/createSignalObjectForShape.ts +0 -35
  80. package/src/connector/initNg.ts +0 -30
  81. package/src/connector/ormConnectionHandler.ts +0 -300
  82. package/src/frontendAdapters/react/index.ts +0 -2
  83. package/src/frontendAdapters/react/useShape.ts +0 -36
  84. package/src/frontendAdapters/svelte/index.ts +0 -2
  85. package/src/frontendAdapters/svelte/useShape.svelte.ts +0 -46
  86. package/src/frontendAdapters/vue/index.ts +0 -2
  87. package/src/frontendAdapters/vue/useShape.ts +0 -33
  88. package/src/index.ts +0 -14
  89. 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,3 +1,4 @@
1
1
  import useShape from "./useShape.ts";
2
- export { useShape };
2
+ import { useDiscrete } from "./useDiscrete.ts";
3
+ export { useShape, useDiscrete };
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -1,2 +1,3 @@
1
1
  import useShape from "./useShape.js";
2
- export { useShape };
2
+ import { useDiscrete } from "./useDiscrete.js";
3
+ export { useShape, useDiscrete };
@@ -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"}