@ng-org/orm 0.1.2-alpha.5 → 0.1.2-alpha.7
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 +1 -1
- package/dist/connector/applyPatches.d.ts +6 -11
- package/dist/connector/applyPatches.d.ts.map +1 -1
- package/dist/connector/applyPatches.js +48 -19
- package/dist/connector/discrete/discreteOrmConnectionHandler.d.ts +45 -0
- package/dist/connector/discrete/discreteOrmConnectionHandler.d.ts.map +1 -0
- package/dist/connector/discrete/discreteOrmConnectionHandler.js +186 -0
- package/dist/connector/getObjects.d.ts +10 -0
- package/dist/connector/getObjects.d.ts.map +1 -0
- package/dist/connector/getObjects.js +25 -0
- package/dist/connector/insertObject.d.ts +8 -0
- package/dist/connector/insertObject.d.ts.map +1 -0
- package/dist/connector/{createSignalObjectForShape.js → insertObject.js} +10 -11
- package/dist/connector/ormConnectionHandler.d.ts +14 -13
- package/dist/connector/ormConnectionHandler.d.ts.map +1 -1
- package/dist/connector/ormConnectionHandler.js +74 -40
- package/dist/connector/utils.d.ts +14 -0
- package/dist/connector/utils.d.ts.map +1 -0
- package/dist/connector/utils.js +65 -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 +84 -0
- package/dist/frontendAdapters/react/useDiscrete.d.ts.map +1 -0
- package/dist/frontendAdapters/react/useDiscrete.js +127 -0
- package/dist/frontendAdapters/react/useShape.d.ts +64 -5
- package/dist/frontendAdapters/react/useShape.d.ts.map +1 -1
- package/dist/frontendAdapters/react/useShape.js +84 -14
- 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 +85 -0
- package/dist/frontendAdapters/svelte/useDiscrete.svelte.d.ts.map +1 -0
- package/dist/frontendAdapters/svelte/useDiscrete.svelte.js +124 -0
- package/dist/frontendAdapters/svelte/useShape.svelte.d.ts +65 -3
- package/dist/frontendAdapters/svelte/useShape.svelte.d.ts.map +1 -1
- package/dist/frontendAdapters/svelte/useShape.svelte.js +68 -6
- 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 +92 -0
- package/dist/frontendAdapters/vue/useDiscrete.d.ts.map +1 -0
- package/dist/frontendAdapters/vue/useDiscrete.js +111 -0
- package/dist/frontendAdapters/vue/useShape.d.ts +76 -1
- package/dist/frontendAdapters/vue/useShape.d.ts.map +1 -1
- package/dist/frontendAdapters/vue/useShape.js +79 -5
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/types.d.ts +48 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/package.json +4 -4
- 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
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { applyPatchesToDeepSignal } from "./applyPatches.js";
|
|
11
11
|
import { ngSession } from "./initNg.js";
|
|
12
12
|
import { deepSignal, watch as watchDeepSignal, batch, } from "@ng-org/alien-deepsignals";
|
|
13
|
+
import { deepPatchesToWasm } from "./utils.js";
|
|
14
|
+
const WAIT_BEFORE_CLOSE = 500;
|
|
13
15
|
export class OrmConnection {
|
|
14
16
|
static idToEntry = new Map();
|
|
15
17
|
/**
|
|
@@ -17,38 +19,45 @@ export class OrmConnection {
|
|
|
17
19
|
* Useful when a hook unsubscribes and resubscribes in a short time interval
|
|
18
20
|
* so that no new connections need to be set up.
|
|
19
21
|
*/
|
|
20
|
-
WAIT_BEFORE_RELEASE = 500;
|
|
21
22
|
shapeType;
|
|
22
23
|
scope;
|
|
23
24
|
signalObject;
|
|
25
|
+
stopSignalListening;
|
|
26
|
+
subscriptionId;
|
|
24
27
|
refCount;
|
|
25
28
|
/** Identifier as a combination of shape type and scope. Prevents duplications. */
|
|
26
29
|
identifier;
|
|
27
30
|
suspendDeepWatcher;
|
|
31
|
+
inTransaction = false;
|
|
32
|
+
/** Aggregation of patches to be sent when in transaction. */
|
|
33
|
+
pendingPatches;
|
|
28
34
|
readyPromise;
|
|
29
|
-
|
|
35
|
+
closeOrmConnection;
|
|
30
36
|
/** Promise that resolves once initial data has been applied. */
|
|
31
37
|
resolveReady;
|
|
32
38
|
// FinalizationRegistry to clean up connections when signal objects are GC'd.
|
|
33
39
|
static cleanupSignalRegistry = typeof FinalizationRegistry === "function"
|
|
34
40
|
? new FinalizationRegistry((connectionId) => {
|
|
41
|
+
console.log("finalization called for", connectionId);
|
|
35
42
|
// Best-effort fallback; look up by id and clean
|
|
36
43
|
const entry = this.idToEntry.get(connectionId);
|
|
37
|
-
|
|
44
|
+
console.log("cleaning up connection", connectionId);
|
|
38
45
|
if (!entry)
|
|
39
46
|
return;
|
|
40
|
-
entry.
|
|
47
|
+
entry.close();
|
|
41
48
|
})
|
|
42
49
|
: null;
|
|
43
50
|
constructor(shapeType, scope) {
|
|
44
51
|
// @ts-expect-error
|
|
45
52
|
window.ormSignalConnections = OrmConnection.idToEntry;
|
|
53
|
+
// @ts-expect-error
|
|
54
|
+
window.OrmConnection = OrmConnection;
|
|
46
55
|
this.shapeType = shapeType;
|
|
47
56
|
this.scope = scope;
|
|
48
57
|
this.refCount = 1;
|
|
49
|
-
this.
|
|
58
|
+
this.closeOrmConnection = () => { };
|
|
50
59
|
this.suspendDeepWatcher = false;
|
|
51
|
-
this.identifier = `${shapeType.shape}
|
|
60
|
+
this.identifier = `${shapeType.shape}|${canonicalScope(scope)}`;
|
|
52
61
|
this.signalObject = deepSignal(new Set(), {
|
|
53
62
|
propGenerator: this.signalObjectPropGenerator,
|
|
54
63
|
// Don't set syntheticIdPropertyName - let propGenerator handle all ID logic
|
|
@@ -57,16 +66,15 @@ export class OrmConnection {
|
|
|
57
66
|
// Schedule cleanup of the connection when the signal object is GC'd.
|
|
58
67
|
OrmConnection.cleanupSignalRegistry?.register(this.signalObject, this.identifier, this.signalObject);
|
|
59
68
|
// Add listener to deep signal object to report changes back to wasm land.
|
|
60
|
-
watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
|
|
69
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
|
|
70
|
+
this.stopSignalListening = stopListening;
|
|
61
71
|
// Initialize per-entry readiness promise that resolves in setUpConnection
|
|
62
72
|
this.readyPromise = new Promise((resolve) => {
|
|
63
73
|
this.resolveReady = resolve;
|
|
64
74
|
});
|
|
65
75
|
ngSession.then(async ({ ng, session }) => {
|
|
66
|
-
//console.log("Creating orm connection. ng and session", ng, session);
|
|
67
76
|
try {
|
|
68
|
-
|
|
69
|
-
this.cancel = await ng.orm_start(scope.length == 0 ? "" : scope, shapeType, session.session_id, this.onBackendMessage);
|
|
77
|
+
this.closeOrmConnection = await ng.orm_start_graph(scope.graphs ?? ["did:ng:i"], scope.subjects ?? [], shapeType, session.session_id, this.onBackendMessage);
|
|
70
78
|
}
|
|
71
79
|
catch (e) {
|
|
72
80
|
console.error(e);
|
|
@@ -74,16 +82,16 @@ export class OrmConnection {
|
|
|
74
82
|
});
|
|
75
83
|
}
|
|
76
84
|
/**
|
|
77
|
-
* Get a connection which contains the ORM and lifecycle methods.
|
|
85
|
+
* Get or create a connection which contains the ORM and lifecycle methods.
|
|
78
86
|
* @param shapeType
|
|
79
87
|
* @param scope
|
|
80
88
|
* @param ng
|
|
81
89
|
* @returns
|
|
82
90
|
*/
|
|
83
|
-
static
|
|
91
|
+
static getOrCreate = (shapeType, scope) => {
|
|
84
92
|
const scopeKey = canonicalScope(scope);
|
|
85
93
|
// Unique identifier for a given shape type and scope.
|
|
86
|
-
const identifier = `${shapeType.shape}
|
|
94
|
+
const identifier = `${shapeType.shape}|${scopeKey}`;
|
|
87
95
|
// If we already have an object for this shape+scope,
|
|
88
96
|
// return it and just increase the reference count.
|
|
89
97
|
// Otherwise, create new one.
|
|
@@ -98,39 +106,45 @@ export class OrmConnection {
|
|
|
98
106
|
return newConnection;
|
|
99
107
|
}
|
|
100
108
|
};
|
|
101
|
-
|
|
109
|
+
close = () => {
|
|
102
110
|
setTimeout(() => {
|
|
103
111
|
if (this.refCount > 0)
|
|
104
112
|
this.refCount--;
|
|
105
113
|
if (this.refCount === 0) {
|
|
106
114
|
OrmConnection.idToEntry.delete(this.identifier);
|
|
107
115
|
OrmConnection.cleanupSignalRegistry?.unregister(this.signalObject);
|
|
108
|
-
this.
|
|
116
|
+
this.closeOrmConnection();
|
|
109
117
|
}
|
|
110
|
-
},
|
|
118
|
+
}, WAIT_BEFORE_CLOSE);
|
|
111
119
|
};
|
|
112
120
|
onSignalObjectUpdate = async ({ patches }) => {
|
|
113
121
|
if (this.suspendDeepWatcher || !patches.length)
|
|
114
122
|
return;
|
|
115
123
|
const ormPatches = deepPatchesToWasm(patches);
|
|
124
|
+
// If in transaction, collect patches immediately (no await before).
|
|
125
|
+
if (this.inTransaction) {
|
|
126
|
+
this.pendingPatches?.push(...ormPatches);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
116
129
|
// Wait for session and subscription to be initialized.
|
|
117
130
|
const { ng, session } = await ngSession;
|
|
118
131
|
await this.readyPromise;
|
|
119
|
-
ng.
|
|
132
|
+
ng.graph_orm_update(this.subscriptionId, ormPatches, session.session_id);
|
|
120
133
|
};
|
|
121
134
|
onBackendMessage = (message) => {
|
|
122
135
|
const data = message?.V0;
|
|
123
|
-
if (data?.
|
|
124
|
-
this.handleInitialResponse(data.
|
|
136
|
+
if (data?.GraphOrmInitial) {
|
|
137
|
+
this.handleInitialResponse(data.GraphOrmInitial);
|
|
125
138
|
}
|
|
126
|
-
else if (data?.
|
|
127
|
-
this.onBackendUpdate(data.
|
|
139
|
+
else if (data?.GraphOrmUpdate) {
|
|
140
|
+
this.onBackendUpdate(data.GraphOrmUpdate);
|
|
128
141
|
}
|
|
129
142
|
else {
|
|
130
143
|
console.warn("Received unknown ORM message from backend", message);
|
|
131
144
|
}
|
|
132
145
|
};
|
|
133
|
-
handleInitialResponse = (initialData) => {
|
|
146
|
+
handleInitialResponse = ([initialData, subscriptionId]) => {
|
|
147
|
+
this.subscriptionId = subscriptionId;
|
|
134
148
|
// Assign initial data to empty signal object without triggering watcher at first.
|
|
135
149
|
this.suspendDeepWatcher = true;
|
|
136
150
|
batch(() => {
|
|
@@ -150,7 +164,7 @@ export class OrmConnection {
|
|
|
150
164
|
};
|
|
151
165
|
onBackendUpdate = (patches) => {
|
|
152
166
|
this.suspendDeepWatcher = true;
|
|
153
|
-
applyPatchesToDeepSignal(this.signalObject, patches);
|
|
167
|
+
applyPatchesToDeepSignal(this.signalObject, patches, "set");
|
|
154
168
|
// Use queueMicrotask to ensure watcher is re-enabled _after_ batch completes
|
|
155
169
|
queueMicrotask(() => {
|
|
156
170
|
this.suspendDeepWatcher = false;
|
|
@@ -198,19 +212,37 @@ export class OrmConnection {
|
|
|
198
212
|
syntheticId: graphIri + "|" + subjectIri,
|
|
199
213
|
};
|
|
200
214
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
beginTransaction = () => {
|
|
216
|
+
this.inTransaction = true;
|
|
217
|
+
this.pendingPatches = [];
|
|
218
|
+
// Use a listener that immediately triggers on object modifications.
|
|
219
|
+
// We don't need the deep-signal's batching (through microtasks) here.
|
|
220
|
+
this.stopSignalListening();
|
|
221
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate, { triggerInstantly: true });
|
|
222
|
+
this.stopSignalListening = stopListening;
|
|
223
|
+
};
|
|
224
|
+
commitTransaction = async () => {
|
|
225
|
+
if (!this.inTransaction) {
|
|
226
|
+
throw new Error("No transaction is open. Call `beginTransaction` first.");
|
|
227
|
+
}
|
|
228
|
+
const { ng, session } = await ngSession;
|
|
229
|
+
await this.readyPromise;
|
|
230
|
+
this.inTransaction = false;
|
|
231
|
+
if (this.pendingPatches?.length == 0) {
|
|
232
|
+
// Nothing to send to the backend.
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Send patches to backend.
|
|
236
|
+
await ng.graph_orm_update(this.subscriptionId, this.pendingPatches, session.session_id);
|
|
237
|
+
}
|
|
238
|
+
this.pendingPatches = undefined;
|
|
239
|
+
// Go back to the regular object modification listening where we want batching
|
|
240
|
+
// scheduled in a microtask only triggered after the main task.
|
|
241
|
+
// This way we prevent excessive calls to the backend.
|
|
242
|
+
this.stopSignalListening();
|
|
243
|
+
const { stopListening } = watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
|
|
244
|
+
this.stopSignalListening = stopListening;
|
|
245
|
+
};
|
|
214
246
|
}
|
|
215
247
|
const parseOrmInitialObject = (obj) => {
|
|
216
248
|
// Regular arrays become sets.
|
|
@@ -231,10 +263,12 @@ const parseOrmInitialObject = (obj) => {
|
|
|
231
263
|
}
|
|
232
264
|
return obj;
|
|
233
265
|
};
|
|
266
|
+
/**
|
|
267
|
+
* Creates a string out of the scope in the format
|
|
268
|
+
* `graphIri1,graphIri2|subjectIri1,subjectIri2`
|
|
269
|
+
*/
|
|
234
270
|
function canonicalScope(scope) {
|
|
235
|
-
if (scope
|
|
271
|
+
if (!scope)
|
|
236
272
|
return "";
|
|
237
|
-
return
|
|
238
|
-
? scope.slice().sort().join(",")
|
|
239
|
-
: String(scope);
|
|
273
|
+
return `${(scope.graphs || []).slice().sort().join(",")}|${(scope.subjects || []).slice().sort().join(",")}`;
|
|
240
274
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
*/
|
|
7
|
+
export declare const deepClone: <T>(object: T, seen?: WeakMap<WeakKey, any>) => T;
|
|
8
|
+
/**
|
|
9
|
+
* Converts DeepSignal patches to ORM Wasm-compatible patches
|
|
10
|
+
* @param patches DeepSignal patches
|
|
11
|
+
* @returns Patches with stringified path
|
|
12
|
+
*/
|
|
13
|
+
export declare function deepPatchesToWasm(patches: DeepPatch[]): Patch[];
|
|
14
|
+
//# 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;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,4BAAoB,KAAG,CAwD9D,CAAC;AAEF;;;;GAIG;AAEH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,CAO/D"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep clone function with support for Sets and Maps.
|
|
3
|
+
* Function generated by Claude Opus 4.5
|
|
4
|
+
*/
|
|
5
|
+
export const deepClone = (object, seen = new WeakMap()) => {
|
|
6
|
+
// Handle primitives and null
|
|
7
|
+
if (object === null || typeof object !== "object") {
|
|
8
|
+
return object;
|
|
9
|
+
}
|
|
10
|
+
// Handle circular references
|
|
11
|
+
if (seen.has(object)) {
|
|
12
|
+
return seen.get(object);
|
|
13
|
+
}
|
|
14
|
+
// Handle Date
|
|
15
|
+
if (object instanceof Date) {
|
|
16
|
+
return new Date(object.getTime());
|
|
17
|
+
}
|
|
18
|
+
// Handle Set
|
|
19
|
+
if (object instanceof Set) {
|
|
20
|
+
const clonedSet = new Set();
|
|
21
|
+
seen.set(object, clonedSet);
|
|
22
|
+
for (const item of object) {
|
|
23
|
+
clonedSet.add(deepClone(item, seen));
|
|
24
|
+
}
|
|
25
|
+
return clonedSet;
|
|
26
|
+
}
|
|
27
|
+
// Handle Map
|
|
28
|
+
if (object instanceof Map) {
|
|
29
|
+
const clonedMap = new Map();
|
|
30
|
+
seen.set(object, clonedMap);
|
|
31
|
+
for (const [key, value] of object) {
|
|
32
|
+
clonedMap.set(deepClone(key, seen), deepClone(value, seen));
|
|
33
|
+
}
|
|
34
|
+
return clonedMap;
|
|
35
|
+
}
|
|
36
|
+
// Handle Array
|
|
37
|
+
if (Array.isArray(object)) {
|
|
38
|
+
const clonedArray = [];
|
|
39
|
+
seen.set(object, clonedArray);
|
|
40
|
+
for (const item of object) {
|
|
41
|
+
clonedArray.push(deepClone(item, seen));
|
|
42
|
+
}
|
|
43
|
+
return clonedArray;
|
|
44
|
+
}
|
|
45
|
+
// Handle plain objects
|
|
46
|
+
const clonedObject = {};
|
|
47
|
+
seen.set(object, clonedObject);
|
|
48
|
+
for (const key of Object.keys(object)) {
|
|
49
|
+
clonedObject[key] = deepClone(object[key], seen);
|
|
50
|
+
}
|
|
51
|
+
return clonedObject;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Converts DeepSignal patches to ORM Wasm-compatible patches
|
|
55
|
+
* @param patches DeepSignal patches
|
|
56
|
+
* @returns Patches with stringified path
|
|
57
|
+
*/
|
|
58
|
+
export function deepPatchesToWasm(patches) {
|
|
59
|
+
return patches.flatMap((patch) => {
|
|
60
|
+
if (patch.op === "add" && patch.type === "set" && !patch.value?.length)
|
|
61
|
+
return [];
|
|
62
|
+
const path = "/" + patch.path.join("/");
|
|
63
|
+
return { ...patch, path };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -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,84 @@
|
|
|
1
|
+
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
2
|
+
import { DiscreteArray, DiscreteObject } from "../../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Hook to subscribe to discrete (JSON) CRDT documents.
|
|
5
|
+
* You can modify the returned object like any other JSON object. Changes are immediately
|
|
6
|
+
* reflected in the CRDT.
|
|
7
|
+
*
|
|
8
|
+
* Establishes a 2-way binding: Modifications to the object are immediately committed,
|
|
9
|
+
* changes coming from the backend (or other components) cause an immediate rerender.
|
|
10
|
+
*
|
|
11
|
+
* In comparison to {@link useShape}, discrete CRDTs are untyped.
|
|
12
|
+
* You can put any JSON data inside and need to validate the schema yourself.
|
|
13
|
+
*
|
|
14
|
+
* @param documentId The IRI of the crdt document.
|
|
15
|
+
* @returns An object that contains as `data` 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 { data } = useDiscrete(documentIdPromise);
|
|
30
|
+
|
|
31
|
+
// If the CRDT document is still empty, we need to initialize it.
|
|
32
|
+
if (data && !data.expenses) {
|
|
33
|
+
data.expenses = [];
|
|
34
|
+
}
|
|
35
|
+
const expenses = data?.expenses;
|
|
36
|
+
|
|
37
|
+
const createExpense = useCallback(() => {
|
|
38
|
+
expenses.add({
|
|
39
|
+
title: "New expense",
|
|
40
|
+
dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
[expenses]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Loaded already?
|
|
47
|
+
if (!expenses) return <div>Loading...</div>;
|
|
48
|
+
|
|
49
|
+
// Note that we use expense["@id"] as a key in the expense list.
|
|
50
|
+
// Every object added to a CRDT array gets a stable `@id` property assigned
|
|
51
|
+
// which you can use for referencing objects in arrays even as
|
|
52
|
+
// objects are removed from the array. The ID is an IRI with the schema `<documentId>:d:<object-specific id>`.
|
|
53
|
+
// Since the `@id` is generated in the backend, the object is preliminarily
|
|
54
|
+
// given a mock id which will be replaced immediately
|
|
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
|
+
export declare function useDiscrete(documentId: string | undefined): {
|
|
82
|
+
data: DeepSignal<DiscreteArray | DiscreteObject> | undefined;
|
|
83
|
+
};
|
|
84
|
+
//# 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,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAI/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AAEH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS;;EAmDzD"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) 2026 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 { useEffect, useMemo, useRef } from "react";
|
|
11
|
+
import { DiscreteOrmConnection } from "../../connector/discrete/discreteOrmConnectionHandler.js";
|
|
12
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
|
|
13
|
+
const EMPTY_OBJECT = {};
|
|
14
|
+
/**
|
|
15
|
+
* Hook to subscribe to discrete (JSON) CRDT documents.
|
|
16
|
+
* You can modify the returned object like any other JSON object. Changes are immediately
|
|
17
|
+
* reflected in the CRDT.
|
|
18
|
+
*
|
|
19
|
+
* Establishes a 2-way binding: Modifications to the object are immediately committed,
|
|
20
|
+
* changes coming from the backend (or other components) cause an immediate rerender.
|
|
21
|
+
*
|
|
22
|
+
* In comparison to {@link useShape}, discrete CRDTs are untyped.
|
|
23
|
+
* You can put any JSON data inside and need to validate the schema yourself.
|
|
24
|
+
*
|
|
25
|
+
* @param documentId The IRI of the crdt document.
|
|
26
|
+
* @returns An object that contains as `data` the reactive DeepSignal object or undefined if `documentId` is undefined.
|
|
27
|
+
*
|
|
28
|
+
*@example
|
|
29
|
+
```tsx
|
|
30
|
+
// We assume you have created a CRDT document already, as below.
|
|
31
|
+
// const documentId = await ng.doc_create(
|
|
32
|
+
// session_id,
|
|
33
|
+
// crdt, // "Automerge" | "YMap" | "YArray". YArray is for root arrays, the other two have objects at root.
|
|
34
|
+
// crdt === "Automerge" ? "data:json" : crdt === "YMap ? "data:map" : "data:array",
|
|
35
|
+
// "store",
|
|
36
|
+
// undefined
|
|
37
|
+
// );
|
|
38
|
+
|
|
39
|
+
function Expenses({documentId}: {documentId: string}) {
|
|
40
|
+
const { data } = useDiscrete(documentIdPromise);
|
|
41
|
+
|
|
42
|
+
// If the CRDT document is still empty, we need to initialize it.
|
|
43
|
+
if (data && !data.expenses) {
|
|
44
|
+
data.expenses = [];
|
|
45
|
+
}
|
|
46
|
+
const expenses = data?.expenses;
|
|
47
|
+
|
|
48
|
+
const createExpense = useCallback(() => {
|
|
49
|
+
expenses.add({
|
|
50
|
+
title: "New expense",
|
|
51
|
+
dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[expenses]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Loaded already?
|
|
58
|
+
if (!expenses) return <div>Loading...</div>;
|
|
59
|
+
|
|
60
|
+
// Note that we use expense["@id"] as a key in the expense list.
|
|
61
|
+
// Every object added to a CRDT array gets a stable `@id` property assigned
|
|
62
|
+
// which you can use for referencing objects in arrays even as
|
|
63
|
+
// objects are removed from the array. The ID is an IRI with the schema `<documentId>:d:<object-specific id>`.
|
|
64
|
+
// Since the `@id` is generated in the backend, the object is preliminarily
|
|
65
|
+
// given a mock id which will be replaced immediately
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => createExpense()}
|
|
71
|
+
>
|
|
72
|
+
+ Add expense
|
|
73
|
+
</button>
|
|
74
|
+
<div>
|
|
75
|
+
{expenses.length === 0 ? (
|
|
76
|
+
<p>
|
|
77
|
+
No expenses yet.
|
|
78
|
+
</p>
|
|
79
|
+
) : (
|
|
80
|
+
expenses.map((expense) => (
|
|
81
|
+
<ExpenseCard
|
|
82
|
+
key={expense["@id"]}
|
|
83
|
+
expense={expense}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
*/
|
|
92
|
+
export function useDiscrete(documentId) {
|
|
93
|
+
const prevDocumentId = useRef(undefined);
|
|
94
|
+
const prevOrmConnection = useRef(undefined);
|
|
95
|
+
const ormConnection = useMemo(() => {
|
|
96
|
+
// Close previous connection if documentId changed.
|
|
97
|
+
if (prevOrmConnection.current &&
|
|
98
|
+
prevDocumentId.current !== documentId) {
|
|
99
|
+
prevOrmConnection.current.close();
|
|
100
|
+
prevOrmConnection.current = undefined;
|
|
101
|
+
}
|
|
102
|
+
// If no documentId, return undefined.
|
|
103
|
+
if (!documentId) {
|
|
104
|
+
prevDocumentId.current = undefined;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
// Create new connection only if needed.
|
|
108
|
+
if (!prevOrmConnection.current ||
|
|
109
|
+
prevDocumentId.current !== documentId) {
|
|
110
|
+
prevOrmConnection.current =
|
|
111
|
+
DiscreteOrmConnection.getOrCreate(documentId);
|
|
112
|
+
prevDocumentId.current = documentId;
|
|
113
|
+
}
|
|
114
|
+
return prevOrmConnection.current;
|
|
115
|
+
}, [documentId]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
return () => {
|
|
118
|
+
prevOrmConnection.current?.close();
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
// useDeepSignal requires an object, so pass empty object when no connection.
|
|
122
|
+
const signalSource = ormConnection?.signalObject ?? EMPTY_OBJECT;
|
|
123
|
+
const state = useDeepSignal(signalSource);
|
|
124
|
+
// Only return data if we have a valid connection with a signal object.
|
|
125
|
+
const data = ormConnection?.signalObject ? state : undefined;
|
|
126
|
+
return { data };
|
|
127
|
+
}
|
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
import type { BaseType } from "@ng-org/shex-orm";
|
|
2
2
|
import type { ShapeType } from "@ng-org/shex-orm";
|
|
3
3
|
import type { Scope } from "../../types.ts";
|
|
4
|
+
import { DeepSignalSet } from "@ng-org/alien-deepsignals";
|
|
4
5
|
/**
|
|
6
|
+
* Hook to subscribe to RDF data in the graph database using a shape, see {@link ShapeType}.
|
|
5
7
|
*
|
|
6
|
-
* @
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Returns a {@link DeepSignalSet} of objects matching the shape and that are within the scope.
|
|
9
|
+
* Establishes a 2-way binding: Modifications to the object are immediately committed,
|
|
10
|
+
* changes coming from the backend (or other components) cause an immediate rerender.
|
|
11
|
+
*
|
|
12
|
+
* @param shape The {@link ShapeType} the objects should have (generated by the shex-orm tool).
|
|
13
|
+
* @param scope The {@link Scope} as graph, array of graphs or scope object with graphs and subjects.
|
|
14
|
+
* @returns A deep {@link DeepSignalSet} with the orm objects or an empty set, if still loading.\
|
|
15
|
+
* If the scope is explicitly set to `undefined`, an empty set is returned which errors
|
|
16
|
+
* if you try to make modifications on it.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
```tsx
|
|
20
|
+
function Expenses() {
|
|
21
|
+
const expenses = useShape(ExpenseShapeType, {graphs: ["<graph IRI>"]});
|
|
22
|
+
|
|
23
|
+
const createExpense = useCallback(
|
|
24
|
+
() => {
|
|
25
|
+
expenses.add({
|
|
26
|
+
"@graph": `<graph IRI>`,
|
|
27
|
+
"@type": "http://example.org/Expense",
|
|
28
|
+
"@id": "", // Assigns id automatically, if set to "".
|
|
29
|
+
title: "New expense",
|
|
30
|
+
dateOfPurchase: obj.dateOfPurchase ?? new Date().toISOString(),
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
[expenses]
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const expensesSorted = [...expenses].sort((a, b) =>
|
|
37
|
+
a.dateOfPurchase.localeCompare(b.dateOfPurchase)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Not that if you use `@id` (the subject IRI) as key, you need to ensure that it is unique within your scope.
|
|
41
|
+
// If it is not, use the combination of `@graph` and `@id`.
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => createExpense({})}
|
|
47
|
+
>
|
|
48
|
+
+ Add expense
|
|
49
|
+
</button>
|
|
50
|
+
<div>
|
|
51
|
+
{expensesSorted.length === 0 ? (
|
|
52
|
+
<p>
|
|
53
|
+
No expenses yet.
|
|
54
|
+
</p>
|
|
55
|
+
) : (
|
|
56
|
+
expensesSorted.map((expense) => (
|
|
57
|
+
<ExpenseCard
|
|
58
|
+
key={expense["@id"]}
|
|
59
|
+
expense={expense}
|
|
60
|
+
availableCategories={expenseCategories}
|
|
61
|
+
/>
|
|
62
|
+
))
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
10
69
|
*/
|
|
11
|
-
declare const useShape: <T extends BaseType>(shape: ShapeType<T>, scope?: Scope |
|
|
70
|
+
declare const useShape: <T extends BaseType>(shape: ShapeType<T>, scope?: Scope | string[] | string) => DeepSignalSet<T>;
|
|
12
71
|
export default useShape;
|
|
13
72
|
//# sourceMappingURL=useShape.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useShape.d.ts","sourceRoot":"","sources":["../../../src/frontendAdapters/react/useShape.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"useShape.d.ts","sourceRoot":"","sources":["../../../src/frontendAdapters/react/useShape.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,QAAA,MAAM,QAAQ,GAAI,CAAC,SAAS,QAAQ,EAChC,OAAO,SAAS,CAAC,CAAC,CAAC,EACnB,QAAO,KAAK,GAAG,MAAM,EAAE,GAAG,MAAW,KA6BrB,aAAa,CAAC,CAAC,CAClC,CAAC;AAiBF,eAAe,QAAQ,CAAC"}
|