Fork of https://github.com/CCherry07/alien-deepsignals ported to alien-signals v2 with support for per-value modification tracking.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
5.6 KiB
168 lines
5.6 KiB
# alien-deepsignals
|
|
|
|
Deep structural reactivity for plain objects / arrays / Sets built on top of `alien-signals`.
|
|
|
|
Core idea: wrap a data tree in a `Proxy` that lazily creates per-property signals the first time you read them. Accessing a property returns the plain value; accessing `$prop` returns the underlying signal function. Deep mutations emit compact batched patch objects you can observe with `watch()`.
|
|
|
|
## Features
|
|
|
|
* Lazy: signals & child proxies created only when touched.
|
|
* Deep: nested objects, arrays, Sets proxied on demand.
|
|
* [ ] TODO: Methods might not be proxied (e.g. array.push)?
|
|
* Per-property signals: fine‑grained invalidation without traversal on each change.
|
|
* Patch stream: microtask‑batched granular mutations (paths + op) for syncing external stores / framework adapters.
|
|
* Getter => computed: property getters become derived (readonly) signals automatically.
|
|
* `$` accessors: TypeScript exposes `$prop` for each non‑function key plus `$` / `$length` for arrays.
|
|
* Sets: structural `add/delete/clear` emit patches; object entries get synthetic stable ids (prefers `id` / `@id` fields or auto‑generated blank IDs).
|
|
* Shallow escape hatch: wrap sub-objects with `shallow(obj)` to track only reference replacement.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
pnpm add alien-deepsignals
|
|
# or
|
|
npm i alien-deepsignals
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import { deepSignal } from 'alien-deepsignals'
|
|
|
|
const state = deepSignal({
|
|
count: 0,
|
|
user: { name: 'Ada' },
|
|
items: [{ id: 'i1', qty: 1 }],
|
|
settings: new Set(['dark'])
|
|
})
|
|
|
|
state.count++ // mutate normally
|
|
state.user.name = 'Grace' // nested write
|
|
state.items.push({ id: 'i2', qty: 2 })
|
|
state.settings.add('beta')
|
|
|
|
// Direct signal access
|
|
state.$count!.set(5) // update via signal
|
|
console.log(state.$count!()) // read via signal function
|
|
```
|
|
|
|
## Watching patches
|
|
|
|
`watch(root, cb, options?)` observes a deepSignal root and invokes your callback with microtask‑batched mutation patches plus snapshots.
|
|
|
|
```ts
|
|
import { watch } from 'alien-deepsignals'
|
|
|
|
const stop = watch(state, ({ patches, oldValue, newValue }) => {
|
|
for (const p of patches) {
|
|
console.log(p.op, p.path.join('.'), 'value' in p ? p.value : p.type)
|
|
}
|
|
})
|
|
|
|
state.user.name = 'Lin'
|
|
state.items[0].qty = 3
|
|
await Promise.resolve() // flush microtask
|
|
stop()
|
|
```
|
|
|
|
### Callback event shape
|
|
|
|
```ts
|
|
type WatchPatchEvent<T> = {
|
|
patches: DeepPatch[] // empty only on immediate
|
|
oldValue: T | undefined // deep-cloned snapshot before batch
|
|
newValue: T // live proxy (already mutated)
|
|
registerCleanup(fn): void // register disposer for next batch/stop
|
|
stopListening(): void // unsubscribe
|
|
}
|
|
```
|
|
|
|
### Options
|
|
|
|
| Option | Type | Default | Description |
|
|
|--------|------|---------|-------------|
|
|
| `immediate` | boolean | false | Fire once right away with `patches: []`. |
|
|
| `once` | boolean | false | Auto stop after first callback (immediate counts). |
|
|
|
|
`observe()` is an alias of `watch()`.
|
|
|
|
## DeepPatch format
|
|
|
|
```ts
|
|
type DeepPatch = {
|
|
root: symbol // stable id per deepSignal root
|
|
path: (string | number)[] // root-relative segments
|
|
} & (
|
|
| { op: 'add'; type: 'object' } // assigned object/array/Set entry object
|
|
| { op: 'add'; value: string | number | boolean } // primitive write
|
|
| { op: 'remove' } // deletion
|
|
| { op: 'add'; type: 'set'; value: [] } // Set.clear()
|
|
| { op: 'add'; type: 'set'; value: (string|number|boolean)[] | { [id: string]: object } } // (reserved)
|
|
)
|
|
```
|
|
|
|
Notes:
|
|
* `type:'object'` omits value to avoid deep cloning; read from `newValue` if needed.
|
|
* `Set.add(entry)` emits object vs primitive form depending on entry type; path ends with synthetic id.
|
|
* `Set.clear()` emits one structural patch and suppresses per‑entry removals in same batch.
|
|
|
|
## Sets & synthetic ids
|
|
|
|
Object entries inside Sets need a stable key. Priority:
|
|
1. `entry.id`
|
|
2. `entry['@id']`
|
|
3. Custom via `setSetEntrySyntheticId(entry, 'myId')` before `add`
|
|
4. Auto `_bN` blank id
|
|
|
|
Helpers:
|
|
```ts
|
|
import { addWithId, setSetEntrySyntheticId } from 'alien-deepsignals'
|
|
|
|
setSetEntrySyntheticId(obj, 'custom')
|
|
state.settings.add(obj)
|
|
addWithId(state.settings as any, { x:1 }, 'x1')
|
|
```
|
|
|
|
## Shallow
|
|
|
|
Skip deep proxying of a subtree (only reference replacement tracked):
|
|
```ts
|
|
import { shallow } from 'alien-deepsignals'
|
|
state.config = shallow({ huge: { blob: true } })
|
|
```
|
|
|
|
## TypeScript ergonomics
|
|
|
|
`DeepSignal<T>` exposes both plain properties and optional `$prop` signal accessors (excluded for function members). Arrays add `$` (index signal map) and `$length`.
|
|
|
|
```ts
|
|
const state = deepSignal({ count: 0, user: { name: 'A' } })
|
|
state.count++ // ok
|
|
state.$count!.set(9) // write via signal
|
|
const n: number = state.$count!() // typed number
|
|
```
|
|
|
|
## API surface
|
|
|
|
| Function | Description |
|
|
|----------|-------------|
|
|
| `deepSignal(obj)` | Create (or reuse) reactive deep proxy. |
|
|
| `watch(root, cb, opts?)` | Observe batched deep mutations. |
|
|
| `observe(root, cb, opts?)` | Alias of `watch`. |
|
|
| `peek(obj,key)` | Untracked property read. |
|
|
| `shallow(obj)` | Mark object to skip deep proxying. |
|
|
| `isDeepSignal(val)` | Runtime predicate. |
|
|
| `isShallow(val)` | Was value marked shallow. |
|
|
| `setSetEntrySyntheticId(obj,id)` | Assign custom Set entry id. |
|
|
| `addWithId(set, entry, id)` | Insert with desired synthetic id. |
|
|
| `subscribeDeepMutations(root, cb)` | Low-level patch stream (used by watch). |
|
|
|
|
## Credits
|
|
|
|
Inspired by [deepsignal](https://github.com/luisherranz/deepsignal) – thanks to @luisherranz. Re-imagined with patch batching & Set support.
|
|
|
|
## License
|
|
|
|
MIT
|
|
|
|
|
|
|