@ng-org/orm 0.1.2-alpha.1 → 0.1.2-alpha.2

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.
@@ -1,300 +0,0 @@
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
-
11
- import type { Diff as Patches, Scope } from "../types.ts";
12
- import { applyPatchesToDeepSignal, Patch } from "./applyPatches.ts";
13
-
14
- import { ngSession } from "./initNg.ts";
15
-
16
- import {
17
- deepSignal,
18
- watch as watchDeepSignal,
19
- batch,
20
- } from "@ng-org/alien-deepsignals";
21
- import type {
22
- DeepPatch,
23
- DeepSignalPropGenFn,
24
- DeepSignalSet,
25
- WatchPatchEvent,
26
- } from "@ng-org/alien-deepsignals";
27
- import type { ShapeType, BaseType } from "@ng-org/shex-orm";
28
-
29
- export class OrmConnection<T extends BaseType> {
30
- private static idToEntry = new Map<string, OrmConnection<any>>();
31
- /**
32
- * Delay in ms to wait before closing connection.\
33
- * Useful when a hook unsubscribes and resubscribes in a short time interval
34
- * so that no new connections need to be set up.
35
- */
36
- private WAIT_BEFORE_RELEASE = 500;
37
-
38
- readonly shapeType: ShapeType<T>;
39
- readonly scope: Scope;
40
- readonly signalObject: DeepSignalSet<T>;
41
- private refCount: number;
42
- /** Identifier as a combination of shape type and scope. Prevents duplications. */
43
- private identifier: string;
44
- suspendDeepWatcher: boolean;
45
- readyPromise: Promise<void>;
46
- cancel: () => void;
47
- /** Promise that resolves once initial data has been applied. */
48
- resolveReady!: () => void;
49
-
50
- // FinalizationRegistry to clean up connections when signal objects are GC'd.
51
- private static cleanupSignalRegistry =
52
- typeof FinalizationRegistry === "function"
53
- ? new FinalizationRegistry<string>((connectionId) => {
54
- // Best-effort fallback; look up by id and clean
55
- const entry = this.idToEntry.get(connectionId);
56
- //console.log("cleaning up connection",connectionId)
57
- if (!entry) return;
58
- entry.release();
59
- })
60
- : null;
61
-
62
- private constructor(shapeType: ShapeType<T>, scope: Scope) {
63
-
64
- // @ts-expect-error
65
- window.ormSignalConnections = OrmConnection.idToEntry;
66
-
67
- this.shapeType = shapeType;
68
- this.scope = scope;
69
- this.refCount = 1;
70
- this.cancel = () => {};
71
- this.suspendDeepWatcher = false;
72
- this.identifier = `${shapeType.shape}::${canonicalScope(scope)}`;
73
- this.signalObject = deepSignal<Set<T>>(new Set(), {
74
- propGenerator: this.signalObjectPropGenerator,
75
- // Don't set syntheticIdPropertyName - let propGenerator handle all ID logic
76
- readOnlyProps: ["@id", "@graph"],
77
- });
78
-
79
- // Schedule cleanup of the connection when the signal object is GC'd.
80
- OrmConnection.cleanupSignalRegistry?.register(
81
- this.signalObject,
82
- this.identifier,
83
- this.signalObject
84
- );
85
-
86
- // Add listener to deep signal object to report changes back to wasm land.
87
- watchDeepSignal(this.signalObject, this.onSignalObjectUpdate);
88
-
89
- // Initialize per-entry readiness promise that resolves in setUpConnection
90
- this.readyPromise = new Promise<void>((resolve) => {
91
- this.resolveReady = resolve;
92
- });
93
-
94
- ngSession.then(async ({ ng, session }) => {
95
- //console.log("Creating orm connection. ng and session", ng, session);
96
- try {
97
- //await new Promise((resolve) => setTimeout(resolve, 4_000));
98
- this.cancel = await ng.orm_start(
99
- scope.length == 0 ? "" : scope,
100
- shapeType,
101
- session.session_id,
102
- this.onBackendMessage
103
- );
104
- } catch (e) {
105
- console.error(e);
106
- }
107
- });
108
- }
109
-
110
- /**
111
- * Get a connection which contains the ORM and lifecycle methods.
112
- * @param shapeType
113
- * @param scope
114
- * @param ng
115
- * @returns
116
- */
117
- public static getConnection = <T extends BaseType>(
118
- shapeType: ShapeType<T>,
119
- scope: Scope
120
- ): OrmConnection<T> => {
121
- const scopeKey = canonicalScope(scope);
122
-
123
- // Unique identifier for a given shape type and scope.
124
- const identifier = `${shapeType.shape}::${scopeKey}`;
125
-
126
- // If we already have an object for this shape+scope,
127
- // return it and just increase the reference count.
128
- // Otherwise, create new one.
129
- const existingConnection = OrmConnection.idToEntry.get(identifier);
130
- if (existingConnection) {
131
- existingConnection.refCount += 1;
132
- return existingConnection;
133
- } else {
134
- const newConnection = new OrmConnection(shapeType, scope);
135
- OrmConnection.idToEntry.set(identifier, newConnection);
136
- return newConnection;
137
- }
138
- };
139
-
140
- public release = () => {
141
- setTimeout(() => {
142
- if (this.refCount > 0) this.refCount--;
143
- if (this.refCount === 0) {
144
- OrmConnection.idToEntry.delete(this.identifier);
145
-
146
- OrmConnection.cleanupSignalRegistry?.unregister(
147
- this.signalObject
148
- );
149
- this.cancel();
150
- }
151
- }, this.WAIT_BEFORE_RELEASE);
152
- };
153
-
154
- private onSignalObjectUpdate = async ({ patches }: WatchPatchEvent<T>) => {
155
- if (this.suspendDeepWatcher || !patches.length) return;
156
-
157
- const ormPatches = deepPatchesToWasm(patches);
158
-
159
- // Wait for session and subscription to be initialized.
160
- const { ng, session } = await ngSession;
161
- await this.readyPromise;
162
-
163
- ng.orm_update(
164
- this.scope.length == 0 ? "" : this.scope,
165
- this.shapeType.shape,
166
- ormPatches,
167
- session.session_id
168
- );
169
- };
170
-
171
- private onBackendMessage = (message: any) => {
172
- const data = message?.V0;
173
- if (data?.OrmInitial) {
174
- this.handleInitialResponse(data.OrmInitial);
175
- } else if (data?.OrmUpdate) {
176
- this.onBackendUpdate(data.OrmUpdate);
177
- } else {
178
- console.warn("Received unknown ORM message from backend", message);
179
- }
180
- };
181
-
182
- private handleInitialResponse = (initialData: any) => {
183
- // Assign initial data to empty signal object without triggering watcher at first.
184
- this.suspendDeepWatcher = true;
185
- batch(() => {
186
- // Note: Instead, we await for the connection to be initialized and send patches after. So no need to remove.
187
- // // Do this in case the there was any (incorrect) data added before initialization.
188
- // this.signalObject.clear();
189
-
190
- // Convert arrays to sets and apply to signalObject (we only have sets but can only transport arrays).
191
- for (const newItem of parseOrmInitialObject(initialData)) {
192
- this.signalObject.add(newItem);
193
- }
194
- });
195
-
196
- queueMicrotask(() => {
197
- this.suspendDeepWatcher = false;
198
- // Resolve readiness after initial data is committed and watcher armed.
199
- this.resolveReady();
200
- });
201
- };
202
- private onBackendUpdate = (patches: Patch[]) => {
203
- this.suspendDeepWatcher = true;
204
- applyPatchesToDeepSignal(this.signalObject, patches);
205
- // Use queueMicrotask to ensure watcher is re-enabled _after_ batch completes
206
- queueMicrotask(() => {
207
- this.suspendDeepWatcher = false;
208
- });
209
- };
210
-
211
- /** Function to create random subject IRIs for newly created nested objects. */
212
- private signalObjectPropGenerator: DeepSignalPropGenFn = ({
213
- path,
214
- object,
215
- }) => {
216
- let graphIri: string | undefined = undefined;
217
- let subjectIri: string | undefined = undefined;
218
-
219
- // If no @graph is set, add the parent's graph IRI. If there is no parent, throw.
220
- if (!object["@graph"] || object["@graph"] === "") {
221
- if (path.length > 1) {
222
- // The first part of the path is the <graphIri>|<subjectIri> composition.
223
- graphIri = (path[0] as string).split("|")[0];
224
- } else {
225
- throw new Error(
226
- "When adding new root orm objects, you must specify the @graph"
227
- );
228
- }
229
- } else {
230
- graphIri = object["@graph"];
231
- }
232
-
233
- if (object["@id"] && object["@id"] !== "") {
234
- subjectIri = object["@id"];
235
- } else {
236
- // Generate 33 random bytes using Web Crypto API
237
- const b = new Uint8Array(33);
238
- crypto.getRandomValues(b);
239
-
240
- // Convert to base64url
241
- const base64url = (bytes: Uint8Array) =>
242
- btoa(String.fromCharCode(...bytes))
243
- .replace(/\+/g, "-")
244
- .replace(/\//g, "_")
245
- .replace(/=+$/, "");
246
- const randomString = base64url(b);
247
-
248
- // We use the root subject's graph as the basis.
249
- // TODO: We could use the closest parent's graph instead.
250
- subjectIri =
251
- ((path[0] ?? graphIri) as string).substring(0, 9 + 44) +
252
- ":q:" +
253
- randomString;
254
- }
255
-
256
- return {
257
- extraProps: { "@id": subjectIri, "@graph": graphIri },
258
- syntheticId: graphIri + "|" + subjectIri,
259
- };
260
- };
261
- }
262
-
263
- /**
264
- * Converts DeepSignal patches to ORM Wasm-compatible patches
265
- * @param patches DeepSignal patches
266
- * @returns Patches with stringified path
267
- */
268
- export function deepPatchesToWasm(patches: DeepPatch[]): Patches {
269
- return patches.flatMap((patch) => {
270
- if (patch.op === "add" && patch.type === "set" && !patch.value?.length)
271
- return [];
272
- const path = "/" + patch.path.join("/");
273
- return { ...patch, path };
274
- }) as Patches;
275
- }
276
-
277
- const parseOrmInitialObject = (obj: any): any => {
278
- // Regular arrays become sets.
279
- if (Array.isArray(obj)) {
280
- return new Set(obj.map(parseOrmInitialObject));
281
- } else if (obj && typeof obj === "object") {
282
- if ("@id" in obj) {
283
- // Regular object.
284
- for (const key of Object.keys(obj)) {
285
- obj[key] = parseOrmInitialObject(obj[key]);
286
- }
287
- } else {
288
- // Object does not have @id, that means it's a set of objects.
289
- return new Set(Object.values(obj).map(parseOrmInitialObject));
290
- }
291
- }
292
- return obj;
293
- };
294
-
295
- function canonicalScope(scope: Scope | Scope[] | undefined): string {
296
- if (scope == null) return "";
297
- return Array.isArray(scope)
298
- ? scope.slice().sort().join(",")
299
- : String(scope);
300
- }
@@ -1,2 +0,0 @@
1
- import useShape from "./useShape.ts";
2
- export { useShape };
@@ -1,36 +0,0 @@
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
-
11
- import type { BaseType } from "@ng-org/shex-orm";
12
- import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
13
- import type { ShapeType } from "@ng-org/shex-orm";
14
- import { useEffect, useRef, useState } from "react";
15
- import { createSignalObjectForShape } from "../../connector/createSignalObjectForShape.ts";
16
- import type { Scope } from "../../types.ts";
17
-
18
- const useShape = <T extends BaseType>(
19
- shape: ShapeType<T>,
20
- scope: Scope = ""
21
- ) => {
22
- const handleRef = useRef(createSignalObjectForShape(shape, scope));
23
-
24
- const handle = handleRef.current;
25
- const state = useDeepSignal(handle.signalObject);
26
-
27
- useEffect(() => {
28
- return () => {
29
- handleRef.current.stop();
30
- };
31
- }, [shape, scope]);
32
-
33
- return state;
34
- };
35
-
36
- export default useShape;
@@ -1,2 +0,0 @@
1
- import useShape from "./useShape.svelte.ts";
2
- export { useShape };
@@ -1,46 +0,0 @@
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
-
11
- import { createSignalObjectForShape } from "../../connector/createSignalObjectForShape.ts";
12
- import type { Scope } from "../../types.ts";
13
- import { onDestroy } from "svelte";
14
- import type { BaseType, ShapeType } from "@ng-org/shex-orm";
15
- import {
16
- useDeepSignal,
17
- type UseDeepSignalResult,
18
- } from "@ng-org/alien-deepsignals/svelte";
19
-
20
- export type { UseDeepSignalResult } from "@ng-org/alien-deepsignals/svelte";
21
-
22
- /** Extended result including the originating root signal wrapper from shape logic. */
23
- export interface UseShapeRuneResult<T extends object>
24
- extends UseDeepSignalResult<T> {
25
- root: any;
26
- }
27
-
28
- /**
29
- * Shape-specific rune: constructs the signal object for a shape then delegates to {@link useDeepSignal}.
30
- */
31
- export function useShapeRune<T extends BaseType>(
32
- shape: ShapeType<T>,
33
- scope?: Scope
34
- ): UseShapeRuneResult<Set<T>> {
35
- const { signalObject: rootSignal, stop } = createSignalObjectForShape(
36
- shape,
37
- scope
38
- );
39
-
40
- onDestroy(stop);
41
-
42
- const ds = useDeepSignal<Set<T>>(rootSignal as Set<T>);
43
- return { root: rootSignal, ...ds } as UseShapeRuneResult<Set<T>>;
44
- }
45
-
46
- export default useShapeRune;
@@ -1,2 +0,0 @@
1
- import useShape from "./useShape.ts";
2
- export { useShape };
@@ -1,33 +0,0 @@
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
-
11
- import { createSignalObjectForShape } from "../../connector/createSignalObjectForShape.ts";
12
- import type { Scope } from "../../types.ts";
13
- import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
14
- import { onBeforeUnmount } from "vue";
15
- import type { BaseType, ShapeType } from "@ng-org/shex-orm";
16
-
17
- export function useShape<T extends BaseType>(
18
- shape: ShapeType<T>,
19
- scope?: Scope
20
- ) {
21
- const handle = createSignalObjectForShape(shape, scope);
22
-
23
- // Cleanup
24
- onBeforeUnmount(() => {
25
- handle.stop();
26
- });
27
-
28
- const ref = useDeepSignal(handle.signalObject);
29
-
30
- return ref;
31
- }
32
-
33
- export default useShape;
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- import { createSignalObjectForShape } from "./connector/createSignalObjectForShape.ts";
2
- import { useShape as svelteUseShape } from "./frontendAdapters/svelte/index.ts";
3
- import { useShape as reactUseShape } from "./frontendAdapters/react/index.ts";
4
- import { useShape as vueUseShape } from "./frontendAdapters/vue/useShape.ts";
5
- import { initNgSignals } from "./connector/initNg.ts";
6
- export * from "./connector/applyPatches.ts";
7
-
8
- export {
9
- initNgSignals as initNg,
10
- createSignalObjectForShape,
11
- svelteUseShape,
12
- reactUseShape,
13
- vueUseShape,
14
- };
package/src/types.ts DELETED
@@ -1,26 +0,0 @@
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
-
11
- import type { Patch } from "./connector/applyPatches.ts";
12
-
13
- /** The shape of an object requested. */
14
- export type Shape = "Shape1" | "Shape2" | "TestShape";
15
-
16
- /** The Scope of a shape request */
17
- export type Scope = string;
18
-
19
- /** The diff format used to communicate updates between wasm-land and js-land. */
20
- export type Diff = Patch[];
21
-
22
- /** A connection established between wasm-land and js-land for subscription of a shape. */
23
- export type Connection = {
24
- id: string;
25
- onUpdateFromWasm: (diff: Diff) => void;
26
- };