@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.
Files changed (62) hide show
  1. package/README.md +55 -374
  2. package/dist/core.d.ts +66 -60
  3. package/dist/core.d.ts.map +1 -1
  4. package/dist/core.js +70 -82
  5. package/dist/deepSignal.d.ts +19 -5
  6. package/dist/deepSignal.d.ts.map +1 -1
  7. package/dist/deepSignal.js +214 -141
  8. package/dist/effect.d.ts.map +1 -1
  9. package/dist/effect.js +8 -1
  10. package/dist/hooks/react/useDeepSignal.d.ts +1 -1
  11. package/dist/hooks/react/useDeepSignal.d.ts.map +1 -1
  12. package/dist/hooks/react/useDeepSignal.js +5 -2
  13. package/dist/hooks/svelte/index.d.ts +0 -1
  14. package/dist/hooks/svelte/index.d.ts.map +1 -1
  15. package/dist/hooks/svelte/useDeepSignal.svelte.d.ts +3 -15
  16. package/dist/hooks/svelte/useDeepSignal.svelte.d.ts.map +1 -1
  17. package/dist/hooks/svelte/useDeepSignal.svelte.js +29 -63
  18. package/dist/hooks/svelte4/index.d.ts +4 -0
  19. package/dist/hooks/svelte4/index.d.ts.map +1 -0
  20. package/dist/hooks/svelte4/index.js +8 -0
  21. package/dist/hooks/svelte4/useDeepSignal.svelte.d.ts +27 -0
  22. package/dist/hooks/svelte4/useDeepSignal.svelte.d.ts.map +1 -0
  23. package/dist/hooks/svelte4/useDeepSignal.svelte.js +91 -0
  24. package/dist/hooks/vue/useDeepSignal.d.ts +2 -2
  25. package/dist/hooks/vue/useDeepSignal.d.ts.map +1 -1
  26. package/dist/hooks/vue/useDeepSignal.js +22 -74
  27. package/dist/index.d.ts +1 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -2
  30. package/dist/test/frontend/astro-app/src/components/ReactPanel.d.ts.map +1 -1
  31. package/dist/test/frontend/astro-app/src/components/{ReactPanel.js → ReactPanel.jsx} +56 -45
  32. package/dist/test/frontend/playwright/crossFrameworkHooks.spec.js +13 -3
  33. package/dist/test/frontend/utils/mockData.d.ts.map +1 -1
  34. package/dist/test/frontend/utils/mockData.js +13 -4
  35. package/dist/test/lib/index.test.js +16 -3
  36. package/dist/test/lib/misc.test.js +104 -1
  37. package/dist/test/lib/watch.test.js +2 -11
  38. package/dist/types.d.ts +94 -19
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +9 -0
  41. package/dist/watch.d.ts +37 -0
  42. package/dist/watch.d.ts.map +1 -1
  43. package/dist/watch.js +35 -0
  44. package/package.json +8 -5
  45. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts +0 -4
  46. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.d.ts.map +0 -1
  47. package/dist/test/frontend/astro-app/src/components/PerfSuiteClient.js +0 -225
  48. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts +0 -4
  49. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.d.ts.map +0 -1
  50. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfDeep.js +0 -150
  51. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts +0 -4
  52. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.d.ts.map +0 -1
  53. package/dist/test/frontend/astro-app/src/components/perf/react/ReactPerfNative.js +0 -184
  54. package/dist/test/frontend/playwright/perfSuite.spec.d.ts +0 -2
  55. package/dist/test/frontend/playwright/perfSuite.spec.d.ts.map +0 -1
  56. package/dist/test/frontend/playwright/perfSuite.spec.js +0 -128
  57. package/dist/test/frontend/utils/perfScenarios.d.ts +0 -15
  58. package/dist/test/frontend/utils/perfScenarios.d.ts.map +0 -1
  59. package/dist/test/frontend/utils/perfScenarios.js +0 -287
  60. package/dist/test/lib/core.test.d.ts +0 -2
  61. package/dist/test/lib/core.test.d.ts.map +0 -1
  62. 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 compact batched patch objects (in a JSON-patch inspired style) that you can track with `watch()`.
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
- - `$` 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.
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 if the object changed (by an event in the component or a modification from elsewhere).
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
- 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).
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
- 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.
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
- // 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);
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
- {{user.name}}
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 { 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
124
+ import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte4";
362
125
 
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" });
126
+ // `users` is a store of type `{username: string}[]`
127
+ const users = useDeepSignal([{ username: "Bob" }]);
386
128
  ```
387
129
 
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:**
130
+ ### Svelte 5
396
131
 
397
132
  ```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):
133
+ import { useDeepSignal } from "@ng-org/alien-deepsignals/svelte";
430
134
 
431
- ```ts
432
- import { shallow } from "alien-deepsignals";
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
- ## 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
- ```
139
+ ### Other Frameworks
446
140
 
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). |
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
- /** 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
- };
1
+ import { computed as alienComputed, signal as alienSignal_, effect as alienEffect } from "alien-signals";
15
2
  /**
16
- * Create a new writable function-form signal enhanced with `.value`, `.peek()`, `.get()`, `.set()`.
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
- * const count = signal(0);
20
- * count(); // 0 (track)
21
- * count(1); // write
22
- * count.value; // 1 (track)
23
- * count.peek(); // 1 (non-tracking)
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 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
- };
17
+ export declare function batch<T>(fn: () => T): T;
35
18
  /**
36
- * Create a lazy computed (readonly) signal derived from other signals.
19
+ * Re-export of alien-signals computed function.
37
20
  *
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.
21
+ * Use the `computed()` function to create lazy derived signals that automatically
22
+ * track their dependencies and recompute only when needed.
41
23
  *
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).
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
- * 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)
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: <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;
69
+ export declare const computed: typeof alienComputed;
61
70
  /**
62
- * Execute multiple signal writes in a single batched update frame.
63
- * All downstream computed/effect re-evaluations are deferred until the function exits.
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
- * 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.
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 function batch<T>(fn: () => T): T;
80
+ export declare const effect: typeof alienEffect;
75
81
  //# sourceMappingURL=core.d.ts.map
@@ -1 +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"}
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"}