@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.
Files changed (260) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +344 -0
  3. package/banner.jpg +0 -0
  4. package/bin/okdb.js +2 -0
  5. package/docs/OKDB-ARCHITECTURE.md +1036 -0
  6. package/docs/auth-and-sync.md +558 -0
  7. package/docs/auth-permissions.md +143 -0
  8. package/docs/change-log.md +169 -0
  9. package/docs/data-model.md +174 -0
  10. package/docs/embeddings.md +338 -0
  11. package/docs/files.md +131 -0
  12. package/docs/fts.md +139 -0
  13. package/docs/functions.md +514 -0
  14. package/docs/getting-started.md +233 -0
  15. package/docs/http-api.md +705 -0
  16. package/docs/index.md +115 -0
  17. package/docs/indexes.md +207 -0
  18. package/docs/logging.md +116 -0
  19. package/docs/manifest.json +38 -0
  20. package/docs/migration.md +154 -0
  21. package/docs/pipelines.md +598 -0
  22. package/docs/plugins.md +144 -0
  23. package/docs/querying.md +204 -0
  24. package/docs/queue.md +637 -0
  25. package/docs/sync.md +156 -0
  26. package/docs/transactions.md +140 -0
  27. package/docs/ttl.md +375 -0
  28. package/okdb.js +1 -0
  29. package/package.json +26 -0
  30. package/public/admin-app.js +1 -0
  31. package/public/android-chrome-192x192.png +0 -0
  32. package/public/android-chrome-512x512.png +0 -0
  33. package/public/apple-touch-icon.png +0 -0
  34. package/public/components/admin-route-host.ok.js +1 -0
  35. package/public/components/env-picker.ok.js +1 -0
  36. package/public/components/field-path-input.ok.js +1 -0
  37. package/public/components/field.html +6 -0
  38. package/public/components/geo-map.ok.js +1 -0
  39. package/public/components/ok-cron-input.ok.js +1 -0
  40. package/public/components/ok-dropzone.ok.js +1 -0
  41. package/public/components/ok-js-input.ok.js +1 -0
  42. package/public/components/ok-json-input.ok.js +1 -0
  43. package/public/components/ok-text-monaco-input.ok.js +1 -0
  44. package/public/components/theme-toggle.ok.js +1 -0
  45. package/public/favicon-16x16.png +0 -0
  46. package/public/favicon-32x32.png +0 -0
  47. package/public/favicon.ico +0 -0
  48. package/public/images/logo.png +0 -0
  49. package/public/images/logo.svg +333 -0
  50. package/public/index.html +6 -0
  51. package/public/layouts/_default.ok.html +1 -0
  52. package/public/layouts/okdb-cube.ok.html +1 -0
  53. package/public/login-app.js +1 -0
  54. package/public/login.html +1 -0
  55. package/public/modules/ok.js +1 -0
  56. package/public/sections/api/index.ok.html +1 -0
  57. package/public/sections/api/parts/api-endpoint.ok.js +1 -0
  58. package/public/sections/auth/index.ok.html +1 -0
  59. package/public/sections/auth/modals/create-token-modal.ok.js +1 -0
  60. package/public/sections/auth/modals/edit-token-modal.ok.js +1 -0
  61. package/public/sections/auth/modals/perm-info-popover.ok.js +1 -0
  62. package/public/sections/auth/parts/token-detail.ok.js +1 -0
  63. package/public/sections/auth/parts/token-list.ok.js +1 -0
  64. package/public/sections/db/index.ok.html +1 -0
  65. package/public/sections/db/modals/add-item-modal.ok.js +1 -0
  66. package/public/sections/db/modals/create-env-modal.ok.js +1 -0
  67. package/public/sections/db/modals/create-fts-modal.ok.js +1 -0
  68. package/public/sections/db/modals/create-index-modal.ok.js +1 -0
  69. package/public/sections/db/modals/create-pipeline-modal.ok.js +1 -0
  70. package/public/sections/db/modals/demo-env-modal.ok.js +1 -0
  71. package/public/sections/db/modals/remove-env-modal.ok.js +1 -0
  72. package/public/sections/db/modals/schema-edit-modal.ok.js +1 -0
  73. package/public/sections/db/parts/db-overview.ok.js +1 -0
  74. package/public/sections/db/parts/db-pipelines-panel.ok.js +1 -0
  75. package/public/sections/db/parts/edit-row.ok.js +1 -0
  76. package/public/sections/db/parts/tm-timeline.ok.js +1 -0
  77. package/public/sections/db/parts/type-changelog.ok.js +1 -0
  78. package/public/sections/db/parts/type-list.ok.js +1 -0
  79. package/public/sections/db/parts/type-pipelines-tab.ok.js +1 -0
  80. package/public/sections/db/parts/type-schema-tab.ok.js +1 -0
  81. package/public/sections/db/parts/type-viewer.ok.js +1 -0
  82. package/public/sections/db/parts/type-violations-tab.ok.js +1 -0
  83. package/public/sections/docs/index.ok.html +1 -0
  84. package/public/sections/embeddings/index.ok.html +1 -0
  85. package/public/sections/embeddings/modals/register-model-modal.ok.js +1 -0
  86. package/public/sections/embeddings/parts/embed-create-panel.ok.js +1 -0
  87. package/public/sections/embeddings/parts/embed-embedder-panel.ok.js +1 -0
  88. package/public/sections/embeddings/parts/embed-engine-declaration-editor.ok.js +1 -0
  89. package/public/sections/embeddings/parts/embed-engine-list.ok.js +1 -0
  90. package/public/sections/embeddings/parts/embed-indexer-panel.ok.js +1 -0
  91. package/public/sections/embeddings/parts/embed-lifecycle-bar.ok.js +1 -0
  92. package/public/sections/embeddings/parts/embed-model-catalog.ok.js +1 -0
  93. package/public/sections/embeddings/parts/embed-overview.ok.js +1 -0
  94. package/public/sections/embeddings/parts/embed-search-panel.ok.js +1 -0
  95. package/public/sections/embeddings/parts/embed-worker-panel.ok.js +1 -0
  96. package/public/sections/embeddings/parts/embeddings-engine-panel.ok.js +1 -0
  97. package/public/sections/embeddings/parts/engines-overview.ok.js +1 -0
  98. package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -0
  99. package/public/sections/engines/engine-panel-shared-style.js +1 -0
  100. package/public/sections/engines/engine-ui-utils.js +1 -0
  101. package/public/sections/engines/index.ok.html +1 -0
  102. package/public/sections/engines/modals/engine-modal.ok.js +1 -0
  103. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -0
  104. package/public/sections/engines/parts/engine-lifecycle-actions.ok.js +1 -0
  105. package/public/sections/engines/parts/engine-panel-shell.ok.js +1 -0
  106. package/public/sections/engines/parts/engine-resource-card.ok.js +1 -0
  107. package/public/sections/engines/parts/engine-row-item.ok.js +1 -0
  108. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -0
  109. package/public/sections/files/index.ok.html +1 -0
  110. package/public/sections/files/parts/files-blob-status.ok.js +1 -0
  111. package/public/sections/files/parts/files-collection-list.ok.js +1 -0
  112. package/public/sections/files/parts/files-collection-panel.ok.js +1 -0
  113. package/public/sections/files/parts/files-file-list.ok.js +1 -0
  114. package/public/sections/files/parts/files-overview.ok.js +1 -0
  115. package/public/sections/files/parts/files-path-browser.ok.js +1 -0
  116. package/public/sections/files/parts/files-upload-panel.ok.js +1 -0
  117. package/public/sections/functions/index.ok.html +1 -0
  118. package/public/sections/functions/modals/function-modal.ok.js +1 -0
  119. package/public/sections/functions/parts/function-detail.ok.js +1 -0
  120. package/public/sections/functions/parts/function-logs.ok.js +1 -0
  121. package/public/sections/functions/parts/function-runner.ok.js +1 -0
  122. package/public/sections/functions/parts/functions-list.ok.js +1 -0
  123. package/public/sections/license/index.ok.html +1 -0
  124. package/public/sections/license/parts/license-panel.ok.js +1 -0
  125. package/public/sections/processors/index.ok.html +1 -0
  126. package/public/sections/queue/index.ok.html +1 -0
  127. package/public/sections/queue/modals/create-bucket-modal.ok.js +1 -0
  128. package/public/sections/queue/modals/edit-bucket-modal.ok.js +1 -0
  129. package/public/sections/queue/modals/edit-job-modal.ok.js +1 -0
  130. package/public/sections/queue/modals/enqueue-job-modal.ok.js +1 -0
  131. package/public/sections/queue/modals/job-type-modal.ok.js +1 -0
  132. package/public/sections/queue/parts/ok-job-form.ok.js +1 -0
  133. package/public/sections/queue/parts/queue-buckets.ok.js +1 -0
  134. package/public/sections/queue/parts/queue-job-types.ok.js +1 -0
  135. package/public/sections/queue/parts/queue-jobs.ok.js +1 -0
  136. package/public/sections/queue/parts/queue-types.ok.js +1 -0
  137. package/public/sections/queue/parts/queue-workers.ok.js +1 -0
  138. package/public/sections/sync/index.ok.html +5 -0
  139. package/public/sections/sync/parts/sync-node-detail.ok.js +1 -0
  140. package/public/sections/sync/parts/sync-retention.ok.js +1 -0
  141. package/public/sections/sync/parts/sync-topology.ok.js +1 -0
  142. package/public/sections/system/index.ok.html +1 -0
  143. package/public/sections/system/modals/data-ops-modal.ok.js +1 -0
  144. package/public/sections/system/parts/system-api-panel.ok.js +1 -0
  145. package/public/sections/system/parts/system-auth-panel.ok.js +1 -0
  146. package/public/sections/system/parts/system-log-history.ok.js +1 -0
  147. package/public/sections/system/parts/system-log.ok.js +1 -0
  148. package/public/sections/system/parts/system-status.ok.js +1 -0
  149. package/public/sections/system/parts/system-sync-panel.ok.js +1 -0
  150. package/public/sections/views/index.ok.html +1 -0
  151. package/public/sections/views/modals/create-view-modal.ok.js +1 -0
  152. package/public/sections/views/parts/view-detail.ok.js +1 -0
  153. package/public/sections/views/parts/views-list.ok.js +1 -0
  154. package/public/sections/views/parts/views-overview.ok.js +1 -0
  155. package/public/setup-app.js +1 -0
  156. package/public/setup.html +1 -0
  157. package/public/site.webmanifest +1 -0
  158. package/public/stores/auth-store.js +1 -0
  159. package/public/stores/events-store.js +1 -0
  160. package/public/stores/functions-store.js +1 -0
  161. package/public/stores/okdb-store.js +1 -0
  162. package/public/stores/theme-store.js +1 -0
  163. package/public/styles.css +1 -0
  164. package/public/test-cron-input.html +10 -0
  165. package/public/test-json-input.html +101 -0
  166. package/public/util/permissions.js +1 -0
  167. package/public/util/sift-schema.js +1 -0
  168. package/public/vendor/okjs/ok.esm.min.js +2 -0
  169. package/public/vendor/okjs/toolkit/behavior/async.ok.js +1 -0
  170. package/public/vendor/okjs/toolkit/components/ok-chart.ok.js +1 -0
  171. package/public/vendor/okjs/toolkit/components/ok-doc-viewer.ok.js +1 -0
  172. package/public/vendor/okjs/toolkit/components/ok-marked.ok.js +1 -0
  173. package/public/vendor/okjs/toolkit/components/ok-monaco.ok.js +1 -0
  174. package/public/vendor/okjs/toolkit/core/ok-layer-service.ok.js +1 -0
  175. package/public/vendor/okjs/toolkit/core/ok-layer.ok.js +1 -0
  176. package/public/vendor/okjs/toolkit/core/ok-overlay.ok.js +1 -0
  177. package/public/vendor/okjs/toolkit/core/ok-sandbox.ok.js +1 -0
  178. package/public/vendor/okjs/toolkit/core/transition.ok.js +1 -0
  179. package/public/vendor/okjs/toolkit/core/virtualized.ok.js +1 -0
  180. package/public/vendor/okjs/toolkit/devtools/ok-console-service.ok.js +1 -0
  181. package/public/vendor/okjs/toolkit/devtools/ok-console.ok.js +1 -0
  182. package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor-service.ok.js +1 -0
  183. package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor.ok.js +1 -0
  184. package/public/vendor/okjs/toolkit/devtools/ok-inspector-service.ok.js +1 -0
  185. package/public/vendor/okjs/toolkit/devtools/ok-inspector.ok.js +1 -0
  186. package/public/vendor/okjs/toolkit/devtools/ok-js-tree.ok.js +1 -0
  187. package/public/vendor/okjs/toolkit/devtools/ok-timeline-service.ok.js +1 -0
  188. package/public/vendor/okjs/toolkit/devtools/ok-timeline.ok.js +1 -0
  189. package/public/vendor/okjs/toolkit/docs/doc-viewer.ok.js +1 -0
  190. package/public/vendor/okjs/toolkit/editor/ok-live-component.ok.js +1 -0
  191. package/public/vendor/okjs/toolkit/editor/ok-live-example.ok.js +1 -0
  192. package/public/vendor/okjs/toolkit/editor/ok-live-shell.ok.js +1 -0
  193. package/public/vendor/okjs/toolkit/elements/ok-dropdown.ok.js +1 -0
  194. package/public/vendor/okjs/toolkit/elements/ok-icon.ok.js +1 -0
  195. package/public/vendor/okjs/toolkit/elements/ok-select.ok.js +1 -0
  196. package/public/vendor/okjs/toolkit/elements/ok-table.ok.js +1 -0
  197. package/public/vendor/okjs/toolkit/errors/ok-crash-boundary.ok.js +1 -0
  198. package/public/vendor/okjs/toolkit/errors/ok-crash-info.ok.js +1 -0
  199. package/public/vendor/okjs/toolkit/i18n/i18n-editor-examples.ok.js +1 -0
  200. package/public/vendor/okjs/toolkit/i18n/i18n-editor.ok.js +1 -0
  201. package/public/vendor/okjs/toolkit/i18n/i18n-key-editor-tips.ok.js +1 -0
  202. package/public/vendor/okjs/toolkit/i18n/i18n-key-editor.ok.js +1 -0
  203. package/public/vendor/okjs/toolkit/i18n/i18n-service.ok.js +1 -0
  204. package/public/vendor/okjs/toolkit/i18n/ok-i18n.js +1 -0
  205. package/public/vendor/okjs/toolkit/interaction/ok-contextmenu.ok.js +1 -0
  206. package/public/vendor/okjs/toolkit/interaction/ok-dismiss-service.ok.js +1 -0
  207. package/public/vendor/okjs/toolkit/interaction/ok-draggable.ok.js +1 -0
  208. package/public/vendor/okjs/toolkit/interaction/ok-flyover-panel.ok.js +1 -0
  209. package/public/vendor/okjs/toolkit/interaction/ok-flyover.ok.js +1 -0
  210. package/public/vendor/okjs/toolkit/interaction/ok-highlight-service.ok.js +1 -0
  211. package/public/vendor/okjs/toolkit/interaction/ok-highlight.ok.js +1 -0
  212. package/public/vendor/okjs/toolkit/interaction/ok-hotkeys-service.ok.js +1 -0
  213. package/public/vendor/okjs/toolkit/interaction/ok-picker-service.ok.js +1 -0
  214. package/public/vendor/okjs/toolkit/interaction/ok-popout-service.ok.js +1 -0
  215. package/public/vendor/okjs/toolkit/interaction/ok-popout.ok.js +1 -0
  216. package/public/vendor/okjs/toolkit/interaction/ok-portal-service.ok.js +1 -0
  217. package/public/vendor/okjs/toolkit/interaction/ok-submenu.ok.js +1 -0
  218. package/public/vendor/okjs/toolkit/layout/ok-resizer.ok.js +1 -0
  219. package/public/vendor/okjs/toolkit/manifest.js +1 -0
  220. package/public/vendor/okjs/toolkit/modals/ok-modal-service.ok.js +1 -0
  221. package/public/vendor/okjs/toolkit/modals/ok-modal.ok.js +1 -0
  222. package/public/vendor/okjs/toolkit/modals/ok-toast-service.ok.js +1 -0
  223. package/public/vendor/okjs/toolkit/modals/ok-window.ok.js +1 -0
  224. package/public/vendor/okjs/toolkit/navigation/hash-router.ok.js +1 -0
  225. package/public/vendor/okjs/toolkit/navigation/history-router.ok.js +1 -0
  226. package/public/vendor/okjs/toolkit/navigation/router.ok.js +1 -0
  227. package/public/vendor/okjs/toolkit/sync/reactive-sync-service.ok.js +1 -0
  228. package/public/vendor/okjs/toolkit/sync/reactive-sync.js +1 -0
  229. package/public/vendor/okjs/toolkit/theming/blocks/ok-design-button.css.js +1 -0
  230. package/public/vendor/okjs/toolkit/theming/blocks/ok-design-input.css.js +1 -0
  231. package/public/vendor/okjs/toolkit/theming/ok-design-style.js +1 -0
  232. package/public/vendor/okjs/toolkit/theming/ok-design-vars.js +1 -0
  233. package/public/vendor/okjs/toolkit/theming/ok-design.ok.js +1 -0
  234. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-input.ok.js +1 -0
  235. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-service.ok.js +1 -0
  236. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-style.js +1 -0
  237. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-utils.js +1 -0
  238. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-variable.ok.js +1 -0
  239. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-vm.js +1 -0
  240. package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor.ok.js +1 -0
  241. package/public/vendor/okjs/toolkit/theming/ok-theme.ok.js +1 -0
  242. package/public/view-models/type-query-vm.js +1 -0
  243. package/types/environment.d.ts +130 -0
  244. package/types/errors.d.ts +98 -0
  245. package/types/features/admin.d.ts +4 -0
  246. package/types/features/api.d.ts +18 -0
  247. package/types/features/auth.d.ts +31 -0
  248. package/types/features/embeddings.d.ts +39 -0
  249. package/types/features/engines.d.ts +23 -0
  250. package/types/features/files.d.ts +29 -0
  251. package/types/features/fts.d.ts +25 -0
  252. package/types/features/functions.d.ts +23 -0
  253. package/types/features/mcp.d.ts +17 -0
  254. package/types/features/pipelines.d.ts +19 -0
  255. package/types/features/queue.d.ts +189 -0
  256. package/types/features/sync.d.ts +34 -0
  257. package/types/features/time-machine.d.ts +32 -0
  258. package/types/features/views.d.ts +16 -0
  259. package/types/index.d.ts +153 -0
  260. 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 |