@kedem/okdb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +57 -0
- package/README.md +344 -0
- package/banner.jpg +0 -0
- package/bin/okdb.js +2 -0
- package/docs/OKDB-ARCHITECTURE.md +1036 -0
- package/docs/auth-and-sync.md +558 -0
- package/docs/auth-permissions.md +143 -0
- package/docs/change-log.md +169 -0
- package/docs/data-model.md +174 -0
- package/docs/embeddings.md +338 -0
- package/docs/files.md +131 -0
- package/docs/fts.md +139 -0
- package/docs/functions.md +514 -0
- package/docs/getting-started.md +233 -0
- package/docs/http-api.md +705 -0
- package/docs/index.md +115 -0
- package/docs/indexes.md +207 -0
- package/docs/logging.md +116 -0
- package/docs/manifest.json +38 -0
- package/docs/migration.md +154 -0
- package/docs/pipelines.md +598 -0
- package/docs/plugins.md +144 -0
- package/docs/querying.md +204 -0
- package/docs/queue.md +637 -0
- package/docs/sync.md +156 -0
- package/docs/transactions.md +140 -0
- package/docs/ttl.md +375 -0
- package/okdb.js +1 -0
- package/package.json +26 -0
- package/public/admin-app.js +1 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/components/admin-route-host.ok.js +1 -0
- package/public/components/env-picker.ok.js +1 -0
- package/public/components/field-path-input.ok.js +1 -0
- package/public/components/field.html +6 -0
- package/public/components/geo-map.ok.js +1 -0
- package/public/components/ok-cron-input.ok.js +1 -0
- package/public/components/ok-dropzone.ok.js +1 -0
- package/public/components/ok-js-input.ok.js +1 -0
- package/public/components/ok-json-input.ok.js +1 -0
- package/public/components/ok-text-monaco-input.ok.js +1 -0
- package/public/components/theme-toggle.ok.js +1 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/images/logo.png +0 -0
- package/public/images/logo.svg +333 -0
- package/public/index.html +6 -0
- package/public/layouts/_default.ok.html +1 -0
- package/public/layouts/okdb-cube.ok.html +1 -0
- package/public/login-app.js +1 -0
- package/public/login.html +1 -0
- package/public/modules/ok.js +1 -0
- package/public/sections/api/index.ok.html +1 -0
- package/public/sections/api/parts/api-endpoint.ok.js +1 -0
- package/public/sections/auth/index.ok.html +1 -0
- package/public/sections/auth/modals/create-token-modal.ok.js +1 -0
- package/public/sections/auth/modals/edit-token-modal.ok.js +1 -0
- package/public/sections/auth/modals/perm-info-popover.ok.js +1 -0
- package/public/sections/auth/parts/token-detail.ok.js +1 -0
- package/public/sections/auth/parts/token-list.ok.js +1 -0
- package/public/sections/db/index.ok.html +1 -0
- package/public/sections/db/modals/add-item-modal.ok.js +1 -0
- package/public/sections/db/modals/create-env-modal.ok.js +1 -0
- package/public/sections/db/modals/create-fts-modal.ok.js +1 -0
- package/public/sections/db/modals/create-index-modal.ok.js +1 -0
- package/public/sections/db/modals/create-pipeline-modal.ok.js +1 -0
- package/public/sections/db/modals/demo-env-modal.ok.js +1 -0
- package/public/sections/db/modals/remove-env-modal.ok.js +1 -0
- package/public/sections/db/modals/schema-edit-modal.ok.js +1 -0
- package/public/sections/db/parts/db-overview.ok.js +1 -0
- package/public/sections/db/parts/db-pipelines-panel.ok.js +1 -0
- package/public/sections/db/parts/edit-row.ok.js +1 -0
- package/public/sections/db/parts/tm-timeline.ok.js +1 -0
- package/public/sections/db/parts/type-changelog.ok.js +1 -0
- package/public/sections/db/parts/type-list.ok.js +1 -0
- package/public/sections/db/parts/type-pipelines-tab.ok.js +1 -0
- package/public/sections/db/parts/type-schema-tab.ok.js +1 -0
- package/public/sections/db/parts/type-viewer.ok.js +1 -0
- package/public/sections/db/parts/type-violations-tab.ok.js +1 -0
- package/public/sections/docs/index.ok.html +1 -0
- package/public/sections/embeddings/index.ok.html +1 -0
- package/public/sections/embeddings/modals/register-model-modal.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-create-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-embedder-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-engine-declaration-editor.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-engine-list.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-indexer-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-lifecycle-bar.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-model-catalog.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-overview.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-search-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embed-worker-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/embeddings-engine-panel.ok.js +1 -0
- package/public/sections/embeddings/parts/engines-overview.ok.js +1 -0
- package/public/sections/embeddings/parts/pipeline-create-panel.ok.js +1 -0
- package/public/sections/engines/engine-panel-shared-style.js +1 -0
- package/public/sections/engines/engine-ui-utils.js +1 -0
- package/public/sections/engines/index.ok.html +1 -0
- package/public/sections/engines/modals/engine-modal.ok.js +1 -0
- package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -0
- package/public/sections/engines/parts/engine-lifecycle-actions.ok.js +1 -0
- package/public/sections/engines/parts/engine-panel-shell.ok.js +1 -0
- package/public/sections/engines/parts/engine-resource-card.ok.js +1 -0
- package/public/sections/engines/parts/engine-row-item.ok.js +1 -0
- package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -0
- package/public/sections/files/index.ok.html +1 -0
- package/public/sections/files/parts/files-blob-status.ok.js +1 -0
- package/public/sections/files/parts/files-collection-list.ok.js +1 -0
- package/public/sections/files/parts/files-collection-panel.ok.js +1 -0
- package/public/sections/files/parts/files-file-list.ok.js +1 -0
- package/public/sections/files/parts/files-overview.ok.js +1 -0
- package/public/sections/files/parts/files-path-browser.ok.js +1 -0
- package/public/sections/files/parts/files-upload-panel.ok.js +1 -0
- package/public/sections/functions/index.ok.html +1 -0
- package/public/sections/functions/modals/function-modal.ok.js +1 -0
- package/public/sections/functions/parts/function-detail.ok.js +1 -0
- package/public/sections/functions/parts/function-logs.ok.js +1 -0
- package/public/sections/functions/parts/function-runner.ok.js +1 -0
- package/public/sections/functions/parts/functions-list.ok.js +1 -0
- package/public/sections/license/index.ok.html +1 -0
- package/public/sections/license/parts/license-panel.ok.js +1 -0
- package/public/sections/processors/index.ok.html +1 -0
- package/public/sections/queue/index.ok.html +1 -0
- package/public/sections/queue/modals/create-bucket-modal.ok.js +1 -0
- package/public/sections/queue/modals/edit-bucket-modal.ok.js +1 -0
- package/public/sections/queue/modals/edit-job-modal.ok.js +1 -0
- package/public/sections/queue/modals/enqueue-job-modal.ok.js +1 -0
- package/public/sections/queue/modals/job-type-modal.ok.js +1 -0
- package/public/sections/queue/parts/ok-job-form.ok.js +1 -0
- package/public/sections/queue/parts/queue-buckets.ok.js +1 -0
- package/public/sections/queue/parts/queue-job-types.ok.js +1 -0
- package/public/sections/queue/parts/queue-jobs.ok.js +1 -0
- package/public/sections/queue/parts/queue-types.ok.js +1 -0
- package/public/sections/queue/parts/queue-workers.ok.js +1 -0
- package/public/sections/sync/index.ok.html +5 -0
- package/public/sections/sync/parts/sync-node-detail.ok.js +1 -0
- package/public/sections/sync/parts/sync-retention.ok.js +1 -0
- package/public/sections/sync/parts/sync-topology.ok.js +1 -0
- package/public/sections/system/index.ok.html +1 -0
- package/public/sections/system/modals/data-ops-modal.ok.js +1 -0
- package/public/sections/system/parts/system-api-panel.ok.js +1 -0
- package/public/sections/system/parts/system-auth-panel.ok.js +1 -0
- package/public/sections/system/parts/system-log-history.ok.js +1 -0
- package/public/sections/system/parts/system-log.ok.js +1 -0
- package/public/sections/system/parts/system-status.ok.js +1 -0
- package/public/sections/system/parts/system-sync-panel.ok.js +1 -0
- package/public/sections/views/index.ok.html +1 -0
- package/public/sections/views/modals/create-view-modal.ok.js +1 -0
- package/public/sections/views/parts/view-detail.ok.js +1 -0
- package/public/sections/views/parts/views-list.ok.js +1 -0
- package/public/sections/views/parts/views-overview.ok.js +1 -0
- package/public/setup-app.js +1 -0
- package/public/setup.html +1 -0
- package/public/site.webmanifest +1 -0
- package/public/stores/auth-store.js +1 -0
- package/public/stores/events-store.js +1 -0
- package/public/stores/functions-store.js +1 -0
- package/public/stores/okdb-store.js +1 -0
- package/public/stores/theme-store.js +1 -0
- package/public/styles.css +1 -0
- package/public/test-cron-input.html +10 -0
- package/public/test-json-input.html +101 -0
- package/public/util/permissions.js +1 -0
- package/public/util/sift-schema.js +1 -0
- package/public/vendor/okjs/ok.esm.min.js +2 -0
- package/public/vendor/okjs/toolkit/behavior/async.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-chart.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-doc-viewer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-marked.ok.js +1 -0
- package/public/vendor/okjs/toolkit/components/ok-monaco.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-layer-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-layer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-overlay.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/ok-sandbox.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/transition.ok.js +1 -0
- package/public/vendor/okjs/toolkit/core/virtualized.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-console-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-console.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-fps-monitor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-inspector-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-inspector.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-js-tree.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-timeline-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/devtools/ok-timeline.ok.js +1 -0
- package/public/vendor/okjs/toolkit/docs/doc-viewer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-component.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-example.ok.js +1 -0
- package/public/vendor/okjs/toolkit/editor/ok-live-shell.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-dropdown.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-icon.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-select.ok.js +1 -0
- package/public/vendor/okjs/toolkit/elements/ok-table.ok.js +1 -0
- package/public/vendor/okjs/toolkit/errors/ok-crash-boundary.ok.js +1 -0
- package/public/vendor/okjs/toolkit/errors/ok-crash-info.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-editor-examples.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-key-editor-tips.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-key-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/i18n-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/i18n/ok-i18n.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-contextmenu.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-dismiss-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-draggable.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-flyover-panel.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-flyover.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-highlight-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-highlight.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-hotkeys-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-picker-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-popout-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-popout.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-portal-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/interaction/ok-submenu.ok.js +1 -0
- package/public/vendor/okjs/toolkit/layout/ok-resizer.ok.js +1 -0
- package/public/vendor/okjs/toolkit/manifest.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-modal-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-modal.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-toast-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/modals/ok-window.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/hash-router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/history-router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/navigation/router.ok.js +1 -0
- package/public/vendor/okjs/toolkit/sync/reactive-sync-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/sync/reactive-sync.js +1 -0
- package/public/vendor/okjs/toolkit/theming/blocks/ok-design-button.css.js +1 -0
- package/public/vendor/okjs/toolkit/theming/blocks/ok-design-input.css.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design-style.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design-vars.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-design.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-input.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-service.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-style.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-utils.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-variable.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor-vm.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme-editor/ok-theme-editor.ok.js +1 -0
- package/public/vendor/okjs/toolkit/theming/ok-theme.ok.js +1 -0
- package/public/view-models/type-query-vm.js +1 -0
- package/types/environment.d.ts +130 -0
- package/types/errors.d.ts +98 -0
- package/types/features/admin.d.ts +4 -0
- package/types/features/api.d.ts +18 -0
- package/types/features/auth.d.ts +31 -0
- package/types/features/embeddings.d.ts +39 -0
- package/types/features/engines.d.ts +23 -0
- package/types/features/files.d.ts +29 -0
- package/types/features/fts.d.ts +25 -0
- package/types/features/functions.d.ts +23 -0
- package/types/features/mcp.d.ts +17 -0
- package/types/features/pipelines.d.ts +19 -0
- package/types/features/queue.d.ts +189 -0
- package/types/features/sync.d.ts +34 -0
- package/types/features/time-machine.d.ts +32 -0
- package/types/features/views.d.ts +16 -0
- package/types/index.d.ts +153 -0
- package/types/options.d.ts +202 -0
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
|