@alneas/eaos-server 0.0.0-stage → 0.2.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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.ja.md +49 -0
- package/README.md +46 -2
- package/dist/agentic/agent-jobs.d.ts +333 -0
- package/dist/agentic/agent-jobs.js +15 -0
- package/dist/agentic/agent-jobs.js.map +1 -0
- package/dist/agentic/agent-node.d.ts +40 -0
- package/dist/agentic/agent-node.js +7 -0
- package/dist/agentic/agent-node.js.map +1 -0
- package/dist/agentic/agent-runner.d.ts +33 -0
- package/dist/agentic/agent-runner.js +7 -0
- package/dist/agentic/agent-runner.js.map +1 -0
- package/dist/agentic/ai-broker.d.ts +169 -0
- package/dist/agentic/ai-broker.js +8 -0
- package/dist/agentic/ai-broker.js.map +1 -0
- package/dist/agentic/atomic-checkpointer.d.ts +137 -0
- package/dist/agentic/atomic-checkpointer.js +9 -0
- package/dist/agentic/atomic-checkpointer.js.map +1 -0
- package/dist/agentic/autonomous-worker.d.ts +122 -0
- package/dist/agentic/autonomous-worker.js +10 -0
- package/dist/agentic/autonomous-worker.js.map +1 -0
- package/dist/agentic/event-bridge.d.ts +350 -0
- package/dist/agentic/event-bridge.js +9 -0
- package/dist/agentic/event-bridge.js.map +1 -0
- package/dist/agentic/executor.d.ts +91 -0
- package/dist/agentic/executor.js +12 -0
- package/dist/agentic/executor.js.map +1 -0
- package/dist/agentic/graph-projection.d.ts +119 -0
- package/dist/agentic/graph-projection.js +7 -0
- package/dist/agentic/graph-projection.js.map +1 -0
- package/dist/agentic/graph.d.ts +524 -0
- package/dist/agentic/graph.js +7 -0
- package/dist/agentic/graph.js.map +1 -0
- package/dist/agentic/index.d.ts +28 -0
- package/dist/agentic/index.js +87 -0
- package/dist/agentic/index.js.map +1 -0
- package/dist/agentic/local-runner.d.ts +106 -0
- package/dist/agentic/local-runner.js +7 -0
- package/dist/agentic/local-runner.js.map +1 -0
- package/dist/agentic/remote-executor.d.ts +67 -0
- package/dist/agentic/remote-executor.js +8 -0
- package/dist/agentic/remote-executor.js.map +1 -0
- package/dist/agentic/types.d.ts +157 -0
- package/dist/agentic/types.js +7 -0
- package/dist/agentic/types.js.map +1 -0
- package/dist/agentic/workflow-catalog.d.ts +152 -0
- package/dist/agentic/workflow-catalog.js +7 -0
- package/dist/agentic/workflow-catalog.js.map +1 -0
- package/dist/agentic/workflow-compiler.d.ts +44 -0
- package/dist/agentic/workflow-compiler.js +8 -0
- package/dist/agentic/workflow-compiler.js.map +1 -0
- package/dist/agentic/workflow-coordinator.d.ts +170 -0
- package/dist/agentic/workflow-coordinator.js +8 -0
- package/dist/agentic/workflow-coordinator.js.map +1 -0
- package/dist/agentic/workflow-manager.d.ts +91 -0
- package/dist/agentic/workflow-manager.js +7 -0
- package/dist/agentic/workflow-manager.js.map +1 -0
- package/dist/agentic/workflow-registry.d.ts +334 -0
- package/dist/agentic/workflow-registry.js +7 -0
- package/dist/agentic/workflow-registry.js.map +1 -0
- package/dist/artifacts-CteHmCMI.d.ts +614 -0
- package/dist/auth-hooks/api-key.d.ts +75 -0
- package/dist/auth-hooks/api-key.js +7 -0
- package/dist/auth-hooks/api-key.js.map +1 -0
- package/dist/auth-hooks/index.d.ts +80 -0
- package/dist/auth-hooks/index.js +28 -0
- package/dist/auth-hooks/index.js.map +1 -0
- package/dist/browser-auth/account-memberships.d.ts +213 -0
- package/dist/browser-auth/account-memberships.js +13 -0
- package/dist/browser-auth/account-memberships.js.map +1 -0
- package/dist/browser-auth/index.d.ts +8 -0
- package/dist/browser-auth/index.js +59 -0
- package/dist/browser-auth/index.js.map +1 -0
- package/dist/browser-auth/kratos-session.d.ts +37 -0
- package/dist/browser-auth/kratos-session.js +7 -0
- package/dist/browser-auth/kratos-session.js.map +1 -0
- package/dist/browser-auth/platform-session.d.ts +120 -0
- package/dist/browser-auth/platform-session.js +7 -0
- package/dist/browser-auth/platform-session.js.map +1 -0
- package/dist/browser-auth/public-memberships.d.ts +22 -0
- package/dist/browser-auth/public-memberships.js +7 -0
- package/dist/browser-auth/public-memberships.js.map +1 -0
- package/dist/cache/index.d.ts +181 -0
- package/dist/cache/index.js +197 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/chunk-32746UQZ.js +131 -0
- package/dist/chunk-32746UQZ.js.map +1 -0
- package/dist/chunk-3NLSWP2Y.js +223 -0
- package/dist/chunk-3NLSWP2Y.js.map +1 -0
- package/dist/chunk-3P4XTTQH.js +346 -0
- package/dist/chunk-3P4XTTQH.js.map +1 -0
- package/dist/chunk-3VITSHN3.js +15 -0
- package/dist/chunk-3VITSHN3.js.map +1 -0
- package/dist/chunk-46UWPEW2.js +226 -0
- package/dist/chunk-46UWPEW2.js.map +1 -0
- package/dist/chunk-4BARRLJR.js +180 -0
- package/dist/chunk-4BARRLJR.js.map +1 -0
- package/dist/chunk-4VORUFKR.js +45 -0
- package/dist/chunk-4VORUFKR.js.map +1 -0
- package/dist/chunk-55D4ERWO.js +1 -0
- package/dist/chunk-55D4ERWO.js.map +1 -0
- package/dist/chunk-5TBBZIND.js +18 -0
- package/dist/chunk-5TBBZIND.js.map +1 -0
- package/dist/chunk-5WOZWPZA.js +211 -0
- package/dist/chunk-5WOZWPZA.js.map +1 -0
- package/dist/chunk-6CBVZ6CT.js +168 -0
- package/dist/chunk-6CBVZ6CT.js.map +1 -0
- package/dist/chunk-6XZXYNJR.js +74 -0
- package/dist/chunk-6XZXYNJR.js.map +1 -0
- package/dist/chunk-753HWDSC.js +222 -0
- package/dist/chunk-753HWDSC.js.map +1 -0
- package/dist/chunk-7AHSS7SO.js +81 -0
- package/dist/chunk-7AHSS7SO.js.map +1 -0
- package/dist/chunk-7FTJN4SA.js +1 -0
- package/dist/chunk-7FTJN4SA.js.map +1 -0
- package/dist/chunk-AVDMLTPF.js +24 -0
- package/dist/chunk-AVDMLTPF.js.map +1 -0
- package/dist/chunk-BKAHUEIT.js +95 -0
- package/dist/chunk-BKAHUEIT.js.map +1 -0
- package/dist/chunk-CJ7OFEF7.js +134 -0
- package/dist/chunk-CJ7OFEF7.js.map +1 -0
- package/dist/chunk-EJKWDZIU.js +111 -0
- package/dist/chunk-EJKWDZIU.js.map +1 -0
- package/dist/chunk-F2J6IQ6I.js +247 -0
- package/dist/chunk-F2J6IQ6I.js.map +1 -0
- package/dist/chunk-F3O7CELI.js +23 -0
- package/dist/chunk-F3O7CELI.js.map +1 -0
- package/dist/chunk-F4IITPGX.js +221 -0
- package/dist/chunk-F4IITPGX.js.map +1 -0
- package/dist/chunk-FD6VC2NA.js +66 -0
- package/dist/chunk-FD6VC2NA.js.map +1 -0
- package/dist/chunk-FRRRWAHO.js +14 -0
- package/dist/chunk-FRRRWAHO.js.map +1 -0
- package/dist/chunk-HCKAGLLB.js +17 -0
- package/dist/chunk-HCKAGLLB.js.map +1 -0
- package/dist/chunk-HVC7FG6B.js +191 -0
- package/dist/chunk-HVC7FG6B.js.map +1 -0
- package/dist/chunk-HXFVOR7Z.js +496 -0
- package/dist/chunk-HXFVOR7Z.js.map +1 -0
- package/dist/chunk-IJWYS2PF.js +122 -0
- package/dist/chunk-IJWYS2PF.js.map +1 -0
- package/dist/chunk-INCHXAFK.js +1395 -0
- package/dist/chunk-INCHXAFK.js.map +1 -0
- package/dist/chunk-JT6BRGQM.js +1 -0
- package/dist/chunk-JT6BRGQM.js.map +1 -0
- package/dist/chunk-L6PMAEMB.js +115 -0
- package/dist/chunk-L6PMAEMB.js.map +1 -0
- package/dist/chunk-LOUGW6MU.js +126 -0
- package/dist/chunk-LOUGW6MU.js.map +1 -0
- package/dist/chunk-LYPZPE53.js +129 -0
- package/dist/chunk-LYPZPE53.js.map +1 -0
- package/dist/chunk-NNCCN7JP.js +186 -0
- package/dist/chunk-NNCCN7JP.js.map +1 -0
- package/dist/chunk-PBC3MZUL.js +212 -0
- package/dist/chunk-PBC3MZUL.js.map +1 -0
- package/dist/chunk-R4ADP2DC.js +20 -0
- package/dist/chunk-R4ADP2DC.js.map +1 -0
- package/dist/chunk-RHN4Y6VS.js +44 -0
- package/dist/chunk-RHN4Y6VS.js.map +1 -0
- package/dist/chunk-SDJNYPYK.js +770 -0
- package/dist/chunk-SDJNYPYK.js.map +1 -0
- package/dist/chunk-UGNTCOVR.js +28 -0
- package/dist/chunk-UGNTCOVR.js.map +1 -0
- package/dist/chunk-VC2PCK2S.js +31 -0
- package/dist/chunk-VC2PCK2S.js.map +1 -0
- package/dist/chunk-VF5YDZJC.js +305 -0
- package/dist/chunk-VF5YDZJC.js.map +1 -0
- package/dist/chunk-VISRDVTK.js +675 -0
- package/dist/chunk-VISRDVTK.js.map +1 -0
- package/dist/chunk-W6O3LVBY.js +102 -0
- package/dist/chunk-W6O3LVBY.js.map +1 -0
- package/dist/chunk-WFEAKJLR.js +47 -0
- package/dist/chunk-WFEAKJLR.js.map +1 -0
- package/dist/chunk-XIEY2OF4.js +67 -0
- package/dist/chunk-XIEY2OF4.js.map +1 -0
- package/dist/chunk-XITLHDG3.js +37 -0
- package/dist/chunk-XITLHDG3.js.map +1 -0
- package/dist/chunk-XMWPYZWZ.js +61 -0
- package/dist/chunk-XMWPYZWZ.js.map +1 -0
- package/dist/chunk-XUP6CNOM.js +34 -0
- package/dist/chunk-XUP6CNOM.js.map +1 -0
- package/dist/chunk-YABTQZDJ.js +25 -0
- package/dist/chunk-YABTQZDJ.js.map +1 -0
- package/dist/chunk-YMHVRBUU.js +199 -0
- package/dist/chunk-YMHVRBUU.js.map +1 -0
- package/dist/chunk-YMJ7BRKU.js +510 -0
- package/dist/chunk-YMJ7BRKU.js.map +1 -0
- package/dist/chunk-ZC3XWU7B.js +28 -0
- package/dist/chunk-ZC3XWU7B.js.map +1 -0
- package/dist/chunk-ZHMTD5KV.js +90 -0
- package/dist/chunk-ZHMTD5KV.js.map +1 -0
- package/dist/chunk-ZWZDLOE7.js +160 -0
- package/dist/chunk-ZWZDLOE7.js.map +1 -0
- package/dist/db-package/db/local-first-sqlite-adapter.d.ts +432 -0
- package/dist/db-package/db/local-first-sqlite-adapter.js +7 -0
- package/dist/db-package/db/local-first-sqlite-adapter.js.map +1 -0
- package/dist/db-package/index.d.ts +7 -0
- package/dist/db-package/index.js +16 -0
- package/dist/db-package/index.js.map +1 -0
- package/dist/db-package/migration/aggregator.d.ts +86 -0
- package/dist/db-package/migration/aggregator.js +7 -0
- package/dist/db-package/migration/aggregator.js.map +1 -0
- package/dist/db-package/migration/index.d.ts +3 -0
- package/dist/db-package/migration/index.js +8 -0
- package/dist/db-package/migration/index.js.map +1 -0
- package/dist/engine.d.ts +205 -0
- package/dist/engine.js +20 -0
- package/dist/engine.js.map +1 -0
- package/dist/entitlements/index.d.ts +313 -0
- package/dist/entitlements/index.js +23 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/execution/chat.d.ts +7 -0
- package/dist/execution/chat.js +7 -0
- package/dist/execution/chat.js.map +1 -0
- package/dist/execution/index.d.ts +342 -0
- package/dist/execution/index.js +66 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/tool-agent.d.ts +7 -0
- package/dist/execution/tool-agent.js +8 -0
- package/dist/execution/tool-agent.js.map +1 -0
- package/dist/extensions/index.d.ts +172 -0
- package/dist/extensions/index.js +95 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/health/index.d.ts +35 -0
- package/dist/health/index.js +7 -0
- package/dist/health/index.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/index.d.ts +133 -0
- package/dist/lifecycle/index.js +13 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle-engine/backend-migration.d.ts +2 -0
- package/dist/lifecycle-engine/backend-migration.js +11 -0
- package/dist/lifecycle-engine/backend-migration.js.map +1 -0
- package/dist/lifecycle-engine/baseline.d.ts +2 -0
- package/dist/lifecycle-engine/baseline.js +9 -0
- package/dist/lifecycle-engine/baseline.js.map +1 -0
- package/dist/lifecycle-engine/index.d.ts +1497 -0
- package/dist/lifecycle-engine/index.js +41 -0
- package/dist/lifecycle-engine/index.js.map +1 -0
- package/dist/lifecycle-engine/platform/artifacts.d.ts +4 -0
- package/dist/lifecycle-engine/platform/artifacts.js +7 -0
- package/dist/lifecycle-engine/platform/artifacts.js.map +1 -0
- package/dist/lifecycle-engine/platform/control-plane.d.ts +12 -0
- package/dist/lifecycle-engine/platform/control-plane.js +2 -0
- package/dist/lifecycle-engine/platform/control-plane.js.map +1 -0
- package/dist/lifecycle-engine/platform/database-router.d.ts +118 -0
- package/dist/lifecycle-engine/platform/database-router.js +7 -0
- package/dist/lifecycle-engine/platform/database-router.js.map +1 -0
- package/dist/lifecycle-engine/platform/deployment-placements.d.ts +464 -0
- package/dist/lifecycle-engine/platform/deployment-placements.js +16 -0
- package/dist/lifecycle-engine/platform/deployment-placements.js.map +1 -0
- package/dist/lifecycle-engine/platform/deployment.d.ts +4 -0
- package/dist/lifecycle-engine/platform/deployment.js +17 -0
- package/dist/lifecycle-engine/platform/deployment.js.map +1 -0
- package/dist/lifecycle-engine/platform/index.d.ts +737 -0
- package/dist/lifecycle-engine/platform/index.js +60 -0
- package/dist/lifecycle-engine/platform/index.js.map +1 -0
- package/dist/lifecycle-engine/platform/runtime.d.ts +80 -0
- package/dist/lifecycle-engine/platform/runtime.js +11 -0
- package/dist/lifecycle-engine/platform/runtime.js.map +1 -0
- package/dist/lifecycle-engine/platform/site-publication.d.ts +102 -0
- package/dist/lifecycle-engine/platform/site-publication.js +7 -0
- package/dist/lifecycle-engine/platform/site-publication.js.map +1 -0
- package/dist/lifecycle-engine/platform/user-directory.d.ts +139 -0
- package/dist/lifecycle-engine/platform/user-directory.js +13 -0
- package/dist/lifecycle-engine/platform/user-directory.js.map +1 -0
- package/dist/mcp.d.ts +92 -0
- package/dist/mcp.js +10 -0
- package/dist/mcp.js.map +1 -0
- package/dist/middleware/auth.d.ts +118 -0
- package/dist/middleware/auth.js +7 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +35 -0
- package/dist/middleware/error-handler.js +7 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/module-guard.d.ts +33 -0
- package/dist/middleware/module-guard.js +7 -0
- package/dist/middleware/module-guard.js.map +1 -0
- package/dist/platform-admin/index.d.ts +37 -0
- package/dist/platform-admin/index.js +11 -0
- package/dist/platform-admin/index.js.map +1 -0
- package/dist/platform-admin/store.d.ts +346 -0
- package/dist/platform-admin/store.js +7 -0
- package/dist/platform-admin/store.js.map +1 -0
- package/dist/plugin-data/annotations.d.ts +6 -0
- package/dist/plugin-data/annotations.js +12 -0
- package/dist/plugin-data/annotations.js.map +1 -0
- package/dist/plugin-data/atomic-context.d.ts +41 -0
- package/dist/plugin-data/atomic-context.js +9 -0
- package/dist/plugin-data/atomic-context.js.map +1 -0
- package/dist/plugin-data/atomic.d.ts +6 -0
- package/dist/plugin-data/atomic.js +26 -0
- package/dist/plugin-data/atomic.js.map +1 -0
- package/dist/plugin-data/audit.d.ts +6 -0
- package/dist/plugin-data/audit.js +12 -0
- package/dist/plugin-data/audit.js.map +1 -0
- package/dist/plugin-data/commands.d.ts +6 -0
- package/dist/plugin-data/commands.js +8 -0
- package/dist/plugin-data/commands.js.map +1 -0
- package/dist/plugin-data/composite.d.ts +6 -0
- package/dist/plugin-data/composite.js +8 -0
- package/dist/plugin-data/composite.js.map +1 -0
- package/dist/plugin-data/delivery.d.ts +6 -0
- package/dist/plugin-data/delivery.js +16 -0
- package/dist/plugin-data/delivery.js.map +1 -0
- package/dist/plugin-data/index.d.ts +2798 -0
- package/dist/plugin-data/index.js +102 -0
- package/dist/plugin-data/index.js.map +1 -0
- package/dist/plugin-data/json-documents.d.ts +6 -0
- package/dist/plugin-data/json-documents.js +16 -0
- package/dist/plugin-data/json-documents.js.map +1 -0
- package/dist/plugin-data/outbox.d.ts +6 -0
- package/dist/plugin-data/outbox.js +16 -0
- package/dist/plugin-data/outbox.js.map +1 -0
- package/dist/plugin-data/scoped-records.d.ts +6 -0
- package/dist/plugin-data/scoped-records.js +10 -0
- package/dist/plugin-data/scoped-records.js.map +1 -0
- package/dist/plugin-sdk.d.ts +41 -0
- package/dist/plugin-sdk.js +10 -0
- package/dist/plugin-sdk.js.map +1 -0
- package/dist/sql-lifecycle/index.d.ts +138 -0
- package/dist/sql-lifecycle/index.js +167 -0
- package/dist/sql-lifecycle/index.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/s3-compatible.d.ts +139 -0
- package/dist/storage/s3-compatible.js +11 -0
- package/dist/storage/s3-compatible.js.map +1 -0
- package/dist/tenant.d.ts +108 -0
- package/dist/tenant.js +9 -0
- package/dist/tenant.js.map +1 -0
- package/dist/workspace-context.d.ts +119 -0
- package/dist/workspace-context.js +9 -0
- package/dist/workspace-context.js.map +1 -0
- package/docs/api.md +30560 -0
- package/docs/db/README.md +49 -0
- package/docs/db/docs/api.md +1613 -0
- package/docs/db/docs/guide.md +47 -0
- package/docs/entitlements.md +60 -0
- package/docs/guide.md +92 -0
- package/docs/index.md +57 -0
- package/docs/ja/db/README.md +46 -0
- package/docs/ja/db/docs/guide.md +53 -0
- package/docs/ja/guide.md +46 -0
- package/docs/ja/lifecycle/README.md +46 -0
- package/docs/ja/lifecycle/docs/guide.md +45 -0
- package/docs/ja/storage/README.md +46 -0
- package/docs/ja/storage/docs/guide.md +43 -0
- package/docs/lifecycle/README.md +49 -0
- package/docs/lifecycle/docs/api.md +3770 -0
- package/docs/lifecycle/docs/baseline.md +50 -0
- package/docs/lifecycle/docs/guide.md +47 -0
- package/docs/storage/README.md +49 -0
- package/docs/storage/docs/api.md +470 -0
- package/docs/storage/docs/guide.md +47 -0
- package/package.json +354 -4
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/db-package/db/local-first-sqlite-adapter.ts
|
|
2
|
+
var LocalFirstSqliteAdapter = class {
|
|
3
|
+
/** Optional create-time tenant_id fallback captured from options; not a read or mutation authorization filter. */
|
|
4
|
+
tenantId;
|
|
5
|
+
sqlite;
|
|
6
|
+
upstream;
|
|
7
|
+
onSyncError;
|
|
8
|
+
autoSyncOnMutation;
|
|
9
|
+
essentialTables;
|
|
10
|
+
syncTimer = null;
|
|
11
|
+
isSyncing = false;
|
|
12
|
+
cleanupListeners;
|
|
13
|
+
/**
|
|
14
|
+
* Captures drivers and starts optional outbox polling and browser lifecycle listeners.
|
|
15
|
+
* @param options - Host drivers and synchronization settings; see {@link LocalFirstSqliteAdapterOptions}.
|
|
16
|
+
* @remarks The constructor performs no schema initialization. Timers/listeners start immediately unless disabled; call destroy when the instance is no longer needed.
|
|
17
|
+
*/
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.tenantId = options.tenantId;
|
|
20
|
+
this.sqlite = options.sqlite;
|
|
21
|
+
this.upstream = options.upstream;
|
|
22
|
+
this.onSyncError = options.onSyncError;
|
|
23
|
+
this.autoSyncOnMutation = options.autoSyncOnMutation !== false;
|
|
24
|
+
this.essentialTables = new Set(options.essentialTables ?? []);
|
|
25
|
+
if (options.syncIntervalMs !== 0) {
|
|
26
|
+
const interval = options.syncIntervalMs || 5e3;
|
|
27
|
+
if (typeof setInterval !== "undefined") {
|
|
28
|
+
this.syncTimer = setInterval(() => this.processOutbox(), interval);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (options.enableLifecycleSync !== false && typeof window !== "undefined") {
|
|
32
|
+
this.attachLifecycleListeners();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initializes SQLite synchronization metadata tables and attempts WAL, NORMAL, and 5000 ms busy-timeout pragmas.
|
|
37
|
+
* @returns No value; resolves after metadata table creation. Pragma failures are deliberately ignored.
|
|
38
|
+
* @throws Rejects metadata table creation failures; earlier tables may already exist.
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
42
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
43
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
44
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
45
|
+
* try {
|
|
46
|
+
* await adapter.initSchema();
|
|
47
|
+
* await adapter.initSchema();
|
|
48
|
+
* } finally { adapter.destroy(); }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async initSchema() {
|
|
53
|
+
try {
|
|
54
|
+
await this.sqlite.execute(`PRAGMA journal_mode = WAL;`);
|
|
55
|
+
await this.sqlite.execute(`PRAGMA synchronous = NORMAL;`);
|
|
56
|
+
await this.sqlite.execute(`PRAGMA busy_timeout = 5000;`);
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
await this.sqlite.execute(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
61
|
+
table_name TEXT PRIMARY KEY,
|
|
62
|
+
last_sync_scn INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
last_sync_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
`);
|
|
66
|
+
await this.sqlite.execute(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS _outbox_queue (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
table_name TEXT NOT NULL,
|
|
70
|
+
record_id TEXT NOT NULL,
|
|
71
|
+
operation TEXT NOT NULL,
|
|
72
|
+
payload TEXT NOT NULL,
|
|
73
|
+
previous_state TEXT,
|
|
74
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
75
|
+
error_message TEXT,
|
|
76
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
created_at TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
`);
|
|
80
|
+
await this.sqlite.execute(`
|
|
81
|
+
CREATE TABLE IF NOT EXISTS _access_frequency (
|
|
82
|
+
table_name TEXT PRIMARY KEY,
|
|
83
|
+
read_count INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
write_count INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
last_accessed_at TEXT NOT NULL
|
|
86
|
+
);
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Attaches window focus & network online hooks to trigger synchronization at user activity moments.
|
|
91
|
+
*/
|
|
92
|
+
attachLifecycleListeners() {
|
|
93
|
+
const handleFocusOrOnline = () => {
|
|
94
|
+
this.processOutbox().catch(() => {
|
|
95
|
+
});
|
|
96
|
+
this.syncActiveTables().catch(() => {
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
window.addEventListener("focus", handleFocusOrOnline);
|
|
100
|
+
window.addEventListener("online", handleFocusOrOnline);
|
|
101
|
+
this.cleanupListeners = () => {
|
|
102
|
+
window.removeEventListener("focus", handleFocusOrOnline);
|
|
103
|
+
window.removeEventListener("online", handleFocusOrOnline);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Increments a table read or write counter and records the current ISO timestamp.
|
|
108
|
+
* @param collection - Table name recorded as a metadata value.
|
|
109
|
+
* @param type - Counter to increment; read contributes weight 2 and write weight 5 in adaptive ranking.
|
|
110
|
+
* @returns No value; resolves after the counter upsert.
|
|
111
|
+
* @throws Rejects SQLite failures.
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
115
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
116
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
117
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
118
|
+
* try {
|
|
119
|
+
* await adapter.initSchema();
|
|
120
|
+
* await adapter.recordAccess("records", "read");
|
|
121
|
+
* } finally { adapter.destroy(); }
|
|
122
|
+
* }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async recordAccess(collection, type) {
|
|
126
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
127
|
+
const readIncrement = type === "read" ? 1 : 0;
|
|
128
|
+
const writeIncrement = type === "write" ? 1 : 0;
|
|
129
|
+
await this.sqlite.execute(
|
|
130
|
+
`INSERT INTO _access_frequency (table_name, read_count, write_count, last_accessed_at)
|
|
131
|
+
VALUES (?, ?, ?, ?)
|
|
132
|
+
ON CONFLICT(table_name) DO UPDATE SET
|
|
133
|
+
read_count = _access_frequency.read_count + excluded.read_count,
|
|
134
|
+
write_count = _access_frequency.write_count + excluded.write_count,
|
|
135
|
+
last_accessed_at = excluded.last_accessed_at`,
|
|
136
|
+
[collection, readIncrement, writeIncrement, now]
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Pulls every essential table plus the highest weighted access-frequency tables sequentially.
|
|
141
|
+
* @param limit - Maximum frequency-ranked tables selected in addition to essential tables; defaults to 8 and is passed to SQLite without additional validation.
|
|
142
|
+
* @returns Names whose pullChanges calls completed, including tables with no new rows; failed tables are warned and omitted.
|
|
143
|
+
* @throws Rejects the initial frequency query failure; individual pull failures are caught.
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
147
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
148
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
149
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
150
|
+
* try {
|
|
151
|
+
* await adapter.initSchema();
|
|
152
|
+
* return await adapter.syncActiveTables(8);
|
|
153
|
+
* } finally { adapter.destroy(); }
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
async syncActiveTables(limit = 8) {
|
|
158
|
+
const frequent = await this.sqlite.select(
|
|
159
|
+
`SELECT table_name FROM _access_frequency ORDER BY (read_count * 2 + write_count * 5) DESC LIMIT ?`,
|
|
160
|
+
[limit]
|
|
161
|
+
);
|
|
162
|
+
const targetTables = new Set(this.essentialTables);
|
|
163
|
+
frequent.forEach((row) => targetTables.add(row.table_name));
|
|
164
|
+
const synced = [];
|
|
165
|
+
for (const table of targetTables) {
|
|
166
|
+
try {
|
|
167
|
+
await this.pullChanges(table);
|
|
168
|
+
synced.push(table);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn(`[LocalFirst] Failed to sync active table '${table}':`, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return synced;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Queries local SQLite rows and schedules a best-effort read-frequency update.
|
|
177
|
+
* @typeParam T - Expected local row shape; no runtime schema validation is performed.
|
|
178
|
+
* @param collection - Local table identifier, quoted by the adapter.
|
|
179
|
+
* @param filter - AND-combined equality/null or $eq/$gt/$gte/$lt/$lte column conditions; other operator objects are rejected.
|
|
180
|
+
* @param options - Supports sort, limit, and skip; limit/skip must be nonnegative safe integers. Projection and other options are ignored.
|
|
181
|
+
* @returns Local rows, or an empty array; remote freshness is not implied.
|
|
182
|
+
* @throws Rejects unsupported filters, invalid pagination, or SQLite query failures. Access-stat failures are ignored.
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
186
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
187
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
188
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
189
|
+
* try {
|
|
190
|
+
* await adapter.initSchema();
|
|
191
|
+
* return await adapter.find("records", { active: true }, { limit: 10 });
|
|
192
|
+
* } finally { adapter.destroy(); }
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
async find(collection, filter, options) {
|
|
197
|
+
this.recordAccess(collection, "read").catch(() => {
|
|
198
|
+
});
|
|
199
|
+
const { sql, params } = this.buildSelectQuery(collection, filter, options);
|
|
200
|
+
return await this.sqlite.select(sql, params);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Counts local rows matching a filter without pagination.
|
|
204
|
+
* @param collection - Local table identifier, quoted by the adapter.
|
|
205
|
+
* @param filter - Same supported AND-combined conditions as find.
|
|
206
|
+
* @param options - Sort is retained; limit and skip are discarded. Other options are ignored.
|
|
207
|
+
* @returns Numeric count, or zero if the driver returns no count row.
|
|
208
|
+
* @throws Rejects unsupported filters or SQLite failures; access-stat failures are ignored.
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
212
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
213
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
214
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
215
|
+
* try {
|
|
216
|
+
* await adapter.initSchema();
|
|
217
|
+
* return await adapter.count("records");
|
|
218
|
+
* } finally { adapter.destroy(); }
|
|
219
|
+
* }
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
async count(collection, filter, options) {
|
|
223
|
+
this.recordAccess(collection, "read").catch(() => {
|
|
224
|
+
});
|
|
225
|
+
const { sql, params } = this.buildSelectQuery(collection, filter, { ...options, limit: void 0, skip: void 0 });
|
|
226
|
+
const rows = await this.sqlite.select(`SELECT COUNT(*) AS total FROM (${sql})`, params);
|
|
227
|
+
return Number(rows[0]?.total ?? 0);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Queries a local row by its id column.
|
|
231
|
+
* @typeParam T - Expected local row shape; no runtime validation is performed.
|
|
232
|
+
* @param collection - Local table identifier.
|
|
233
|
+
* @param id - ID value bound to an equality filter.
|
|
234
|
+
* @param options - find options with limit overridden to 1; skip, if supplied, still applies.
|
|
235
|
+
* @returns First matching row, or null.
|
|
236
|
+
* @throws Rejects invalid pagination or SQLite query failures.
|
|
237
|
+
* @example
|
|
238
|
+
* ```typescript
|
|
239
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
240
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
241
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
242
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
243
|
+
* try {
|
|
244
|
+
* await adapter.initSchema();
|
|
245
|
+
* return await adapter.findOne("records", "record-1");
|
|
246
|
+
* } finally { adapter.destroy(); }
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
async findOne(collection, id, options) {
|
|
251
|
+
const rows = await this.find(collection, { id: { $eq: id } }, { ...options, limit: 1 });
|
|
252
|
+
return rows.length > 0 ? rows[0] : null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Inserts a local record before separately enqueueing its upstream INSERT.
|
|
256
|
+
* @typeParam T - Expected returned record shape; no runtime validation is performed.
|
|
257
|
+
* @param collection - Provisioned local table identifier.
|
|
258
|
+
* @param data - Column values; a truthy id is retained, otherwise an ID is generated. Missing tenant columns may receive the configured tenantId. Object values are JSON-encoded for local SQL.
|
|
259
|
+
* @param _options - Accepted for DataAccessAdapter compatibility and ignored.
|
|
260
|
+
* @returns The input record with its chosen ID and optional tenant default, before upstream acknowledgement.
|
|
261
|
+
* @throws Rejects local SQL, serialization, or enqueue failure; a local insert may already have committed. Background outbox errors are ignored by this call.
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
265
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
266
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
267
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
268
|
+
* try {
|
|
269
|
+
* await adapter.initSchema();
|
|
270
|
+
* return await adapter.create("records", { title: "Example" });
|
|
271
|
+
* } finally { adapter.destroy(); }
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
async create(collection, data, _options) {
|
|
276
|
+
this.recordAccess(collection, "write").catch(() => {
|
|
277
|
+
});
|
|
278
|
+
const recordId = data.id || (typeof crypto !== "undefined" ? crypto.randomUUID() : `rec_${Date.now()}`);
|
|
279
|
+
const recordWithId = { ...data, id: recordId };
|
|
280
|
+
if (this.tenantId && !recordWithId.tenant_id && !recordWithId.tenantId) {
|
|
281
|
+
recordWithId.tenant_id = this.tenantId;
|
|
282
|
+
}
|
|
283
|
+
await this.insertLocal(collection, recordWithId);
|
|
284
|
+
await this.enqueueOutbox({
|
|
285
|
+
id: typeof crypto !== "undefined" ? crypto.randomUUID() : `out_${Date.now()}`,
|
|
286
|
+
tableName: collection,
|
|
287
|
+
recordId,
|
|
288
|
+
operation: "INSERT",
|
|
289
|
+
payload: recordWithId,
|
|
290
|
+
previousState: null,
|
|
291
|
+
status: "pending",
|
|
292
|
+
retryCount: 0,
|
|
293
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
294
|
+
});
|
|
295
|
+
if (this.autoSyncOnMutation) {
|
|
296
|
+
this.processOutbox().catch(() => {
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return recordWithId;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Snapshots a local row, updates its columns, and separately enqueues an upstream UPDATE.
|
|
303
|
+
* @typeParam T - Expected returned merged record shape.
|
|
304
|
+
* @param collection - Provisioned local table identifier.
|
|
305
|
+
* @param id - Record ID bound in local SQL and used for upstream update.
|
|
306
|
+
* @param data - Updated columns; local SQL excludes id but the payload is forwarded unchanged upstream. An empty update can cause a SQLite syntax failure.
|
|
307
|
+
* @param _options - Accepted for DataAccessAdapter compatibility and ignored.
|
|
308
|
+
* @returns Snapshot merged with data and the supplied id, even if no local row existed.
|
|
309
|
+
* @throws Rejects snapshot, local SQL, serialization, or enqueue failures; local changes may already have committed.
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
313
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
314
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
315
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
316
|
+
* try {
|
|
317
|
+
* await adapter.initSchema();
|
|
318
|
+
* return await adapter.update("records", "record-1", { title: "Changed" });
|
|
319
|
+
* } finally { adapter.destroy(); }
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
async update(collection, id, data, _options) {
|
|
324
|
+
this.recordAccess(collection, "write").catch(() => {
|
|
325
|
+
});
|
|
326
|
+
const existing = await this.findOne(collection, id);
|
|
327
|
+
await this.updateLocal(collection, id, data);
|
|
328
|
+
const updatedRecord = { ...existing || {}, ...data, id };
|
|
329
|
+
await this.enqueueOutbox({
|
|
330
|
+
id: typeof crypto !== "undefined" ? crypto.randomUUID() : `out_${Date.now()}`,
|
|
331
|
+
tableName: collection,
|
|
332
|
+
recordId: id,
|
|
333
|
+
operation: "UPDATE",
|
|
334
|
+
payload: data,
|
|
335
|
+
previousState: existing || null,
|
|
336
|
+
status: "pending",
|
|
337
|
+
retryCount: 0,
|
|
338
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
339
|
+
});
|
|
340
|
+
if (this.autoSyncOnMutation) {
|
|
341
|
+
this.processOutbox().catch(() => {
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return updatedRecord;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Deletes a local row and separately enqueues upstream deletion with a rollback snapshot.
|
|
348
|
+
* @typeParam T - Expected deleted record shape.
|
|
349
|
+
* @param collection - Provisioned local table identifier.
|
|
350
|
+
* @param id - Record ID used for local and upstream deletion.
|
|
351
|
+
* @param _options - Accepted for DataAccessAdapter compatibility and ignored.
|
|
352
|
+
* @returns The previous local record, or an object containing only id when absent.
|
|
353
|
+
* @throws Rejects snapshot, local delete, or enqueue failures; local deletion may already have committed.
|
|
354
|
+
* @example
|
|
355
|
+
* ```typescript
|
|
356
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
357
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
358
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
359
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
360
|
+
* try {
|
|
361
|
+
* await adapter.initSchema();
|
|
362
|
+
* return await adapter.delete("records", "record-1");
|
|
363
|
+
* } finally { adapter.destroy(); }
|
|
364
|
+
* }
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
async delete(collection, id, _options) {
|
|
368
|
+
this.recordAccess(collection, "write").catch(() => {
|
|
369
|
+
});
|
|
370
|
+
const existing = await this.findOne(collection, id);
|
|
371
|
+
await this.sqlite.execute(`DELETE FROM ${this.escapeId(collection)} WHERE id = ?`, [id]);
|
|
372
|
+
await this.enqueueOutbox({
|
|
373
|
+
id: typeof crypto !== "undefined" ? crypto.randomUUID() : `out_${Date.now()}`,
|
|
374
|
+
tableName: collection,
|
|
375
|
+
recordId: id,
|
|
376
|
+
operation: "DELETE",
|
|
377
|
+
payload: { id },
|
|
378
|
+
previousState: existing || null,
|
|
379
|
+
status: "pending",
|
|
380
|
+
retryCount: 0,
|
|
381
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
382
|
+
});
|
|
383
|
+
if (this.autoSyncOnMutation) {
|
|
384
|
+
this.processOutbox().catch(() => {
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return existing || { id };
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Pulls incremental remote changes from Oracle ADB using ORA_ROWSCN cursor within a batch transaction.
|
|
391
|
+
*
|
|
392
|
+
* @param collection - Provisioned local table whose upstream supports ORA_ROWSCN filtering, ordering, and projection.
|
|
393
|
+
* @returns Number of remote rows upserted, or zero when no changes were returned.
|
|
394
|
+
* @throws Rejects upstream or local SQL failures; an opened local transaction is rolled back best-effort on upsert/cursor failure.
|
|
395
|
+
* @remarks Remote deletes are not discovered by this pull. No per-table synchronization lock prevents overlapping pulls or concurrent host transactions.
|
|
396
|
+
* @example
|
|
397
|
+
* ```typescript
|
|
398
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
399
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
400
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
401
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
402
|
+
* try {
|
|
403
|
+
* await adapter.initSchema();
|
|
404
|
+
* return await adapter.pullChanges("records");
|
|
405
|
+
* } finally { adapter.destroy(); }
|
|
406
|
+
* }
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
async pullChanges(collection) {
|
|
410
|
+
const meta = await this.sqlite.select(
|
|
411
|
+
`SELECT last_sync_scn FROM _sync_meta WHERE table_name = ?`,
|
|
412
|
+
[collection]
|
|
413
|
+
);
|
|
414
|
+
const lastScn = meta.length > 0 ? meta[0].last_sync_scn : 0;
|
|
415
|
+
const filter = lastScn > 0 ? { ORA_ROWSCN: { $gt: lastScn } } : {};
|
|
416
|
+
const remoteRecords = await this.upstream.find(collection, filter, {
|
|
417
|
+
sort: { ORA_ROWSCN: 1 },
|
|
418
|
+
projection: ["*", "ORA_ROWSCN"]
|
|
419
|
+
});
|
|
420
|
+
if (remoteRecords.length === 0) {
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
let maxScn = lastScn;
|
|
424
|
+
await this.sqlite.execute(`BEGIN TRANSACTION;`);
|
|
425
|
+
try {
|
|
426
|
+
for (const record of remoteRecords) {
|
|
427
|
+
const recordScn = Number(record.ORA_ROWSCN) || 0;
|
|
428
|
+
if (recordScn > maxScn) {
|
|
429
|
+
maxScn = recordScn;
|
|
430
|
+
}
|
|
431
|
+
await this.upsertLocal(collection, record);
|
|
432
|
+
}
|
|
433
|
+
await this.sqlite.execute(
|
|
434
|
+
`INSERT INTO _sync_meta (table_name, last_sync_scn, last_sync_at)
|
|
435
|
+
VALUES (?, ?, ?)
|
|
436
|
+
ON CONFLICT(table_name) DO UPDATE SET
|
|
437
|
+
last_sync_scn = excluded.last_sync_scn,
|
|
438
|
+
last_sync_at = excluded.last_sync_at`,
|
|
439
|
+
[collection, maxScn, (/* @__PURE__ */ new Date()).toISOString()]
|
|
440
|
+
);
|
|
441
|
+
await this.sqlite.execute(`COMMIT;`);
|
|
442
|
+
} catch (txErr) {
|
|
443
|
+
await this.sqlite.execute(`ROLLBACK;`).catch(() => {
|
|
444
|
+
});
|
|
445
|
+
throw txErr;
|
|
446
|
+
}
|
|
447
|
+
return remoteRecords.length;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Background Outbox Worker:
|
|
451
|
+
* Processes at most 20 pending mutations in creation order, pushes upstream, and attempts rollback for non-network failures.
|
|
452
|
+
* @returns No value; resolves after one batch, or immediately if this instance already has a batch running.
|
|
453
|
+
* @throws Rejects metadata reads, malformed persisted JSON, or error-handling SQL failures. Ordinary upstream failures are recorded or rolled back rather than rejected.
|
|
454
|
+
* @remarks Recognized network failures return rows to pending and increment retry_count; no delay/backoff is applied here. Other failures attempt local snapshot rollback and mark failed; rollback/callback errors are logged. Remote success followed by local queue-delete failure may leave an uncertain delivery outcome. There is no cross-instance lease or deduplication.
|
|
455
|
+
* @example
|
|
456
|
+
* ```typescript
|
|
457
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
458
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
459
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
460
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
461
|
+
* try {
|
|
462
|
+
* await adapter.initSchema();
|
|
463
|
+
* await adapter.processOutbox();
|
|
464
|
+
* } finally { adapter.destroy(); }
|
|
465
|
+
* }
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
async processOutbox() {
|
|
469
|
+
if (this.isSyncing) return;
|
|
470
|
+
this.isSyncing = true;
|
|
471
|
+
try {
|
|
472
|
+
const pendingItems = await this.sqlite.select(
|
|
473
|
+
`SELECT * FROM _outbox_queue WHERE status = 'pending' ORDER BY created_at ASC LIMIT 20`
|
|
474
|
+
);
|
|
475
|
+
for (const rawItem of pendingItems) {
|
|
476
|
+
const item = {
|
|
477
|
+
id: rawItem.id,
|
|
478
|
+
tableName: rawItem.table_name || rawItem.tableName,
|
|
479
|
+
recordId: rawItem.record_id || rawItem.recordId,
|
|
480
|
+
operation: rawItem.operation,
|
|
481
|
+
payload: typeof rawItem.payload === "string" ? JSON.parse(rawItem.payload) : rawItem.payload,
|
|
482
|
+
previousState: rawItem.previous_state ? typeof rawItem.previous_state === "string" ? JSON.parse(rawItem.previous_state) : rawItem.previous_state : null,
|
|
483
|
+
status: rawItem.status,
|
|
484
|
+
retryCount: rawItem.retry_count ?? rawItem.retryCount ?? 0,
|
|
485
|
+
createdAt: rawItem.created_at || rawItem.createdAt
|
|
486
|
+
};
|
|
487
|
+
try {
|
|
488
|
+
await this.sqlite.execute(`UPDATE _outbox_queue SET status = 'syncing' WHERE id = ?`, [item.id]);
|
|
489
|
+
if (item.operation === "INSERT") {
|
|
490
|
+
await this.upstream.create(item.tableName, item.payload);
|
|
491
|
+
} else if (item.operation === "UPDATE") {
|
|
492
|
+
await this.upstream.update(item.tableName, item.recordId, item.payload);
|
|
493
|
+
} else if (item.operation === "DELETE") {
|
|
494
|
+
await this.upstream.delete(item.tableName, item.recordId);
|
|
495
|
+
}
|
|
496
|
+
await this.sqlite.execute(`DELETE FROM _outbox_queue WHERE id = ?`, [item.id]);
|
|
497
|
+
} catch (err) {
|
|
498
|
+
const isNetworkError = err.name === "AbortError" || err.code === "ECONNREFUSED" || err.message?.includes("fetch failed") || err.message?.includes("NetworkError");
|
|
499
|
+
if (isNetworkError) {
|
|
500
|
+
await this.sqlite.execute(
|
|
501
|
+
`UPDATE _outbox_queue SET status = 'pending', retry_count = retry_count + 1, error_message = ? WHERE id = ?`,
|
|
502
|
+
[err.message, item.id]
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
await this.rollbackItem(item, err);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} finally {
|
|
510
|
+
this.isSyncing = false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Rolls back a failed mutation in local SQLite using its previousState snapshot.
|
|
515
|
+
*/
|
|
516
|
+
async rollbackItem(item, error) {
|
|
517
|
+
console.error(`[LocalFirst] Rolling back ${item.tableName}:${item.recordId} due to error:`, error);
|
|
518
|
+
try {
|
|
519
|
+
if (item.operation === "INSERT") {
|
|
520
|
+
await this.sqlite.execute(`DELETE FROM ${this.escapeId(item.tableName)} WHERE id = ?`, [item.recordId]);
|
|
521
|
+
} else if (item.operation === "UPDATE" || item.operation === "DELETE") {
|
|
522
|
+
if (item.previousState) {
|
|
523
|
+
await this.upsertLocal(item.tableName, item.previousState);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
await this.sqlite.execute(`UPDATE _outbox_queue SET status = 'failed', error_message = ? WHERE id = ?`, [
|
|
527
|
+
error.message,
|
|
528
|
+
item.id
|
|
529
|
+
]);
|
|
530
|
+
if (this.onSyncError) {
|
|
531
|
+
this.onSyncError({
|
|
532
|
+
item,
|
|
533
|
+
error,
|
|
534
|
+
rolledBack: true
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
} catch (rbErr) {
|
|
538
|
+
console.error("[LocalFirst] Critical: Failed to rollback record:", rbErr);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async enqueueOutbox(item) {
|
|
542
|
+
await this.sqlite.execute(
|
|
543
|
+
`INSERT INTO _outbox_queue (id, table_name, record_id, operation, payload, previous_state, status, retry_count, created_at)
|
|
544
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
545
|
+
[
|
|
546
|
+
item.id,
|
|
547
|
+
item.tableName,
|
|
548
|
+
item.recordId,
|
|
549
|
+
item.operation,
|
|
550
|
+
JSON.stringify(item.payload),
|
|
551
|
+
item.previousState ? JSON.stringify(item.previousState) : null,
|
|
552
|
+
item.status,
|
|
553
|
+
item.retryCount,
|
|
554
|
+
item.createdAt
|
|
555
|
+
]
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
async insertLocal(table, record) {
|
|
559
|
+
const keys = Object.keys(record);
|
|
560
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
561
|
+
const values = keys.map((k) => typeof record[k] === "object" ? JSON.stringify(record[k]) : record[k]);
|
|
562
|
+
const escapedKeys = keys.map((k) => this.escapeId(k)).join(", ");
|
|
563
|
+
await this.sqlite.execute(
|
|
564
|
+
`INSERT INTO ${this.escapeId(table)} (${escapedKeys}) VALUES (${placeholders})`,
|
|
565
|
+
values
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
async updateLocal(table, id, data) {
|
|
569
|
+
const keys = Object.keys(data).filter((k) => k !== "id");
|
|
570
|
+
const setClause = keys.map((k) => `${this.escapeId(k)} = ?`).join(", ");
|
|
571
|
+
const values = keys.map((k) => typeof data[k] === "object" ? JSON.stringify(data[k]) : data[k]);
|
|
572
|
+
await this.sqlite.execute(
|
|
573
|
+
`UPDATE ${this.escapeId(table)} SET ${setClause} WHERE id = ?`,
|
|
574
|
+
[...values, id]
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
async upsertLocal(table, record) {
|
|
578
|
+
const keys = Object.keys(record);
|
|
579
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
580
|
+
const values = keys.map((k) => typeof record[k] === "object" ? JSON.stringify(record[k]) : record[k]);
|
|
581
|
+
const escapedKeys = keys.map((k) => this.escapeId(k)).join(", ");
|
|
582
|
+
const updateClauses = keys.filter((k) => k !== "id").map((k) => `${this.escapeId(k)} = excluded.${this.escapeId(k)}`).join(", ");
|
|
583
|
+
await this.sqlite.execute(
|
|
584
|
+
`INSERT INTO ${this.escapeId(table)} (${escapedKeys}) VALUES (${placeholders})
|
|
585
|
+
ON CONFLICT(id) DO UPDATE SET ${updateClauses}`,
|
|
586
|
+
values
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
buildSelectQuery(table, filter, options) {
|
|
590
|
+
let sql = `SELECT * FROM ${this.escapeId(table)}`;
|
|
591
|
+
const params = [];
|
|
592
|
+
const whereParts = [];
|
|
593
|
+
for (const value of [options?.limit, options?.skip]) if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new Error("Invalid SQLite pagination");
|
|
594
|
+
if (filter) {
|
|
595
|
+
for (const [key, val] of Object.entries(filter)) {
|
|
596
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
597
|
+
const opObj = val;
|
|
598
|
+
if ("$eq" in opObj) {
|
|
599
|
+
whereParts.push(`${this.escapeId(key)} = ?`);
|
|
600
|
+
params.push(opObj.$eq);
|
|
601
|
+
} else if ("$gt" in opObj) {
|
|
602
|
+
whereParts.push(`${this.escapeId(key)} > ?`);
|
|
603
|
+
params.push(opObj.$gt);
|
|
604
|
+
} else if ("$gte" in opObj) {
|
|
605
|
+
whereParts.push(`${this.escapeId(key)} >= ?`);
|
|
606
|
+
params.push(opObj.$gte);
|
|
607
|
+
} else if ("$lt" in opObj) {
|
|
608
|
+
whereParts.push(`${this.escapeId(key)} < ?`);
|
|
609
|
+
params.push(opObj.$lt);
|
|
610
|
+
} else if ("$lte" in opObj) {
|
|
611
|
+
whereParts.push(`${this.escapeId(key)} <= ?`);
|
|
612
|
+
params.push(opObj.$lte);
|
|
613
|
+
} else {
|
|
614
|
+
throw new Error("Unsupported SQLite filter");
|
|
615
|
+
}
|
|
616
|
+
} else if (val === null) {
|
|
617
|
+
whereParts.push(`${this.escapeId(key)} IS NULL`);
|
|
618
|
+
} else {
|
|
619
|
+
whereParts.push(`${this.escapeId(key)} = ?`);
|
|
620
|
+
params.push(val);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (whereParts.length > 0) {
|
|
625
|
+
sql += ` WHERE ${whereParts.join(" AND ")}`;
|
|
626
|
+
}
|
|
627
|
+
if (options?.sort) {
|
|
628
|
+
const orderParts = Object.entries(options.sort).map(
|
|
629
|
+
([col, dir]) => `${this.escapeId(col)} ${dir === -1 ? "DESC" : "ASC"}`
|
|
630
|
+
);
|
|
631
|
+
sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
632
|
+
}
|
|
633
|
+
if (options?.limit !== void 0 || options?.skip !== void 0) {
|
|
634
|
+
sql += ` LIMIT ${options.limit ?? -1}`;
|
|
635
|
+
if (options?.skip) {
|
|
636
|
+
sql += ` OFFSET ${options.skip}`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return { sql, params };
|
|
640
|
+
}
|
|
641
|
+
escapeId(id) {
|
|
642
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Stops this instance's polling timer and removes its browser focus/online listeners.
|
|
646
|
+
* @returns No value; repeated calls are safe. In-flight synchronization continues and drivers remain open.
|
|
647
|
+
* @example
|
|
648
|
+
* ```typescript
|
|
649
|
+
* import { LocalFirstSqliteAdapter, type LocalFirstSqliteAdapterOptions } from "@alneas/eaos-server/db/local-first-sqlite-adapter";
|
|
650
|
+
* export async function example(options: LocalFirstSqliteAdapterOptions) {
|
|
651
|
+
* // The host supplies an initialized domain schema and authorized upstream adapter.
|
|
652
|
+
* const adapter = new LocalFirstSqliteAdapter({ ...options, syncIntervalMs: 0, enableLifecycleSync: false });
|
|
653
|
+
* try {
|
|
654
|
+
* await adapter.initSchema();
|
|
655
|
+
* adapter.destroy();
|
|
656
|
+
* } finally { adapter.destroy(); }
|
|
657
|
+
* }
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
destroy() {
|
|
661
|
+
if (this.syncTimer) {
|
|
662
|
+
clearInterval(this.syncTimer);
|
|
663
|
+
this.syncTimer = null;
|
|
664
|
+
}
|
|
665
|
+
if (this.cleanupListeners) {
|
|
666
|
+
this.cleanupListeners();
|
|
667
|
+
this.cleanupListeners = void 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
export {
|
|
673
|
+
LocalFirstSqliteAdapter
|
|
674
|
+
};
|
|
675
|
+
//# sourceMappingURL=chunk-VISRDVTK.js.map
|