@ng-org/alien-deepsignals 0.1.2-alpha.1
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 +476 -0
- package/dist/contents.d.ts +6 -0
- package/dist/contents.d.ts.map +1 -0
- package/dist/contents.js +18 -0
- package/dist/core.d.ts +75 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +113 -0
- package/dist/deepSignal.d.ts +26 -0
- package/dist/deepSignal.d.ts.map +1 -0
- package/dist/deepSignal.js +916 -0
- package/dist/effect.d.ts +5 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +35 -0
- package/dist/hooks/react/index.d.ts +3 -0
- package/dist/hooks/react/index.d.ts.map +1 -0
- package/dist/hooks/react/index.js +8 -0
- package/dist/hooks/react/useDeepSignal.d.ts +14 -0
- package/dist/hooks/react/useDeepSignal.d.ts.map +1 -0
- package/dist/hooks/react/useDeepSignal.js +39 -0
- package/dist/hooks/svelte/index.d.ts +4 -0
- package/dist/hooks/svelte/index.d.ts.map +1 -0
- package/dist/hooks/svelte/index.js +8 -0
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts +31 -0
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts.map +1 -0
- package/dist/hooks/svelte/useDeepSignal.svelte.js +74 -0
- package/dist/hooks/vue/index.d.ts +3 -0
- package/dist/hooks/vue/index.d.ts.map +1 -0
- package/dist/hooks/vue/index.js +8 -0
- package/dist/hooks/vue/useDeepSignal.d.ts +15 -0
- package/dist/hooks/vue/useDeepSignal.d.ts.map +1 -0
- package/dist/hooks/vue/useDeepSignal.js +73 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/iteratorHelpers.d.ts +3 -0
- package/dist/iteratorHelpers.d.ts.map +1 -0
- package/dist/iteratorHelpers.js +38 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +47 -0
- package/dist/watch.d.ts +27 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +72 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# NextGraph alien-deepsignals
|
|
2
|
+
|
|
3
|
+
Deep structural reactivity for plain objects / arrays / Sets built on top of `alien-signals`.
|
|
4
|
+
|
|
5
|
+
Hooks for Svelte, Vue, and React.
|
|
6
|
+
|
|
7
|
+
Core idea: wrap a data tree in a `Proxy` that lazily creates per-property signals the first time you read them. Deep mutations emit compact batched patch objects (in a JSON-patch inspired style) that you can track with `watch()`.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Lazy: signals & child proxies created only when touched.
|
|
12
|
+
- Deep: nested objects, arrays, Sets proxied.
|
|
13
|
+
- Per-property signals: fine‑grained invalidation without traversal on each change.
|
|
14
|
+
- Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
|
|
15
|
+
- Getter => computed: property getters become derived (readonly) signals automatically.
|
|
16
|
+
- `$` accessors: TypeScript exposes `$prop` for each non‑function key plus `$` / `$length` for arrays.
|
|
17
|
+
- Sets: structural `add/delete/clear` emit patches; object entries get synthetic stable ids.
|
|
18
|
+
- Configurable synthetic IDs: custom property generator - the synthetic id is used in the paths of patches to identify objects in sets.
|
|
19
|
+
- Read-only properties: protect specific properties from modification.
|
|
20
|
+
- Shallow escape hatch: wrap sub-objects with `shallow(obj)` to track only reference replacement.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @ng-org/alien-deepsignals
|
|
26
|
+
# or
|
|
27
|
+
npm i @ng-org/alien-deepsignals
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { deepSignal } from "@ng-org/alien-deepsignals";
|
|
34
|
+
|
|
35
|
+
const state = deepSignal({
|
|
36
|
+
count: 0,
|
|
37
|
+
user: { name: "Ada" },
|
|
38
|
+
items: [{ id: "i1", qty: 1 }],
|
|
39
|
+
settings: new Set(["dark"]),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
state.count++; // mutate normally
|
|
43
|
+
state.user.name = "Grace"; // nested write
|
|
44
|
+
state.items.push({ id: "i2", qty: 2 });
|
|
45
|
+
state.settings.add("beta");
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Frontend Hooks
|
|
49
|
+
|
|
50
|
+
We provide hooks for Svelte, Vue, and React so that you can use deepSignal objects in your frontend framework. Modifying the object within those components works as usual, just that the component will rerender automatically if the object changed (by an event in the component or a modification from elsewhere).
|
|
51
|
+
|
|
52
|
+
Note that you can pass existing deepSignal objects (that you are using elsewhere too, for example as shared state) as well as plain JavaScript objects (which are then wrapped).
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
|
|
56
|
+
|
|
57
|
+
const users = useDeepSignal([{username: "Bob"}]);
|
|
58
|
+
// Note: Instead of calling `setState`, you just need to modify a property. That will trigger the required re-render.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Vue
|
|
62
|
+
|
|
63
|
+
In component `UserManager.vue`
|
|
64
|
+
```vue
|
|
65
|
+
<script setup lang="ts">
|
|
66
|
+
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
67
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
|
|
68
|
+
import UserComponent from "./User.vue";
|
|
69
|
+
import { User } from "./types.ts";
|
|
70
|
+
|
|
71
|
+
const users: DeepSignal<User> = useDeepSignal([{username: "Bob", id: 1}]);
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<UserComponent
|
|
76
|
+
v-for="user in users"
|
|
77
|
+
:key="user.id"
|
|
78
|
+
:user="user"
|
|
79
|
+
/>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
In a child component, `User.vue`
|
|
83
|
+
```vue
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
|
|
86
|
+
|
|
87
|
+
const props = defineProps<{
|
|
88
|
+
user: DeepSignal<User>;
|
|
89
|
+
}>();
|
|
90
|
+
|
|
91
|
+
// Important!
|
|
92
|
+
// In vue child components, you need to wrap deepSignal objects into useDeepSignal hooks, to ensure the component re-renders.
|
|
93
|
+
const user = useDeepSignal(props.user);
|
|
94
|
+
</script>
|
|
95
|
+
<template>
|
|
96
|
+
{{user.name}}
|
|
97
|
+
</template>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Svelte
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte";
|
|
104
|
+
|
|
105
|
+
// `users` is a rune of type `{username: string}[]`
|
|
106
|
+
const users = useDeepSignal([{username: "Bob"}]);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration options
|
|
110
|
+
|
|
111
|
+
`deepSignal(obj, options?)` accepts an optional configuration object:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type DeepSignalOptions = {
|
|
115
|
+
propGenerator?: (props: {
|
|
116
|
+
path: (string | number)[];
|
|
117
|
+
inSet: boolean;
|
|
118
|
+
object: any;
|
|
119
|
+
}) => {
|
|
120
|
+
syntheticId?: string;
|
|
121
|
+
extraProps?: Record<string, unknown>;
|
|
122
|
+
};
|
|
123
|
+
syntheticIdPropertyName?: string;
|
|
124
|
+
readOnlyProps?: string[];
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Property generator function
|
|
129
|
+
|
|
130
|
+
The `propGenerator` function is called when a new object is added to the deep signal tree. It receives:
|
|
131
|
+
|
|
132
|
+
- `path`: The path of the newly added object
|
|
133
|
+
- `inSet`: Whether the object is being added to a Set (true) or not (false)
|
|
134
|
+
- `object`: The newly added object itself
|
|
135
|
+
|
|
136
|
+
It can return:
|
|
137
|
+
|
|
138
|
+
- `syntheticId`: A custom identifier for the object (used in Set entry paths and optionally as a property)
|
|
139
|
+
- `extraProps`: Additional properties to be added to the object (overwriting existing ones).
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
let counter = 0;
|
|
143
|
+
const state = deepSignal(
|
|
144
|
+
{ items: new Set() },
|
|
145
|
+
{
|
|
146
|
+
propGenerator: ({ path, inSet, object }) => ({
|
|
147
|
+
syntheticId: inSet
|
|
148
|
+
? `urn:item:${++counter}`
|
|
149
|
+
: `urn:obj:${path.join("-")}`,
|
|
150
|
+
extraProps: { createdAt: new Date().toISOString() },
|
|
151
|
+
}),
|
|
152
|
+
syntheticIdPropertyName: "@id",
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
state.items.add({ name: "Item 1" }); // Gets @id: "urn:item:1" and createdAt property
|
|
157
|
+
state.items.add({ name: "Item 2" }); // Gets @id: "urn:item:2"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Synthetic ID property name
|
|
161
|
+
|
|
162
|
+
When `syntheticIdPropertyName` is set (e.g., to `"@id"`), objects receive a readonly, enumerable property with the generated synthetic ID:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const state = deepSignal(
|
|
166
|
+
{ data: {} },
|
|
167
|
+
{
|
|
168
|
+
propGenerator: ({ path, inSet, object }) => ({
|
|
169
|
+
syntheticId: `urn:uuid:${crypto.randomUUID()}`,
|
|
170
|
+
}),
|
|
171
|
+
syntheticIdPropertyName: "@id",
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
state.data.user = { name: "Ada" };
|
|
176
|
+
console.log(state.data.user["@id"]); // e.g., "urn:uuid:550e8400-e29b-41d4-a716-446655440000"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Read-only properties
|
|
180
|
+
|
|
181
|
+
The `readOnlyProps` option lets you specify property names that cannot be modified:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const state = deepSignal(
|
|
185
|
+
{ data: {} },
|
|
186
|
+
{
|
|
187
|
+
propGenerator: ({ path, inSet, object }) => ({
|
|
188
|
+
syntheticId: `urn:uuid:${crypto.randomUUID()}`,
|
|
189
|
+
}),
|
|
190
|
+
syntheticIdPropertyName: "@id",
|
|
191
|
+
readOnlyProps: ["@id", "@graph"],
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
state.data.user = { name: "Ada" };
|
|
196
|
+
state.data.user["@id"] = "new-id"; // TypeError: Cannot modify readonly property '@id'
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Key behaviors:**
|
|
200
|
+
|
|
201
|
+
- Synthetic IDs are assigned **before** the object is proxied, ensuring availability immediately
|
|
202
|
+
- Properties specified in `readOnlyProps` are **readonly** and **enumerable**
|
|
203
|
+
- Synthetic ID assignment emits a patch just like any other property
|
|
204
|
+
- Objects with existing properties matching `syntheticIdPropertyName` keep their values (not overwritten)
|
|
205
|
+
- Options propagate to all nested objects created after initialization
|
|
206
|
+
- The `propGenerator` function is called for both Set entries (`inSet: true`) and regular objects (`inSet: false`)
|
|
207
|
+
|
|
208
|
+
## Watching patches
|
|
209
|
+
|
|
210
|
+
`watch(root, cb, options?)` observes a deepSignal root and invokes your callback with microtask‑batched mutation patches plus snapshots.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { watch } from "alien-deepsignals";
|
|
214
|
+
|
|
215
|
+
const stop = watch(state, ({ patches, oldValue, newValue }) => {
|
|
216
|
+
for (const p of patches) {
|
|
217
|
+
console.log(p.op, p.path.join("."), "value" in p ? p.value : p.type);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
state.user.name = "Lin";
|
|
222
|
+
state.items[0].qty = 3;
|
|
223
|
+
await Promise.resolve(); // flush microtask
|
|
224
|
+
stop();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Computed (derived) values
|
|
228
|
+
|
|
229
|
+
Use the `computed()` function to create lazy derived signals that automatically track their dependencies and recompute only when needed.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { computed } from "@ng-org/alien-deepsignals";
|
|
233
|
+
|
|
234
|
+
const state = deepSignal({
|
|
235
|
+
firstName: "Ada",
|
|
236
|
+
lastName: "Lovelace",
|
|
237
|
+
items: [1, 2, 3],
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Create a computed signal that derives from reactive state
|
|
241
|
+
const fullNaAdd documentationme = computed(() => `${state.firstName} ${state.lastName}`);
|
|
242
|
+
const itemCount = computed(() => state.items.length);
|
|
243
|
+
|
|
244
|
+
console.log(fullName()); // "Ada Lovelace" - computes on first access
|
|
245
|
+
console.log(itemCount()); // 3
|
|
246
|
+
|
|
247
|
+
state.firstName = "Grace";
|
|
248
|
+
console.log(fullName()); // "Grace Lovelace" - recomputes automatically
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Key benefits:**
|
|
252
|
+
|
|
253
|
+
- **Lazy evaluation**: The computation runs only when you actually read the computed value. If you never access `fullName()`, the concatenation never happens—no wasted CPU cycles.
|
|
254
|
+
- **Automatic caching**: Once computed, the result is cached until a dependency changes. Multiple reads return the cached value without re-running the getter.
|
|
255
|
+
- **Fine-grained reactivity**: Only recomputes when its tracked dependencies change. Unrelated state mutations don't trigger unnecessary recalculation.
|
|
256
|
+
- **Composable**: Computed signals can depend on other computed signals, forming efficient dependency chains.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Expensive computation only runs when accessed and dependencies change
|
|
260
|
+
const expensiveResult = computed(() => {
|
|
261
|
+
console.log("Computing...");
|
|
262
|
+
return state.items.reduce((sum, n) => sum + n * n, 0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// No computation happens yet!
|
|
266
|
+
state.items.push(4);
|
|
267
|
+
// Still no computation...
|
|
268
|
+
|
|
269
|
+
console.log(expensiveResult()); // "Computing..." + result
|
|
270
|
+
console.log(expensiveResult()); // Cached, no log
|
|
271
|
+
state.items.push(5);
|
|
272
|
+
console.log(expensiveResult()); // "Computing..." again (dependency changed)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Callback event shape
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
type WatchPatchEvent<T> = {
|
|
279
|
+
patches: DeepPatch[]; // empty only on immediate
|
|
280
|
+
oldValue: T | undefined; // deep-cloned snapshot before batch
|
|
281
|
+
newValue: T; // live proxy (already mutated)
|
|
282
|
+
registerCleanup(fn): void; // register disposer for next batch/stop
|
|
283
|
+
stopListening(): void; // unsubscribe
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Options
|
|
288
|
+
|
|
289
|
+
| Option | Type | Default | Description |
|
|
290
|
+
| ----------- | ------- | ------- | -------------------------------------------------- |
|
|
291
|
+
| `immediate` | boolean | false | Fire once right away with `patches: []`. |
|
|
292
|
+
| `once` | boolean | false | Auto stop after first callback (immediate counts). |
|
|
293
|
+
|
|
294
|
+
`observe()` is an alias of `watch()`.
|
|
295
|
+
|
|
296
|
+
## DeepPatch format
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
type DeepPatch = {
|
|
300
|
+
root: symbol; // stable id per deepSignal root
|
|
301
|
+
path: (string | number)[]; // root-relative segments
|
|
302
|
+
} & (
|
|
303
|
+
| { op: "add"; type: "object" } // assigned object/array/Set entry object
|
|
304
|
+
| { op: "add"; value: string | number | boolean } // primitive write
|
|
305
|
+
| { op: "remove" } // deletion
|
|
306
|
+
| { op: "add"; type: "set"; value: [] } // Set.clear()
|
|
307
|
+
| {
|
|
308
|
+
op: "add";
|
|
309
|
+
type: "set";
|
|
310
|
+
value: (string | number | boolean)[] | { [id: string]: object };
|
|
311
|
+
} // (reserved)
|
|
312
|
+
);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Notes:
|
|
316
|
+
|
|
317
|
+
- `type:'object'` omits value to avoid deep cloning; read from `newValue` if needed.
|
|
318
|
+
- `Set.add(entry)` emits object vs primitive form depending on entry type; path ends with synthetic id.
|
|
319
|
+
- `Set.clear()` emits one structural patch and suppresses per‑entry removals in same batch.
|
|
320
|
+
|
|
321
|
+
## Sets & synthetic ids
|
|
322
|
+
|
|
323
|
+
Object entries inside Sets need a stable key for patch paths. The synthetic ID resolution follows this priority:
|
|
324
|
+
|
|
325
|
+
1. Explicit custom ID via `setSetEntrySyntheticId(entry, 'myId')` (before `add`)
|
|
326
|
+
2. Custom ID property specified by `syntheticIdPropertyName` option (e.g., `entry['@id']`)
|
|
327
|
+
3. Auto-generated blank node ID (`_bN` format)
|
|
328
|
+
|
|
329
|
+
### Working with Sets
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { addWithId, setSetEntrySyntheticId } from "@ng-org/alien-deepsignals";
|
|
333
|
+
|
|
334
|
+
// Option 1: Use automatic ID generation via propGenerator
|
|
335
|
+
const state = deepSignal(
|
|
336
|
+
{ items: new Set() },
|
|
337
|
+
{
|
|
338
|
+
propGenerator: ({ path, inSet, object }) => ({
|
|
339
|
+
syntheticId: inSet ? `urn:uuid:${crypto.randomUUID()}` : undefined,
|
|
340
|
+
}),
|
|
341
|
+
syntheticIdPropertyName: "@id",
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
const item = { name: "Item 1" };
|
|
345
|
+
state.items.add(item); // Automatically gets @id before being added
|
|
346
|
+
console.log(item["@id"]); // e.g., "urn:uuid:550e8400-..."
|
|
347
|
+
|
|
348
|
+
// Option 2: Manually set synthetic ID
|
|
349
|
+
const obj = { value: 42 };
|
|
350
|
+
setSetEntrySyntheticId(obj, "urn:custom:my-id");
|
|
351
|
+
state.items.add(obj);
|
|
352
|
+
|
|
353
|
+
// Option 3: Use convenience helper
|
|
354
|
+
addWithId(state.items as any, { value: 99 }, "urn:item:special");
|
|
355
|
+
|
|
356
|
+
// Option 4: Pre-assign property matching syntheticIdPropertyName
|
|
357
|
+
const preTagged = { "@id": "urn:explicit:123", data: "..." };
|
|
358
|
+
state.items.add(preTagged); // Uses "urn:explicit:123" as synthetic ID
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Set entry patches and paths
|
|
362
|
+
|
|
363
|
+
When objects are added to Sets, their **synthetic ID becomes part of the patch path**. This allows patches to uniquely identify which Set entry is being mutated.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
const state = deepSignal(
|
|
367
|
+
{ s: new Set() },
|
|
368
|
+
{
|
|
369
|
+
propGenerator: ({ inSet }) => ({
|
|
370
|
+
syntheticId: inSet ? "urn:entry:set-entry-1" : undefined,
|
|
371
|
+
}),
|
|
372
|
+
syntheticIdPropertyName: "@id",
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
watch(state, ({ patches }) => {
|
|
377
|
+
console.log(JSON.stringify(patches));
|
|
378
|
+
// [
|
|
379
|
+
// {"path":["s","urn:entry:set-entry-1"],"op":"add","type":"object"},
|
|
380
|
+
// {"path":["s","urn:entry:set-entry-1","@id"],"op":"add","value":"urn:entry:set-entry-1"},
|
|
381
|
+
// {"path":["s","urn:entry:set-entry-1","data"],"op":"add","value":"test"}
|
|
382
|
+
// ]
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
state.s.add({ data: "test" });
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Path structure explained:**
|
|
389
|
+
|
|
390
|
+
- `["s", "urn:entry:set-entry-1"]` - The structural Set patch; the IRI identifies the entry
|
|
391
|
+
- `["s", "urn:entry:set-entry-1", "@id"]` - Patch for the @id property assignment
|
|
392
|
+
- `["s", "urn:entry:set-entry-1", "data"]` - Nested property patch; the IRI identifies which Set entry
|
|
393
|
+
- The synthetic ID (the IRI) is stable across mutations, allowing tracking of the same object
|
|
394
|
+
|
|
395
|
+
**Mutating nested properties:**
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
const state = deepSignal(
|
|
399
|
+
{ users: new Set() },
|
|
400
|
+
{
|
|
401
|
+
propGenerator: ({ path, inSet }) => ({
|
|
402
|
+
syntheticId: inSet ? `urn:user:${crypto.randomUUID()}` : undefined,
|
|
403
|
+
}),
|
|
404
|
+
syntheticIdPropertyName: "@id",
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
const user = { name: "Ada", age: 30 };
|
|
408
|
+
state.users.add(user); // Gets @id, e.g., "urn:user:550e8400-..."
|
|
409
|
+
|
|
410
|
+
watch(state, ({ patches }) => {
|
|
411
|
+
console.log(JSON.stringify(patches));
|
|
412
|
+
// [{"path":["users","urn:user:550e8400-...","age"],"op":"add","value":31}]
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Later mutation: synthetic ID identifies which Set entry changed
|
|
416
|
+
user.age = 31;
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The path `["users", "urn:user:550e8400-...", "age"]` shows:
|
|
420
|
+
|
|
421
|
+
1. `users` - the Set container
|
|
422
|
+
2. `urn:user:550e8400-...` - the IRI identifying which object in the Set
|
|
423
|
+
3. `age` - the property being mutated
|
|
424
|
+
|
|
425
|
+
This structure enables precise tracking of nested changes within Set entries, critical for syncing state changes or implementing undo/redo.
|
|
426
|
+
|
|
427
|
+
## Shallow
|
|
428
|
+
|
|
429
|
+
Skip deep proxying of a subtree (only reference replacement tracked):
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import { shallow } from "alien-deepsignals";
|
|
433
|
+
state.config = shallow({ huge: { blob: true } });
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## TypeScript ergonomics
|
|
437
|
+
|
|
438
|
+
`DeepSignal<T>` exposes both plain properties and optional `$prop` signal accessors (excluded for function members). Arrays add `$` (index signal map) and `$length`.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const state = deepSignal({ count: 0, user: { name: "A" } });
|
|
442
|
+
state.count++; // ok
|
|
443
|
+
state.$count!.set(9); // write via signal
|
|
444
|
+
const n: number = state.$count!(); // typed number
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## API surface
|
|
448
|
+
|
|
449
|
+
| Function | Description |
|
|
450
|
+
| ---------------------------------- | ------------------------------------------------------------------ |
|
|
451
|
+
| `deepSignal(obj, options?)` | Create (or reuse) reactive deep proxy with optional configuration. |
|
|
452
|
+
| `watch(root, cb, opts?)` | Observe batched deep mutations. |
|
|
453
|
+
| `observe(root, cb, opts?)` | Alias of `watch`. |
|
|
454
|
+
| `peek(obj,key)` | Untracked property read. |
|
|
455
|
+
| `shallow(obj)` | Mark object to skip deep proxying. |
|
|
456
|
+
| `isDeepSignal(val)` | Runtime predicate. |
|
|
457
|
+
| `isShallow(val)` | Was value marked shallow. |
|
|
458
|
+
| `setSetEntrySyntheticId(obj,id)` | Assign custom Set entry id (highest priority). |
|
|
459
|
+
| `addWithId(set, entry, id)` | Insert with desired synthetic id (convenience). |
|
|
460
|
+
| `subscribeDeepMutations(root, cb)` | Low-level patch stream (used by watch). |
|
|
461
|
+
|
|
462
|
+
## License
|
|
463
|
+
|
|
464
|
+
This project is a fork of https://github.com/CCherry07/alien-deepsignals, forked at commit `b691dc9202c58f63c1bf78675577c811316396db`. All code previous to this commit is licensed under MIT, and author is CCherry. No copyright attribution is present. This codebase is therefor relicensed under dual MIT and Apache 2.0 licensing.
|
|
465
|
+
|
|
466
|
+
All subsequent commits are from Laurin Weger and are licensed under either of
|
|
467
|
+
|
|
468
|
+
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
|
|
469
|
+
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
|
470
|
+
at your option.
|
|
471
|
+
|
|
472
|
+
`SPDX-License-Identifier: Apache-2.0 OR MIT`
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../src/contents.ts"],"names":[],"mappings":"AAUA,oBAAY,aAAa;IACvB,SAAS,iBAAiB;IAC1B,IAAI,aAAa;IACjB,UAAU,kBAAkB;CAC7B"}
|
package/dist/contents.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2025 Laurin Weger, Par le Peuple, NextGraph.org developers
|
|
3
|
+
// All rights reserved.
|
|
4
|
+
// Licensed under the Apache License, Version 2.0
|
|
5
|
+
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
|
|
6
|
+
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
|
7
|
+
// at your option. All files in the project carrying such
|
|
8
|
+
// notice may not be copied, modified, or distributed except
|
|
9
|
+
// according to those terms.
|
|
10
|
+
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ReactiveFlags = void 0;
|
|
13
|
+
var ReactiveFlags;
|
|
14
|
+
(function (ReactiveFlags) {
|
|
15
|
+
ReactiveFlags["IS_SIGNAL"] = "__v_isSignal";
|
|
16
|
+
ReactiveFlags["SKIP"] = "__v_skip";
|
|
17
|
+
ReactiveFlags["IS_SHALLOW"] = "__v_isShallow";
|
|
18
|
+
})(ReactiveFlags || (exports.ReactiveFlags = ReactiveFlags = {}));
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Lightweight facade adding ergonomic helpers (.value/.peek/.get/.set) to native alien-signals function signals. */
|
|
2
|
+
export { signal as _rawSignal, computed as _rawComputed, startBatch as _rawStartBatch, endBatch as _rawEndBatch, getCurrentSub as _rawGetCurrentSub, setCurrentSub as _rawSetCurrentSub, effect as _rawEffect, } from "alien-signals";
|
|
3
|
+
import { signal as alienSignal, computed as alienComputed } from "alien-signals";
|
|
4
|
+
/** Internal shape of a tagged writable signal after adding ergonomic helpers. */
|
|
5
|
+
type TaggedSignal<T> = ReturnType<typeof alienSignal<T>> & {
|
|
6
|
+
/** Tracking read / write via property syntax */
|
|
7
|
+
value: T;
|
|
8
|
+
/** Non-tracking read */
|
|
9
|
+
peek(): T;
|
|
10
|
+
/** Alias for tracking read */
|
|
11
|
+
get(): T;
|
|
12
|
+
/** Write helper */
|
|
13
|
+
set(v: T): void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create a new writable function-form signal enhanced with `.value`, `.peek()`, `.get()`, `.set()`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const count = signal(0);
|
|
20
|
+
* count(); // 0 (track)
|
|
21
|
+
* count(1); // write
|
|
22
|
+
* count.value; // 1 (track)
|
|
23
|
+
* count.peek(); // 1 (non-tracking)
|
|
24
|
+
*/
|
|
25
|
+
export declare const signal: <T>(v?: T) => TaggedSignal<any>;
|
|
26
|
+
/** Internal shape of a tagged computed signal after adding ergonomic helpers. */
|
|
27
|
+
type TaggedComputed<T> = ReturnType<typeof alienComputed<T>> & {
|
|
28
|
+
/** Tracking read via property syntax (readonly) */
|
|
29
|
+
readonly value: T;
|
|
30
|
+
/** Non-tracking read */
|
|
31
|
+
peek(): T;
|
|
32
|
+
/** Alias for tracking read */
|
|
33
|
+
get(): T;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create a lazy computed (readonly) signal derived from other signals.
|
|
37
|
+
*
|
|
38
|
+
* Computed signals are automatically cached and only recompute when their tracked
|
|
39
|
+
* dependencies change. The getter function is evaluated lazily—if you never read
|
|
40
|
+
* the computed value, the computation never runs.
|
|
41
|
+
*
|
|
42
|
+
* The returned function can be called directly `computed()` or accessed via `.value`.
|
|
43
|
+
* Use `.peek()` for non-tracking reads (won't establish reactive dependency).
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const count = signal(5);
|
|
47
|
+
* const doubled = computed(() => count() * 2);
|
|
48
|
+
* doubled(); // 10 (establishes dependency, caches result)
|
|
49
|
+
* doubled.value; // 10 (cached, same as calling it)
|
|
50
|
+
* doubled.peek(); // 10 (no dependency tracking)
|
|
51
|
+
* count(10);
|
|
52
|
+
* doubled(); // 20 (recomputed because count changed)
|
|
53
|
+
*/
|
|
54
|
+
export declare const computed: <T>(getter: () => T) => TaggedComputed<T>;
|
|
55
|
+
/** Union allowing a plain value or a writable signal wrapping that value. */
|
|
56
|
+
export type MaybeSignal<T = any> = T | ReturnType<typeof signal>;
|
|
57
|
+
/** Union allowing value, writable signal, computed signal or plain getter function. */
|
|
58
|
+
export type MaybeSignalOrGetter<T = any> = MaybeSignal<T> | ReturnType<typeof computed> | (() => T);
|
|
59
|
+
/** Runtime guard that an unknown value is one of our tagged signals/computeds. */
|
|
60
|
+
export declare const isSignal: (s: any) => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Execute multiple signal writes in a single batched update frame.
|
|
63
|
+
* All downstream computed/effect re-evaluations are deferred until the function exits.
|
|
64
|
+
*
|
|
65
|
+
* IMPORTANT: The callback MUST be synchronous. If it returns a Promise the batch will
|
|
66
|
+
* still end immediately after scheduling, possibly causing mid-async flushes.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* batch(() => {
|
|
70
|
+
* count(count() + 1);
|
|
71
|
+
* other(other() + 2);
|
|
72
|
+
* }); // effects observing both run only once
|
|
73
|
+
*/
|
|
74
|
+
export declare function batch<T>(fn: () => T): T;
|
|
75
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAUA,qHAAqH;AAGrH,OAAO,EACH,MAAM,IAAI,UAAU,EACpB,QAAQ,IAAI,YAAY,EACxB,UAAU,IAAI,cAAc,EAC5B,QAAQ,IAAI,YAAY,EACxB,aAAa,IAAI,iBAAiB,EAClC,aAAa,IAAI,iBAAiB,EAClC,MAAM,IAAI,UAAU,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACH,MAAM,IAAI,WAAW,EACrB,QAAQ,IAAI,aAAa,EAI5B,MAAM,eAAe,CAAC;AAKvB,iFAAiF;AACjF,KAAK,YAAY,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG;IACvD,gDAAgD;IAChD,KAAK,EAAE,CAAC,CAAC;IACT,wBAAwB;IACxB,IAAI,IAAI,CAAC,CAAC;IACV,8BAA8B;IAC9B,GAAG,IAAI,CAAC,CAAC;IACT,mBAAmB;IACnB,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;CACnB,CAAC;AA6BF;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,EAAE,IAAI,CAAC,sBAA8B,CAAC;AAC9D,iFAAiF;AACjF,KAAK,cAAc,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG;IAC3D,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,wBAAwB;IACxB,IAAI,IAAI,CAAC,CAAC;IACV,8BAA8B;IAC9B,GAAG,IAAI,CAAC,CAAC;CACZ,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,QAAQ,MAAM,CAAC,KAAG,cAAc,CAAC,CAAC,CACxB,CAAC;AAEvC,6EAA6E;AAC7E,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACjE,uFAAuF;AACvF,MAAM,MAAM,mBAAmB,CAAC,CAAC,GAAG,GAAG,IACjC,WAAW,CAAC,CAAC,CAAC,GACd,UAAU,CAAC,OAAO,QAAQ,CAAC,GAC3B,CAAC,MAAM,CAAC,CAAC,CAAC;AAChB,kFAAkF;AAClF,eAAO,MAAM,QAAQ,GAAI,GAAG,GAAG,KAAG,OACiC,CAAC;AAEpE;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAOvC"}
|