memorio 5.0.0 → 5.1.0

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 (60) hide show
  1. package/README.md +80 -449
  2. package/SECURITY.md +152 -42
  3. package/SUMMARY.md +1 -1
  4. package/adr/001-state-proxy-model.md +95 -96
  5. package/adr/002-observer-semantics.md +179 -180
  6. package/adr/003-deep-mutation-semantics.md +7 -8
  7. package/adr/004-array-mutation-semantics.md +127 -128
  8. package/adr/005-scheduler-contract.md +148 -149
  9. package/adr/006-context-isolation.md +91 -92
  10. package/adr/007-mutation-records.md +5 -6
  11. package/adr/008-transactions.md +6 -7
  12. package/adr/009-history-model.md +6 -7
  13. package/adr/README.md +46 -46
  14. package/adr/template.md +48 -49
  15. package/bin/cli.js +68 -0
  16. package/global.cjs +1462 -323
  17. package/global.js +1459 -324
  18. package/index.cjs +1462 -323
  19. package/index.d.ts +1 -0
  20. package/index.js +1459 -324
  21. package/llms.txt +42 -5
  22. package/markdown/AUDIT-REPORT.md +7 -8
  23. package/markdown/CACHE.md +190 -99
  24. package/markdown/DEVTOOLS.md +0 -1
  25. package/markdown/DISPATCH.md +0 -1
  26. package/markdown/HISTORY.md +0 -1
  27. package/markdown/IDB.md +0 -1
  28. package/markdown/IMPORT.md +0 -1
  29. package/markdown/INSPECT.md +0 -1
  30. package/markdown/LOGGER.md +0 -1
  31. package/markdown/MEMORY-ATTACHMENT.md +0 -1
  32. package/markdown/MEMORY.md +0 -1
  33. package/markdown/OBSERVER.md +0 -1
  34. package/markdown/PLATFORM.md +277 -271
  35. package/markdown/REDUX.md +54 -0
  36. package/markdown/SCHEMA.md +0 -1
  37. package/markdown/SESSION.md +0 -1
  38. package/markdown/SQLITE.md +0 -1
  39. package/markdown/STATE.md +0 -1
  40. package/markdown/STORE.md +0 -1
  41. package/markdown/SYNC.md +0 -1
  42. package/markdown/TYPED.md +0 -1
  43. package/markdown/USEOBSERVER.md +0 -1
  44. package/modules/redux.cjs +381 -10
  45. package/modules/redux.cjs.map +1 -1
  46. package/modules/redux.js +381 -10
  47. package/modules/redux.js.map +1 -1
  48. package/package.json +14 -2
  49. package/types/broadcast.d.ts +61 -0
  50. package/types/computed.d.ts +96 -0
  51. package/types/encryption.d.ts +129 -0
  52. package/types/exports.d.ts +9 -0
  53. package/types/memorio.d.ts +19 -12
  54. package/types/security.d.ts +67 -0
  55. package/types/session.d.ts +23 -5
  56. package/types/store.d.ts +19 -3
  57. package/vsix/memorio.vsix +0 -0
  58. package/markdown/CHANGELOG.md +0 -243
  59. package/markdown/PROJECT.md +0 -311
  60. package/markdown/SECURITY.md +0 -330
@@ -1,128 +1,127 @@
1
- # ADR-004: Array Mutation Semantics
2
-
3
- > **Status:** Proposed
4
- > **Date:** 2026-09-12
5
- > **Deciders:** Memorio 5.x Core Team
6
-
7
- ## Context
8
-
9
- Arrays are the most error-prone data structure in proxy-based state
10
- libraries. The 4.7.3 baseline handled `push` and `pop` but had no
11
- explicit contract for `sort`, `splice`, `shift`, `unshift`, `fill`,
12
- `reverse`, `length` changes, or whole-array replacement. Section 9 of
13
- the Memorio 5.x plan demands explicit, regression-tested behavior for
14
- every array operation.
15
-
16
- A mutation that persists but does not notify, or notifies but does not
17
- persist, is considered a **semantic defect** unless explicitly
18
- documented.
19
-
20
- ## Assumptions
21
-
22
- - Arrays are plain JS arrays stored on the Proxy raw target.
23
- - Array methods (`push`, `pop`, `splice`, etc.) mutate the array in
24
- place and trigger the set trap on the Proxy wrapping the array.
25
- - The set trap's `key` for array mutations is the array index or
26
- `length`.
27
- - The callback receives `{ action, path, newValue, previousValue }`.
28
- - The state Proxy dispatches via `dispatch.set("state." + path)`.
29
-
30
- ## Decision
31
-
32
- ### Mutation → Persistence mapping
33
-
34
- Every array operation below **must** persist to the underlying raw
35
- array and **must** dispatch an event on the **array path**
36
- (`state.<arrayPath>`), not on the individual index path. The event
37
- path is computed from the Proxy `tree` (e.g. for `state.items`, the
38
- tree is `["items"]`, so the path is `items` and the event is
39
- `state.items`).
40
-
41
- | Operation | Persists? | Dispatches on | Patch op |
42
- |-----------|-----------|---------------|----------|
43
- | `state.arr[i] = val` | Yes | `state.arr` | `replace` (index i) |
44
- | `state.arr.push(val)` | Yes | `state.arr` | `add` (at index `arr.length`) |
45
- | `state.arr.pop()` | Yes | `state.arr` | `remove` (last index) |
46
- | `state.arr.shift()` | Yes | `state.arr` | `remove` (index 0) |
47
- | `state.arr.unshift(val)` | Yes | `state.arr` | `add` (at index 0) |
48
- | `state.arr.splice(i, n, ...items)` | Yes | `state.arr` | `remove` + `add` |
49
- | `state.arr.sort()` | Yes | `state.arr` | `replace` (whole array) |
50
- | `state.arr.reverse()` | Yes | `state.arr` | `replace` (whole array) |
51
- | `state.arr.fill(val, start, end)` | Yes | `state.arr` | `replace` (whole array) |
52
- | `state.arr.length = n` (truncate) | Yes | `state.arr` | `remove` (removed indices) |
53
- | `state.arr.length = n` (extend) | Yes | `state.arr` | `add` (new indices) |
54
- | `state.arr = newArray` | Yes | `state.arr` | `replace` (whole array) |
55
- | `state.arr[i].prop = val` | Yes | `state.arr.i.prop` | `replace` (leaf) |
56
-
57
- ### sort() persistence
58
-
59
- `sort()` mutates the array in place and returns a reference to the same
60
- (sorted) array. The Proxy set trap fires because `length` changes and
61
- indices are reassigned. The event dispatched is `state.arr`. The
62
- mutation is **persisted** to the raw target.
63
-
64
- **Known constraint:** `sort()` with a custom comparator must also
65
- persist correctly. The comparator function is applied by V8's native
66
- `Array.prototype.sort`; the Proxy only sees the resulting index
67
- reassignments.
68
-
69
- ### splice() semantics
70
-
71
- `splice()` can add, remove, or replace elements at an index. The Proxy
72
- sees multiple set/delete traps. The event dispatched is `state.arr`
73
- (the first trap fires, and subsequent traps on the same array path
74
- are de-duplicated by the dispatch layer's microtask scheduling).
75
-
76
- ### length changes
77
-
78
- Setting `arr.length = n` where `n < arr.length` truncates the array.
79
- The deleteProperty trap fires for each removed index. Setting
80
- `arr.length = n` where `n > arr.length` extends the array with
81
- `undefined` holes; the set trap fires for `length`.
82
-
83
- ### Whole-array replacement
84
-
85
- `state.arr = [1, 2, 3]` replaces the entire array. The set trap fires
86
- once with `path = "arr"`, `newValue = [1, 2, 3]` (deepRaw'd). The
87
- event dispatched is `state.arr`. Observers on `state.arr` fire.
88
- Previous array observers (on the old array instance) are orphaned —
89
- the consumer must re-register if they observe specific indices.
90
-
91
- ### Array element object mutation
92
-
93
- `state.arr[0].name = "X"` mutates an object inside the array. This
94
- fires the set trap on the Proxy wrapping `arr[0]` with
95
- `path = "arr.0.name"`. The event dispatched is `state.arr.0.name`.
96
-
97
- Per ADR-003, this does **not** notify observers on `state.arr`. This
98
- is a known limitation. Consumers who need to observe changes to
99
- objects within arrays must observe the specific element path
100
- (`state.arr.0.name`) or use `useObserver` auto-discovery.
101
-
102
- ### Event deduplication
103
-
104
- Because a single array operation (e.g. `splice`) can produce multiple
105
- set/delete traps, the dispatch layer may fire `state.arr` multiple
106
- times within the same synchronous block. Each dispatch schedules a
107
- separate microtask. Observers on `state.arr` will fire **multiple
108
- times** — once per trap. A future scheduler enhancement may coalesce
109
- these. This ADR documents the current behavior.
110
-
111
- ## Consequences
112
-
113
- - **Positive:** Every array operation persists and notifies — no
114
- silent failures.
115
- - **Positive:** Array observers fire on the array path, not individual
116
- indices — this matches React list-reconciliation mental model.
117
- - **Positive:** `sort()`, `splice()`, and other complex operations are
118
- explicitly tested for persistence + notification.
119
- - **Negative:** Multiple traps from a single array operation may
120
- produce multiple observer notifications — consumers must
121
- de-duplicate if needed.
122
- - **Negative:** Array element object mutations do not notify the
123
- array-path observer — this is a fundamental limitation of the
124
- exact-path observer model.
125
-
126
- ## Compliance tests
127
-
128
- - `tests/vitest/tests/contracts/adr-004-arrays.test.ts`
1
+ # ADR-004: Array Mutation Semantics
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ Arrays are the most error-prone data structure in proxy-based state
9
+ libraries. The 4.7.3 baseline handled `push` and `pop` but had no
10
+ explicit contract for `sort`, `splice`, `shift`, `unshift`, `fill`,
11
+ `reverse`, `length` changes, or whole-array replacement. Section 9 of
12
+ the Memorio 5.x plan demands explicit, regression-tested behavior for
13
+ every array operation.
14
+
15
+ A mutation that persists but does not notify, or notifies but does not
16
+ persist, is considered a **semantic defect** unless explicitly
17
+ documented.
18
+
19
+ ## Assumptions
20
+
21
+ - Arrays are plain JS arrays stored on the Proxy raw target.
22
+ - Array methods (`push`, `pop`, `splice`, etc.) mutate the array in
23
+ place and trigger the set trap on the Proxy wrapping the array.
24
+ - The set trap's `key` for array mutations is the array index or
25
+ `length`.
26
+ - The callback receives `{ action, path, newValue, previousValue }`.
27
+ - The state Proxy dispatches via `dispatch.set("state." + path)`.
28
+
29
+ ## Decision
30
+
31
+ ### Mutation → Persistence mapping
32
+
33
+ Every array operation below **must** persist to the underlying raw
34
+ array and **must** dispatch an event on the **array path**
35
+ (`state.<arrayPath>`), not on the individual index path. The event
36
+ path is computed from the Proxy `tree` (e.g. for `state.items`, the
37
+ tree is `["items"]`, so the path is `items` and the event is
38
+ `state.items`).
39
+
40
+ | Operation | Persists? | Dispatches on | Patch op |
41
+ |-----------|-----------|---------------|----------|
42
+ | `state.arr[i] = val` | Yes | `state.arr` | `replace` (index i) |
43
+ | `state.arr.push(val)` | Yes | `state.arr` | `add` (at index `arr.length`) |
44
+ | `state.arr.pop()` | Yes | `state.arr` | `remove` (last index) |
45
+ | `state.arr.shift()` | Yes | `state.arr` | `remove` (index 0) |
46
+ | `state.arr.unshift(val)` | Yes | `state.arr` | `add` (at index 0) |
47
+ | `state.arr.splice(i, n, ...items)` | Yes | `state.arr` | `remove` + `add` |
48
+ | `state.arr.sort()` | Yes | `state.arr` | `replace` (whole array) |
49
+ | `state.arr.reverse()` | Yes | `state.arr` | `replace` (whole array) |
50
+ | `state.arr.fill(val, start, end)` | Yes | `state.arr` | `replace` (whole array) |
51
+ | `state.arr.length = n` (truncate) | Yes | `state.arr` | `remove` (removed indices) |
52
+ | `state.arr.length = n` (extend) | Yes | `state.arr` | `add` (new indices) |
53
+ | `state.arr = newArray` | Yes | `state.arr` | `replace` (whole array) |
54
+ | `state.arr[i].prop = val` | Yes | `state.arr.i.prop` | `replace` (leaf) |
55
+
56
+ ### sort() persistence
57
+
58
+ `sort()` mutates the array in place and returns a reference to the same
59
+ (sorted) array. The Proxy set trap fires because `length` changes and
60
+ indices are reassigned. The event dispatched is `state.arr`. The
61
+ mutation is **persisted** to the raw target.
62
+
63
+ **Known constraint:** `sort()` with a custom comparator must also
64
+ persist correctly. The comparator function is applied by V8's native
65
+ `Array.prototype.sort`; the Proxy only sees the resulting index
66
+ reassignments.
67
+
68
+ ### splice() semantics
69
+
70
+ `splice()` can add, remove, or replace elements at an index. The Proxy
71
+ sees multiple set/delete traps. The event dispatched is `state.arr`
72
+ (the first trap fires, and subsequent traps on the same array path
73
+ are de-duplicated by the dispatch layer's microtask scheduling).
74
+
75
+ ### length changes
76
+
77
+ Setting `arr.length = n` where `n < arr.length` truncates the array.
78
+ The deleteProperty trap fires for each removed index. Setting
79
+ `arr.length = n` where `n > arr.length` extends the array with
80
+ `undefined` holes; the set trap fires for `length`.
81
+
82
+ ### Whole-array replacement
83
+
84
+ `state.arr = [1, 2, 3]` replaces the entire array. The set trap fires
85
+ once with `path = "arr"`, `newValue = [1, 2, 3]` (deepRaw'd). The
86
+ event dispatched is `state.arr`. Observers on `state.arr` fire.
87
+ Previous array observers (on the old array instance) are orphaned -
88
+ the consumer must re-register if they observe specific indices.
89
+
90
+ ### Array element object mutation
91
+
92
+ `state.arr[0].name = "X"` mutates an object inside the array. This
93
+ fires the set trap on the Proxy wrapping `arr[0]` with
94
+ `path = "arr.0.name"`. The event dispatched is `state.arr.0.name`.
95
+
96
+ Per ADR-003, this does **not** notify observers on `state.arr`. This
97
+ is a known limitation. Consumers who need to observe changes to
98
+ objects within arrays must observe the specific element path
99
+ (`state.arr.0.name`) or use `useObserver` auto-discovery.
100
+
101
+ ### Event deduplication
102
+
103
+ Because a single array operation (e.g. `splice`) can produce multiple
104
+ set/delete traps, the dispatch layer may fire `state.arr` multiple
105
+ times within the same synchronous block. Each dispatch schedules a
106
+ separate microtask. Observers on `state.arr` will fire **multiple
107
+ times** - once per trap. A future scheduler enhancement may coalesce
108
+ these. This ADR documents the current behavior.
109
+
110
+ ## Consequences
111
+
112
+ - **Positive:** Every array operation persists and notifies - no
113
+ silent failures.
114
+ - **Positive:** Array observers fire on the array path, not individual
115
+ indices - this matches React list-reconciliation mental model.
116
+ - **Positive:** `sort()`, `splice()`, and other complex operations are
117
+ explicitly tested for persistence + notification.
118
+ - **Negative:** Multiple traps from a single array operation may
119
+ produce multiple observer notifications - consumers must
120
+ de-duplicate if needed.
121
+ - **Negative:** Array element object mutations do not notify the
122
+ array-path observer - this is a fundamental limitation of the
123
+ exact-path observer model.
124
+
125
+ ## Compliance tests
126
+
127
+ - `tests/vitest/tests/contracts/adr-004-arrays.test.ts`
@@ -1,149 +1,148 @@
1
- # ADR-005: Scheduler Contract
2
-
3
- > **Status:** Proposed
4
- > **Date:** 2026-09-12
5
- > **Deciders:** Memorio 5.x Core Team
6
-
7
- ## Context
8
-
9
- The Memorio 5.x plan (Section 10) requires an explicit, configurable
10
- scheduler. The 4.7.3 baseline dispatches observer callbacks via
11
- `queueMicrotask` (in `core/dispatch.ts`). There is no configuration
12
- point, no batching, and no flush control. This must be formalized into
13
- a contract that supports future UI-framework integration (React
14
- batched mode, requestAnimationFrame, manual flush for tests).
15
-
16
- The scheduler contract must define: ordering, batching, reentrancy,
17
- error isolation, nested mutations, transaction interaction, and flush
18
- behavior.
19
-
20
- ## Assumptions
21
-
22
- - The core engine dispatches events via `dispatch.set(name, value)`.
23
- - `dispatch.listen(name, cb)` wraps `cb` in a microtask boundary.
24
- - The scheduler is orthogonal to the Mutation Engine (ADR-007) and
25
- Transactions (ADR-008) — it only affects *when* observer callbacks
26
- fire, not *what* is recorded.
27
- - `__DEV__` is a build-time constant.
28
-
29
- ## Decision
30
-
31
- ### Current default: microtask
32
-
33
- The current dispatch layer uses `queueMicrotask` (or
34
- `Promise.resolve().then(cb)` fallback). This means:
35
-
36
- 1. **Ordering:** Observer callbacks fire in registration order (FIFO),
37
- within the microtask queue, after the current synchronous block
38
- completes.
39
- 2. **Asynchronous:** Callbacks fire after the mutation is committed
40
- to the raw Proxy target.
41
- 3. **No batching:** Each `dispatch.set` schedules its own microtask.
42
-
43
- ### Proposed scheduler configuration
44
-
45
- A future `createMemorio({ scheduler: ... })` or
46
- `memorio.configureScheduler({ mode, batching })` API would allow:
47
-
48
- ```ts
49
- type SchedulerMode = "sync" | "microtask" | "macrotask" | "raf" | "manual"
50
- ```
51
-
52
- | Mode | Behavior |
53
- |------|----------|
54
- | `sync` | Callbacks fire synchronously during `dispatch.set`. |
55
- | `microtask` | Callbacks fire in the microtask queue (default). |
56
- | `macrotask` | Callbacks fire via `setTimeout(cb, 0)` (macro task). |
57
- | `raf` | Callbacks fire via `requestAnimationFrame`. |
58
- | `manual` | Callbacks only fire when `flush()` is called. |
59
-
60
- ### Batching semantics (proposed)
61
-
62
- When `batching: true` (only meaningful with `microtask` or `macrotask`
63
- mode):
64
-
65
- 1. All `dispatch.set` calls within a single synchronous block are
66
- collected.
67
- 2. Each unique event name is invoked once, after the synchronous
68
- block completes, in the scheduled task.
69
- 3. The order of batched callbacks follows the order of first
70
- `dispatch.set` for each unique event name.
71
-
72
- When `batching: false` (default):
73
-
74
- 1. Each `dispatch.set` schedules its own callback independently.
75
-
76
- ### Reentrancy
77
-
78
- - Reentrant `dispatch.set` calls (observer callbacks that dispatch new
79
- events) are queued after the current callback completes.
80
- - `sync` mode has no reentrancy boundary — recursive
81
- `dispatch.set` calls fire immediately, risking stack overflow.
82
- - No built-in guard against infinite dispatch loops. Consumers must
83
- break the cycle.
84
-
85
- ### Error isolation
86
-
87
- - A throwing callback in `microtask`/`macrotask`/`raf` mode produces
88
- an unhandled rejection / error. Other callbacks scheduled in the same
89
- tick are unaffected (each runs in its own closure).
90
- - In `sync` mode, a throwing callback propagates to the caller of
91
- `dispatch.set`.
92
- - A future enhancement may wrap each callback in `try/catch` with an
93
- optional error reporter. This ADR does **not** implement that.
94
-
95
- ### Nested mutations
96
-
97
- Mutations inside observer callbacks create a new dispatch cycle. They
98
- are **not** coalesced with the current cycle. This means:
99
-
100
- ```js
101
- state.counter = 1
102
- // microtask fires:
103
- observer('state.counter', () => {
104
- state.counter = 2 // → schedules a NEW microtask
105
- })
106
- ```
107
-
108
- ### Transaction interaction
109
-
110
- - During a transaction, each mutation dispatches normally (no
111
- suppression).
112
- - `undo()` / `redo()` set `internal.historyEnabled = false` to prevent
113
- re-recording, but the Proxy set trap still dispatches events to
114
- observers.
115
- - A future "transaction-level notification" may suppress individual
116
- mutation dispatches during a transaction and fire a single
117
- "transaction-end" event. This ADR does **not** define that.
118
-
119
- ### Flush behavior
120
-
121
- - In `manual` mode, `memorio.flush()` (or equivalent) processes all
122
- queued callbacks. This is primarily for testing.
123
- - In other modes, "flush" is implicit (microtask queue drains
124
- naturally).
125
-
126
- ## Current status
127
-
128
- The scheduler is currently **not configurable**. The default
129
- `microtask` behavior is in effect. This ADR is **Proposed** — the
130
- configuration API is a Phase 2/3 enhancement. The compliance tests
131
- for the default behavior lock in the current semantics so that future
132
- scheduler changes do not break existing code.
133
-
134
- ## Consequences
135
-
136
- - **Positive:** Documenting the microtask default gives consumers a
137
- deterministic mental model.
138
- - **Positive:** The proposed API is framework-agnostic — all modes are
139
- expressible as platform primitives.
140
- - **Positive:** `manual` mode enables deterministic testing.
141
- - **Negative:** No batching means multiple synchronous mutations each
142
- produce a microtask — this is a known performance limitation for
143
- bulk updates.
144
- - **Negative:** No error isolation means a buggy observer can crash
145
- the app via unhandled rejection.
146
-
147
- ## Compliance tests
148
-
149
- - `tests/vitest/tests/contracts/adr-005-scheduler.test.ts`
1
+ # ADR-005: Scheduler Contract
2
+
3
+ > **Status:** Proposed
4
+ > **Date:** 2026-09-12
5
+
6
+ ## Context
7
+
8
+ The Memorio 5.x plan (Section 10) requires an explicit, configurable
9
+ scheduler. The 4.7.3 baseline dispatches observer callbacks via
10
+ `queueMicrotask` (in `core/dispatch.ts`). There is no configuration
11
+ point, no batching, and no flush control. This must be formalized into
12
+ a contract that supports future UI-framework integration (React
13
+ batched mode, requestAnimationFrame, manual flush for tests).
14
+
15
+ The scheduler contract must define: ordering, batching, reentrancy,
16
+ error isolation, nested mutations, transaction interaction, and flush
17
+ behavior.
18
+
19
+ ## Assumptions
20
+
21
+ - The core engine dispatches events via `dispatch.set(name, value)`.
22
+ - `dispatch.listen(name, cb)` wraps `cb` in a microtask boundary.
23
+ - The scheduler is orthogonal to the Mutation Engine (ADR-007) and
24
+ Transactions (ADR-008) - it only affects *when* observer callbacks
25
+ fire, not *what* is recorded.
26
+ - `__DEV__` is a build-time constant.
27
+
28
+ ## Decision
29
+
30
+ ### Current default: microtask
31
+
32
+ The current dispatch layer uses `queueMicrotask` (or
33
+ `Promise.resolve().then(cb)` fallback). This means:
34
+
35
+ 1. **Ordering:** Observer callbacks fire in registration order (FIFO),
36
+ within the microtask queue, after the current synchronous block
37
+ completes.
38
+ 2. **Asynchronous:** Callbacks fire after the mutation is committed
39
+ to the raw Proxy target.
40
+ 3. **No batching:** Each `dispatch.set` schedules its own microtask.
41
+
42
+ ### Proposed scheduler configuration
43
+
44
+ A future `createMemorio({ scheduler: ... })` or
45
+ `memorio.configureScheduler({ mode, batching })` API would allow:
46
+
47
+ ```ts
48
+ type SchedulerMode = "sync" | "microtask" | "macrotask" | "raf" | "manual"
49
+ ```
50
+
51
+ | Mode | Behavior |
52
+ |------|----------|
53
+ | `sync` | Callbacks fire synchronously during `dispatch.set`. |
54
+ | `microtask` | Callbacks fire in the microtask queue (default). |
55
+ | `macrotask` | Callbacks fire via `setTimeout(cb, 0)` (macro task). |
56
+ | `raf` | Callbacks fire via `requestAnimationFrame`. |
57
+ | `manual` | Callbacks only fire when `flush()` is called. |
58
+
59
+ ### Batching semantics (proposed)
60
+
61
+ When `batching: true` (only meaningful with `microtask` or `macrotask`
62
+ mode):
63
+
64
+ 1. All `dispatch.set` calls within a single synchronous block are
65
+ collected.
66
+ 2. Each unique event name is invoked once, after the synchronous
67
+ block completes, in the scheduled task.
68
+ 3. The order of batched callbacks follows the order of first
69
+ `dispatch.set` for each unique event name.
70
+
71
+ When `batching: false` (default):
72
+
73
+ 1. Each `dispatch.set` schedules its own callback independently.
74
+
75
+ ### Reentrancy
76
+
77
+ - Reentrant `dispatch.set` calls (observer callbacks that dispatch new
78
+ events) are queued after the current callback completes.
79
+ - `sync` mode has no reentrancy boundary - recursive
80
+ `dispatch.set` calls fire immediately, risking stack overflow.
81
+ - No built-in guard against infinite dispatch loops. Consumers must
82
+ break the cycle.
83
+
84
+ ### Error isolation
85
+
86
+ - A throwing callback in `microtask`/`macrotask`/`raf` mode produces
87
+ an unhandled rejection / error. Other callbacks scheduled in the same
88
+ tick are unaffected (each runs in its own closure).
89
+ - In `sync` mode, a throwing callback propagates to the caller of
90
+ `dispatch.set`.
91
+ - A future enhancement may wrap each callback in `try/catch` with an
92
+ optional error reporter. This ADR does **not** implement that.
93
+
94
+ ### Nested mutations
95
+
96
+ Mutations inside observer callbacks create a new dispatch cycle. They
97
+ are **not** coalesced with the current cycle. This means:
98
+
99
+ ```js
100
+ state.counter = 1
101
+ // microtask fires:
102
+ observer('state.counter', () => {
103
+ state.counter = 2 // → schedules a NEW microtask
104
+ })
105
+ ```
106
+
107
+ ### Transaction interaction
108
+
109
+ - During a transaction, each mutation dispatches normally (no
110
+ suppression).
111
+ - `undo()` / `redo()` set `internal.historyEnabled = false` to prevent
112
+ re-recording, but the Proxy set trap still dispatches events to
113
+ observers.
114
+ - A future "transaction-level notification" may suppress individual
115
+ mutation dispatches during a transaction and fire a single
116
+ "transaction-end" event. This ADR does **not** define that.
117
+
118
+ ### Flush behavior
119
+
120
+ - In `manual` mode, `memorio.flush()` (or equivalent) processes all
121
+ queued callbacks. This is primarily for testing.
122
+ - In other modes, "flush" is implicit (microtask queue drains
123
+ naturally).
124
+
125
+ ## Current status
126
+
127
+ The scheduler is currently **not configurable**. The default
128
+ `microtask` behavior is in effect. This ADR is **Proposed** - the
129
+ configuration API is a Phase 2/3 enhancement. The compliance tests
130
+ for the default behavior lock in the current semantics so that future
131
+ scheduler changes do not break existing code.
132
+
133
+ ## Consequences
134
+
135
+ - **Positive:** Documenting the microtask default gives consumers a
136
+ deterministic mental model.
137
+ - **Positive:** The proposed API is framework-agnostic - all modes are
138
+ expressible as platform primitives.
139
+ - **Positive:** `manual` mode enables deterministic testing.
140
+ - **Negative:** No batching means multiple synchronous mutations each
141
+ produce a microtask - this is a known performance limitation for
142
+ bulk updates.
143
+ - **Negative:** No error isolation means a buggy observer can crash
144
+ the app via unhandled rejection.
145
+
146
+ ## Compliance tests
147
+
148
+ - `tests/vitest/tests/contracts/adr-005-scheduler.test.ts`