@kedem/okdb 1.6.4 → 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.
- package/bin/okdb.js +1 -1
- package/docs/change-log.md +23 -0
- package/docs/pipelines.md +6 -1
- package/docs/time-machine.md +177 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/db/parts/type-viewer.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-locks-panel.ok.js +1 -0
- package/public/sections/views/parts/view-detail.ok.js +1 -1
package/docs/change-log.md
CHANGED
|
@@ -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.
|