@kedem/okdb 1.9.1 → 2.0.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/README.md +3 -2
- package/bin/okdb.js +1 -1
- package/docs/deployment.md +155 -0
- package/docs/diagnostics.md +115 -0
- package/docs/embeddings.md +1 -1
- package/docs/functions.md +30 -27
- package/docs/getting-started.md +14 -3
- package/docs/http-api.md +3 -3
- package/docs/http-cluster.md +93 -60
- package/docs/index.md +13 -1
- package/docs/manifest.json +6 -3
- package/docs/pipelines.md +1 -1
- package/docs/process-registry.md +5 -5
- package/docs/processors.md +35 -18
- package/docs/queue.md +112 -42
- package/docs/sync.md +10 -7
- package/docs/ttl.md +1 -1
- package/docs/upgrade-2.0.md +290 -0
- package/okdb-functions-sandbox-worker.js +1 -1
- package/okdb-http-worker-child.js +1 -1
- package/okdb-views-bootstrap-worker.js +1 -1
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/public/layouts/_default.ok.html +1 -1
- package/public/sections/db/modals/create-env-modal.ok.js +1 -1
- package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
- package/public/sections/db/parts/db-overview.ok.js +1 -1
- package/public/sections/engines/engine-ui-utils.js +1 -1
- package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
- package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
- package/public/sections/queue/parts/code-panel.ok.js +1 -1
- package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
- package/public/sections/sync/parts/sync-topology.ok.js +1 -1
- package/public/sections/system/index.ok.html +1 -1
- package/public/sections/system/parts/system-process-panel.ok.js +1 -1
- package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
- package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
- package/public/setup-app.js +1 -1
- package/public/setup.html +116 -55
- package/types/index.d.ts +33 -3
- package/types/options.d.ts +19 -10
- package/docs/worker-fleet.md +0 -139
- package/okdb-functions-runner-child.js +0 -1
- package/okdb-queue-load-handler.js +0 -1
- package/okdb-queue-spawn-child.js +0 -1
- package/okdb-worker-child.js +0 -1
- package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
package/docs/index.md
CHANGED
|
@@ -72,7 +72,7 @@ All environments are LMDB directories on disk. Each has its own clock, changelog
|
|
|
72
72
|
## Quick example
|
|
73
73
|
|
|
74
74
|
```javascript
|
|
75
|
-
const OKDB = require('
|
|
75
|
+
const OKDB = require('@kedem/okdb');
|
|
76
76
|
|
|
77
77
|
const okdb = new OKDB('./mydb');
|
|
78
78
|
await okdb.open();
|
|
@@ -99,18 +99,30 @@ await okdb.close();
|
|
|
99
99
|
## Docs map
|
|
100
100
|
|
|
101
101
|
- [Getting Started](./getting-started.md) — install, open, first writes
|
|
102
|
+
- [Roles & Deployment](./deployment.md) — role flags, single-process vs multi-process topologies, deferred processing
|
|
102
103
|
- [Data Model](./data-model.md) — types, keys, environments, storage layout
|
|
103
104
|
- [Querying](./querying.md) — reads, ranges, MongoDB-style filters
|
|
104
105
|
- [Indexes](./indexes.md) — secondary, composite, unique, geo
|
|
105
106
|
- [Transactions](./transactions.md) — batching, versioning, optimistic concurrency
|
|
106
107
|
- [Change Log & Events](./change-log.md) — clock, changelog, processor, events
|
|
108
|
+
- [Processors](./processors.md) — the change-subscription primitive: modes, registration, leases
|
|
107
109
|
- [Sync](./sync.md) — multi-node replication
|
|
108
110
|
- [Full-Text Search](./fts.md) — FTS index, tokenizer, query
|
|
109
111
|
- [Embeddings](./embeddings.md) — vector pipeline, semantic search
|
|
112
|
+
- [Materialized Views](./views.md) — reactive reduce/group views over types
|
|
113
|
+
- [Time Machine](./time-machine.md) — per-type field-level history, point-in-time reads
|
|
110
114
|
- [Queue](./queue.md) — durable job queue
|
|
111
115
|
- [Live Subscriptions](./subscriptions.md) — signal-SSE change streams with durable sessions
|
|
112
116
|
- [Files](./files.md) — blob / file storage
|
|
117
|
+
- [TTL](./ttl.md) — per-record time-to-live
|
|
113
118
|
- [Custom Functions](./functions.md) — sandboxed stored JS functions, env/global scope, HTTP execution
|
|
119
|
+
- [Pipelines](./pipelines.md) — declarative content/embedding pipelines
|
|
114
120
|
- [HTTP API](./http-api.md) — built-in REST API
|
|
121
|
+
- [HTTP Clustering](./http-cluster.md) — the CLI's N-capable-nodes cluster
|
|
122
|
+
- [Process Registry](./process-registry.md) — root-wide process visibility and control
|
|
123
|
+
- [Auth & Sync](./auth-and-sync.md) / [Auth Permissions](./auth-permissions.md) — tokens, OAuth, grants
|
|
124
|
+
- [Logging](./logging.md) — structured logs, retention
|
|
125
|
+
- [Diagnostics](./diagnostics.md) — opt-in triage tooling (write-origin ring, fatal report, loop lag)
|
|
115
126
|
- [Plugins](./plugins.md) — extending OKDB before open
|
|
116
127
|
- [Migration](./migration.md) — export, import, backup, blob integrity
|
|
128
|
+
- [Upgrading to 2.0](./upgrade-2.0.md) — the node-model runtime redesign: constructor changes, workers dissolved, claimed responsibilities
|
package/docs/manifest.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[
|
|
2
2
|
{ "file": "index.md", "label": "Overview" },
|
|
3
3
|
{ "file": "getting-started.md", "label": "Getting Started" },
|
|
4
|
+
{ "file": "deployment.md", "label": "Roles & Deployment" },
|
|
4
5
|
{
|
|
5
6
|
"label": "Core",
|
|
6
7
|
"children": [
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
{ "file": "queue.md", "label": "Queue" },
|
|
24
25
|
{ "file": "files.md", "label": "Files" },
|
|
25
26
|
{ "file": "functions.md", "label": "Custom Functions" },
|
|
26
|
-
{ "file": "ttl.md", "label": "TTL (Time-to-Live)" }
|
|
27
|
+
{ "file": "ttl.md", "label": "TTL (Time-to-Live)" },
|
|
28
|
+
{ "file": "time-machine.md", "label": "Time Machine" }
|
|
27
29
|
]
|
|
28
30
|
},
|
|
29
31
|
{
|
|
@@ -36,9 +38,10 @@
|
|
|
36
38
|
{ "file": "process-registry.md", "label": "Process Registry" },
|
|
37
39
|
{ "file": "plugins.md", "label": "Plugins" },
|
|
38
40
|
{ "file": "processors.md", "label": "Processors (modes & registration)" },
|
|
39
|
-
{ "file": "worker-fleet.md", "label": "Workers (population & placement)" },
|
|
40
41
|
{ "file": "logging.md", "label": "Logging" },
|
|
41
|
-
{ "file": "
|
|
42
|
+
{ "file": "diagnostics.md", "label": "Diagnostics" },
|
|
43
|
+
{ "file": "migration.md", "label": "Migration" },
|
|
44
|
+
{ "file": "upgrade-2.0.md", "label": "Upgrading to 2.0" }
|
|
42
45
|
]
|
|
43
46
|
}
|
|
44
47
|
]
|
package/docs/pipelines.md
CHANGED
|
@@ -216,7 +216,7 @@ Config notes:
|
|
|
216
216
|
- `handler.env` — optional env name for the function registry; defaults to the engine owning env
|
|
217
217
|
- `concurrency`, `pollInterval`, `ttl` — forwarded to the underlying queue worker runtime
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
Each claimed payload's function runs in the executing node's local sandbox thread (lazy, idle-reaped); `concurrency` bounds in-flight invocations — there is no pool to configure.
|
|
220
220
|
|
|
221
221
|
Behavior:
|
|
222
222
|
|
package/docs/process-registry.md
CHANGED
|
@@ -29,18 +29,18 @@ The primary entity is the **process**, not the processor. Each long-lived proces
|
|
|
29
29
|
pid, ppid, host,
|
|
30
30
|
supervisorKey, // nodeKey of the supervising process (null = a root) → builds the tree
|
|
31
31
|
slot, slotKey, // supervised position; slotKey is the STABLE durable control key (see below)
|
|
32
|
-
role, // { engines,
|
|
32
|
+
role, // { engines, processors, compaction, http } — booleans
|
|
33
33
|
listenAddr,
|
|
34
34
|
caps, // { controllable, decommissionable, killable, killConfirm? } — see Capabilities
|
|
35
35
|
lifecycle, // { state: 'running' | 'decommissioning', desiredVersionApplied }
|
|
36
|
-
processing, // FACET (
|
|
36
|
+
processing, // FACET (participants only): { participate, processors[] }
|
|
37
37
|
heartbeatAt, expiresAt,
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
A processor (FTS, views, materializer, time-machine, …) is **not** a separate entity — it is summarized
|
|
42
|
-
on the owner's `processing` facet.
|
|
43
|
-
|
|
42
|
+
on the owner's `processing` facet (`processing.processors[]`). There are no worker pools in 2.0 —
|
|
43
|
+
drains run on the claiming node's loop, so there is nothing pool-shaped to give its own row.
|
|
44
44
|
|
|
45
45
|
### Two stores, split by lifetime
|
|
46
46
|
|
|
@@ -163,7 +163,7 @@ const db = new OKDB('./data', { processes: { register: true, kind: 'embedded' }
|
|
|
163
163
|
const db = new OKDB('./data', { processes: { kind: 'standalone', listenAddr: ':8484' } });
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
`register: true` makes a process publish its own row even when it is not
|
|
166
|
+
`register: true` makes a process publish its own row even when it is not a `processors`-role owner.
|
|
167
167
|
`bin/okdb` sets `kind` automatically (`primary` / `http-server`); embedders pass it explicitly.
|
|
168
168
|
|
|
169
169
|
---
|
package/docs/processors.md
CHANGED
|
@@ -9,15 +9,18 @@ A **processor** maintains derived state from a source type's change log. You reg
|
|
|
9
9
|
|
|
10
10
|
OKDB's own features (indexes, views, FTS, materializer, embeddings, time-machine) are built on this primitive — and it's a public extension point you can use directly.
|
|
11
11
|
|
|
12
|
-
> **
|
|
13
|
-
>
|
|
14
|
-
> (
|
|
12
|
+
> **What vs. where.** This doc defines _what_ a processor is and its **modes** (the
|
|
13
|
+
> guarantee). _Whether_ a node runs `single` processors is the boolean `processors: true|false`
|
|
14
|
+
> (participate vs. passive); the per-`(env,type)` lease distributes the work across whatever
|
|
15
|
+
> participating nodes share the root (1-of-N, with failover). There is no filtered claim API and
|
|
16
|
+
> no managed "worker population" — see [Upgrading to 2.0](upgrade-2.0.md) for the node-model
|
|
17
|
+
> placement story.
|
|
15
18
|
|
|
16
19
|
---
|
|
17
20
|
|
|
18
21
|
## Modes — the delivery guarantee
|
|
19
22
|
|
|
20
|
-
A mode names the **delivery guarantee**: how many instances run the handler.
|
|
23
|
+
A mode names the **delivery guarantee**: how many instances run the handler. All drains run on the owner process's **event loop**, chunked into cooperative quanta (`FLUSH_QUANTUM`-bounded batches with a yield between them) so a large drain never blocks the loop monolithically. There is no execution placement to configure — no thread pool, no `runIn`; the only thread in the engine is the **sandbox** that runs untrusted user-function code. Handlers register per process: pass `handler:'<name>'` (named registry), `module:{path,export}`, or — for internal features — a closure paired with a reconstructible `quantum`.
|
|
21
24
|
|
|
22
25
|
| mode | cardinality | cursor | use when |
|
|
23
26
|
| -------- | ----------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
@@ -69,7 +72,7 @@ The `handler` is always a **closure** (a function). The portable `handler: {modu
|
|
|
69
72
|
|
|
70
73
|
## Snapshot bootstrap and chunked drains
|
|
71
74
|
|
|
72
|
-
A `single` processor with `bootstrap: 'snapshot'` performs a cooperative snapshot scan
|
|
75
|
+
A `single` processor with `bootstrap: 'snapshot'` performs a cooperative snapshot scan, yielding every batch so the event loop stays responsive while a large type is bootstrapped. Live drains run on the loop via chunked `_flush` (a `FLUSH_QUANTUM` bounds each quantum, then yields; heavy per-item features pass a smaller per-processor `flushQuantum`). A feature may supply a reconstructible **`quantum`** module — it re-derives the work for a clock range and commits it durably (optionally advancing the cursor atomically with its output, `selfCursor`) before the cursor moves. This is exactly the path FTS uses — every FTS-indexed type registers a `single`-mode processor (hydrate + tokenize + posting writes) drained on the loop in small bounded quanta.
|
|
73
76
|
|
|
74
77
|
The cursor only advances on a successful drain, so processing is **at-least-once**; make handlers idempotent. Derived state is eventually-consistent; if you need a deterministic "caught up" point, wait on `stop.status().lastClock` reaching the source clock.
|
|
75
78
|
|
|
@@ -79,8 +82,12 @@ The cursor only advances on a successful drain, so processing is **at-least-once
|
|
|
79
82
|
|
|
80
83
|
Each processor takes a lease (the `'proc'` lock). With several processes open on the same env, exactly one runs each `single` processor; the others wait and take over if the holder dies. The lease-holding process drains on its own loop, so there is always a single writer to your derived store.
|
|
81
84
|
|
|
85
|
+
Under sustained load the lease **load-shares by cooperative hold-window claiming**: the holder drains for up to `OKDB_LEASE_HOLD_MS` (200 ms) per window, and at the window boundary yields the lease **only if a peer has signalled it wants a turn** (`~lock:want`, a decaying marker written by waiting peers with backlog). A lone process therefore keeps its leases with zero lock churn; two loaded processes rotate and split the work ≈evenly. Waiting peers re-contend on a short cadence (~one hold window) while they have backlog and on the slow `ttl/2` failover cadence when idle.
|
|
86
|
+
|
|
82
87
|
A processor that hits an error **keeps its lease** (in-place `retry()`/`restart()` keeps its slot against standbys in other processes). Re-bootstrap reuses the held lease handle, and `tryAcquire` treats a fresh lease held by the same pid + processorId as a re-acquire — so an errored processor can never deadlock in `waiting` against its own heartbeat.
|
|
83
88
|
|
|
89
|
+
Cross-process wakes ride the UDP bus (`SYSTEM_POKE`), which is lossy by design. An online holder therefore also runs an **idle catch-up tick** (`OKDB_PROCESSOR_CATCHUP_MS`, default 15 000, `0` disables): every interval it compares each single-mode registration's type head against its cursor and schedules a drain when behind, so a lost poke bounds staleness at one tick instead of "until the next write". The changelog GC in turn **never prunes past the slowest durable processor cursor** — a lagging drain finds its entries intact rather than silently skipping writes.
|
|
90
|
+
|
|
84
91
|
---
|
|
85
92
|
|
|
86
93
|
## Cookbook
|
|
@@ -110,16 +117,26 @@ that safe. The per-write gate costs one boolean compare per write when no flip i
|
|
|
110
117
|
|
|
111
118
|
---
|
|
112
119
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
The `
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
| `
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
## Where a processor runs — claiming
|
|
121
|
+
|
|
122
|
+
The `processors` constructor option is a **boolean** controlling whether **this node**
|
|
123
|
+
participates in claiming `single` (1-of-N) processors. It is independent of the mode declared at
|
|
124
|
+
`register()`.
|
|
125
|
+
|
|
126
|
+
| value | behavior |
|
|
127
|
+
| ------- | ------------------------------------------------------------------------------------------- |
|
|
128
|
+
| `true` | **default.** Participate — claim every unclaimed `single` lease this node sees. |
|
|
129
|
+
| `false` | Passive — claim nothing (reads/writes + inline processors still work; another node drains). |
|
|
130
|
+
|
|
131
|
+
The per-`(env,type)` lease + WAITING-retry is the load balancer — start more participating nodes
|
|
132
|
+
over the same path and the work spreads automatically (1-of-N, with failover when a holder dies);
|
|
133
|
+
there is no population to configure and no per-node dedication.
|
|
134
|
+
|
|
135
|
+
> **Removed before 2.0 shipped:** the _filtered_ claim form — `processors: '<filter>'` (throws at
|
|
136
|
+
> construction) and `db.processors.process(filter)` / `processRest()` / `processEverything()`
|
|
137
|
+
> (throw with a migration message), plus per-node dedication/intent. Also removed: the
|
|
138
|
+
> `processing:` option family (`'auto'|'main'|'none'`, `'threads'|'processes'`) — there is no
|
|
139
|
+
> execution placement; every drain runs on its claimant's loop in bounded quanta.
|
|
140
|
+
> `asyncProcessors: true/false` is now `processors: true/false`. To dedicate hardware to
|
|
141
|
+
> processing, run that node `processors:true` and the others `processors:false`. See
|
|
142
|
+
> [Upgrading to 2.0](upgrade-2.0.md).
|
package/docs/queue.md
CHANGED
|
@@ -22,9 +22,9 @@ async function main() {
|
|
|
22
22
|
const okdb = new OKDB('./db');
|
|
23
23
|
await okdb.open();
|
|
24
24
|
|
|
25
|
-
// process() runs the consumer IN THIS process
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// process() runs the consumer IN THIS process — this is the model.
|
|
26
|
+
// okdb does not auto-run queue handlers; keeping a consumer process alive is the
|
|
27
|
+
// launcher's job. Scale by running more processes (pm2, systemd, docker).
|
|
28
28
|
const worker = okdb.queue.process(
|
|
29
29
|
'send-email',
|
|
30
30
|
async (payload, ctx) => {
|
|
@@ -175,6 +175,63 @@ const worker = okdb.queue.process(
|
|
|
175
175
|
);
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
### Several types, one lane pool (array form)
|
|
179
|
+
|
|
180
|
+
Passing an **array of entries** runs several job types through ONE shared lane pool. Each
|
|
181
|
+
entry is exactly the single form folded into an object — `{ type, handler, ...perTypeOptions }`
|
|
182
|
+
— and the second argument becomes the **pool options** shared by every lane:
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const worker = okdb.queue.process(
|
|
186
|
+
[
|
|
187
|
+
{ type: 'sync:forward', handler: forwardSync, weight: 3, ttl: 60_000 },
|
|
188
|
+
{ type: 'sync:backfill', handler: backfillSync, weight: 1, ttl: 300_000 },
|
|
189
|
+
{ type: 'content:page', handler: fetchPage },
|
|
190
|
+
],
|
|
191
|
+
{
|
|
192
|
+
concurrency: 4, // the PROCESS-WIDE cap: at most 4 handlers in flight across ALL types
|
|
193
|
+
admission: () => okdb.pressure().score < 1, // load-aware: skip claiming under distress
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Why this exists: stacking N single-type consumers gives N _independent_ lane pools — a process
|
|
199
|
+
consuming 10 types at `concurrency: 2` each could run 20 handlers at once, with no total cap
|
|
200
|
+
and no cross-type fairness. The array form gives you:
|
|
201
|
+
|
|
202
|
+
- **One concurrency budget** across every type (the pool's `concurrency`).
|
|
203
|
+
- **Weighted fairness** — `weight` (default 1) biases the claim sweep, so under contention a
|
|
204
|
+
weight-3 type gets ~3× the first-claim attempts of a weight-1 type; an idle type costs
|
|
205
|
+
nothing.
|
|
206
|
+
- **One admission gate** — `admission: () => boolean` is consulted before every claim sweep;
|
|
207
|
+
while it returns false the pool claims nothing (in-flight handlers finish normally, and the
|
|
208
|
+
durable jobs remain claimable by other processes). Re-checked every `admissionInterval` ms
|
|
209
|
+
(default 500). Combine with [`db.pressure()`](#load-aware-consumers) for a load-aware
|
|
210
|
+
consumer in one line.
|
|
211
|
+
|
|
212
|
+
Per-type knobs (`ttl`, `autoHeartbeat`, `onPermanentFail`) live on the entry; cluster-wide
|
|
213
|
+
controls (`max_concurrency`, buckets, priority) keep working unchanged — they're enforced at
|
|
214
|
+
claim time regardless of consumer shape.
|
|
215
|
+
|
|
216
|
+
### Load-aware consumers
|
|
217
|
+
|
|
218
|
+
`db.pressure()` returns a composite load signal for this node, cached for 250 ms (safe to
|
|
219
|
+
poll per claim):
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
{
|
|
223
|
+
writerStallMs, // max oldest-pending commit age across envs — THE stall tell
|
|
224
|
+
writerDepth, // max in-flight commit depth
|
|
225
|
+
maxDurableLag, // max durable processor lag (FTS/views/TM/embeddings behind)
|
|
226
|
+
queuePending, // pending+retry jobs across envs (workload, not in the score)
|
|
227
|
+
loopLagMs, // mean event-loop delay since the previous read
|
|
228
|
+
score, // max of the normalized distress signals; 1 ≈ at the limit
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`admission: () => okdb.pressure().score < 1` makes a consumer yield to indexing/write load
|
|
233
|
+
automatically; an external autoscaler can poll the same read to decide when to add nodes.
|
|
234
|
+
|
|
178
235
|
### Handler signature
|
|
179
236
|
|
|
180
237
|
The handler receives:
|
|
@@ -198,19 +255,61 @@ These local worker helpers now map to the same public queue API that remote work
|
|
|
198
255
|
- `okdb.queue.markJobComplete(jobId, claimId, result?)`
|
|
199
256
|
- `okdb.queue.markJobFail(jobId, claimId, error, code?)`
|
|
200
257
|
|
|
201
|
-
### Placement: `process`
|
|
258
|
+
### Placement: `process`
|
|
202
259
|
|
|
203
|
-
The
|
|
260
|
+
The queue is a **substrate okdb coordinates but does not place** — handlers are trusted user code
|
|
261
|
+
of unknown nature, so okdb never auto-runs them on a population. The surface is:
|
|
204
262
|
|
|
205
|
-
| Verb
|
|
206
|
-
|
|
|
207
|
-
| `queue.process(type, fn)`
|
|
208
|
-
| `queue.worker(type, module)` | the **worker population** (other processes) | a **module path** / `{module, export}` | durable; the fleet runs it |
|
|
209
|
-
| `queue.spawn(type, module)` | a **dedicated forked child** | a **module path** | tied to the child |
|
|
263
|
+
| Verb | Runs in | 2nd arg | Lifetime |
|
|
264
|
+
| ------------------------- | --------------------- | ------------- | ----------------------- |
|
|
265
|
+
| `queue.process(type, fn)` | **this** Node process | a **closure** | dies with this instance |
|
|
210
266
|
|
|
211
|
-
|
|
267
|
+
`queue.process(type, closure)` is **the model** — single-execution and at-least-once come from
|
|
268
|
+
the CAS claim on the durable job row. Scale by running more independent processes: each opens
|
|
269
|
+
okdb and calls `process()`, and the durable CAS claim hands each job to exactly one consumer.
|
|
270
|
+
The canonical worker pattern is a standalone script (`workers/default.js`) launched by pm2,
|
|
271
|
+
systemd, or docker alongside your main service.
|
|
212
272
|
|
|
213
|
-
`
|
|
273
|
+
> **Removed:** `queue.worker(type, module)` — the auto-adopted "shared pool" form — throws
|
|
274
|
+
> `QUEUE_WORKER_REMOVED`. `queue.spawn(type, module)` — the dedicated forked child — was also
|
|
275
|
+
> removed in okdb 2.0. Run independent consumer processes instead; they claim the same durable
|
|
276
|
+
> jobs exactly once via CAS. See docs/upgrade-2.0.md for migration.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## The worker pattern
|
|
281
|
+
|
|
282
|
+
The canonical way to run queue consumers outside your main process:
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
// workers/default.js — run alongside the main service
|
|
286
|
+
const OKDB = require('okdb');
|
|
287
|
+
|
|
288
|
+
async function main() {
|
|
289
|
+
const okdb = new OKDB('./db');
|
|
290
|
+
await okdb.open();
|
|
291
|
+
|
|
292
|
+
okdb.queue.process(
|
|
293
|
+
'send-email',
|
|
294
|
+
async (payload, ctx) => {
|
|
295
|
+
await sendEmail(payload.to, payload.subject);
|
|
296
|
+
},
|
|
297
|
+
{ concurrency: 4 },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// okdb.queue.process('resize-image', async (payload, ctx) => { ... });
|
|
301
|
+
|
|
302
|
+
process.on('SIGTERM', async () => {
|
|
303
|
+
await okdb.close();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
main().catch(console.error);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Launch it with pm2, systemd, docker, or kubernetes — run N replicas and each drains jobs via CAS
|
|
312
|
+
without coordination. A replica that dies leaves its in-flight jobs to re-queue on claim expiry.
|
|
214
313
|
|
|
215
314
|
---
|
|
216
315
|
|
|
@@ -320,35 +419,6 @@ POST /api/queue/job/<id>/fail
|
|
|
320
419
|
|
|
321
420
|
---
|
|
322
421
|
|
|
323
|
-
## Spawned workers
|
|
324
|
-
|
|
325
|
-
If you want queue handling in a **separate Node process**, use `spawn(...)`:
|
|
326
|
-
|
|
327
|
-
```javascript
|
|
328
|
-
const spawned = await okdb.queue.spawn('send-email', './workers/email.js', {
|
|
329
|
-
concurrency: 4,
|
|
330
|
-
pollInterval: 100,
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// later
|
|
334
|
-
await spawned.stop();
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
The module must export a function:
|
|
338
|
-
|
|
339
|
-
```javascript
|
|
340
|
-
module.exports = async function (payload, ctx) {
|
|
341
|
-
await ctx.okdb.put('email_log', payload.orderId, {
|
|
342
|
-
sentAt: Date.now(),
|
|
343
|
-
jobId: ctx.jobId,
|
|
344
|
-
});
|
|
345
|
-
};
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
The child process opens the same OKDB path and receives the same `ctx.okdb` semantics as a normal worker.
|
|
349
|
-
|
|
350
|
-
---
|
|
351
|
-
|
|
352
422
|
## Job lifecycle
|
|
353
423
|
|
|
354
424
|
```text
|
|
@@ -630,7 +700,7 @@ The queue-worker engine is a pipeline-compatible engine type that:
|
|
|
630
700
|
- invokes a stored function for each claimed payload
|
|
631
701
|
- handles completion/failure using normal queue semantics
|
|
632
702
|
|
|
633
|
-
Worker `concurrency` controls job throughput.
|
|
703
|
+
Worker `concurrency` controls job throughput. Each invoked function runs in the executing node's local sandbox thread (lazy, idle-reaped) — no pool to size.
|
|
634
704
|
|
|
635
705
|
---
|
|
636
706
|
|
package/docs/sync.md
CHANGED
|
@@ -96,7 +96,7 @@ Returns up to `delta_limit` (default 500) change records from clock `N` onwards
|
|
|
96
96
|
|
|
97
97
|
### Multi-environment sync
|
|
98
98
|
|
|
99
|
-
The delta spans **all
|
|
99
|
+
The delta spans **all changelog-enabled user environments** simultaneously. The response carries a `clocks` map (`{ default: N, queue: N, ... }`), and each environment's cursor advances independently. Custom environments created via `createEnvironment` are included automatically — every user env enables its changelog and participates in sync (the per-env `sync` option was removed in 2.0; internal `~`-prefixed envs never participate).
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
@@ -140,7 +140,11 @@ LWW depends on timestamps. If two nodes have significantly different system cloc
|
|
|
140
140
|
:::
|
|
141
141
|
|
|
142
142
|
:::note Non-syncable data
|
|
143
|
-
|
|
143
|
+
Internal `~`-prefixed environments (FTS indexes under `~fts`, vector stores, the `~sub`
|
|
144
|
+
subscriptions env) carry no changelog and are excluded from sync. They are rebuilt locally from
|
|
145
|
+
synced data. This is intentional — posting lists and HNSW graphs are large, computable, and
|
|
146
|
+
node-local. (In 1.9 these were opted out with `sync: false`; that option was removed in 2.0 —
|
|
147
|
+
exclusion is now derived from the env being a rebuildable internal store.)
|
|
144
148
|
:::
|
|
145
149
|
|
|
146
150
|
---
|
|
@@ -149,8 +153,7 @@ Environments with `sync: false` (FTS indexes, vector stores) are excluded from s
|
|
|
149
153
|
|
|
150
154
|
`okdb-bus` sends a UDP multicast datagram on `239.1.2.3:30303` whenever the clock changes. This is how peers on the same LAN discover that new data is available without polling. Loss is acceptable — sync reconciles correctly even if some pokes are dropped.
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```
|
|
156
|
+
The bus is **auto-enabled** when the LMDB path is shared (the shmbuf native binding present);
|
|
157
|
+
it is off for a lone single-process node. The `bus` constructor option was removed in 2.0 (it
|
|
158
|
+
threw nothing useful as a manual switch — it is load-bearing for POKE/DRAIN coherence and so is
|
|
159
|
+
derived, not asked). See [Upgrading to 2.0](upgrade-2.0.md).
|
package/docs/ttl.md
CHANGED