@kedem/okdb 1.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/LICENSE +57 -0
- package/README.md +344 -0
- package/banner.jpg +0 -0
- package/bin/okdb.js +2 -0
- package/docs/OKDB-ARCHITECTURE.md +1036 -0
- package/docs/auth-and-sync.md +558 -0
- package/docs/auth-permissions.md +143 -0
- package/docs/change-log.md +169 -0
- package/docs/data-model.md +174 -0
- package/docs/embeddings.md +338 -0
- package/docs/files.md +131 -0
- package/docs/fts.md +139 -0
- package/docs/functions.md +514 -0
- package/docs/getting-started.md +233 -0
- package/docs/http-api.md +705 -0
- package/docs/index.md +115 -0
- package/docs/indexes.md +207 -0
- package/docs/logging.md +116 -0
- package/docs/manifest.json +38 -0
- package/docs/migration.md +154 -0
- package/docs/pipelines.md +598 -0
- package/docs/plugins.md +144 -0
- package/docs/querying.md +204 -0
- package/docs/queue.md +637 -0
- package/docs/sync.md +156 -0
- package/docs/transactions.md +140 -0
- package/docs/ttl.md +375 -0
- package/okdb.js +1 -0
- package/package.json +26 -0
- package/public/admin-app.js +1 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/components/admin-route-host.ok.js +1 -0
- package/public/components/env-picker.ok.js +1 -0
- package/public/components/field-path-input.ok.js +1 -0
- package/public/components/field.html +6 -0
- package/public/components/geo-map.ok.js +1 -0
- package/public/components/ok-cron-input.ok.js +1 -0
- package/public/components/ok-dropzone.ok.js +1 -0
- package/public/components/ok-js-input.ok.js +1 -0
- package/public/components/ok-json-input.ok.js +1 -0
- package/public/components/ok-text-monaco-input.ok.js +1 -0
- package/public/components/theme-toggle.ok.js +1 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/images/logo.png +0 -0
- package/public/images/logo.svg +333 -0
- package/public/index.html +6 -0
- package/public/layouts/_default.ok.html +1 -0
- package/public/layouts/okdb-cube.ok.html +1 -0
- package/public/login-app.js +1 -0
- package/public/login.html +1 -0
- package/public/modules/ok.js +1 -0
- package/public/sections/api/index.ok.html +1 -0
- package/public/sections/api/parts/api-endpoint.ok.js +1 -0
- package/public/sections/auth/index.ok.html +1 -0
- package/public/sections/auth/modals/create-token-modal.ok.js +1 -0
- package/public/sections/auth/modals/edit-token-modal.ok.js +1 -0
- package/public/sections/auth/modals/perm-info-popover.ok.js +1 -0
- package/public/sections/auth/parts/token-detail.ok.js +1 -0
- package/public/sections/auth/parts/token-list.ok.js +1 -0
- package/public/sections/db/index.ok.html +1 -0
- package/public/sections/db/modals/add-item-modal.ok.js +1 -0
- package/public/sections/db/modals/create-env-modal.ok.js +1 -0
- package/public/sections/db/modals/create-fts-modal.ok.js +1 -0
- package/public/sections/db/modals/create-index-modal.ok.js +1 -0
- package/public/sections/db/modals/create-pipeline-modal.ok.js +1 -0
- package/public/sections/db/modals/demo-env-modal.ok.js +1 -0
- package/public/sections/db/modals/remove-env-modal.ok.js +1 -0
- package/public/sections/db/modals/schema-edit-modal.ok.js +1 -0
- package/public/sections/db/parts/db-overview.ok.js +1 -0
- package/public/sections/db/parts/db-pipelines-panel.ok.js +1 -0
- package/public/sections/db/parts/edit-row.ok.js +1 -0
- package/public/sections/db/parts/tm-timeline.ok.js +1 -0
- package/public/sections/db/parts/type-changelog.ok.js +1 -0
- package/public/sections/db/parts/type-list.ok.js +1 -0
- package/public/sections/db/parts/type-pipelines-tab.ok.js +1 -0
- package/public/sections/db/parts/type-schema-tab.ok.js +1 -0
- package/public/sections/db/parts/type-viewer.ok.js +1 -0
- package/public/sections/db/parts/type-violations-tab.ok.js +1 -0
- package/public/sections/docs/index.ok.html +1 -0
- package/public/sections/embeddings/index.ok.html +1 -0
- package/public/sections/embeddings/modals/register-model-modal.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-create-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-embedder-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-engine-declaration-editor.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-engine-list.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-indexer-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-lifecycle-bar.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-model-catalog.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-overview.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-search-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-worker-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embeddings-engine-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/engines-overview.ok.js +1 -0
- package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -0
- package/public/sections/engines/engine-panel-shared-style.js +1 -0
- package/public/sections/engines/engine-ui-utils.js +1 -0
- package/public/sections/engines/index.ok.html +1 -0
- package/public/sections/engines/modals/engine-modal.ok.js +1 -0
- package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -0
- package/public/sections/engines/parts/engine-lifecycle-actions.ok.js +1 -0
- package/public/sections/engines/parts/engine-panel-shell.ok.js +1 -0
- package/public/sections/engines/parts/engine-resource-card.ok.js +1 -0
- package/public/sections/engines/parts/engine-row-item.ok.js +1 -0
- package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -0
- package/public/sections/files/index.ok.html +1 -0
- package/public/sections/files/parts/files-blob-status.ok.js +1 -0
- package/public/sections/files/parts/files-collection-list.ok.js +1 -0
- package/public/sections/files/parts/files-collection-panel.ok.js +1 -0
- package/public/sections/files/parts/files-file-list.ok.js +1 -0
- package/public/sections/files/parts/files-overview.ok.js +1 -0
- package/public/sections/files/parts/files-path-browser.ok.js +1 -0
- package/public/sections/files/parts/files-upload-panel.ok.js +1 -0
- package/public/sections/functions/index.ok.html +1 -0
- package/public/sections/functions/modals/function-modal.ok.js +1 -0
- package/public/sections/functions/parts/function-detail.ok.js +1 -0
- package/public/sections/functions/parts/function-logs.ok.js +1 -0
- package/public/sections/functions/parts/function-runner.ok.js +1 -0
- package/public/sections/functions/parts/functions-list.ok.js +1 -0
- package/public/sections/license/index.ok.html +1 -0
- package/public/sections/license/parts/license-panel.ok.js +1 -0
- package/public/sections/processors/index.ok.html +1 -0
- package/public/sections/queue/index.ok.html +1 -0
- package/public/sections/queue/modals/create-bucket-modal.ok.js +1 -0
- package/public/sections/queue/modals/edit-bucket-modal.ok.js +1 -0
- package/public/sections/queue/modals/edit-job-modal.ok.js +1 -0
- package/public/sections/queue/modals/enqueue-job-modal.ok.js +1 -0
- package/public/sections/queue/modals/job-type-modal.ok.js +1 -0
- package/public/sections/queue/parts/ok-job-form.ok.js +1 -0
- package/public/sections/queue/parts/queue-buckets.ok.js +1 -0
- package/public/sections/queue/parts/queue-job-types.ok.js +1 -0
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -0
- package/public/sections/queue/parts/queue-types.ok.js +1 -0
- package/public/sections/queue/parts/queue-workers.ok.js +1 -0
- package/public/sections/sync/index.ok.html +5 -0
- package/public/sections/sync/parts/sync-node-detail.ok.js +1 -0
- package/public/sections/sync/parts/sync-retention.ok.js +1 -0
- package/public/sections/sync/parts/sync-topology.ok.js +1 -0
- package/public/sections/system/index.ok.html +1 -0
- package/public/sections/system/modals/data-ops-modal.ok.js +1 -0
- package/public/sections/system/parts/system-api-panel.ok.js +1 -0
- package/public/sections/system/parts/system-auth-panel.ok.js +1 -0
- package/public/sections/system/parts/system-log-history.ok.js +1 -0
- package/public/sections/system/parts/system-log.ok.js +1 -0
- package/public/sections/system/parts/system-status.ok.js +1 -0
- package/public/sections/system/parts/system-sync-panel.ok.js +1 -0
- package/public/sections/views/index.ok.html +1 -0
- package/public/sections/views/modals/create-view-modal.ok.js +1 -0
- package/public/sections/views/parts/view-detail.ok.js +1 -0
- package/public/sections/views/parts/views-list.ok.js +1 -0
- package/public/sections/views/parts/views-overview.ok.js +1 -0
- package/public/setup-app.js +1 -0
- package/public/setup.html +1 -0
- package/public/site.webmanifest +1 -0
- package/public/stores/auth-store.js +1 -0
- package/public/stores/events-store.js +1 -0
- package/public/stores/functions-store.js +1 -0
- package/public/stores/okdb-store.js +1 -0
- package/public/stores/theme-store.js +1 -0
- package/public/styles.css +1 -0
- package/public/test-cron-input.html +10 -0
- package/public/test-json-input.html +101 -0
- package/public/util/permissions.js +1 -0
- package/public/util/sift-schema.js +1 -0
- package/public/vendor/okjs/ok.esm.min.js +2 -0
- package/public/vendor/okjs/toolkit/behavior/async.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-chart.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-doc-viewer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-marked.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-monaco.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-layer-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-layer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-overlay.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-sandbox.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/transition.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/virtualized.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-console-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-console.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-inspector-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-inspector.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-js-tree.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-timeline-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-timeline.ok.js +1 -0
- package/public/vendor/okjs/toolkit/docs/doc-viewer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-component.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-example.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-shell.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-dropdown.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-icon.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-select.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-table.ok.js +1 -0
- package/public/vendor/okjs/toolkit/errors/ok-crash-boundary.ok.js +1 -0
- package/public/vendor/okjs/toolkit/errors/ok-crash-info.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-editor-examples.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-key-editor-tips.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-key-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/ok-i18n.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-contextmenu.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-dismiss-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-draggable.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-flyover-panel.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-flyover.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-highlight-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-highlight.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-hotkeys-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-picker-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-popout-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-popout.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-portal-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-submenu.ok.js +1 -0
- package/public/vendor/okjs/toolkit/layout/ok-resizer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/manifest.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-modal-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-modal.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-toast-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-window.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/hash-router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/history-router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/sync/reactive-sync-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/sync/reactive-sync.js +1 -0
- package/public/vendor/okjs/toolkit/theming/blocks/ok-design-button.css.js +1 -0
- package/public/vendor/okjs/toolkit/theming/blocks/ok-design-input.css.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design-style.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design-vars.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-input.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-style.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-utils.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-variable.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-vm.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme.ok.js +1 -0
- package/public/view-models/type-query-vm.js +1 -0
- package/types/environment.d.ts +130 -0
- package/types/errors.d.ts +98 -0
- package/types/features/admin.d.ts +4 -0
- package/types/features/api.d.ts +18 -0
- package/types/features/auth.d.ts +31 -0
- package/types/features/embeddings.d.ts +39 -0
- package/types/features/engines.d.ts +23 -0
- package/types/features/files.d.ts +29 -0
- package/types/features/fts.d.ts +25 -0
- package/types/features/functions.d.ts +23 -0
- package/types/features/mcp.d.ts +17 -0
- package/types/features/pipelines.d.ts +19 -0
- package/types/features/queue.d.ts +189 -0
- package/types/features/sync.d.ts +34 -0
- package/types/features/time-machine.d.ts +32 -0
- package/types/features/views.d.ts +16 -0
- package/types/index.d.ts +153 -0
- package/types/options.d.ts +202 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
# OKDB — Technical Architecture Reference
|
|
2
|
+
|
|
3
|
+
> Written for AI consumption. Dense, precise, no fluff.
|
|
4
|
+
> Covers: data model, storage layer, change tracking, indexes, transactions,
|
|
5
|
+
> features, plugin model, known design constraints, and potential enhancements.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What OKDB Is
|
|
10
|
+
|
|
11
|
+
OKDB is a **document-centric embedded database** for Node.js, built as a structured
|
|
12
|
+
layer on top of [LMDB](https://www.symas.com/lmdb) (via the `lmdb` npm package).
|
|
13
|
+
It is **not** a client/server database — it runs in-process, memory-mapped, with
|
|
14
|
+
full ACID transactions inherited from LMDB.
|
|
15
|
+
|
|
16
|
+
The core value proposition is: take raw LMDB key/value durability and add on top
|
|
17
|
+
of it typed collections, composite secondary indexes, a global change-log with
|
|
18
|
+
logical clocks, vector embeddings, multi-node sync, a job queue, and a pluggable
|
|
19
|
+
search engine — all without a separate process or network hop.
|
|
20
|
+
|
|
21
|
+
**Runtime: Node.js only.** No browser, no Bun (untested), no Deno. Designed for
|
|
22
|
+
single-node embedded use or multi-process cluster topologies where each process
|
|
23
|
+
has its own LMDB environment.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Dependency Stack
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Application code
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
OKDB class (okdb.js) ← single public API surface
|
|
34
|
+
│
|
|
35
|
+
├─ lmdb (npm) ← ACID B-tree storage, mmap, ordered-binary encoding
|
|
36
|
+
├─ sift (npm) ← MongoDB-style filter expressions for query()
|
|
37
|
+
├─ uuid (npm) ← transaction IDs
|
|
38
|
+
├─ hnswlib-node (npm) ← HNSW nearest-neighbour graph for vector search
|
|
39
|
+
└─ ws / socketit ← WebSocket transport for sync/admin UI
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
LMDB provides: copy-on-write B+ trees, MVCC readers, `dupSort` duplicate-key
|
|
43
|
+
sub-databases, `ordered-binary` encoding (consistent ordering of typed keys),
|
|
44
|
+
`useVersions` for optimistic concurrency, automatic compression (LZ4), and
|
|
45
|
+
optional AES-256 encryption at rest. OKDB exposes all of these.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Physical Storage Layout (Multi-Environment)
|
|
50
|
+
|
|
51
|
+
OKDB uses **multiple LMDB environments** (separate on-disk directories), each
|
|
52
|
+
opened with `lmdb.open(path)`. Each environment contains multiple named
|
|
53
|
+
**sub-databases** (`openDB`). This design keeps high-churn derived data (queues,
|
|
54
|
+
FTS posting lists, vector indexes) physically separate from user data, enabling
|
|
55
|
+
independent compaction, encryption, and sync control.
|
|
56
|
+
|
|
57
|
+
### 3.1 Directory Layout
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
<okdb-root>/
|
|
61
|
+
root/ ← Root LMDB (tiny, metadata only — no user data)
|
|
62
|
+
default/ ← User types, changelog, HLC clock (sync: on)
|
|
63
|
+
queue/ ← Queue jobs/payloads/stats (sync: on)
|
|
64
|
+
embeddings/ ← Embedding doc_status + models (sync: on)
|
|
65
|
+
vectors/ ← HNSW graph files + LMDB snapshots (sync: off)
|
|
66
|
+
fts_<type>_<name>/ ← Per-FTS-index LMDB env (sync: off, rebuildable)
|
|
67
|
+
blobs/ ← File attachment BLOBs
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Custom envs (created via `okdb.createEnvironment(name, opts)`) each get their
|
|
71
|
+
own sub-directory named after the env.
|
|
72
|
+
|
|
73
|
+
### 3.2 Root LMDB
|
|
74
|
+
|
|
75
|
+
Tiny metadata-only store. Never holds user data.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
root/
|
|
79
|
+
├── __identity → { id, created } node UUID
|
|
80
|
+
├── __env_registry → { name, sync, options } per-env config
|
|
81
|
+
└── __sync_peers → { peer_id → peerState } per-peer sync cursor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3.3 Default / User Env (and all sync:on envs)
|
|
85
|
+
|
|
86
|
+
Each sync-enabled environment has the same internal structure:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
<env>/
|
|
90
|
+
├── "clock" → global monotonic integer clock
|
|
91
|
+
├── types → typeName → metadata (ftsIndexes, fieldSchema, …)
|
|
92
|
+
├── changes → changeId → latest clock(int)
|
|
93
|
+
├── clocks → clock(int) → change object
|
|
94
|
+
│
|
|
95
|
+
└── per-type (for each registered type T):
|
|
96
|
+
type:T root sub-db (lifecycle, transaction records)
|
|
97
|
+
type:T:data key → value (useVersions=true, compression)
|
|
98
|
+
type:T:index indexName → index metadata
|
|
99
|
+
type:T:clockToChange clock(int) → changeId
|
|
100
|
+
type:T:keyToChange primaryKey → changeId
|
|
101
|
+
type:T:ttl expiresAt → primaryKey (dupSort, ordered-binary)
|
|
102
|
+
type:T:ttlByKey primaryKey → expiresAt
|
|
103
|
+
│
|
|
104
|
+
└── per-index (for each index I on type T):
|
|
105
|
+
type:T:index:I dupSort=true, ordered-binary keys+values
|
|
106
|
+
key = [...fieldValues, null(sentinel)]
|
|
107
|
+
value = primaryKey(string)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Key design notes:**
|
|
111
|
+
|
|
112
|
+
- The `clocks` DB is the authoritative append-only event log (WAL replay source).
|
|
113
|
+
- `changes` deduplicates: each `changeId` maps to the _latest_ clock it was
|
|
114
|
+
written at. Old entries are tombstoned when a key is overwritten (LWW per key).
|
|
115
|
+
- `clockToChange` / `keyToChange` are per-type projections allowing efficient
|
|
116
|
+
per-type clock scans and key→latest-change lookups without scanning the global log.
|
|
117
|
+
- Index sub-dbs use `dupSort: true`. The same index key (e.g. `["alpha", null]`)
|
|
118
|
+
may map to multiple primary keys. `null < string < number` ordering via
|
|
119
|
+
`ordered-binary` codec..
|
|
120
|
+
|
|
121
|
+
### 3.3a Unique Secondary Indexes
|
|
122
|
+
|
|
123
|
+
A secondary index may be declared **unique** by passing `{ unique: true }` to
|
|
124
|
+
`registerIndex()`. The flag is stored in index metadata and replicated via sync.
|
|
125
|
+
|
|
126
|
+
**Local enforcement:**
|
|
127
|
+
|
|
128
|
+
- `_write()` calls `_checkUniqueConstraints()` before the LMDB write.
|
|
129
|
+
- If an existing record holds the same index key, `OKDBUniqueConstraintError`
|
|
130
|
+
(code `UNIQUE_CONSTRAINT`) is thrown inside `db.transactionSync()`, aborting
|
|
131
|
+
the entire transaction atomically.
|
|
132
|
+
|
|
133
|
+
**Sync-origin writes (CRDT conflict model):**
|
|
134
|
+
|
|
135
|
+
- When `origin !== null` (write arrived via sync), the uniqueness check is bypassed
|
|
136
|
+
so no incoming change is ever lost.
|
|
137
|
+
- If duplicate index keys exist after the write, the conflict is recorded in
|
|
138
|
+
the `__unique_violations` LMDB sub-db.
|
|
139
|
+
- **Winner selection** is deterministic: highest HLC timestamp wins; ties broken
|
|
140
|
+
by lexicographically largest origin string (same ordering as `compareChangeLWW`).
|
|
141
|
+
- Violations resolve automatically when the losing record is removed or its
|
|
142
|
+
index-field value changes.
|
|
143
|
+
|
|
144
|
+
**Violation tracking storage:**
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
__unique_violations key = type@indexName@serializedIndexKey
|
|
148
|
+
value = { type, index, indexKey, entries[], winner, count,
|
|
149
|
+
detectedAt, updatedAt }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- NOT synced — computed locally from synced data.
|
|
153
|
+
- Emits `EVENTS.UNIQUE_VIOLATION` on conflict, `EVENTS.UNIQUE_VIOLATION_RESOLVED`
|
|
154
|
+
on resolution.
|
|
155
|
+
|
|
156
|
+
**Query API:**
|
|
157
|
+
|
|
158
|
+
| Method | Description |
|
|
159
|
+
| ------------------------------------------ | -------------------------------------------------------- |
|
|
160
|
+
| `env.getUniqueViolations(type, index?)` | List all violations (optional index filter) |
|
|
161
|
+
| `env.hasUniqueViolations(type, index)` | Boolean — any violations? |
|
|
162
|
+
| `env.getUniqueViolation(type, index, key)` | Single violation record or `null` |
|
|
163
|
+
| `env.byIndex(type, index)` | Winner-only by default; `includeViolations:true` for all |
|
|
164
|
+
|
|
165
|
+
**REST endpoints:**
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
GET /api/:env/type/:type/unique-violations
|
|
169
|
+
GET /api/:env/type/:type/index/:index/violations
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Geo indexes cannot be unique.** Geohash collisions make uniqueness semantics
|
|
173
|
+
ambiguous; registration is rejected at the API layer.
|
|
174
|
+
|
|
175
|
+
**Admin UX:**
|
|
176
|
+
|
|
177
|
+
- The Create Index modal exposes a **◆ Unique** checkbox (disabled when Geo is selected).
|
|
178
|
+
- The Manage tab shows **◆ unique** badge alongside index names.
|
|
179
|
+
- The index dropdown in the Data tab appends **◆** to unique index option labels.
|
|
180
|
+
|
|
181
|
+
### 3.3b Foreign Keys (Ref Constraints)
|
|
182
|
+
|
|
183
|
+
Foreign keys are declared inline in JSON Schema via a custom `ref` annotation on
|
|
184
|
+
string properties. They enforce referential integrity within a single environment.
|
|
185
|
+
|
|
186
|
+
**Schema declaration:**
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"authorId": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"ref": { "type": "Author", "onDelete": "cascade" }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `ref.type` — target type name (must exist in the same env).
|
|
201
|
+
- `ref.onDelete` — one of `no_action` (default), `cascade`, `set_null`, `restrict`.
|
|
202
|
+
- Nested properties are supported (e.g. `properties.meta.properties.ownerId`).
|
|
203
|
+
The field path is tracked as dot-notation (`meta.ownerId`).
|
|
204
|
+
- `null` values in ref fields are treated as "no reference" and always allowed.
|
|
205
|
+
|
|
206
|
+
**Ref extraction and caching:**
|
|
207
|
+
|
|
208
|
+
On `open()` and on every `setSchema()` / `dropSchema()`, the env scans all stored
|
|
209
|
+
schemas and rebuilds an in-memory **ref cache**:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
_refCache = {
|
|
213
|
+
outgoing: Map<sourceType, [{ field, targetType, onDelete }]>,
|
|
214
|
+
incoming: Map<targetType, [{ sourceType, field, onDelete }]>
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This allows O(1) lookup of "what refs does this type have?" and "what types
|
|
219
|
+
reference this target?".
|
|
220
|
+
|
|
221
|
+
**Reverse ref index (`__ref_index`):**
|
|
222
|
+
|
|
223
|
+
A dedicated LMDB sub-db tracks which documents reference which targets:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
__ref_index key = targetType@targetKey@sourceType@sourceKey@fieldPath
|
|
227
|
+
value = true
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- Updated atomically inside `_write()` after `db.put()`.
|
|
231
|
+
- Old ref entries are removed before new ones are written (handles updates).
|
|
232
|
+
- Enables efficient prefix scan on delete: "find all docs referencing Author@123".
|
|
233
|
+
|
|
234
|
+
**Write-time validation (`_checkForeignKeys`):**
|
|
235
|
+
|
|
236
|
+
Called inside `_write()` after schema validation, before index updates:
|
|
237
|
+
|
|
238
|
+
1. For each ref field in the document, resolve the field value.
|
|
239
|
+
2. If value is `null`/`undefined` → skip (nullable FK).
|
|
240
|
+
3. Check if `targetType@targetKey` exists via `env.get()`.
|
|
241
|
+
4. If target missing:
|
|
242
|
+
- **enforce:true + local write** → throw `OKDBForeignKeyError` (code
|
|
243
|
+
`FOREIGN_KEY_VIOLATION`), aborting the LMDB transaction.
|
|
244
|
+
- **enforce:false or synced write** → write succeeds, violation recorded in
|
|
245
|
+
`~ref_violations` internal type.
|
|
246
|
+
|
|
247
|
+
**Delete-time cascade (`_processDeleteCascade`):**
|
|
248
|
+
|
|
249
|
+
Called inside `_remove()` before `db.remove()`:
|
|
250
|
+
|
|
251
|
+
1. Prefix-scan `__ref_index` for all entries matching `targetType@targetKey@`.
|
|
252
|
+
2. Group results by `onDelete` rule.
|
|
253
|
+
3. **restrict:** If any incoming refs have `onDelete: restrict` AND the delete is
|
|
254
|
+
local (`origin === null`), throw `OKDBForeignKeyDeleteError`. Synced deletes
|
|
255
|
+
bypass restrict (CRDT-safe — never reject incoming data).
|
|
256
|
+
4. **cascade:** Recursively call `_remove()` on each referencing document. A
|
|
257
|
+
`_cascadeVisited` Set tracks `type@key` to prevent infinite cycles.
|
|
258
|
+
5. **set_null:** Call `_write()` on each referencing document with the ref field
|
|
259
|
+
set to `null`. The `_cascading` flag is passed to suppress re-validation.
|
|
260
|
+
6. **no_action:** No cleanup — dangling refs may result.
|
|
261
|
+
|
|
262
|
+
**Violation tracking:**
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
~ref_violations key = sourceType@sourceKey@fieldPath
|
|
266
|
+
value = { sourceType, sourceKey, fieldPath, targetType,
|
|
267
|
+
targetKey, detectedAt }
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- NOT synced — locally computed from synced data.
|
|
271
|
+
- Automatically cleared when the referencing doc is updated to a valid target,
|
|
272
|
+
removed, or when the target is created.
|
|
273
|
+
- Emits `EVENTS.REF_VIOLATION` on detection, `EVENTS.REF_VIOLATION_RESOLVED`
|
|
274
|
+
on resolution.
|
|
275
|
+
|
|
276
|
+
**Retroactive schema application (`_syncRefIndex`):**
|
|
277
|
+
|
|
278
|
+
When `setSchema()` introduces refs on a type that already has data:
|
|
279
|
+
|
|
280
|
+
1. Clear all existing `__ref_index` entries for that source type.
|
|
281
|
+
2. Scan all documents of the type.
|
|
282
|
+
3. For each document, populate `__ref_index` entries.
|
|
283
|
+
4. If a ref target is missing, record a `~ref_violations` entry.
|
|
284
|
+
|
|
285
|
+
`dropSchema()` clears all ref index entries and ref violations for the type and
|
|
286
|
+
rebuilds the ref cache.
|
|
287
|
+
|
|
288
|
+
**Query API:**
|
|
289
|
+
|
|
290
|
+
| Method | Description |
|
|
291
|
+
| ------------------------------------------------------- | ------------------------------------ |
|
|
292
|
+
| `env.getRefViolation(sourceType, sourceKey, fieldPath)` | Single violation or `null` |
|
|
293
|
+
| `env.listRefViolations(type?)` | All violations, optional type filter |
|
|
294
|
+
|
|
295
|
+
**REST endpoints:**
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
GET /api/env/:env/ref-violations → all ref violations
|
|
299
|
+
GET /api/env/:env/ref-violations/:type → violations for a specific type
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Design constraints:**
|
|
303
|
+
|
|
304
|
+
- **Same-env only.** Cross-environment refs are not supported — each env is an
|
|
305
|
+
independent replication unit.
|
|
306
|
+
- **Scalar refs only.** Array-of-refs (e.g. `{ type: "array", items: { ref: … } }`)
|
|
307
|
+
is not yet supported.
|
|
308
|
+
- **No composite refs.** Each ref field points to exactly one target type/key.
|
|
309
|
+
|
|
310
|
+
### 3.4 FTS Environments
|
|
311
|
+
|
|
312
|
+
Each FTS index gets its own LMDB environment:
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
fts_<type>_<name>/
|
|
316
|
+
├── post dupSort=true token → docKey (inverted posting list)
|
|
317
|
+
└── docs docKey → [tokens] (forward index for update/delete)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- Always `sync: false` — fully rebuildable from the default env's type data.
|
|
321
|
+
- FTS metadata (field config, status, created/updated timestamps) is stored in
|
|
322
|
+
`default/types` under `ftsIndexes[name]` — this IS synced.
|
|
323
|
+
- On inline writes, `putSync`/`removeSync` are used so results are immediately
|
|
324
|
+
visible within the same event loop tick.
|
|
325
|
+
|
|
326
|
+
### 3.5 Vectors Environment
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
vectors/
|
|
330
|
+
├── <name>:meta HNSW graph metadata + snapshot clock
|
|
331
|
+
├── <name>:vectors key → Float32Array (LMDB-backed vector store)
|
|
332
|
+
└── <name>.hnsw HNSW graph file (separate file, not a sub-db)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- `sync: false` — rebuilt from embeddings pipeline, not synced.
|
|
336
|
+
|
|
337
|
+
### 3.6 Queue Environment
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
queue/
|
|
341
|
+
├── ~queue_jobs job records
|
|
342
|
+
├── ~queue_payloads job payloads (separate for large-payload efficiency)
|
|
343
|
+
├── ~queue_buckets scheduling buckets
|
|
344
|
+
└── ~queue_stats counters
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- `sync: on` — queue state replicates across cluster nodes.
|
|
348
|
+
- Changelog and HLC clock are independent from the default env.
|
|
349
|
+
|
|
350
|
+
### 3.7 Embeddings Environment
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
embeddings/
|
|
354
|
+
├── ~emb:doc_status per-document embedding computation status
|
|
355
|
+
└── ~emb:models embedding model registry
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- `sync: on` — embedding status and model registry replicate.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## 4. OKDB Orchestrator + OKDBEnv Model
|
|
365
|
+
|
|
366
|
+
### 4.1 Two-Layer Architecture
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
OKDB (okdb.js) ← orchestrator: lifecycle, features, public API
|
|
370
|
+
│
|
|
371
|
+
├── _rootDb ← Root LMDB (metadata: identity, env registry, peers)
|
|
372
|
+
│
|
|
373
|
+
├── _envs: Map<name, OKDBEnv>
|
|
374
|
+
│ ├── "default" ← OKDBEnv (default user-data env)
|
|
375
|
+
│ ├── "queue" ← OKDBEnv (queue env, auto-created on open)
|
|
376
|
+
│ ├── "embeddings" ← OKDBEnv (embeddings env, auto-created on open)
|
|
377
|
+
│ ├── "vectors" ← OKDBEnv (vectors env, sync:false)
|
|
378
|
+
│ └── "<custom>" ← OKDBEnv (user-created via createEnvironment)
|
|
379
|
+
│
|
|
380
|
+
└── features
|
|
381
|
+
fts, sync, queue, embeddings, engines, files, plugins, meta, …
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`OKDB` is the **orchestrator**. It owns lifecycle, wires features together,
|
|
385
|
+
and exposes a convenience API that delegates to `env('default')` for all
|
|
386
|
+
common operations (`put`, `get`, `query`, `registerType`, etc.).
|
|
387
|
+
|
|
388
|
+
`OKDBEnv` is the **data layer**: one LMDB environment + its type sub-dbs +
|
|
389
|
+
its own clock + its own HLC + its own processor + its own changelog.
|
|
390
|
+
|
|
391
|
+
### 4.2 OKDBEnv Internals
|
|
392
|
+
|
|
393
|
+
Each `OKDBEnv` instance:
|
|
394
|
+
|
|
395
|
+
| Field | Purpose |
|
|
396
|
+
| ----------------- | ---------------------------------------------------------- |
|
|
397
|
+
| `db` | The `lmdb.RootDatabase` for this env |
|
|
398
|
+
| `typeSubDbs` | `Map<typeName, typeMap>` — all open type sub-dbs |
|
|
399
|
+
| `typesDb` | Sub-db storing type metadata (field schema, FTS config, …) |
|
|
400
|
+
| `clockToChangeDb` | `clock(int) → change object` — the event log |
|
|
401
|
+
| `changeToClockDb` | `changeId → clock` — deduplication index |
|
|
402
|
+
| `processor` | `OKDBProcessor` instance scoped to this env |
|
|
403
|
+
| `_sync` | `boolean` — whether this env participates in sync |
|
|
404
|
+
| `_hlc` | HLC state (uint64 encoded) |
|
|
405
|
+
|
|
406
|
+
### 4.3 Environment API
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
// Get an env by name (throws if not found)
|
|
410
|
+
okdb.env('default') // → OKDBEnv
|
|
411
|
+
okdb.env('queue') // → OKDBEnv
|
|
412
|
+
|
|
413
|
+
// Create a new env (registers in root LMDB + opens LMDB directory)
|
|
414
|
+
const myEnv = await okdb.createEnvironment('analytics', { sync: true })
|
|
415
|
+
|
|
416
|
+
// Envs have the same write/read API as OKDB itself
|
|
417
|
+
await myEnv.registerType('events')
|
|
418
|
+
await myEnv.put('events', 'e1', { action: 'click' })
|
|
419
|
+
const val = myEnv.get('events', 'e1')
|
|
420
|
+
for (const e of myEnv.query('events', { action: 'click' })) { … }
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### 4.4 Sync Flag
|
|
424
|
+
|
|
425
|
+
| `sync: true` | `sync: false` |
|
|
426
|
+
| ------------------------------------------- | --------------------------- |
|
|
427
|
+
| Env changes appear in `sync.calculateDelta` | Excluded from sync entirely |
|
|
428
|
+
| Has changelog + HLC clock | No changelog |
|
|
429
|
+
| `_env` tag on changes | Never replicated |
|
|
430
|
+
| default, queue, embeddings | vectors, fts\_\* |
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## 5. The `OKDB` Class — Public API Surface
|
|
435
|
+
|
|
436
|
+
`okdb.js` is the single export and entry point. All features hang off it.
|
|
437
|
+
|
|
438
|
+
### Lifecycle
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
const okdb = new OKDB(path, options); // sync — no IO
|
|
442
|
+
await okdb.open(); // opens LMDB env, starts features
|
|
443
|
+
await okdb.close(); // drains, stops features, closes env
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
State machine: `CREATED → STARTING → STARTED → STOPPING → STOPPED`
|
|
447
|
+
|
|
448
|
+
### Schema Management
|
|
449
|
+
|
|
450
|
+
```js
|
|
451
|
+
await okdb.registerType(type); // creates per-type sub-dbs
|
|
452
|
+
await okdb.dropType(type); // drops all sub-dbs for type
|
|
453
|
+
await okdb.registerIndex(type, ['field']); // registers composite index, async rebuild
|
|
454
|
+
await okdb.dropIndex(type, ['field']); // clears index sub-db
|
|
455
|
+
await okdb.ensureType(type, { indexes: [] }); // idempotent upsert
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Index names are stored as canonical `parts.join('~')` strings (e.g. `category~score`).
|
|
459
|
+
The `~` separator is the only place where string splitting is allowed — everywhere
|
|
460
|
+
else index identity is always a `string[]`.
|
|
461
|
+
|
|
462
|
+
### Write API
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
// All four methods create a single-op transaction internally
|
|
466
|
+
await okdb.put(type, key, value); // upsert
|
|
467
|
+
await okdb.create(type, key, value); // throws ALREADY_EXISTS if key exists
|
|
468
|
+
await okdb.update(type, key, value); // throws NOT_FOUND if key absent
|
|
469
|
+
await okdb.remove(type, key); // throws NOT_FOUND if key absent
|
|
470
|
+
|
|
471
|
+
// All accept { ifVersion, version, timestamp, origin }
|
|
472
|
+
// ifVersion: optimistic concurrency guard
|
|
473
|
+
// version: explicit version override (for sync/import)
|
|
474
|
+
// origin: node UUID for change attribution (sync anti-echo)
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Read API
|
|
478
|
+
|
|
479
|
+
```js
|
|
480
|
+
okdb.get(type, key); // value | undefined, synchronous
|
|
481
|
+
okdb.getMany(type, keys); // value[], synchronous
|
|
482
|
+
okdb.getEntry(type, key); // { value, version } | undefined
|
|
483
|
+
okdb.getRange(type, rangeOptions); // lazy LMDB cursor (iterable)
|
|
484
|
+
okdb.getByPrefix(type, prefix); // lexicographic prefix scan
|
|
485
|
+
okdb.getValues(type, rangeOptions); // lazy iterable of values only
|
|
486
|
+
okdb.getKeys(type, rangeOptions); // lazy iterable of keys only
|
|
487
|
+
okdb.getCount(type); // integer record count
|
|
488
|
+
okdb.byIndex(type, index, rangeOptions); // lazy iterable of { key, value, version, indexKey }
|
|
489
|
+
okdb.query(type, filter, options); // MongoDB-style filter via sift; optional index
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**All reads are synchronous** (LMDB mmap means no async I/O for reads).
|
|
493
|
+
`getRange`, `byIndex`, `query` return LMDB lazy iterables — they are cursor-backed
|
|
494
|
+
and **invalidated by any write transaction opened on the same LMDB environment**.
|
|
495
|
+
Callers that mix async work with open cursors must materialize first
|
|
496
|
+
(`Array.from(cursor)`).
|
|
497
|
+
|
|
498
|
+
### Change Log API
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
okdb.getClock(type?) // current global or per-type clock
|
|
502
|
+
okdb.getChanges(type?, start, end, options) // inclusive [start, end] range
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`getChanges` returns an iterable of `{ clock, id, type, key, action, timestamp, origin, txnId }`.
|
|
506
|
+
The `end` bound is **inclusive** (OKDB adds `+1` internally before passing to LMDB).
|
|
507
|
+
Without `type`, it scans the global `clockToChangeDb`.
|
|
508
|
+
With `type`, it scans the per-type `clockToChange` projection then cross-references
|
|
509
|
+
the global `clockToChangeDb` for the full change object.
|
|
510
|
+
|
|
511
|
+
### Batch Transactions
|
|
512
|
+
|
|
513
|
+
```js
|
|
514
|
+
const txn = okdb.transaction();
|
|
515
|
+
txn.put(type, key, value);
|
|
516
|
+
txn.remove(type, key);
|
|
517
|
+
// ... N ops
|
|
518
|
+
await txn.commit(); // all-or-nothing, one LMDB transaction
|
|
519
|
+
txn.rollback(); // discard pending actions
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
`OKDBTransaction` accumulates operations as an `actions[]` array and replays them
|
|
523
|
+
inside a single `okdb.db.transaction()` call on commit. It also optionally holds
|
|
524
|
+
an LMDB read transaction (`useVersions` snapshot) for consistent reads during
|
|
525
|
+
the accumulation phase.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 6. Change Logging — The Logical Clock
|
|
530
|
+
|
|
531
|
+
Every write (put, remove, registerType, registerIndex, dropType, dropIndex) calls
|
|
532
|
+
`_logChange(change, oldValue, newValue)` _inside_ the active LMDB transaction.
|
|
533
|
+
|
|
534
|
+
```
|
|
535
|
+
global clock ← atomically incremented (stored as kv pair "clock" in root env)
|
|
536
|
+
global clocks db clock → { id, type, key, action, timestamp, origin, txnId }
|
|
537
|
+
global changes db changeId → latestClock (dedup index)
|
|
538
|
+
|
|
539
|
+
per-type clockToChange clock → changeId
|
|
540
|
+
per-type keyToChange key → changeId (latest change per primary key)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Deduplication semantics:** When a key is written twice, the first clock entry
|
|
544
|
+
in the global log is removed (tombstoned from `clockToChangeDb`) and the
|
|
545
|
+
`changeToClockDb` pointer is updated. The per-type `clockToChange` mirrors this.
|
|
546
|
+
Result: the log contains at most **one entry per primary key** — always the most
|
|
547
|
+
recent action. This makes `getChanges` a true "what is the current state delta"
|
|
548
|
+
query rather than a full audit trail.
|
|
549
|
+
|
|
550
|
+
**Post-commit event emission:** Events (`item:create`, `item:update`, `item:remove`,
|
|
551
|
+
`system:clock_change`, `system:clock_change@type`, `bus:poke`) are emitted via
|
|
552
|
+
`okdb.db.committed.then(...)` — after the LMDB transaction durably commits.
|
|
553
|
+
This ensures listeners never see speculative data.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## 7. Secondary Indexes
|
|
558
|
+
|
|
559
|
+
Indexes are:
|
|
560
|
+
|
|
561
|
+
- Composite: defined as `string[]` of field paths (dot-notation supported)
|
|
562
|
+
- Stored in `dupSort: true` LMDB sub-databases with `ordered-binary` key encoding
|
|
563
|
+
- Key format: `[...fieldValues, null]` — the trailing `null` is a sentinel that
|
|
564
|
+
ensures the ordered-binary sort places all entries for a given composite prefix
|
|
565
|
+
contiguously, enabling efficient range scans
|
|
566
|
+
- Value: primary key string
|
|
567
|
+
|
|
568
|
+
**Index maintenance** happens inside `_write`/`_remove` via `_updateItemIndexes`:
|
|
569
|
+
for each registered index, `_updateItemIndex` extracts the old and new field
|
|
570
|
+
values, and if they differ, removes the old entry and inserts the new one — all
|
|
571
|
+
within the same LMDB transaction.
|
|
572
|
+
|
|
573
|
+
**Index build** (`resetIndex`) reads a snapshot of the data DB and processes it
|
|
574
|
+
in batches of 10,000, using `ifVersion` to skip stale entries. An `INDEX_STATE`
|
|
575
|
+
enum tracks `CREATING → RESETTING → READY`. A `_indexReadyPromises` map allows
|
|
576
|
+
consumers to `await okdb.indexReady(type, index)`.
|
|
577
|
+
|
|
578
|
+
**Indexes on CREATING-state types** skip the old-value removal step — prevents
|
|
579
|
+
spurious removes during initial populate.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## 8. Features
|
|
584
|
+
|
|
585
|
+
All features are instantiated in the `OKDB` constructor and attached as
|
|
586
|
+
`this.{name}`. They start/stop with the database lifecycle.
|
|
587
|
+
|
|
588
|
+
### 8.1 `okdb-http` — Internal Route Dispatcher
|
|
589
|
+
|
|
590
|
+
Not an HTTP framework. A minimal hand-rolled dispatcher that:
|
|
591
|
+
|
|
592
|
+
- Compiles URL patterns into regexes with named capture groups
|
|
593
|
+
- Supports middleware chains (`okdb.http.use(fn)`)
|
|
594
|
+
- Handles `Basic` and `Bearer` auth, HMAC signatures, cookie parsing
|
|
595
|
+
- Can optionally start a `node:http` server (`okdb.http.listen(port)`)
|
|
596
|
+
- Used by all features to register their REST endpoints without external deps
|
|
597
|
+
|
|
598
|
+
### 8.2 `okdb-bus` — UDP Multicast Discovery Bus
|
|
599
|
+
|
|
600
|
+
Sends `bus:poke` UDP datagrams on a multicast group (`239.1.2.3:30303` by default)
|
|
601
|
+
whenever the clock changes. Purpose: notify sibling processes on the same LAN that
|
|
602
|
+
new data is available so they trigger a sync reconcile. Stateless — carries only
|
|
603
|
+
a type name hint. No reliability guarantees; loss is acceptable (sync reconciles
|
|
604
|
+
periodically anyway).
|
|
605
|
+
|
|
606
|
+
### 8.3 `okdb-sync` — Last-Write-Wins Multi-Node Replication
|
|
607
|
+
|
|
608
|
+
Peer-to-peer sync over HTTP (no central coordinator):
|
|
609
|
+
|
|
610
|
+
- Each node exposes `GET /api/sync/delta?from_clock=N` (via `okdb-sync-http.js`)
|
|
611
|
+
- Peers are stored in the `~sync_nodes` OKDB type (replicated) and tracked with
|
|
612
|
+
local-only progress records (stored in root LMDB under `__sync_peers`, not replicated)
|
|
613
|
+
- Reconciliation: pull delta from peer → apply LWW comparison → write if newer
|
|
614
|
+
- LWW comparison: `(timestamp, origin)` — lexicographic tie-break on origin UUID
|
|
615
|
+
- Anti-echo: changes whose `origin === okdb.id` are skipped on apply
|
|
616
|
+
- Auto-reconcile is clock-change-driven, not timer-based
|
|
617
|
+
|
|
618
|
+
**Multi-env sync (step 4):** `calculateDelta` spans ALL `sync:true` envs.
|
|
619
|
+
Each change is tagged with `_env: envName`. The response includes a `clocks`
|
|
620
|
+
map (`{ default: N, queue: N, embeddings: N }`) so each env's cursor advances
|
|
621
|
+
independently. Legacy `from_clock` (single integer) is treated as
|
|
622
|
+
`{ default: N }` for backward compatibility. `applyChange` routes each change
|
|
623
|
+
to the correct env on the receiving node.
|
|
624
|
+
|
|
625
|
+
**Non-syncable envs** (`sync: false`): vectors, fts\_\* — excluded from delta.
|
|
626
|
+
These are rebuilt from synced data on each node.
|
|
627
|
+
|
|
628
|
+
**Conflict model:** Timestamp-based LWW. No CRDT, no vector clocks. The node
|
|
629
|
+
with the higher wall-clock timestamp wins. This is intentional simplicity — the
|
|
630
|
+
system is designed for loosely-coupled eventual consistency, not strong ordering.
|
|
631
|
+
|
|
632
|
+
### 8.4 `okdb-queue` — Durable Job Queue
|
|
633
|
+
|
|
634
|
+
A full-featured job queue built entirely on OKDB types — stored in the `queue/`
|
|
635
|
+
env (not the default env):
|
|
636
|
+
|
|
637
|
+
- Types: `~queue_jobs`, `~queue_payloads`, `~queue_buckets`
|
|
638
|
+
- Supports: priorities, tags, TTLs, retry with exponential backoff, dead-letter,
|
|
639
|
+
cron scheduling, token-bucket rate limiting (buckets), and per-job result storage
|
|
640
|
+
- Workers claim jobs (`STATUS.PENDING → CLAIMED`) and mark them done/failed
|
|
641
|
+
- Reconciler handles stale claims, retry scheduling, cron advancement
|
|
642
|
+
- Stats (`OKDBQueueStats`) tracked per-bucket in a separate LMDB sub-db
|
|
643
|
+
|
|
644
|
+
The queue is OKDB-native: jobs survive crashes, job history is queryable,
|
|
645
|
+
all mutations go through the queue env's change log, and queue state replicates
|
|
646
|
+
across cluster nodes via multi-env sync.
|
|
647
|
+
|
|
648
|
+
### 8.5 `okdb-embeddings` — Vector Embedding Pipeline
|
|
649
|
+
|
|
650
|
+
Orchestrates four engine types for document embedding and nearest-neighbour search:
|
|
651
|
+
|
|
652
|
+
| Engine type | Role |
|
|
653
|
+
| --------------- | --------------------------------------------------------------------- |
|
|
654
|
+
| `embedder` | Named connection to an ML model (Ollama/OpenAI/custom) |
|
|
655
|
+
| `indexer` | Watches a OKDB type's change log, embeds docs inline or enqueues jobs |
|
|
656
|
+
| `embed-worker` | Consumes queue jobs, calls embedder, calls `markDone` |
|
|
657
|
+
| `vector-search` | In-memory HNSW graph over a persistent vector store |
|
|
658
|
+
|
|
659
|
+
Storage: `OKDBVectorStore` stores `Float32Array` vectors in LMDB sub-databases
|
|
660
|
+
keyed by `storage_key`. The HNSW graph (`hnswlib-node`) is rebuilt from storage
|
|
661
|
+
on restart.
|
|
662
|
+
|
|
663
|
+
Two pipeline modes:
|
|
664
|
+
|
|
665
|
+
- **inline**: indexer → embedder directly. Simple. Blocks the change-log consumer.
|
|
666
|
+
- **queue**: indexer → `okdb-queue` → embed-worker(s) → embedder. Allows
|
|
667
|
+
concurrency control, retries, and multi-process workers.
|
|
668
|
+
|
|
669
|
+
`createPipeline(key, config)` is the one-call convenience API that provisions all
|
|
670
|
+
four engine types and wires them together.
|
|
671
|
+
|
|
672
|
+
### 8.6 `okdb-engines` — Persistent Named Services
|
|
673
|
+
|
|
674
|
+
A generic lifecycle manager for named, typed, driver-backed services whose
|
|
675
|
+
configuration and status are stored in the `~engines` OKDB type. Engines survive
|
|
676
|
+
restarts: on `open()`, all persisted engines are restored and their drivers' `start`
|
|
677
|
+
methods are called.
|
|
678
|
+
|
|
679
|
+
Lifecycle: `created → online ↔ error → stopped`
|
|
680
|
+
|
|
681
|
+
Used by the embeddings feature internally. Also available as a general extension
|
|
682
|
+
point: register a driver, create an engine, its `config` object lives in OKDB and
|
|
683
|
+
can be updated at runtime.
|
|
684
|
+
|
|
685
|
+
### 8.7 `okdb-search` — Pluggable Search Engine Framework
|
|
686
|
+
|
|
687
|
+
A driver/engine pattern for full-text or structured search that is independent of
|
|
688
|
+
the embeddings feature:
|
|
689
|
+
|
|
690
|
+
- Register drivers (`registerDriver(driverDef)`) — each driver provides a `build`
|
|
691
|
+
function that returns a runtime with an API
|
|
692
|
+
- Create engine instances keyed by `searchKey`, backed by a definition stored in
|
|
693
|
+
`~search` OKDB type
|
|
694
|
+
- `OKDBSearchEngine` handles lifecycle, clock subscription, mutation serialization,
|
|
695
|
+
and checkpoint orchestration (via `CheckpointController`)
|
|
696
|
+
- Projections, materializers, executors are driver responsibilities — the engine
|
|
697
|
+
never inspects documents directly
|
|
698
|
+
|
|
699
|
+
### 8.8 `okdb-plugins` — Plugin System
|
|
700
|
+
|
|
701
|
+
Pre-open extension mechanism:
|
|
702
|
+
|
|
703
|
+
- `okdb.plugins.register(module, options)` — registers a plugin before `open()`
|
|
704
|
+
- Dependency declaration via `module.requires: string[]`
|
|
705
|
+
- Two lifecycle hooks: `start(okdb)` (after open, async) and `stop(okdb)` (before close)
|
|
706
|
+
- Plugins can attach to `okdb.{module.name}` — namespace is guarded against collisions
|
|
707
|
+
|
|
708
|
+
### 8.9 `okdb-meta` — Schema Introspection
|
|
709
|
+
|
|
710
|
+
Read-only. Exposes: `listTypes()`, `describeType(type)` (count, fields, indexes),
|
|
711
|
+
`describeIndex(type, index)`, `listChanges(...)`. No caching.
|
|
712
|
+
|
|
713
|
+
Field schema is observed on write via `_mergeFieldSchema` — each new field/type
|
|
714
|
+
combination is persisted to the type metadata. Provides coarse type information
|
|
715
|
+
(`string | number | boolean | null | array | object`) per field.
|
|
716
|
+
|
|
717
|
+
### 8.10 `okdb-migrate` — Export / Import
|
|
718
|
+
|
|
719
|
+
Serializes all types and their records to a JSON-friendly structure. Indexes are
|
|
720
|
+
excluded (rebuilt on import). Optionally exports the change log. Import drops all
|
|
721
|
+
existing types and recreates them.
|
|
722
|
+
|
|
723
|
+
### 8.11 `okdb-admin` — Admin UI Backend
|
|
724
|
+
|
|
725
|
+
Serves a static web UI from `src/features/admin/public/`. Registers HTTP routes
|
|
726
|
+
for type/index/record management, system info, and live monitoring.
|
|
727
|
+
|
|
728
|
+
### 8.12 `okdb-api` — REST API
|
|
729
|
+
|
|
730
|
+
Registers standard CRUD HTTP routes:
|
|
731
|
+
|
|
732
|
+
- `GET/POST/PUT/PATCH/DELETE /api/type/:type/:key`
|
|
733
|
+
- `GET /api/type/:type` (list with range/limit/offset)
|
|
734
|
+
- `GET /api/changelog`, `GET /api/type/:type/changelog`
|
|
735
|
+
- `GET /api/info`
|
|
736
|
+
|
|
737
|
+
Changelog endpoints expose `before`/`after` clock query params (inclusive bounds).
|
|
738
|
+
|
|
739
|
+
### 8.13 `okdb-logger` — Structured Logger
|
|
740
|
+
|
|
741
|
+
Set-based sink router. Entries have the shape `{ level, msg, meta, context, ts }`.
|
|
742
|
+
|
|
743
|
+
- **`meta`** — machine-readable system identity, set at `child()` call time: `feature`, `env`, `fn`, `runId`, `runnerId`.
|
|
744
|
+
- **`context`** — user-provided call-site data (second arg to any log call). Display only, not filtered on.
|
|
745
|
+
- **`child(extraMeta)`** — returns a new logger sharing the same sinks but with additional `meta` fields shallow-merged. Used by every feature to stamp entries with `{ feature: '...' }`.
|
|
746
|
+
- **`attach(fn)`** / **`okdb.events.on('log', fn)`** / **`okdb.logs`** ring buffer — three consumer APIs.
|
|
747
|
+
- Console sink attached by default; suppresses `meta.feature === 'functions'` (function logs are stored in run records and surfaced via Admin UI).
|
|
748
|
+
- Optional Pino plugin available for structured JSON output in production.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## 9. Transaction Model In Detail
|
|
753
|
+
|
|
754
|
+
```
|
|
755
|
+
okdb.transaction() → OKDBTransaction (accumulates actions)
|
|
756
|
+
.put / .remove / ... → pushes { action, args } onto this.actions[]
|
|
757
|
+
.commit() → calls okdb.db.transaction(cb)
|
|
758
|
+
cb: replays each action via okdb._put / _remove / etc.
|
|
759
|
+
each _* method:
|
|
760
|
+
1. validates key/value
|
|
761
|
+
2. reads existing entry (for version/oldValue)
|
|
762
|
+
3. calls db.put / db.remove
|
|
763
|
+
4. calls _updateItemIndexes (synchronous, in-txn)
|
|
764
|
+
5. calls _logChange (increments clock, writes change DBs)
|
|
765
|
+
all inside ONE lmdb transaction → atomic
|
|
766
|
+
.rollback() → clears actions[], closes read txn handle
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Consequences:**
|
|
770
|
+
|
|
771
|
+
- All index updates and change-log entries are written atomically with the data.
|
|
772
|
+
There is no window where a secondary index is inconsistent with the primary data.
|
|
773
|
+
- The clock is incremented inside the transaction — clock values are contiguous
|
|
774
|
+
and monotonically increasing per LMDB commit.
|
|
775
|
+
- Post-commit events are deferred via `.committed` promise — guaranteed
|
|
776
|
+
not-before-durable.
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
## 10. Internal Naming Conventions
|
|
781
|
+
|
|
782
|
+
| Convention | Meaning |
|
|
783
|
+
| ------------------- | ------------------------------------------------------------ |
|
|
784
|
+
| `type:T` | Prefix for all sub-databases belonging to type T |
|
|
785
|
+
| `~type_name` | System-internal OKDB type (not user-visible by default) |
|
|
786
|
+
| `item:T@key` | Change ID for a data record in type T with primary key `key` |
|
|
787
|
+
| `type:T` (changeId) | Change ID for a type registration/drop event |
|
|
788
|
+
| `index:T@indexName` | Change ID for an index registration/drop event |
|
|
789
|
+
| `parts.join('~')` | Canonical index name (only ever split at storage boundaries) |
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## 11. Known Architectural Constraints
|
|
794
|
+
|
|
795
|
+
### C1. Single-process writer
|
|
796
|
+
|
|
797
|
+
LMDB supports one writer at a time. Multiple Node.js processes can open the same
|
|
798
|
+
environment, but writes are serialized. OKDB does not implement a write-queue
|
|
799
|
+
across processes — that is the application's responsibility (or use the sync feature for multi-node topologies with separate environments).
|
|
800
|
+
|
|
801
|
+
### C2. Cursor invalidation on async writes
|
|
802
|
+
|
|
803
|
+
LMDB lazy cursors (`getRange`, `byIndex`) are invalidated the moment a write
|
|
804
|
+
transaction is opened in the same environment. Any `await` between cursor creation
|
|
805
|
+
and exhaustion risks stale/invalid cursor state. OKDB documents this and the
|
|
806
|
+
indexer explicitly materializes with `Array.from()` before any `await`.
|
|
807
|
+
|
|
808
|
+
### C3. ~~No TTL / expiry on data records~~ (Resolved)
|
|
809
|
+
|
|
810
|
+
Native per-record TTL is now supported. Each type has two TTL sub-databases
|
|
811
|
+
(`type:T:ttl` for sweep ordering and `type:T:ttlByKey` for O(1) lookup).
|
|
812
|
+
Public API: `setTTL(type, key, ttlMs)`, `getTTL(type, key)`, `clearTTL(type, key)`.
|
|
813
|
+
Background sweep via `startTTLSweep(intervalMs)` and manual `sweepExpiredTTL()`.
|
|
814
|
+
TTL is also available inside transactions and via HTTP API routes.
|
|
815
|
+
|
|
816
|
+
### C4. LWW sync is not causally ordered
|
|
817
|
+
|
|
818
|
+
The sync feature uses `(timestamp, origin)` LWW. Wall-clock drift between nodes
|
|
819
|
+
can cause causally-later writes to lose to causally-earlier ones. There is no
|
|
820
|
+
vector clock or HLC (Hybrid Logical Clock) to prevent this.
|
|
821
|
+
|
|
822
|
+
### C5. In-memory HNSW index
|
|
823
|
+
|
|
824
|
+
The vector search graph (HNSW via `hnswlib-node`) is fully in-memory. Vectors are
|
|
825
|
+
persisted in LMDB but the graph is rebuilt on each restart. For large corpora
|
|
826
|
+
(millions of vectors) this startup time and memory footprint become significant.
|
|
827
|
+
|
|
828
|
+
### C6. Field schema is append-only and coarse
|
|
829
|
+
|
|
830
|
+
`_mergeFieldSchema` only adds to the observed field/type set — it never prunes.
|
|
831
|
+
Schema is coarse (`string | number | ...`) not structural. No schema validation
|
|
832
|
+
on write; bad data silently persists.
|
|
833
|
+
|
|
834
|
+
### C7. Change log deduplication loses history
|
|
835
|
+
|
|
836
|
+
The LWW deduplication in the change log means the full mutation history is NOT
|
|
837
|
+
preserved — only the most recent action per primary key. This is by design for
|
|
838
|
+
sync efficiency but makes audit logging and event sourcing impossible with the
|
|
839
|
+
current log.
|
|
840
|
+
|
|
841
|
+
### C8. No query planner
|
|
842
|
+
|
|
843
|
+
`query()` applies the `sift` filter post-scan. When `options.index` is given it
|
|
844
|
+
narrows the scan range, but there is no cost-based index selection. Multi-field
|
|
845
|
+
queries without the right composite index will do full scans.
|
|
846
|
+
|
|
847
|
+
### C9. Single EventEmitter (no backpressure)
|
|
848
|
+
|
|
849
|
+
All events (`item:create`, `system:clock_change`, etc.) are emitted synchronously
|
|
850
|
+
after commit via a standard Node.js `EventEmitter`. There is no backpressure,
|
|
851
|
+
buffering, or slow-listener isolation. A slow `item:create` handler blocks all
|
|
852
|
+
subsequent event delivery for that process.
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
## 12. Potential Enhancements
|
|
857
|
+
|
|
858
|
+
These are ordered roughly by impact-to-effort ratio.
|
|
859
|
+
|
|
860
|
+
### E1. Hybrid Logical Clocks (HLC) for sync
|
|
861
|
+
|
|
862
|
+
**Problem:** Wall-clock LWW cannot correctly order concurrent writes on nodes
|
|
863
|
+
with clock skew.
|
|
864
|
+
**Enhancement:** Replace `timestamp` with an HLC value `(physicalMs, logicalCounter)`
|
|
865
|
+
encoded as a 64-bit integer. HLC advances monotonically and embeds causal ordering
|
|
866
|
+
without coordination. All existing sync comparison logic (`compareChangeLWW`) can
|
|
867
|
+
be adapted to compare HLC values. This is a well-known, low-complexity fix.
|
|
868
|
+
|
|
869
|
+
### E2. ~~TTL index on data records~~ (Implemented)
|
|
870
|
+
|
|
871
|
+
**Problem:** No native expiry (C3).
|
|
872
|
+
**Resolution:** Two LMDB sub-databases per type: `type:T:ttl` (dupSort,
|
|
873
|
+
`expiresAt → primaryKey` for efficient range sweep) and `type:T:ttlByKey`
|
|
874
|
+
(`primaryKey → expiresAt` for O(1) lookup/cleanup). `_remove()` atomically
|
|
875
|
+
clears TTL entries. Background sweep via `startTTLSweep()` with configurable
|
|
876
|
+
interval. Public API: `setTTL`, `getTTL`, `clearTTL`, `sweepExpiredTTL`.
|
|
877
|
+
Transaction support via `txn.setTTL()` / `txn.clearTTL()`. HTTP endpoints:
|
|
878
|
+
`GET/PUT/DELETE /api/:env/type/:type/item/:key/ttl`, `POST /api/:env/ttl/sweep`.
|
|
879
|
+
|
|
880
|
+
### E3. Retained event log (audit trail mode)
|
|
881
|
+
|
|
882
|
+
**Problem:** Deduplication erases history (C7).
|
|
883
|
+
**Enhancement:** Add an `auditLog: true` option per type that disables change-log
|
|
884
|
+
deduplication for that type. Write entries to a separate `type:T:audit` sub-database
|
|
885
|
+
keyed by `clock` only (no tombstoning). Consumers can then replay the full write
|
|
886
|
+
history. Useful for event-sourced types.
|
|
887
|
+
|
|
888
|
+
### E4. Persistent HNSW checkpoints
|
|
889
|
+
|
|
890
|
+
**Problem:** HNSW graph rebuilt from scratch on every restart (C5).
|
|
891
|
+
**Enhancement:** `hnswlib-node` supports `saveIndex(path)` / `loadIndex(path)`.
|
|
892
|
+
Serialize the graph to a file (or LMDB value) after each flush. On restart, load
|
|
893
|
+
the checkpoint and replay only changes since the checkpoint clock. Eliminates
|
|
894
|
+
potentially multi-minute rebuild times for large corpora.
|
|
895
|
+
|
|
896
|
+
### E5. Query planner with index scoring
|
|
897
|
+
|
|
898
|
+
**Problem:** No cost-based index selection (C8).
|
|
899
|
+
**Enhancement:** At query time, inspect `options.index` hints and available
|
|
900
|
+
indexes. Score candidate indexes by estimated selectivity (derived from index
|
|
901
|
+
cardinality metadata already stored per-index). Select the most selective index
|
|
902
|
+
automatically. This is feasible because LMDB stores page-count stats per sub-DB
|
|
903
|
+
(`db.getStats()`) which provide rough cardinality.
|
|
904
|
+
|
|
905
|
+
### E6. Multi-writer coordination via advisory lock
|
|
906
|
+
|
|
907
|
+
**Problem:** Multiple processes opening the same OKDB path can silently conflict (C1).
|
|
908
|
+
**Enhancement:** On `open()`, attempt to acquire an advisory file lock (`fs.open`
|
|
909
|
+
with exclusive flag on a `.lock` file). If acquired, this process is the primary
|
|
910
|
+
writer. Others open in read-only mode and receive clock updates via the bus. This
|
|
911
|
+
makes the multi-process safety contract explicit and machine-enforceable.
|
|
912
|
+
|
|
913
|
+
### E7. Streaming cursor backpressure
|
|
914
|
+
|
|
915
|
+
**Problem:** No backpressure on event emission (C9).
|
|
916
|
+
**Enhancement:** Wrap `EventEmitter` with an async-aware dispatcher for high-
|
|
917
|
+
frequency events (`item:create`, `system:clock_change`). When a listener is async,
|
|
918
|
+
collect it into a microtask queue and drain sequentially. Alternatively, expose a
|
|
919
|
+
`okdb.subscribe(type, asyncHandler)` API that serializes delivery per subscriber.
|
|
920
|
+
|
|
921
|
+
### E8. Schema validation on write — ✅ IMPLEMENTED
|
|
922
|
+
|
|
923
|
+
**Status:** Implemented via `env.setSchema(type, jsonSchema, { enforce })`.
|
|
924
|
+
JSON Schema validation runs inside `_write()` before mutation. Enforce mode
|
|
925
|
+
throws `OKDBSchemaError`; non-enforce mode records violations in `~schema_violations`.
|
|
926
|
+
Synced writes always land and record violations. See `okdb-schema-validator.js`.
|
|
927
|
+
|
|
928
|
+
**Companion feature — Foreign keys (ref constraints):** Also implemented.
|
|
929
|
+
`ref` annotations in JSON Schema properties declare referential integrity
|
|
930
|
+
constraints with `onDelete` cascade rules (`cascade`, `set_null`, `restrict`,
|
|
931
|
+
`no_action`). See section 3.3b for full details.
|
|
932
|
+
|
|
933
|
+
### E9. Transactional type and index operations
|
|
934
|
+
|
|
935
|
+
**Problem:** `registerType` / `registerIndex` each open their own internal
|
|
936
|
+
transaction, making it impossible to atomically create a type and populate it as
|
|
937
|
+
part of a single user transaction.
|
|
938
|
+
**Enhancement:** Expose `txn.registerType(type)` and `txn.registerIndex(type, parts)`
|
|
939
|
+
on `OKDBTransaction` so schema changes and data writes can be batched atomically.
|
|
940
|
+
|
|
941
|
+
### E10. Change log compaction
|
|
942
|
+
|
|
943
|
+
**Problem:** As the `clockToChangeDb` grows unboundedly, range scans become
|
|
944
|
+
slower. Currently no compaction mechanism exists.
|
|
945
|
+
**Enhancement:** Add `okdb.compact(beforeClock)` that removes all clock entries
|
|
946
|
+
older than `beforeClock` from both global and per-type change DBs, after
|
|
947
|
+
checkpointing all live data. Can be gated behind a configurable retention window.
|
|
948
|
+
Useful for long-running deployments.
|
|
949
|
+
|
|
950
|
+
### E11. Read replicas via LMDB `MDB_RDONLY`
|
|
951
|
+
|
|
952
|
+
**Problem:** Reads contend with writes for LMDB page cache in high-throughput
|
|
953
|
+
scenarios.
|
|
954
|
+
**Enhancement:** Expose a `okdb.openReadReplica(path)` factory that opens the
|
|
955
|
+
same LMDB environment in `MDB_RDONLY` mode. Read-only handles can serve queries
|
|
956
|
+
without ever blocking on write transactions. Combined with the bus poke mechanism,
|
|
957
|
+
replicas can invalidate in-process caches on clock change.
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## 13. Data Flow Summary
|
|
962
|
+
|
|
963
|
+
```
|
|
964
|
+
User code
|
|
965
|
+
│ txn.put('users', 'alice', { name: 'Alice' })
|
|
966
|
+
│ await txn.commit()
|
|
967
|
+
│
|
|
968
|
+
▼
|
|
969
|
+
okdb.db.transaction(cb) ← LMDB write transaction begins
|
|
970
|
+
│
|
|
971
|
+
├─ _validateSchema(...) ← JSON Schema validation (if schema set)
|
|
972
|
+
├─ _checkForeignKeys(...) ← ref constraint validation (if refs declared)
|
|
973
|
+
├─ _checkUniqueConstraints(...)← unique index validation (if unique indexes)
|
|
974
|
+
├─ db.put(key, value, version) ← primary data written
|
|
975
|
+
├─ _updateRefIndex(...) ← reverse ref index updated (old entries removed, new added)
|
|
976
|
+
├─ _updateItemIndexes(...) ← all secondary indexes updated
|
|
977
|
+
├─ _logChange(...) ← clock++, global+per-type change DBs written
|
|
978
|
+
│
|
|
979
|
+
└─ LMDB commit ← fsync — single durable write
|
|
980
|
+
│
|
|
981
|
+
▼
|
|
982
|
+
.committed.then(...)
|
|
983
|
+
│
|
|
984
|
+
├─ events.emit('item:create', ...)
|
|
985
|
+
├─ events.emit('system:clock_change', ...)
|
|
986
|
+
├─ events.emit('system:clock_change@users')
|
|
987
|
+
└─ bus.emit('bus:poke', 'users') ← UDP multicast to LAN peers
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**Delete path additions (when refs exist):**
|
|
991
|
+
|
|
992
|
+
```
|
|
993
|
+
_remove(type, key, ...)
|
|
994
|
+
│
|
|
995
|
+
├─ _processDeleteCascade(...) ← prefix-scan __ref_index for incoming refs
|
|
996
|
+
│ ├─ restrict → throw OKDBForeignKeyDeleteError (local only)
|
|
997
|
+
│ ├─ cascade → recursive _remove() with cycle detection
|
|
998
|
+
│ └─ set_null → _write() with ref field set to null
|
|
999
|
+
│
|
|
1000
|
+
├─ _removeRefIndexEntries(...) ← clear outgoing ref index entries for this doc
|
|
1001
|
+
├─ db.remove(key) ← primary data removed
|
|
1002
|
+
├─ _clearRefViolationsForKey(...) ← clear any violation records for this key
|
|
1003
|
+
└─ _logChange(...) ← clock++, change DBs written
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
Everything between "LMDB write transaction begins" and "LMDB commit" is
|
|
1007
|
+
synchronous and atomic. No partial state is ever observable.
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
## 14. Quick-Reference: Files
|
|
1012
|
+
|
|
1013
|
+
| File | Role |
|
|
1014
|
+
| -------------------------------------------- | -------------------------------------------------------- |
|
|
1015
|
+
| `okdb.js` | Core class, all read/write/index/change-log logic |
|
|
1016
|
+
| `okdb-env.js` | Per-environment data layer: CRUD, schema, FK, violations |
|
|
1017
|
+
| `okdb-transaction.js` | Batch transaction accumulator and executor |
|
|
1018
|
+
| `okdb-enums.js` | `CHANGE_ACTIONS`, `EVENTS`, `INDEX_STATE`, `OKDB_STATE` |
|
|
1019
|
+
| `okdb-error.js` | Typed error hierarchy |
|
|
1020
|
+
| `okdb-schema-validator.js` | JSON Schema compilation and validation |
|
|
1021
|
+
| `okdb-schema-http.js` | HTTP routes for schema/violations/ref-violations |
|
|
1022
|
+
| `okdb-http.js` | Internal route dispatcher + optional HTTP server |
|
|
1023
|
+
| `okdb-bus.js` | UDP multicast poke bus |
|
|
1024
|
+
| `okdb-meta.js` | Read-only schema introspection |
|
|
1025
|
+
| `okdb-migrate.js` | Export/import |
|
|
1026
|
+
| `okdb-plugins.js` | Plugin lifecycle manager |
|
|
1027
|
+
| `okdb-logger.js` | Pino wrapper |
|
|
1028
|
+
| `src/features/sync/okdb-sync.js` | LWW multi-node sync |
|
|
1029
|
+
| `src/features/queue/okdb-queue.js` | Durable job queue |
|
|
1030
|
+
| `src/features/embeddings/okdb-embeddings.js` | Vector pipeline orchestrator |
|
|
1031
|
+
| `src/features/engines/okdb-engines.js` | Persistent named service manager |
|
|
1032
|
+
| `src/features/api/okdb-api.js` | REST CRUD + changelog endpoints |
|
|
1033
|
+
| `src/features/admin/okdb-admin.js` | Admin UI backend |
|
|
1034
|
+
| `search/okdb-search.js` | Pluggable search framework |
|
|
1035
|
+
| `search/okdb-search-engine.js` | Search engine lifecycle + clock subscription |
|
|
1036
|
+
| `okdb-benchmark-suite.js` | 8-test performance + integrity benchmark suite |
|