@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
package/docs/sync.md ADDED
@@ -0,0 +1,156 @@
1
+ # Sync
2
+
3
+ OKDB includes a peer-to-peer, last-write-wins (LWW) replication system. Nodes sync over HTTP — no central coordinator, no Raft, no consensus protocol. Just pull deltas from peers, compare timestamps, keep the newer one.
4
+
5
+ ---
6
+
7
+ ## How it works
8
+
9
+ 1. Every write increments the local clock and logs a change entry
10
+ 2. When the clock advances, a UDP multicast poke (`bus:poke`) is sent on the LAN
11
+ 3. Peers that receive the poke (or poll on a timer) pull a **delta** from the changed node
12
+ 4. Each incoming change is compared to the local version using HLC timestamp
13
+ 5. The newer timestamp wins; ties are broken by origin node ID (lexicographic)
14
+ 6. Applied changes are written through the normal write path (with `origin` set to prevent echo-loops)
15
+
16
+ This is eventual consistency. It's designed for loosely-coupled multi-node topologies: desktop clusters, distributed edge devices, multi-instance app servers.
17
+
18
+ ---
19
+
20
+ ## Setting up a cluster
21
+
22
+ ### Node configuration
23
+
24
+ ```javascript
25
+ const okdb = new OKDB('./node1', {
26
+ auth: {
27
+ tokens: { 'cluster-secret': ['admin'] },
28
+ },
29
+ sync: {
30
+ token: 'cluster-secret',
31
+ address: 'http://192.168.1.10:8080', // this node's public address
32
+ },
33
+ });
34
+ await okdb.open();
35
+ okdb.http.listen(8080);
36
+ ```
37
+
38
+ Every node needs:
39
+
40
+ - `auth.tokens` — includes the sync token so peers can call the delta endpoint
41
+ - `sync.token` — shared secret used to authenticate outbound sync HTTP requests
42
+ - `sync.address` — the URL other nodes will use to reach this one
43
+
44
+ ### Joining a cluster
45
+
46
+ ```javascript
47
+ // Node 2 joins node 1
48
+ await okdb.sync.join('http://192.168.1.10:8080');
49
+ ```
50
+
51
+ `join` does three things:
52
+
53
+ 1. Registers node 1 as a known peer in `~sync_nodes` type (which itself syncs)
54
+ 2. Pulls an initial delta from node 1
55
+ 3. From that point, auto-reconcile keeps them in sync on clock change
56
+
57
+ ### Multi-node example
58
+
59
+ ```javascript
60
+ const nodes = [8080, 8081, 8082].map((port) => {
61
+ const okdb = new OKDB(`./node-${port}`, {
62
+ auth: { tokens: { secret: ['admin'] } },
63
+ sync: { token: 'secret', address: `http://localhost:${port}` },
64
+ });
65
+ okdb._port = port;
66
+ return okdb;
67
+ });
68
+
69
+ await Promise.all(nodes.map((k) => k.open()));
70
+ for (const node of nodes) node.http.listen(node._port);
71
+
72
+ // Join node 1 and 2 to node 0
73
+ for (const node of nodes.slice(1)) {
74
+ await node.sync.join('http://localhost:8080');
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Sync internals
81
+
82
+ ### Peer registry
83
+
84
+ Peers are stored in the `~sync_nodes` OKDB type — which is itself synced. So adding a peer on one node propagates to all other nodes automatically, forming a mesh.
85
+
86
+ Local peer progress (which clock was last seen from each peer) is stored outside of sync in `~system` — it's local state that doesn't replicate.
87
+
88
+ ### Delta endpoint
89
+
90
+ ```
91
+ GET /api/sync/delta?from_clock=<N>
92
+ Authorization: Bearer <token>
93
+ ```
94
+
95
+ Returns up to `delta_limit` (default 500) change records from clock `N` onwards across all sync-enabled environments. Each change includes `_env` so the receiver routes it to the right environment.
96
+
97
+ ### Multi-environment sync
98
+
99
+ The delta spans **all `sync:true` environments** simultaneously. The response carries a `clocks` map (`{ default: N, queue: N, ... }`), and each environment's cursor advances independently. Custom environments created via `createEnvironment` are included automatically if `sync: true`.
100
+
101
+ ---
102
+
103
+ ## LWW conflict model
104
+
105
+ When two nodes write the same key concurrently:
106
+
107
+ - The **higher HLC timestamp** wins
108
+ - If timestamps are equal, the **lexicographically larger origin node ID** wins
109
+ - This is fully deterministic — every node arrives at the same winner independently
110
+
111
+ ### Anti-echo
112
+
113
+ Changes that originated on the local node (`origin === okdb.id`) are never re-applied when they arrive back from a peer. This prevents infinite re-broadcast loops.
114
+
115
+ ---
116
+
117
+ ## Sync info and status
118
+
119
+ ```javascript
120
+ const info = okdb.sync.info();
121
+ // {
122
+ // node_id: '...',
123
+ // clock: 142,
124
+ // auto_reconcile: true,
125
+ // peers: 2,
126
+ // reconciling: [],
127
+ // }
128
+
129
+ // List known peers
130
+ const self = okdb.sync.getSelfNode();
131
+ // { id, address, meta, updated }
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Caveats
137
+
138
+ :::warning Wall-clock drift
139
+ LWW depends on timestamps. If two nodes have significantly different system clocks, causally-later writes can lose. NTP keeps this manageable in practice, but be aware of it. HLC mitigates this for rapid local writes, but not cross-node drift.
140
+ :::
141
+
142
+ :::note Non-syncable data
143
+ Environments with `sync: false` (FTS indexes, vector stores) are excluded from sync. They are rebuilt locally from synced data. This is intentional — posting lists and HNSW graphs are large, computable, and node-local.
144
+ :::
145
+
146
+ ---
147
+
148
+ ## UDP discovery bus
149
+
150
+ `okdb-bus` sends a UDP multicast datagram on `239.1.2.3:30303` whenever the clock changes. This is how peers on the same LAN discover that new data is available without polling. Loss is acceptable — sync reconciles correctly even if some pokes are dropped.
151
+
152
+ To disable:
153
+
154
+ ```javascript
155
+ const okdb = new OKDB('./db', { bus: false });
156
+ ```
@@ -0,0 +1,140 @@
1
+ # Transactions
2
+
3
+ OKDB supports two write styles: single-operation writes (the default) and explicit batch transactions. Both are fully atomic.
4
+
5
+ ---
6
+
7
+ ## Single-operation writes
8
+
9
+ Each of these creates and commits its own LMDB transaction internally:
10
+
11
+ ```javascript
12
+ await okdb.put('orders', 'o1', { item: 'widget', qty: 3, status: 'pending' });
13
+ await okdb.create('orders', 'o2', { item: 'gadget', qty: 1, status: 'pending' });
14
+ await okdb.update('orders', 'o1', { item: 'widget', qty: 5, status: 'pending' });
15
+ await okdb.remove('orders', 'o1');
16
+ ```
17
+
18
+ | Method | Behaviour |
19
+ | -------- | --------------------------------------------- |
20
+ | `put` | Upsert — create or overwrite |
21
+ | `create` | Throws `OKDBAlreadyExistsError` if key exists |
22
+ | `update` | Throws `OKDBNotFoundError` if key absent |
23
+ | `remove` | Throws `OKDBNotFoundError` if key absent |
24
+
25
+ ---
26
+
27
+ ## Batch transactions
28
+
29
+ When you need multiple operations to succeed or fail together, use `okdb.transaction()`:
30
+
31
+ ```javascript
32
+ const txn = okdb.transaction();
33
+
34
+ txn.put('accounts', 'alice', { balance: 900 });
35
+ txn.put('accounts', 'bob', { balance: 1100 });
36
+ txn.put('transfers', 'tx1', { from: 'alice', to: 'bob', amount: 100, ts: Date.now() });
37
+
38
+ await txn.commit(); // all three land in one LMDB write transaction
39
+ ```
40
+
41
+ Rollback (discard without writing):
42
+
43
+ ```javascript
44
+ txn.rollback();
45
+ ```
46
+
47
+ ### How it works
48
+
49
+ `OKDBTransaction` accumulates operations in memory. On `commit()`, it replays them all inside a single `okdb.db.transaction()` call. Every secondary index update and change-log entry for all operations is written atomically in that one transaction — there is no partial state.
50
+
51
+ ---
52
+
53
+ ## Optimistic concurrency
54
+
55
+ Guard writes with a version check using `ifVersion`:
56
+
57
+ ```javascript
58
+ // Read the current version
59
+ const entry = okdb.getEntry('products', 'p42');
60
+ // entry.version = 7
61
+
62
+ // Only write if the record hasn't changed since we read it
63
+ await okdb.put(
64
+ 'products',
65
+ 'p42',
66
+ { ...entry.value, stock: entry.value.stock - 1 },
67
+ {
68
+ ifVersion: entry.version, // throws OKDBVersionMismatchError if version changed
69
+ },
70
+ );
71
+ ```
72
+
73
+ This is a classic compare-and-swap. If another writer modified the record between your read and your write, the write throws and you can retry.
74
+
75
+ In a batch transaction:
76
+
77
+ ```javascript
78
+ const txn = okdb.transaction();
79
+ txn.put('products', 'p42', newValue, { ifVersion: 7 });
80
+ txn.put('products', 'p43', newValue2);
81
+ await txn.commit(); // both succeed or both fail
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Error types
87
+
88
+ | Error | Code | Thrown by |
89
+ | ----------------------------- | ---------------------- | -------------------------- |
90
+ | `OKDBAlreadyExistsError` | `ALREADY_EXISTS` | `create()` |
91
+ | `OKDBNotFoundError` | `NOT_FOUND` | `update()`, `remove()` |
92
+ | `OKDBVersionMismatchError` | `VERSION_MISMATCH` | any write with `ifVersion` |
93
+ | `OKDBUniqueConstraintError` | `UNIQUE_CONSTRAINT` | unique index violation |
94
+ | `OKDBTypeNotRegisteredError` | `TYPE_NOT_REGISTERED` | write to unknown type |
95
+ | `OKDBIndexNotRegisteredError` | `INDEX_NOT_REGISTERED` | query on unknown index |
96
+
97
+ All errors extend `OKDBError` and carry a `code` property.
98
+
99
+ ```javascript
100
+ const { OKDBError, OKDBVersionMismatchError } = require('okdb/okdb-error');
101
+
102
+ try {
103
+ await okdb.put('products', 'p42', newValue, { ifVersion: oldVersion });
104
+ } catch (err) {
105
+ if (err instanceof OKDBVersionMismatchError) {
106
+ // retry logic
107
+ } else {
108
+ throw err;
109
+ }
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Timestamp and origin
116
+
117
+ Every write accepts a `timestamp` and `origin` option used by the sync system:
118
+
119
+ ```javascript
120
+ await okdb.put('items', 'i1', value, {
121
+ timestamp: Date.now(), // used for LWW conflict resolution
122
+ origin: 'node-uuid-...', // prevents echo-loops in sync
123
+ });
124
+ ```
125
+
126
+ You rarely need to set these manually — they are used internally by the sync feature.
127
+
128
+ ---
129
+
130
+ ## What's atomic
131
+
132
+ Everything that happens inside a single `db.transaction()` call is atomic:
133
+
134
+ 1. Primary data written to `type:T:data`
135
+ 2. All secondary index entries updated (`type:T:index:I` sub-dbs)
136
+ 3. Clock incremented and change log entries written (`clocks`, `changes`, `clockToChange`, `keyToChange`)
137
+
138
+ No partial state is ever visible. If the process crashes mid-write, LMDB rolls back the transaction on the next open.
139
+
140
+ Post-commit events (`item:create`, `system:clock_change`, etc.) fire **after** the LMDB transaction has been durably committed — never speculatively.
package/docs/ttl.md ADDED
@@ -0,0 +1,375 @@
1
+ # TTL (Time-to-Live)
2
+
3
+ OKDB supports per-record TTL — set an expiration on any item and it will be automatically removed after the specified duration.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ```javascript
10
+ const OKDB = require('src/okdb');
11
+ const db = new OKDB('./my-data');
12
+ await db.open();
13
+
14
+ await db.registerType('sessions');
15
+ await db.put('sessions', 's1', { user: 'alice', token: 'abc123' });
16
+
17
+ // Expire this session in 30 minutes
18
+ await db.setTTL('sessions', 's1', 30 * 60 * 1000);
19
+
20
+ // Check remaining time
21
+ const ttl = db.getTTL('sessions', 's1');
22
+ // { expiresAt: 1712345678000, remainingMs: 1799432 }
23
+
24
+ // Make the item permanent again
25
+ await db.clearTTL('sessions', 's1');
26
+ ```
27
+
28
+ ---
29
+
30
+ ## API Reference
31
+
32
+ ### `setTTL(type, key, ttlMs)`
33
+
34
+ Sets a TTL on an existing item. The item will be eligible for removal after `ttlMs` milliseconds from now.
35
+
36
+ | Parameter | Type | Description |
37
+ | --------- | -------- | ----------------------------------------------- |
38
+ | `type` | `string` | The type (collection) name |
39
+ | `key` | `string` | The item's primary key |
40
+ | `ttlMs` | `number` | Time-to-live in milliseconds (must be positive) |
41
+
42
+ **Throws:**
43
+
44
+ - `OKDBNotFoundError` if the item doesn't exist
45
+ - `OKDBInvalidValueError` if `ttlMs` is not a positive number
46
+
47
+ ```javascript
48
+ await db.setTTL('cache', 'page:/home', 60_000); // expire in 1 minute
49
+ ```
50
+
51
+ Calling `setTTL` on an item that already has a TTL replaces the previous expiration.
52
+
53
+ ### `getTTL(type, key)`
54
+
55
+ Returns the TTL info for an item, or `null` if no TTL is set.
56
+
57
+ | Parameter | Type | Description |
58
+ | --------- | -------- | ---------------------- |
59
+ | `type` | `string` | The type name |
60
+ | `key` | `string` | The item's primary key |
61
+
62
+ **Returns:** `{ expiresAt: number, remainingMs: number } | null`
63
+
64
+ - `expiresAt` — absolute Unix timestamp (ms) when the item expires
65
+ - `remainingMs` — milliseconds until expiration (0 if already expired but not yet swept)
66
+
67
+ ```javascript
68
+ const ttl = db.getTTL('cache', 'page:/home');
69
+ if (ttl && ttl.remainingMs < 5000) {
70
+ console.log('About to expire!');
71
+ }
72
+ ```
73
+
74
+ ### `clearTTL(type, key)`
75
+
76
+ Removes the TTL from an item, making it permanent. The item itself is not deleted.
77
+
78
+ ```javascript
79
+ await db.clearTTL('sessions', 's1');
80
+ ```
81
+
82
+ ### `sweepExpiredTTL(batchSize?)`
83
+
84
+ Manually triggers a sweep of all expired items across all types.
85
+
86
+ | Parameter | Type | Default | Description |
87
+ | ----------- | -------- | ------- | -------------------------------------- |
88
+ | `batchSize` | `number` | `1000` | Max items to remove per type per sweep |
89
+
90
+ **Returns:** `{ removed: number, types: { [typeName]: number } }`
91
+
92
+ ```javascript
93
+ const result = await db.sweepExpiredTTL();
94
+ console.log(`Removed ${result.removed} expired items`);
95
+ // result.types = { sessions: 3, cache: 12 }
96
+ ```
97
+
98
+ ### `ttlStats(type?)`
99
+
100
+ Returns aggregate TTL statistics for the default environment, optionally filtered to a single type.
101
+
102
+ | Parameter | Type | Description |
103
+ | --------- | -------- | ------------------------------------------ |
104
+ | `type` | `string` | _(optional)_ Filter stats to a single type |
105
+
106
+ **Returns:** `{ enabled, totalEntries, expiredEntries, nextExpiry, byType }`
107
+
108
+ | Field | Type | Description |
109
+ | ---------------- | -------------- | --------------------------------------------------- |
110
+ | `enabled` | `boolean` | Whether any TTL sub-databases exist |
111
+ | `totalEntries` | `number` | Total number of TTL entries |
112
+ | `expiredEntries` | `number` | Entries that have already expired but not yet swept |
113
+ | `nextExpiry` | `number\|null` | Timestamp (ms) of the soonest expiry, or `null` |
114
+ | `byType` | `object` | `{ [typeName]: count }` breakdown |
115
+
116
+ ```javascript
117
+ const stats = db.ttlStats();
118
+ // { enabled: true, totalEntries: 15, expiredEntries: 2,
119
+ // nextExpiry: 1712345678000, byType: { sessions: 10, cache: 5 } }
120
+
121
+ const sessionStats = db.ttlStats('sessions');
122
+ // filtered to sessions only
123
+ ```
124
+
125
+ ### `listTTL(opts?)`
126
+
127
+ Lists all TTL entries sorted by expiry time (soonest first).
128
+
129
+ | Parameter | Type | Description |
130
+ | ------------- | -------- | --------------------------------------------- |
131
+ | `opts.type` | `string` | _(optional)_ Filter to a single type |
132
+ | `opts.limit` | `number` | _(optional, default 100)_ Max items to return |
133
+ | `opts.offset` | `number` | _(optional, default 0)_ Skip this many items |
134
+
135
+ **Returns:** `{ total, items }`
136
+
137
+ Each item: `{ type, key, expiresAt, remainingMs }`
138
+
139
+ ```javascript
140
+ const result = db.listTTL({ type: 'sessions', limit: 10 });
141
+ // { total: 42, items: [ { type: 'sessions', key: 's1', expiresAt: ..., remainingMs: ... }, ... ] }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Background Sweep
147
+
148
+ Expired items are not deleted the instant they expire. Instead, they are removed by a background sweep timer or by calling `sweepExpiredTTL()` manually.
149
+
150
+ ### Start / Stop
151
+
152
+ ```javascript
153
+ const env = db.env('default');
154
+
155
+ // Sweep every 60 seconds (default)
156
+ env.startTTLSweep();
157
+
158
+ // Sweep every 10 seconds
159
+ env.startTTLSweep(10_000);
160
+
161
+ // Stop the background sweep
162
+ env.stopTTLSweep();
163
+ ```
164
+
165
+ The sweep timer is automatically stopped when the database is closed. The timer is created with `unref()` so it won't keep your process alive.
166
+
167
+ ### Sweep Behaviour
168
+
169
+ - Iterates all types, scanning the TTL index for entries where `expiresAt <= Date.now()`
170
+ - Removes each expired item using the standard `_remove()` path (indexes, foreign keys, change log all updated)
171
+ - Emits a `ttl:expired` event for each removed item
172
+ - Respects `batchSize` to avoid long-running transactions
173
+ - Safe to call concurrently — duplicate removal attempts are handled gracefully
174
+
175
+ ---
176
+
177
+ ## Transactions
178
+
179
+ TTL operations can be included in batch transactions for atomic create-and-expire patterns:
180
+
181
+ ```javascript
182
+ const txn = db.transaction();
183
+
184
+ txn.put('sessions', 's1', { user: 'alice', token: 'abc123' });
185
+ txn.setTTL('sessions', 's1', 30 * 60 * 1000); // 30 min
186
+
187
+ txn.put('sessions', 's2', { user: 'bob', token: 'xyz789' });
188
+ txn.setTTL('sessions', 's2', 60 * 60 * 1000); // 1 hour
189
+
190
+ await txn.commit(); // both items and their TTLs set atomically
191
+ ```
192
+
193
+ Also works with the `txn()` helper:
194
+
195
+ ```javascript
196
+ await db.txn(async (t) => {
197
+ t.put('cache', 'k1', { data: 'hello' });
198
+ t.setTTL('cache', 'k1', 30_000);
199
+ });
200
+ ```
201
+
202
+ Clear TTL within a transaction:
203
+
204
+ ```javascript
205
+ const txn = db.transaction();
206
+ txn.clearTTL('sessions', 's1');
207
+ await txn.commit();
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Events
213
+
214
+ TTL operations emit events on `db.events`:
215
+
216
+ | Event | Payload | When |
217
+ | ------------- | ------------------------------- | ------------------------------- |
218
+ | `ttl:set` | `{ type, key, expiresAt, env }` | A TTL is set or updated |
219
+ | `ttl:clear` | `{ type, key, env }` | A TTL is cleared |
220
+ | `ttl:expired` | `{ type, key, env }` | An item is removed by the sweep |
221
+
222
+ ```javascript
223
+ const { EVENTS } = require('okdb/okdb-enums');
224
+
225
+ db.events.on(EVENTS.TTL_EXPIRED, ({ type, key }) => {
226
+ console.log(`Expired: ${type}/${key}`);
227
+ });
228
+ ```
229
+
230
+ ---
231
+
232
+ ## HTTP API
233
+
234
+ ### Get item TTL
235
+
236
+ ```
237
+ GET /api/:env/type/:type/item/:key/ttl
238
+ ```
239
+
240
+ Returns `{ result: { expiresAt, remainingMs } }` or `{ result: null }`.
241
+
242
+ ### Set item TTL
243
+
244
+ ```
245
+ PUT /api/:env/type/:type/item/:key/ttl
246
+ Content-Type: application/json
247
+
248
+ { "ttl": 60000 }
249
+ ```
250
+
251
+ ### Clear item TTL
252
+
253
+ ```
254
+ DELETE /api/:env/type/:type/item/:key/ttl
255
+ ```
256
+
257
+ ### Put item with TTL
258
+
259
+ The standard PUT item route accepts an optional `ttl` field:
260
+
261
+ ```
262
+ PUT /api/:env/type/:type/item/:key
263
+ Content-Type: application/json
264
+
265
+ { "value": { "user": "alice" }, "ttl": 60000 }
266
+ ```
267
+
268
+ ### Sweep expired items
269
+
270
+ ```
271
+ POST /api/:env/ttl/sweep
272
+ Content-Type: application/json
273
+
274
+ { "batchSize": 500 }
275
+ ```
276
+
277
+ Returns `{ result: { removed: 5, types: { sessions: 3, cache: 2 } } }`.
278
+
279
+ ### TTL stats
280
+
281
+ ```
282
+ GET /api/:env/ttl/stats
283
+ GET /api/:env/ttl/stats?type=sessions
284
+ ```
285
+
286
+ Returns `{ result: { enabled, totalEntries, expiredEntries, nextExpiry, byType } }`.
287
+
288
+ ### List TTL entries
289
+
290
+ ```
291
+ GET /api/:env/ttl/list
292
+ GET /api/:env/ttl/list?type=sessions&limit=20&offset=0
293
+ ```
294
+
295
+ Returns `{ result: { total, items: [{ type, key, expiresAt, remainingMs }] } }`.
296
+
297
+ ### GET item includes TTL
298
+
299
+ When a TTL is set on an item, the `GET /api/:env/type/:type/item/:key` response includes it:
300
+
301
+ ```json
302
+ {
303
+ "result": {
304
+ "key": "s1",
305
+ "value": { "user": "alice" },
306
+ "ttl": { "expiresAt": 1712345678000, "remainingMs": 1799432 }
307
+ }
308
+ }
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Environments
314
+
315
+ TTL works independently per environment:
316
+
317
+ ```javascript
318
+ await db.createEnvironment('analytics');
319
+ const analytics = db.env('analytics');
320
+
321
+ await analytics.registerType('events');
322
+ await analytics.put('events', 'e1', { name: 'pageview' });
323
+ await analytics.setTTL('events', 'e1', 86_400_000); // 24h
324
+
325
+ analytics.startTTLSweep(60_000); // independent sweep timer
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Stored Functions
331
+
332
+ TTL methods are available in the stored function facade:
333
+
334
+ ```javascript
335
+ // Inside a stored function
336
+ async function handler({ db }) {
337
+ await db.setTTL('sessions', key, 30 * 60 * 1000);
338
+ const ttl = db.getTTL('sessions', key);
339
+ await db.clearTTL('sessions', key);
340
+ await db.sweepExpiredTTL();
341
+ }
342
+ ```
343
+
344
+ ---
345
+
346
+ ## How It Works
347
+
348
+ Each registered type gets two additional LMDB sub-databases:
349
+
350
+ | Sub-database | Key | Value | Purpose |
351
+ | ----------------- | ---------------------------- | ----------------------------- | ------------------------------ |
352
+ | `type:T:ttl` | `expiresAt` (ordered-binary) | `primaryKey` (ordered-binary) | Range scan for sweep (dupSort) |
353
+ | `type:T:ttlByKey` | `primaryKey` | `expiresAt` | O(1) lookup for get/clear |
354
+
355
+ - **Setting a TTL** writes to both sub-databases. If a previous TTL exists, the old entry in `ttl` is removed first.
356
+ - **Clearing a TTL** removes from both sub-databases.
357
+ - **Removing an item** (`db.remove()`) automatically clears its TTL entries.
358
+ - **Updating an item** (`db.put()`, `db.update()`) does **not** change its TTL. The TTL must be explicitly updated with `setTTL()`.
359
+ - **Sweeping** does a range scan on `type:T:ttl` for all entries where `expiresAt <= now`, then removes the corresponding items.
360
+
361
+ ---
362
+
363
+ ## Persistence
364
+
365
+ TTL metadata is stored in LMDB alongside the data. It survives restarts — items that expired while the database was closed will be swept on the next `sweepExpiredTTL()` call (or by the background timer).
366
+
367
+ ---
368
+
369
+ ## Use Cases
370
+
371
+ - **Session management** — expire user sessions after inactivity
372
+ - **Cache layer** — use OKDB as a persistent cache with automatic eviction
373
+ - **Rate limiting** — store rate-limit counters with short TTLs
374
+ - **Temporary data** — upload staging, preview tokens, OTP codes
375
+ - **CDN-like caching** — serve cached responses with configurable freshness