@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.
Files changed (48) hide show
  1. package/README.md +3 -2
  2. package/bin/okdb.js +1 -1
  3. package/docs/deployment.md +155 -0
  4. package/docs/diagnostics.md +115 -0
  5. package/docs/embeddings.md +1 -1
  6. package/docs/functions.md +30 -27
  7. package/docs/getting-started.md +14 -3
  8. package/docs/http-api.md +3 -3
  9. package/docs/http-cluster.md +93 -60
  10. package/docs/index.md +13 -1
  11. package/docs/manifest.json +6 -3
  12. package/docs/pipelines.md +1 -1
  13. package/docs/process-registry.md +5 -5
  14. package/docs/processors.md +35 -18
  15. package/docs/queue.md +112 -42
  16. package/docs/sync.md +10 -7
  17. package/docs/ttl.md +1 -1
  18. package/docs/upgrade-2.0.md +290 -0
  19. package/okdb-functions-sandbox-worker.js +1 -1
  20. package/okdb-http-worker-child.js +1 -1
  21. package/okdb-views-bootstrap-worker.js +1 -1
  22. package/okdb.js +1 -1
  23. package/package.json +1 -1
  24. package/public/layouts/_default.ok.html +1 -1
  25. package/public/sections/db/modals/create-env-modal.ok.js +1 -1
  26. package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
  27. package/public/sections/db/parts/db-overview.ok.js +1 -1
  28. package/public/sections/engines/engine-ui-utils.js +1 -1
  29. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
  30. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
  31. package/public/sections/queue/parts/code-panel.ok.js +1 -1
  32. package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
  33. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  34. package/public/sections/sync/parts/sync-topology.ok.js +1 -1
  35. package/public/sections/system/index.ok.html +1 -1
  36. package/public/sections/system/parts/system-process-panel.ok.js +1 -1
  37. package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
  38. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
  39. package/public/setup-app.js +1 -1
  40. package/public/setup.html +116 -55
  41. package/types/index.d.ts +33 -3
  42. package/types/options.d.ts +19 -10
  43. package/docs/worker-fleet.md +0 -139
  44. package/okdb-functions-runner-child.js +0 -1
  45. package/okdb-queue-load-handler.js +0 -1
  46. package/okdb-queue-spawn-child.js +0 -1
  47. package/okdb-worker-child.js +0 -1
  48. package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
@@ -0,0 +1,155 @@
1
+ # Roles & Deployment
2
+
3
+ okdb 2.0 has one runtime model: **the process is the control unit**. Every process that opens
4
+ a data path declares — at construction — which background work it runs, and okdb never forks
5
+ or supervises processes for you. Scale-out is always the same move: **run more okdb processes
6
+ on the same path**; the per-processor 1-of-N lease distributes the work automatically, with no
7
+ coordinator.
8
+
9
+ If you only ever run one process, you can stop reading: `new OKDB(path)` is a full
10
+ do-everything node and the defaults are correct.
11
+
12
+ ---
13
+
14
+ ## The role flags
15
+
16
+ ```js
17
+ new OKDB(path, {
18
+ processors: true, // claim processor leases + drain derived work (default true)
19
+ engines: true, // run embeddings / vector-search engines (default true)
20
+ compaction: true, // eligible to claim the per-env compaction lease (default true)
21
+ });
22
+ ```
23
+
24
+ | Flag | `true` (default) | `false` (passive) |
25
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
26
+ | `processors` | Claim every unclaimed `single` processor lease; drain FTS / views / time-machine / materializer / embeddings on this loop | Claim nothing. Reads, writes, and **inline** processors (indexes, schema, FK, TTL) still run |
27
+ | `engines` | Run queue worker engines, pipeline workers, embeddings engines | None of those start here |
28
+ | `compaction` | Eligible to claim the per-env compaction lease | Never auto-compacts (`env.compact()` still works when called explicitly) |
29
+
30
+ Three things to internalize:
31
+
32
+ - **Roles are policy, not placement.** A passive node is a full read/write citizen — its writes
33
+ land in the changelog and are indexed by whichever participating node holds the lease. Inline
34
+ processors are mandatory on every writer regardless of role.
35
+ - **The lease is the load balancer.** With N participating nodes, each `single` processor is
36
+ claimed by exactly one; a dead node's leases lapse and survivors re-claim. Under sustained
37
+ backlog holders rotate work in cooperative hold windows. See [Processors](./processors.md).
38
+ - **Ephemeral processes should be passive.** A CLI one-shot or short-lived script that grabs a
39
+ 1-of-N lease and exits mid-quantum just thrashes it — pass `processors: false`.
40
+
41
+ ---
42
+
43
+ ## Topologies
44
+
45
+ ### 1. One embedded process (the default)
46
+
47
+ ```js
48
+ const db = new OKDB('./data');
49
+ await db.open();
50
+ ```
51
+
52
+ Serves your app's reads/writes and does all derived work on its own loop, in bounded quanta.
53
+ This is crash-safe and correct on its own; you add processes when you decide you need them.
54
+
55
+ ### 2. Embedded + HTTP
56
+
57
+ ```js
58
+ db.http.listen(8080); // returns a plain http.Server
59
+ ```
60
+
61
+ Same single process, plus the REST API and admin UI. The embedded server is always
62
+ single-process — `http.listen(port, { workers })` throws `HTTP_CLUSTER_REMOVED`.
63
+
64
+ ### 3. N capable nodes (the CLI's shape)
65
+
66
+ Run N identical full-role processes on the same path — either via `bin/okdb` (shared listen
67
+ socket, thin passive supervisor; see [HTTP Clustering](./http-cluster.md)) or as N independent
68
+ `new OKDB(path)` + `http.listen()` processes behind your own load balancer. Each node serves
69
+ **and** processes; the leases spread the processors across them and fail over when a node dies.
70
+
71
+ ### 4. Dedicated workers + a passive serving node
72
+
73
+ When you want the serving loop insulated from heavy backfill/indexing work:
74
+
75
+ ```js
76
+ // serving process — loop only serves; never claims, never compacts
77
+ const front = new OKDB(path, { processors: false, engines: false, compaction: false });
78
+ front.http.listen(8080);
79
+
80
+ // N worker processes — headless, do all the processing
81
+ const worker = new OKDB(path, { processors: true }); // engines/compaction default true
82
+ ```
83
+
84
+ Workers need no ports: coordination is the shared LMDB + the UDP bus. Placement and respawn
85
+ are your launcher's job (systemd, pm2, docker, k8s, a PowerShell script — anything that can
86
+ start N processes).
87
+
88
+ **Boot order tip:** start processes one at a time — wait until a node has fully opened (e.g.
89
+ it bound its port, or touched a ready-file of your choosing) before starting the next, so
90
+ concurrent first-opens of the same envs never race.
91
+
92
+ ---
93
+
94
+ ## Dynamic participation: `processors.start()` / `processors.stop()`
95
+
96
+ The `processors` constructor option is only the **initial** state — participation is
97
+ controlled at runtime:
98
+
99
+ ```js
100
+ db.processors.start(); // begin participating: un-gate claiming, run the deferred view boot
101
+ db.processors.stop(); // cease: finish in-flight quanta, release leases (peers fail over)
102
+ ```
103
+
104
+ Both are idempotent and resolve `true` when the state actually changed. `stop()` never
105
+ touches reads, writes, or inline processors — the node stays a full read/write citizen.
106
+
107
+ **Fast startup** falls out of this: open without participating, start once your service is up.
108
+
109
+ ```js
110
+ const db = new OKDB(path, { processors: false });
111
+ await db.open(); // fast — no view boot, no FTS/TM drains competing with your startup
112
+
113
+ app.listen(PORT, async () => {
114
+ await db.processors.start(); // now begin claiming + draining
115
+ });
116
+ ```
117
+
118
+ **Maintenance windows** too: `stop()` during a heavy backfill to keep this node's loop free,
119
+ `start()` after — the leases redistribute to peers and back via normal failover.
120
+
121
+ ---
122
+
123
+ ## Controlling processors at runtime
124
+
125
+ Static role flags decide _participation_; the running system is controlled per processor
126
+ through durable desired-state (applies across every process, no IPC):
127
+
128
+ - **Node-level participation** — `db.processors.start()` / `stop()` (see above); this-process,
129
+ in-memory.
130
+ - **Pause / resume / retry / cursor reset** — per processor, from the admin UI's Processors
131
+ view or `stop.pause()` / `stop.resume()` on the registration handle. A durable pause
132
+ survives restarts and re-claims (cross-process, unlike node-level start/stop).
133
+ - **Mode switching** — `setMode(logicalKey, mode)` flips a processor between sync/async at a
134
+ clock boundary. See [Processors](./processors.md).
135
+ - **Observability** — `GET /api/processors/status` (per-processor durable lag — the honest
136
+ backlog signal from any node), `GET /api/processes/tree` (the census of OS processes on the
137
+ root), and the per-env Write queue (`depth`, `oldestPendingMs` — the stall tell). These are
138
+ the signals an external autoscaler would watch; the scaling _policy_ lives with your
139
+ launcher, not inside okdb.
140
+ - **`db.pressure()`** — the composite of those signals for THIS node (writer stall/depth, max
141
+ durable lag, queue backlog, loop lag, and a normalized `score`), cached 250 ms. Feed it to a
142
+ queue consumer's `admission` option (`() => db.pressure().score < 1`) for load-aware
143
+ consumption, or poll it from your orchestrator. See docs/queue.md → "Load-aware consumers".
144
+
145
+ ---
146
+
147
+ ## Liveness guarantees (what you can rely on)
148
+
149
+ - Cross-process wakes ride the UDP bus; because UDP is lossy, every participating node also
150
+ runs an idle **catch-up tick** (`OKDB_PROCESSOR_CATCHUP_MS`, default 15 s) that drains any
151
+ backlog a lost poke left behind — a write from a short-lived process is picked up within
152
+ one tick, not "whenever the next write happens".
153
+ - The **changelog is never pruned past the slowest processor cursor**, so a lagging drain
154
+ finds its entries intact instead of silently skipping writes. A durably paused processor
155
+ therefore pins changelog growth — visible and recoverable, by design.
@@ -0,0 +1,115 @@
1
+ # Diagnostics — finding crashes, hangs, and stalls
2
+
3
+ okdb's hardest failures are timing races on LMDB's native memory map: a write or read that
4
+ touches an env after it was closed/unmapped for a compaction swap or `removeEnvironment`. They
5
+ surface as:
6
+
7
+ - **`The environment is already closed`** — an uncaught JS error thrown from inside lmdb's own
8
+ deferred callback (no okdb stack at throw time). The "crash".
9
+ - **`0xC0000005` / access violation** (Windows) / **SIGSEGV** (POSIX) — a native read/write
10
+ after unmap. The "hard crash".
11
+ - **A hang / "halt"** — a compaction swap that never completes because something pins the env
12
+ across the swap (a busy slot, a wedged commit).
13
+
14
+ These are rare and timing-dependent, so the fix is **resident, opt-in instrumentation** you can
15
+ turn on in the failing deployment and leave on until it fires. Three independent tools, each a
16
+ single env var. **None has any cost when off.**
17
+
18
+ > **Before anything else: are you running current code?** These races have been fixed
19
+ > incrementally. okdb ships as a **build** (`dist/` / `release/` / npm package), not `src/`. If
20
+ > your deployment's build predates the fix, you are chasing a ghost. Rebuild
21
+ > (`npm run build` / `npm run build:release`) and redeploy, then reproduce. Check the build date
22
+ > against the relevant commits before spending time on instrumentation.
23
+
24
+ ---
25
+
26
+ ## 1. `OKDB_DIAG` — the write-orphan ring + drain-stall watchdog
27
+
28
+ The primary tool for the "already closed" orphan and the compaction "halt". JS-level, works on
29
+ the main process **and** slot/worker isolates.
30
+
31
+ | Env var | Default | Effect |
32
+ | ---------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `OKDB_DIAG=1` | off | Enable the write-origin ring, the orphan crash-dump handler, and the drain-stall watchdog. |
34
+ | `OKDB_DIAG_STACK=1` | off | Capture a JS stack **at each write**, so a dump names the exact call site (doc commit / cursor save / view-fts-tm replay). A few µs/write — fine for a debugging run. |
35
+ | `OKDB_DIAG_DIR=<path>` | stderr only | Also append every dump to `<path>/okdb-diag.<pid>.log` (survives the crash). |
36
+ | `OKDB_DIAG_RING=N` | 256 | Size of the recent-writes ring. |
37
+ | `OKDB_DIAG_STALL_MS=N` | 8000 | A **swap** drain held longer than this is dumped as a stall (re-dumped every `N` ms while it persists). |
38
+ | `OKDB_DIAG_WATCH_MS=N` | 2000 | Stall-watchdog poll interval. |
39
+ | `OKDB_DIAG_SURVIVE=1` | off | After dumping an orphan, **do not exit** — ride through it (the orphaned write is lost, but the process keeps running). Use to gather more than one dump per run. |
40
+
41
+ **On an orphan** (`already closed` / invalid-read-txn signature) you get the recent write
42
+ origins, newest first — the orphaning call site is named (with `OKDB_DIAG_STACK=1`):
43
+
44
+ ```
45
+ === OKDB_DIAG ORPHAN (env-closed write) @ ... pid=19932 ===
46
+ error: Error: The environment is already closed
47
+ recent writes (newest first):
48
+ #4821 -3ms env=default raw-put
49
+ at OKDBProcessor._saveCursor (.../okdb-processor.js:...)
50
+ ...
51
+ #4820 -4ms env=default commit
52
+ ...
53
+ drain state (envs with a non-zero drain):
54
+ env=default phase=SWAP drainAge=120ms activeWriters=1 writer{depth=1 oldestPendingMs=118 ...}
55
+ ```
56
+
57
+ **On a stall** the watchdog dumps _why_ the swap can't finish — which writer is in flight and
58
+ which slots are pinning the env:
59
+
60
+ ```
61
+ === OKDB_DIAG STALL: swap drain held 8400ms on env=default @ ... ===
62
+ env=default phase=SWAP drainAge=8400ms activeWriters=0 writer{depth=0 oldestPendingMs=0 parkedNow=0}
63
+ slots PINNING this env: #3/fts/fts:default:Order
64
+ ```
65
+
66
+ **Live, no crash needed:** `db.diag()` / `env.diag()` return the same snapshot
67
+ (`{recentWrites, drain, ...}`) for an ad-hoc tap or an admin endpoint.
68
+
69
+ ---
70
+
71
+ ## 2. `OKDB_FATAL_REPORT` — native fault report
72
+
73
+ For the **native** crash (`0xC0000005` / SIGSEGV) that `OKDB_DIAG` can't catch (it's not a JS
74
+ exception). Makes Node write a diagnostic report naming the **faulting thread's** JS + native
75
+ stack, env, and thread id.
76
+
77
+ | Env var | Effect |
78
+ | ------------------------- | ------------------------------------------------------------------------ |
79
+ | `OKDB_FATAL_REPORT=1` | Write a Node report on a fatal native error. |
80
+ | `OKDB_REPORT_DIR=<path>` | Where to write reports (created if missing). |
81
+ | `OKDB_FATAL_REPORT_ENV=1` | Include env vars in the report (off by default — they may hold secrets). |
82
+
83
+ ---
84
+
85
+ ## 3. `OKDB_NATIVE_OP_LOG` — the unmap timeline
86
+
87
+ For an **open-vs-close race** on lmdb-js's shared (process-global, refcounted) native env: a
88
+ synchronous breadcrumb right before each risky native op that unmaps/remaps memory
89
+ (`db.close`, env swap/rename, `removeEnvironment`, residency evict, reopen, slot stop) **and**
90
+ the matching open. Pairs with `OKDB_FATAL_REPORT`: the report gives the faulting stack, this
91
+ gives the cross-thread open/close timeline.
92
+
93
+ | Env var | Effect |
94
+ | --------------------------- | ----------------------------------------------------------------- |
95
+ | `OKDB_NATIVE_OP_LOG=<path>` | Append a one-line breadcrumb per risky native op. |
96
+ | `OKDB_NATIVE_OP_STACK=1` | Also append the JS stack of each op (names the exact close path). |
97
+
98
+ ---
99
+
100
+ ## Triage workflow
101
+
102
+ 1. **Confirm the build is current** (see the note above). Most "still crashing after a fix"
103
+ reports are a stale build.
104
+ 2. Reproduce with **all three** on:
105
+ `OKDB_DIAG=1 OKDB_DIAG_STACK=1 OKDB_FATAL_REPORT=1 OKDB_NATIVE_OP_LOG=ops.log OKDB_NATIVE_OP_STACK=1 OKDB_DIAG_DIR=diag/ OKDB_REPORT_DIR=reports/`
106
+ 3. On a crash:
107
+ - `already closed` → `OKDB_DIAG` dump names the write site. The fix is to route that write
108
+ through `OKDBWriter` with a **synchronous** `putSync`/`removeSync` inside the txn (an async
109
+ `db.put` inside a writer `childTransaction` escapes into lmdb's deferred event-turn-batch and
110
+ orphans on close).
111
+ - `0xC0000005` → cross-reference the `OKDB_FATAL_REPORT` faulting stack with the
112
+ `OKDB_NATIVE_OP_LOG` timeline to find the close that raced the read/write.
113
+ 4. On a hang: the `OKDB_DIAG` **stall** dump names the writer/slot pinning the swap.
114
+ 5. For a deterministic local repro, `OKDB_SLOTS_INLINE=1` runs slot quanta inline on the owner
115
+ loop — it makes several of these races reproduce 100% instead of intermittently.
@@ -237,7 +237,7 @@ await okdb.embeddings.createPipeline('articles', {
237
237
 
238
238
  ### Queue mode
239
239
 
240
- For high-throughput or external embedder services, the indexer enqueues jobs and the worker population claims and processes them concurrently:
240
+ For high-throughput or external embedder services, the indexer enqueues jobs and queue consumers (the `embed-worker`, a [queue](queue.md) consumer) claim and process them concurrently across whatever nodes run the embeddings engine:
241
241
 
242
242
  ```javascript
243
243
  await okdb.embeddings.createPipeline('articles', {
package/docs/functions.md CHANGED
@@ -9,7 +9,7 @@ They are designed for:
9
9
  - stable HTTP entry points
10
10
  - MCP-accessible orchestration routines
11
11
 
12
- Custom functions are **stored**, **versioned**, **validated**, **audited**, and executed in **pooled sandbox runners**.
12
+ Custom functions are **stored**, **versioned**, **validated**, **audited**, and executed in a per-process **sandbox thread**.
13
13
 
14
14
  ---
15
15
 
@@ -110,32 +110,35 @@ Use `ctx.log(...)` instead of `console`.
110
110
 
111
111
  ## Runtime model
112
112
 
113
- A function invocation is a **durable work unit**, never a closure: `functions.run()` writes a
114
- `~fn:requests` row and the response comes back as a `~fn:responses` row (the UDP bus carries a
115
- wake hint so dispatch is ~ms). Because the stored function's script lives in the database, the
116
- work is fully portable whoever claims the row can run it. Execution **never blocks the
117
- caller's main loop**; untrusted user code always runs off-loop.
118
-
119
- Where it runs depends on whether a [worker population](worker-fleet.md) exists:
120
-
121
- - **With workers** — a worker claims the request row and executes the user code in its
122
- **sandbox thread**: a dedicated `worker_thread` with its own okdb instance, heap-capped from
123
- `runtime.memoryMb` via `resourceLimits`, and `terminate()` as the timeout/wedge watchdog.
124
- One sandbox per claimer; invocations run concurrently in it, capped. Containment is
125
- thread-level — a wedged function is `terminate()`d without taking the worker's other claims
126
- down.
127
- - **No workers (embedded / scale 0)** — the instance claims its own requests and runs them in a
128
- **fork pool** of child-process runners (warm between calls, idle-reaped, one run at a time per
129
- runner). This is the zero-config default and the **memory fence**: a thread heap cap is
130
- process-fatal on OOM, so only a separate process can contain a memory-hog function
131
- (`FUNCTION_OOM`) without crashing the host. Embedders who prefer thread-cheapness can opt into
132
- the sandbox locally; the fork pool can be forced with `OKDB_FN_LEGACY_POOL=1`.
133
-
134
- The **transport is identical** either way (durable `~fn` rows + bus fast path) only the
135
- execution site differs (sandbox thread vs. runner process). Both: scripts are validated and
136
- compile-checked before storage; timeouts kill/replace the executor; memory is capped; every run
137
- is written to a run ledger. This makes functions suitable for operational entry points without
138
- paying spawn cost on every call.
113
+ A function runs on **the node it was asked on**, in that node's per-process **sandbox thread**:
114
+ a dedicated `worker_thread` with its own okdb instance, heap-capped from `runtime.memoryMb` via
115
+ `resourceLimits`, with `terminate()` as the timeout/wedge watchdog. One sandbox per node,
116
+ lazy-spawned and idle-reaped; invocations run concurrently in it, capped. Execution **never
117
+ blocks the caller's main loop** untrusted user code always runs off-loop — and containment is
118
+ thread-level: a wedged function is `terminate()`d without taking the node's other work down.
119
+ This is what makes "run the function locally" safe, so it no longer needs a separate worker
120
+ process.
121
+
122
+ Sandbox concurrency is capped **per process** (`OKDB_FN_SANDBOX_CONCURRENCY`, default 4):
123
+ invocations beyond the cap queue at the claim layer. Host-level CPU budgeting is the operator's
124
+ job size the number of okdb processes (and their sandbox caps) to the machine.
125
+
126
+ Two invocation modes:
127
+
128
+ - **Synchronous** (`functions.run()` / HTTP execution) routed straight to the local sandbox,
129
+ no `~fn:requests` round-trip, lowest latency.
130
+ - **Durable / run-eventually** (survive the caller, store the result) a [queue](queue.md) job
131
+ on a designated node. The `~fn:requests` / `~fn:responses` substrate is retained as the
132
+ durable invocation mode (the UDP bus carries a wake hint so dispatch is ~ms).
133
+
134
+ Either way: scripts are validated and compile-checked before storage; timeouts terminate and
135
+ replace the executor; memory is capped; every run is written to a run ledger. This makes
136
+ functions suitable for operational entry points without paying spawn cost on every call.
137
+
138
+ > **Removed in 2.0:** the anonymous worker-population dispatch and the child-process **fork
139
+ > pool** (`OKDB_FN_LEGACY_POOL`). Functions execute in the asking node's local sandbox. See
140
+ > [Upgrading to 2.0](upgrade-2.0.md). If you don't want every HTTP node carrying a sandbox,
141
+ > designate which nodes serve functions — placement is yours.
139
142
 
140
143
  ---
141
144
 
@@ -17,7 +17,7 @@ Dependencies that OKDB needs internally (`lmdb`, `sift`, `hnswlib-node`, etc.) a
17
17
  ## Opening a database
18
18
 
19
19
  ```javascript
20
- const OKDB = require('src/okdb');
20
+ const OKDB = require('@kedem/okdb');
21
21
 
22
22
  const okdb = new OKDB('./mydb', options);
23
23
  await okdb.open();
@@ -63,9 +63,20 @@ const okdb = new OKDB('./mydb', {
63
63
  token: 'cluster-secret',
64
64
  address: 'http://localhost:8080', // this node's public address
65
65
  },
66
+
67
+ // Role flags (okdb 2.0) — what background work THIS process runs.
68
+ // All default to true; a plain `new OKDB(path)` is a full do-everything node.
69
+ // `processors` is the INITIAL participation only — okdb.processors.start()/stop()
70
+ // flip it at runtime (open with false + start() later = fast startup).
71
+ processors: true, // claim processor leases and drain derived work (FTS, views, …)
72
+ engines: true, // run embeddings / vector-search engines
73
+ compaction: true, // eligible to claim the per-env compaction lease
66
74
  });
67
75
  ```
68
76
 
77
+ See [Roles & Deployment](./deployment.md) for what the role flags mean and how to run
78
+ multi-process topologies (N capable nodes, dedicated workers + passive serving nodes).
79
+
69
80
  ---
70
81
 
71
82
  ## Your first type
@@ -76,7 +87,7 @@ A **type** is OKDB's equivalent of a collection or table. Records in a type shar
76
87
  await okdb.registerType('users');
77
88
  ```
78
89
 
79
- Types are idempotent to register — calling `registerType` on an already-registered type throws. Use `ensureType` for idempotent setup:
90
+ `registerType` throws if the type already exists. Use `ensureType` for idempotent setup:
80
91
 
81
92
  ```javascript
82
93
  await okdb.ensureType('users', {
@@ -197,7 +208,7 @@ process.on('SIGTERM', () => okdb.close().then(() => process.exit(0)));
197
208
 
198
209
  ```javascript
199
210
  'use strict';
200
- const OKDB = require('src/okdb');
211
+ const OKDB = require('@kedem/okdb');
201
212
 
202
213
  async function main() {
203
214
  const okdb = new OKDB('./demo');
package/docs/http-api.md CHANGED
@@ -600,7 +600,7 @@ OKDB's HTTP layer is **framework-agnostic**. `okdb.http.listen()` is entirely op
600
600
 
601
601
  ```javascript
602
602
  const express = require('express');
603
- const OKDB = require('src/okdb');
603
+ const OKDB = require('@kedem/okdb');
604
604
 
605
605
  const app = express();
606
606
  const okdb = new OKDB('./mydb');
@@ -643,7 +643,7 @@ app.listen(3000);
643
643
 
644
644
  ```javascript
645
645
  const fastify = require('fastify')({ logger: false });
646
- const OKDB = require('src/okdb');
646
+ const OKDB = require('@kedem/okdb');
647
647
 
648
648
  const okdb = new OKDB('./mydb');
649
649
  await okdb.open();
@@ -684,7 +684,7 @@ await fastify.listen({ port: 3000 });
684
684
 
685
685
  ```javascript
686
686
  const http = require('http');
687
- const OKDB = require('src/okdb');
687
+ const OKDB = require('@kedem/okdb');
688
688
 
689
689
  const okdb = new OKDB('./mydb');
690
690
  await okdb.open();
@@ -1,12 +1,21 @@
1
1
  # HTTP Clustering
2
2
 
3
- By default, the `okdb` CLI runs HTTP serving across **multiple worker processes** so that
4
- request/response latency is never starved by background work (indexing, view rebuilds,
5
- compaction, async catch-up). The processing tier runs in a single **primary** process; HTTP +
6
- SSE are served by **N passive worker processes** that share the listen socket.
7
-
8
- This is a **CLI-only default**. The embedded library (`new OKDB(...).http.listen(port)`) stays
9
- single-process unless you opt in see [Embedded clustering](#embedded-clustering).
3
+ By default, the `okdb` CLI runs across **N identical capable worker processes** that share
4
+ the listen socket: each one serves HTTP + SSE **and** participates in processing. There is **no
5
+ privileged processor** the 1-of-N `OKDBLock` lease distributes claims across the workers
6
+ (cooperative hold-window rotation load-shares under sustained backlog), and each worker drains
7
+ its claims on its own loop in bounded quanta, so a heavy drain on one worker never blocks the
8
+ others. A dead worker's claims fail over to the survivors. The Node-cluster primary is a thin
9
+ **passive supervisor** (it forks/respawns the workers and seeds the shared token secret; it
10
+ does not serve or process).
11
+
12
+ This is a **CLI-only feature** (`okdb` / `bin/okdb`). The embedded library
13
+ (`new OKDB(...).http.listen(port)`) is always single-process — `http.listen({workers})` was
14
+ removed in 2.0. For multi-process embedding, run **N independent full nodes** (each its own
15
+ `new OKDB(path).http.listen(port)`) on the same data path behind your own load balancer: they
16
+ already coordinate through the shared LMDB + bus + the 1-of-N lease exactly like the CLI's
17
+ workers (this is the same "N capable nodes" shape, just supervised by your balancer instead of
18
+ `bin/okdb`). See [Embedded clustering](#embedded-clustering).
10
19
 
11
20
  ---
12
21
 
@@ -14,36 +23,40 @@ single-process unless you opt in — see [Embedded clustering](#embedded-cluster
14
23
 
15
24
  ```
16
25
  ┌──────────────────────────────────────────────┐
17
- primary (full role engines, async
18
- processors, compaction:'active', bus)
19
- │ • runs ALL processing / indexing / compaction
20
- │ • HTTP-SILENT (never serves requests itself)
26
+ supervisor (PASSIVEprocessors:false,
27
+ engines:false, compaction:passive)
28
+ │ • forks/respawns/decommissions the workers
29
+ │ • owns the ~processes registry (publishes a
30
+ │ row per worker; workers are registry-passive)│
21
31
  │ • opens its OKDB BEFORE forking (seeds the │
22
32
  │ shared __tokenSecret so workers agree) │
33
+ │ • HTTP-SILENT; does NOT process │
23
34
  └───────────────┬──────────────────────────────┘
24
35
  │ forks N
25
36
  ┌───────────────┬───────┴───────┬───────────────┐
26
37
  ▼ ▼ ▼ ▼
27
38
  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
28
- │ worker │ │ worker │ ... │ worker │ │ worker │ passive role:
29
- HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSE│ │ HTTP+SSEengines:false
30
- └─────────┘ └─────────┘ └─────────┘ └─────────┘ asyncProcessors:false
31
- └───────────────┴── shared listen socket ──┴───────────┘ compaction:'passive'
32
- bus:true
39
+ │ worker │ │ worker │ ... │ worker │ │ worker │ CAPABLE role:
40
+ │HTTP+proc│ │HTTP+proc│ │HTTP+proc│ │HTTP+procprocessors:true
41
+ └─────────┘ └─────────┘ └─────────┘ └─────────┘ (serve + process +
42
+ └───────────────┴── shared listen socket ──┴───────────┘ compact)
33
43
  ```
34
44
 
35
- - **Primary = processor.** Exactly one process runs the processing pool, async processors, and
36
- compaction. It is HTTP-silent; it never serves requests. (The role flag is the belt; the
37
- `OKDBLock` proc-lease is the backstop no worker ever processes.)
38
- - **Workers = passive HTTP servers.** Each forked worker opens a **passive** OKDB
39
- (`engines:false`, `asyncProcessors:false`, `compaction:'passive'`, `bus:true`) and serves
40
- HTTP + SSE on the shared socket. Workers do real reads/writes inline index/schema/FK/TTL
41
- updates run synchronously inside the writer regardless of role but never run background
42
- processing. The primary's async processors pick up the changelog via the UDP bus poke.
43
- - **`workers:1`** (or `--no-cluster`) is byte-identical to the pre-cluster single-process path.
44
-
45
- Worker writes update indexes correctly; the primary's processors then index FTS / refresh
46
- views / run the materializer / etc. from the shared changelog.
45
+ - **Workers = N identical capable nodes.** Each forked worker opens a **full-role** OKDB
46
+ (`processors:true`) and both serves HTTP + SSE on the shared socket **and** processes it
47
+ claims the 1-of-N `OKDBLock` lease, builds/indexes FTS + views, and drains its claims on its
48
+ own loop in bounded quanta. Claims **distribute** across the workers (no
49
+ privileged processor); a dead worker's leases lapse and the survivors re-claim. Workers are
50
+ **registry-passive** (the supervisor publishes their `~processes` rows; a capable worker
51
+ reports its processing facet to the supervisor over IPC so cluster processing stays visible).
52
+ - **Supervisor = passive.** It opens a passive OKDB (`processors:false`, `engines:false`,
53
+ `compaction:false`) only to seed the shared `__tokenSecret` before forking and to own the
54
+ registry. It is HTTP-silent and never processes.
55
+ - **`workers:1`** (or `--no-cluster`) is byte-identical to the single-process path: one full
56
+ capable process serves + processes.
57
+
58
+ A write on any worker is indexed by whichever worker holds the lease (cross-process POKE + the
59
+ shared changelog); bounded quanta keep that indexing from monopolizing the holder's loop.
47
60
 
48
61
  ---
49
62
 
@@ -59,9 +72,9 @@ The CLI resolves the worker count from the first source present, highest priorit
59
72
  | 4 | `http.workers` | `.kdbconfig` config file. |
60
73
  | 5 | _default_ | `max(1, cores − 1)` where `cores = os.cpus()`. |
61
74
 
62
- A non-integer or `< 1` explicit value clamps to `1` with a warning. The default reserves one
63
- core for the processing primary and serves HTTP from the rest; on a single-core box it collapses
64
- to one process.
75
+ A non-integer or `< 1` explicit value clamps to `1` with a warning. The default leaves one core
76
+ for the OS / the thin passive supervisor and runs N capable workers on the rest (each serves HTTP
77
+ _and_ processes); on a single-core box it collapses to one process.
65
78
 
66
79
  ```bash
67
80
  okdb # default: max(1, cores − 1) HTTP workers
@@ -107,16 +120,21 @@ client on a given worker will and won't see:
107
120
  reconstruct data-change events (item put/remove, index, view, type). A write committed on
108
121
  worker A surfaces in worker B's SSE within the poke window. The bus carries the **signal**;
109
122
  the **data** is read from shared LMDB — the changelog is the single source of truth.
110
- - **Processing progress is primary-owned, forwarded over IPC.** Ephemeral processing events
111
- (view-rebuild progress, FTS/index lifecycle, processor lifecycle) originate only in the
112
- primary (the sole processor). The primary forwards a curated set to all workers over
113
- **cluster IPC**, plus a workers / processing status snapshot. Workers serve
114
- `/api/processing/status` and `/api/cluster/status` from that snapshot.
115
- - **Worker-local logs stay local.** A log line emitted inside worker A is **not** replicated to
116
- worker B's SSE. Logs are per-process; only data changes (via the changelog) and primary
117
- progress (via IPC) are coherent across the cluster.
118
-
119
- `/api/cluster/status` returns the cluster shape (primary pid, worker pids, uptime).
123
+ - **Processing progress is per-worker; the cluster-wide view is the durable registry.** There is
124
+ no privileged processor each capable worker processes its own claimed leases and emits its own
125
+ ephemeral processing events (view-rebuild progress, FTS/index lifecycle, processor lifecycle) to
126
+ **its own** SSE. The cluster-wide picture is **read** from the durable `~processes` registry:
127
+ each worker reports its processing facet to the supervisor over **cluster IPC**, the supervisor
128
+ stamps it onto the worker's registry row, and any node serves `/api/processing/status`,
129
+ `/api/processors/status`, and `/api/cluster/status` from that shared registry. So a processing
130
+ event on worker A is not mirrored into worker B's live SSE, but A's processor **state** is
131
+ visible cluster-wide via the registry-sourced status routes.
132
+ - **Worker logs are forwarded to the supervisor's log pipeline** over cluster IPC (tagged per
133
+ worker) so the operator log is unified; they are **not** mirrored into other workers' SSE
134
+ streams (SSE log frames are per-process). Only data changes (via the changelog) are coherent
135
+ across every worker's SSE.
136
+
137
+ `/api/cluster/status` returns the cluster shape (supervisor pid, worker pids, uptime).
120
138
 
121
139
  ---
122
140
 
@@ -132,26 +150,41 @@ client on a given worker will and won't see:
132
150
 
133
151
  ## Embedded clustering
134
152
 
135
- The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)` with
136
- no `workers` option returns a plain single-process `http.Server`. To opt an embedded app into
137
- clustering, pass `workers` (and a passive-worker OKDB factory) to `http.listen`:
153
+ The **library default is single-process** and unchanged. `new OKDB(...).http.listen(port)`
154
+ returns a plain single-process `http.Server`.
155
+
156
+ > **Removed in 2.0:** the embedded `http.listen(port, { workers, makeOkdb, primaryOkdb })` API
157
+ > (it **throws `HTTP_CLUSTER_REMOVED`**). A library node never forks — HTTP clustering is a
158
+ > **launcher** concern. See [Upgrading to 2.0](upgrade-2.0.md).
159
+
160
+ Two ways to cluster an embedded app — both are the same **"N identical capable nodes"** shape
161
+ (every node serves _and_ processes; the 1-of-N lease distributes the work; no privileged processor):
162
+
163
+ 1. **N independent nodes behind your balancer** (simplest). Run your entry as N separate processes,
164
+ each a full `new OKDB(path).http.listen(port_i)`, and put a load balancer in front. They
165
+ coordinate through the shared LMDB + bus + the lease — no Node `cluster`, no supervisor needed.
166
+ 2. **Plain Node `cluster`** (one listen port, shared socket). Fork your entry; each worker opens a
167
+ **capable** OKDB and calls `http.listen(port)` — a cluster worker's `listen` shares the socket
168
+ natively, with no okdb involvement.
138
169
 
139
170
  ```js
140
- okdb.http.listen(port, {
141
- workers: 4,
142
- primaryOkdb: okdb, // full-role processor, opened before listen() (open-before-fork)
143
- makeOkdb: async () => {
144
- const w = new OKDB(dbPath, {
145
- engines: false,
146
- asyncProcessors: false,
147
- compaction: 'passive',
148
- bus: true,
149
- });
150
- await w.open();
151
- return w;
152
- },
153
- });
171
+ const cluster = require('node:cluster');
172
+
173
+ if (cluster.isPrimary) {
174
+ // Thin supervisor: open a passive node BEFORE forking to seed the shared __tokenSecret,
175
+ // then fork N capable workers. It does not serve or process.
176
+ const supervisor = new OKDB(dbPath, { processors: false, compaction: false, http: false });
177
+ await supervisor.open();
178
+ for (let i = 0; i < 4; i++) cluster.fork();
179
+ } else {
180
+ // Each worker = an identical CAPABLE node: serves HTTP AND processes (claims leases,
181
+ // pool-drains heavy work off its loop). The lease distributes processing across them.
182
+ const w = new OKDB(dbPath, { processors: true });
183
+ await w.open();
184
+ w.http.listen(port); // shared socket via Node cluster; bus auto-derives from the shared path
185
+ }
154
186
  ```
155
187
 
156
- The same shmbuf requirement applies — only cluster an embedded app on a host with the native
157
- `shmbuf` binding.
188
+ The same shmbuf requirement applies — only cluster on a host with the native `shmbuf` binding.
189
+ For a turnkey cluster (supervisor, respawn, registry rows, log forwarding), `bin/okdb` does all of
190
+ this for you.