@kedem/okdb 1.6.3 → 1.8.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.
@@ -113,6 +113,27 @@ const stop = okdb.env('default').processor.register('orders', {
113
113
  stop();
114
114
  ```
115
115
 
116
+ ### Register options reference
117
+
118
+ | Option | Type | Default | Description |
119
+ | -------------------- | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `handler` | `function` | required | Async callback receiving `(changes[], info)` |
121
+ | `mode` | `string` | `'async'` | `'async'` \| `'inline'` \| `'worker'` |
122
+ | `bootstrap` | `string` | `'log'` | `'snapshot'` \| `'log'` — how the initial backfill runs |
123
+ | `originMode` | `string` | `'self'` | `'all'` \| `'self'` \| `'remote'` |
124
+ | `batchSize` | `number` | `256` | Max changes delivered per handler call |
125
+ | `hydrateValues` | `boolean` | `true` | Attach current document value to each change |
126
+ | `cursorKey` | `string` | `null` | Persist the cursor under this key so progress survives restarts |
127
+ | `lockMode` | `string` | `null` | `'exclusive'` — serialise concurrent flushes for this processor |
128
+ | `failOnHandlerError` | `boolean` | `false` | Put the processor into error state on handler exception |
129
+ | `flushDebounce` | `number` | `0` | Trailing-edge debounce in ms. Rapid write bursts coalesce into a single flush after `flushDebounce` ms of quiet. `0` = flush immediately (default). |
130
+ | `flushInterval` | `number` | `null` | Start a `setInterval` that ticks `_scheduleFlush` every `flushInterval` ms after bootstrap. Useful for polling-style processors or `originMode: 'remote'` subscriptions where local writes don't trigger `_onAfterCommit`. `null` = disabled. |
131
+ | `name` | `string` | `null` | Human-readable label shown in admin UI and status |
132
+ | `meta` | `object` | `{}` | Arbitrary metadata attached to status objects |
133
+ | `leaseTtlMs` | `number` | `30000` | Lease TTL for cross-process exclusive execution |
134
+
135
+ `stop.status()` (alias `stop.getStatus()`) returns the live processor state including `flushDebounce` and `flushInterval`. `stop.pause()` clears both timers; `stop.resume()` restarts the interval if configured.
136
+
116
137
  ### Change object shape
117
138
 
118
139
  ```javascript
@@ -138,6 +159,8 @@ stop();
138
159
 
139
160
  This is useful for reacting differently to local vs. replicated changes — e.g., sending a notification only when your own node creates a record, not when it receives one from a peer.
140
161
 
162
+ **Multi-process deployments:** Async and worker processors with `originMode: 'all'` or `'remote'` automatically subscribe to `EVENTS.SYSTEM_POKE` — the UDP bus's cross-process commit signal. When another process writes to the same LMDB environment, it sends a `POKE` via the bus; the local process receives it and wakes all eligible processors within tens of milliseconds. No configuration is needed — the behavior is automatic based on `originMode`. Processors with `originMode: 'self'` do not subscribe (they explicitly don't care about remote-origin changes).
163
+
141
164
  ### Processor on a custom env
142
165
 
143
166
  Each environment has its own processor:
package/docs/pipelines.md CHANGED
@@ -309,6 +309,8 @@ await analytics.put('articles', 'a1', { title: 'Hello' });
309
309
  cursorKey: 'processor@article-projector',
310
310
  lockMode: 'exclusive',
311
311
  failOnHandlerError: true,
312
+ flushDebounce: 0, // ms — trailing-edge debounce; 0 = immediate
313
+ flushInterval: null, // ms — periodic poll interval; null = disabled
312
314
  }
313
315
  ```
314
316
 
@@ -319,8 +321,11 @@ Config notes:
319
321
  - `handler.name` — function to invoke for each delivered batch of changes
320
322
  - `source_env` — optional env name for the source type; defaults to the engine owning env
321
323
  - `handler.env` — optional env name for the function registry; defaults to the engine owning env
322
- - `mode`, `bootstrap`, `originMode`, `batchSize`, `hydrateValues`, `cursorKey`, `lockMode` — forwarded to the underlying processor runtime
324
+ - `mode`, `bootstrap`, `originMode`, `batchSize`, `hydrateValues`, `cursorKey`, `lockMode`, `flushDebounce`, `flushInterval` — forwarded to the underlying processor runtime
323
325
  - `failOnHandlerError` — included in runtime metadata; V1 engine behavior treats handler failures as engine errors
326
+ - `flushDebounce` — coalesces rapid write bursts into a single handler delivery after the quiet period
327
+ - `flushInterval` — periodic poll; useful for `originMode: 'remote'` subscriptions where local writes don't trigger delivery
328
+ - When `originMode: 'all'` or `'remote'`, the underlying processor automatically subscribes to `EVENTS.SYSTEM_POKE` for cross-process change notification. No additional configuration is needed for correct multi-process behavior.
324
329
 
325
330
  Behavior:
326
331
 
@@ -0,0 +1,177 @@
1
+ # Time Machine
2
+
3
+ Time Machine tracks the change history of individual documents. For each tracked type, it records field-level diffs whenever a document is created, updated, or deleted — so you can reconstruct the exact value of any document at any past point in time.
4
+
5
+ ## Overview
6
+
7
+ Time Machine works at the **type** level. You opt in a type for tracking; from that moment, every write to that type is recorded as a diff against the previous state. Tracking is non-blocking and asynchronous — writes are not slowed down.
8
+
9
+ ```js
10
+ const db = new OKDB('./mydb');
11
+ await db.open();
12
+ const env = db.env('myenv');
13
+
14
+ // Enable tracking for a single type
15
+ await env.timeMachine.enable('Order');
16
+
17
+ // Or enable tracking for all current and future types
18
+ await env.timeMachine.enableAll();
19
+ ```
20
+
21
+ ## Enabling and disabling
22
+
23
+ ### Per-type control
24
+
25
+ ```js
26
+ await env.timeMachine.enable('Order'); // start tracking
27
+ await env.timeMachine.disable('Order'); // stop tracking, keep history
28
+ await env.timeMachine.drop('Order'); // stop tracking, delete all history
29
+ ```
30
+
31
+ `disable()` stops recording new diffs but leaves the full history intact. Future `enable()` calls resume from where tracking stopped — no gaps, no re-seed.
32
+
33
+ `drop()` deletes all stored diffs and heads for the type and resets the cursor. The next `enable()` will perform a fresh initial snapshot.
34
+
35
+ ### Env-wide control
36
+
37
+ ```js
38
+ await env.timeMachine.enableAll(); // enable all registered types now + auto-enable future types
39
+ await env.timeMachine.disableAll(); // stop all tracking, clear auto-enable flag
40
+ ```
41
+
42
+ When `enableAll()` is called, all types registered at that moment are enabled. Any type registered afterward is also automatically enabled (via a `TYPE_REGISTERED` listener).
43
+
44
+ ## Querying history
45
+
46
+ ### Full history for a key
47
+
48
+ ```js
49
+ const history = env.timeMachine.getHistory('Order', 'order-42', {
50
+ limit: 50, // max entries (optional)
51
+ before: someClock, // only diffs at or before this clock (optional)
52
+ after: someClock, // only diffs after this clock (optional)
53
+ });
54
+ // Returns: [{ clock, fromClock, timestamp, put: {...}, delete: [...] }, ...]
55
+ ```
56
+
57
+ Each entry describes what changed at that clock:
58
+
59
+ - `put` — fields that were set or updated (nested paths for schema-aware types)
60
+ - `delete` — field paths that were removed
61
+ - `clock` — HLC timestamp of the change
62
+ - `fromClock` — the previous clock value (0 for the initial snapshot)
63
+ - `timestamp` — wall-clock time the diff was recorded
64
+
65
+ ### Point-in-time reconstruction
66
+
67
+ ```js
68
+ const value = env.timeMachine.getStateAt('Order', 'order-42', targetClock);
69
+ // Returns the document value at `targetClock`, or undefined if not found
70
+ ```
71
+
72
+ ### Change log across types
73
+
74
+ ```js
75
+ const changes = [...env.timeMachine.getChanges('Order', fromClock, toClock)];
76
+ // Or all types:
77
+ const allChanges = [...env.timeMachine.getChanges(null, fromClock, toClock)];
78
+ ```
79
+
80
+ ## Inspecting status
81
+
82
+ ```js
83
+ // Per-type status
84
+ const status = env.timeMachine.status('Order');
85
+ // {
86
+ // type: 'Order',
87
+ // enabled: true,
88
+ // startClock: 1234567,
89
+ // lastProcessedClock: 9876543,
90
+ // headCount: 412,
91
+ // enabledAt: 1716300000000,
92
+ // }
93
+
94
+ // Summary across all types
95
+ const summary = env.timeMachine.status();
96
+ // { enabled: true, autoEnable: true, types: [...] }
97
+
98
+ // Check if a type is enabled
99
+ env.timeMachine.isEnabled('Order'); // boolean
100
+
101
+ // List all tracked types
102
+ env.timeMachine.list();
103
+ // [{ type: 'Order', enabled: true, startClock: ..., ... }, ...]
104
+ ```
105
+
106
+ ## HTTP API
107
+
108
+ ### Per-type management
109
+
110
+ | Method | Route | Description |
111
+ | ------ | ------------------------------------------------ | ------------------ |
112
+ | GET | `/api/env/:env/time-machine` | Summary status |
113
+ | GET | `/api/env/:env/time-machine/types` | List tracked types |
114
+ | GET | `/api/env/:env/time-machine/types/:type` | Per-type status |
115
+ | POST | `/api/env/:env/time-machine/types/:type/enable` | Enable tracking |
116
+ | POST | `/api/env/:env/time-machine/types/:type/disable` | Disable tracking |
117
+ | POST | `/api/env/:env/time-machine/types/:type/drop` | Drop history |
118
+ | POST | `/api/env/:env/time-machine/enable-all` | Enable all types |
119
+ | POST | `/api/env/:env/time-machine/disable-all` | Disable all types |
120
+
121
+ ### Query routes
122
+
123
+ | Method | Route | Description |
124
+ | ------ | ------------------------------------------------------- | ----------------- |
125
+ | GET | `/api/env/:env/time-machine/:type/:key/history` | History for a key |
126
+ | GET | `/api/env/:env/time-machine/:type/:key/at/:clock` | State at a clock |
127
+ | GET | `/api/env/:env/time-machine/changes?type=T&from=N&to=M` | Change log |
128
+
129
+ History and point-in-time routes return 409 if the requested type is not currently enabled.
130
+
131
+ Query params for `/history`: `limit`, `before` (max clock, inclusive), `after` (min clock, exclusive).
132
+
133
+ ## Schema-aware diffs
134
+
135
+ When a type has a registered schema with nested object properties, Time Machine records diffs at the path level rather than the field level. For example, if `User.address` is a nested object:
136
+
137
+ ```
138
+ // Before: { name: 'Alice', address: { city: 'NYC', zip: '10001' } }
139
+ // After: { name: 'Alice', address: { city: 'LA', zip: '10001' } }
140
+
141
+ // Diff records:
142
+ // put: { 'address.city': 'LA' }
143
+ // (not: { address: { city: 'LA', zip: '10001' } })
144
+ ```
145
+
146
+ ## Storage
147
+
148
+ Time Machine stores data in a dedicated LMDB environment at `<env-path>/time-machine/` with four sub-DBs:
149
+
150
+ - `head` — current snapshot of each tracked document (`[type, key]` → `{ value, clock }`)
151
+ - `diffs` — ordered-binary keyed diffs (`[type, key, clock]` → diff data)
152
+ - `clockToKeys` — reverse index from clock to keys changed at that clock
153
+ - `config` — per-type enabled state, auto-enable flag, and migration metadata
154
+
155
+ Processor cursors (the HLC watermark for each type's change stream) are stored in `~proc:state`, not in the time-machine sub-env.
156
+
157
+ ## Migration from the old per-env API
158
+
159
+ Before version 1.7, Time Machine was enabled env-wide with `env.timeMachine.enable()` / `env.timeMachine.disable()`. This API is now **deprecated** and will be removed in a future release.
160
+
161
+ **What changed:**
162
+
163
+ | Old | New |
164
+ | ------------------------------------------ | ------------------------------------------------- |
165
+ | `enable()` | `enable(type)` or `enableAll()` |
166
+ | `disable()` | `disable(type)` or `disableAll()` |
167
+ | `drop()` | `drop(type)` or `dropAll()` |
168
+ | `isEnabled()` | `isEnabled(type)` or `isEnabled()` (any enabled?) |
169
+ | `POST /api/env/:env/time-machine/enable` | `POST /api/env/:env/time-machine/enable-all` |
170
+ | `DELETE /api/env/:env/time-machine/enable` | `POST /api/env/:env/time-machine/disable-all` |
171
+ | `GET /api/env/:env/time-machine/status` | `GET /api/env/:env/time-machine` |
172
+
173
+ **Automatic migration:** on first open after upgrading, OKDB detects the old per-env config and automatically migrates it to the per-type model. All previously tracked types are re-enabled with their history intact, and each type's processor cursor is set to the old global cursor value. This is one-shot and idempotent.
174
+
175
+ The deprecated no-arg forms (`enable()`, `disable()`, `drop()`) still work and log a deprecation warning once per process. They delegate to `enableAll()` / `disableAll()` / `dropAll()` respectively. Update your code before the next major release.
176
+
177
+ See `UPGRADING.md` for the full migration guide.