@ng-org/alien-deepsignals 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 +55 -374
- package/dist/core.d.ts +66 -60
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +70 -82
- package/dist/deepSignal.d.ts +19 -5
- package/dist/deepSignal.d.ts.map +1 -1
- package/dist/deepSignal.js +214 -141
- package/dist/effect.d.ts.map +1 -1
- package/dist/effect.js +8 -1
- package/dist/hooks/react/useDeepSignal.d.ts +1 -1
- package/dist/hooks/react/useDeepSignal.d.ts.map +1 -1
- package/dist/hooks/react/useDeepSignal.js +5 -2
- package/dist/hooks/svelte/index.d.ts +0 -1
- package/dist/hooks/svelte/index.d.ts.map +1 -1
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts +3 -15
- package/dist/hooks/svelte/useDeepSignal.svelte.d.ts.map +1 -1
- package/dist/hooks/svelte/useDeepSignal.svelte.js +29 -63
- package/dist/hooks/svelte4/index.d.ts +4 -0
- package/dist/hooks/svelte4/index.d.ts.map +1 -0
- package/dist/hooks/svelte4/index.js +8 -0
- package/dist/hooks/svelte4/useDeepSignal.svelte.d.ts +27 -0
- package/dist/hooks/svelte4/useDeepSignal.svelte.d.ts.map +1 -0
- package/dist/hooks/svelte4/useDeepSignal.svelte.js +91 -0
- package/dist/hooks/vue/useDeepSignal.d.ts +2 -2
- package/dist/hooks/vue/useDeepSignal.d.ts.map +1 -1
- package/dist/hooks/vue/useDeepSignal.js +22 -74
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts.map +1 -1
- package/dist/test/frontend/astro-app/src/components/{ReactPanel.js → ReactPanel.jsx} +56 -45
- package/dist/test/frontend/playwright/crossFrameworkHooks.spec.js +13 -3
- package/dist/test/frontend/utils/mockData.d.ts.map +1 -1
- package/dist/test/frontend/utils/mockData.js +13 -4
- package/dist/test/lib/index.test.js +16 -3
- package/dist/test/lib/misc.test.js +104 -1
- package/dist/test/lib/watch.test.js +2 -11
- package/dist/types.d.ts +94 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/dist/watch.d.ts +37 -0
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +35 -0
- package/package.json +8 -5
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts +0 -4
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts.map +0 -1
- package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.js +0 -225
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts +0 -4
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts.map +0 -1
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.js +0 -150
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts +0 -4
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts.map +0 -1
- package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.js +0 -184
- package/dist/test/frontend/playwright/perfSuite.spec.d.ts +0 -2
- package/dist/test/frontend/playwright/perfSuite.spec.d.ts.map +0 -1
- package/dist/test/frontend/playwright/perfSuite.spec.js +0 -128
- package/dist/test/frontend/utils/perfScenarios.d.ts +0 -15
- package/dist/test/frontend/utils/perfScenarios.d.ts.map +0 -1
- package/dist/test/frontend/utils/perfScenarios.js +0 -287
- package/dist/test/lib/core.test.d.ts +0 -2
- package/dist/test/lib/core.test.d.ts.map +0 -1
- package/dist/test/lib/core.test.js +0 -53
package/README.md
CHANGED
|
@@ -4,21 +4,23 @@ Deep structural reactivity for plain objects / arrays / Sets built on top of `al
|
|
|
4
4
|
|
|
5
5
|
Hooks for Svelte, Vue, and React.
|
|
6
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
|
|
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 batched patch objects (in a JSON-patch inspired style) that you can track with `watch()`.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- Lazy: signals & child proxies created only when touched.
|
|
12
12
|
- Deep: nested objects, arrays, Sets proxied.
|
|
13
|
-
- Per-property signals: fine‑grained invalidation without traversal on each change.
|
|
14
13
|
- Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
|
|
15
14
|
- Getter => computed: property getters become derived (readonly) signals automatically.
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Configurable synthetic IDs: custom property generator - the synthetic id is used in the paths of patches to identify objects in sets.
|
|
15
|
+
- Sets: `add/delete/clear/...` methods emit patches; object entries get synthetic stable ids.
|
|
16
|
+
- Configurable synthetic IDs: custom property generator - the synthetic ID is used in the paths of patches to identify objects in sets. By default attached as `@id` property.
|
|
19
17
|
- Read-only properties: protect specific properties from modification.
|
|
20
18
|
- Shallow escape hatch: wrap sub-objects with `shallow(obj)` to track only reference replacement.
|
|
21
19
|
|
|
20
|
+
## Reference documentation
|
|
21
|
+
|
|
22
|
+
[Reference documentation is available here on docs.nextgraph.org](https://docs.nextgraph.org/en/reference/alien-deepsignals/).
|
|
23
|
+
|
|
22
24
|
## Install
|
|
23
25
|
|
|
24
26
|
```bash
|
|
@@ -47,417 +49,96 @@ state.settings.add("beta");
|
|
|
47
49
|
|
|
48
50
|
## Frontend Hooks
|
|
49
51
|
|
|
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
|
|
52
|
+
We provide hooks for Svelte 3/4, Svelte 5, 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 when the object changed (by a modification in the component or a modification from elsewhere).
|
|
53
|
+
|
|
54
|
+
Note that you can pass existing deepSignal objects to useDeepSignal (that you are using elsewhere too, for example as shared state) as well as plain JavaScript objects (which are then wrapped).
|
|
55
|
+
|
|
56
|
+
You can (and are often advised to) use deepSignals as a shared state (and sub objects thereof) across components.
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
### React
|
|
53
59
|
|
|
54
60
|
```tsx
|
|
55
61
|
import { useDeepSignal } from "@ng-org/alien-deepsignals/react";
|
|
62
|
+
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
63
|
+
import UserComponent from "./User.tsx";
|
|
64
|
+
import type { User } from "./types.ts";
|
|
65
|
+
|
|
66
|
+
function UserManager() {
|
|
67
|
+
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob" }]);
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
return users.map((user) => <UserComponent key={user.id} user={user} />);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
In child component `User.tsx`:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
function UserComponent({ user }: { user: DeepSignal<User> }) {
|
|
77
|
+
// Modifications here will trigger a re-render in the parent component
|
|
78
|
+
// which updates this component.
|
|
79
|
+
// For performance reasons, you are advised to call `useDeepSignal`
|
|
80
|
+
// close to where its return value is used.
|
|
81
|
+
return <input type="text" value={user.name} />;
|
|
82
|
+
}
|
|
59
83
|
```
|
|
60
84
|
|
|
61
85
|
### Vue
|
|
62
86
|
|
|
63
87
|
In component `UserManager.vue`
|
|
88
|
+
|
|
64
89
|
```vue
|
|
65
|
-
<script setup lang="ts">
|
|
66
|
-
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
90
|
+
<script setup lang="ts">
|
|
67
91
|
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
|
|
92
|
+
import { DeepSignal } from "@ng-org/alien-deepsignals";
|
|
68
93
|
import UserComponent from "./User.vue";
|
|
69
|
-
import { User } from "./types.ts";
|
|
94
|
+
import type { User } from "./types.ts";
|
|
70
95
|
|
|
71
|
-
const users: DeepSignal<User> = useDeepSignal([{username: "Bob", id: 1}]);
|
|
96
|
+
const users: DeepSignal<User[]> = useDeepSignal([{ username: "Bob", id: 1 }]);
|
|
72
97
|
</script>
|
|
73
98
|
|
|
74
99
|
<template>
|
|
75
|
-
<UserComponent
|
|
76
|
-
v-for="user in users"
|
|
77
|
-
:key="user.id"
|
|
78
|
-
:user="user"
|
|
79
|
-
/>
|
|
100
|
+
<UserComponent v-for="user in users" :key="user.id" :user="user" />
|
|
80
101
|
</template>
|
|
81
102
|
```
|
|
103
|
+
|
|
82
104
|
In a child component, `User.vue`
|
|
105
|
+
|
|
83
106
|
```vue
|
|
84
107
|
<script setup lang="ts">
|
|
85
|
-
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
|
|
86
|
-
|
|
87
108
|
const props = defineProps<{
|
|
88
109
|
user: DeepSignal<User>;
|
|
89
110
|
}>();
|
|
90
111
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
const user =
|
|
112
|
+
// The component only rerenders when user.name changes.
|
|
113
|
+
// It behaves the same as an object wrapped with `reactive()`
|
|
114
|
+
const user = props.user;
|
|
94
115
|
</script>
|
|
95
116
|
<template>
|
|
96
|
-
|
|
117
|
+
<input type="text" v-model:value="user.name" />
|
|
97
118
|
</template>
|
|
98
119
|
```
|
|
99
120
|
|
|
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
|
|
121
|
+
### Svelte 3 / 4
|
|
330
122
|
|
|
331
123
|
```ts
|
|
332
|
-
import {
|
|
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
|
|
124
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte4";
|
|
362
125
|
|
|
363
|
-
|
|
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" });
|
|
126
|
+
// `users` is a store of type `{username: string}[]`
|
|
127
|
+
const users = useDeepSignal([{ username: "Bob" }]);
|
|
386
128
|
```
|
|
387
129
|
|
|
388
|
-
|
|
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:**
|
|
130
|
+
### Svelte 5
|
|
396
131
|
|
|
397
132
|
```ts
|
|
398
|
-
|
|
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):
|
|
133
|
+
import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte";
|
|
430
134
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
state.config = shallow({ huge: { blob: true } });
|
|
135
|
+
// `users` is a rune of type `{username: string}[]`
|
|
136
|
+
const users = useDeepSignal([{ username: "Bob" }]);
|
|
434
137
|
```
|
|
435
138
|
|
|
436
|
-
|
|
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
|
-
```
|
|
139
|
+
### Other Frameworks
|
|
446
140
|
|
|
447
|
-
|
|
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). |
|
|
141
|
+
Integrating new frontend frameworks is fairly easy. Get in touch if you are interested.
|
|
461
142
|
|
|
462
143
|
## License
|
|
463
144
|
|
package/dist/core.d.ts
CHANGED
|
@@ -1,75 +1,81 @@
|
|
|
1
|
-
|
|
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
|
-
};
|
|
1
|
+
import { computed as alienComputed, signal as alienSignal_, effect as alienEffect } from "alien-signals";
|
|
15
2
|
/**
|
|
16
|
-
*
|
|
3
|
+
* Execute multiple signal writes in a single batched update frame.
|
|
4
|
+
* All downstream computed/effect re-evaluations are deferred until the function exits.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: The callback must be synchronous. If it returns a Promise the batch will
|
|
7
|
+
* still end immediately after scheduling, possibly causing mid-async flushes.
|
|
17
8
|
*
|
|
18
9
|
* @example
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* batch(() => {
|
|
12
|
+
* count(count() + 1);
|
|
13
|
+
* other(other() + 2);
|
|
14
|
+
* }); // effects observing both run only once
|
|
15
|
+
* ```
|
|
24
16
|
*/
|
|
25
|
-
export declare
|
|
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
|
-
};
|
|
17
|
+
export declare function batch<T>(fn: () => T): T;
|
|
35
18
|
/**
|
|
36
|
-
*
|
|
19
|
+
* Re-export of alien-signals computed function.
|
|
37
20
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* the computed value, the computation never runs.
|
|
21
|
+
* Use the `computed()` function to create lazy derived signals that automatically
|
|
22
|
+
* track their dependencies and recompute only when needed.
|
|
41
23
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
24
|
+
* Key features:
|
|
25
|
+
* - **Lazy evaluation**: The computation runs only when you actually read the computed value.
|
|
26
|
+
* If you never access `fullName()`, the concatenation never happens—no wasted CPU cycles.
|
|
27
|
+
* - **Automatic caching**: Once computed, the result is cached until a dependency changes.
|
|
28
|
+
* Multiple reads return the cached value without re-running the getter.
|
|
29
|
+
* - **Fine-grained reactivity**: Only recomputes when its tracked dependencies change.
|
|
30
|
+
* Unrelated state mutations don't trigger unnecessary recalculation.
|
|
31
|
+
* - **Composable**: Computed signals can depend on other computed signals,
|
|
32
|
+
* forming efficient dependency chains.
|
|
44
33
|
*
|
|
45
34
|
* @example
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { computed } from "@ng-org/alien-deepsignals";
|
|
37
|
+
*
|
|
38
|
+
* const state = deepSignal({
|
|
39
|
+
* firstName: "Ada",
|
|
40
|
+
* lastName: "Lovelace",
|
|
41
|
+
* items: [1, 2, 3],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Create a computed signal that derives from reactive state
|
|
45
|
+
* const fullName = computed(() => `${state.firstName} ${state.lastName}`);
|
|
46
|
+
*
|
|
47
|
+
* console.log(fullName()); // "Ada Lovelace" - computes on first access
|
|
48
|
+
*
|
|
49
|
+
* state.firstName = "Grace";
|
|
50
|
+
* console.log(fullName()); // "Grace Lovelace" - recomputes automatically
|
|
51
|
+
*
|
|
52
|
+
* // Expensive computation only runs when accessed and dependencies change
|
|
53
|
+
* const expensiveResult = computed(() => {
|
|
54
|
+
* console.log("Computing...");
|
|
55
|
+
* return state.items.reduce((sum, n) => sum + n * n, 0);
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // No computation happens yet!
|
|
59
|
+
* state.items.push(4);
|
|
60
|
+
* // Still no computation...
|
|
61
|
+
*
|
|
62
|
+
* console.log(expensiveResult()); // "Computing..." + result
|
|
63
|
+
* console.log(expensiveResult()); // Cached, no log
|
|
64
|
+
* state.items.push(5);
|
|
65
|
+
* console.log(expensiveResult()); // "Computing..." again (dependency changed)
|
|
66
|
+
*
|
|
67
|
+
* ```
|
|
53
68
|
*/
|
|
54
|
-
export declare const computed:
|
|
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;
|
|
69
|
+
export declare const computed: typeof alienComputed;
|
|
61
70
|
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
71
|
+
* Re-export of alien-signals `signal` function which creates a basic signal.
|
|
72
|
+
*/
|
|
73
|
+
export declare const alienSignal: typeof alienSignal_;
|
|
74
|
+
/**
|
|
75
|
+
* Re-export of alien-signals effect function.
|
|
64
76
|
*
|
|
65
|
-
*
|
|
66
|
-
* still end immediately after scheduling, possibly causing mid-async flushes.
|
|
77
|
+
* Callback reruns on every signal modification that is used within its callback.
|
|
67
78
|
*
|
|
68
|
-
* @example
|
|
69
|
-
* batch(() => {
|
|
70
|
-
* count(count() + 1);
|
|
71
|
-
* other(other() + 2);
|
|
72
|
-
* }); // effects observing both run only once
|
|
73
79
|
*/
|
|
74
|
-
export declare
|
|
80
|
+
export declare const effect: typeof alienEffect;
|
|
75
81
|
//# sourceMappingURL=core.d.ts.map
|
package/dist/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAUA,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAUA,OAAO,EAGH,QAAQ,IAAI,aAAa,EACzB,MAAM,IAAI,YAAY,EACtB,MAAM,IAAI,WAAW,EACxB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAOvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,eAAO,MAAM,QAAQ,sBAAgB,CAAC;AAEtC;;GAEG;AACH,eAAO,MAAM,WAAW,qBAAe,CAAC;AAExC;;;;;GAKG;AACH,eAAO,MAAM,MAAM,oBAAc,CAAC"}
|