@tanstack/offline-transactions 0.0.0 → 0.1.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 +21 -0
- package/dist/cjs/OfflineExecutor.cjs +141 -37
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +10 -1
- package/dist/cjs/api/OfflineAction.cjs +8 -1
- package/dist/cjs/api/OfflineAction.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.cjs +39 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +8 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs +31 -0
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -1
- package/dist/cjs/storage/LocalStorageAdapter.d.cts +8 -0
- package/dist/cjs/telemetry/tracer.cjs +1 -1
- package/dist/cjs/telemetry/tracer.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +9 -4
- package/dist/esm/OfflineExecutor.d.ts +10 -1
- package/dist/esm/OfflineExecutor.js +141 -37
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineAction.js +8 -1
- package/dist/esm/api/OfflineAction.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/storage/IndexedDBAdapter.d.ts +8 -0
- package/dist/esm/storage/IndexedDBAdapter.js +39 -0
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/storage/LocalStorageAdapter.d.ts +8 -0
- package/dist/esm/storage/LocalStorageAdapter.js +31 -0
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -1
- package/dist/esm/telemetry/tracer.js +1 -1
- package/dist/esm/telemetry/tracer.js.map +1 -1
- package/dist/esm/types.d.ts +9 -4
- package/dist/esm/types.js.map +1 -1
- package/package.json +12 -12
- package/src/OfflineExecutor.ts +200 -43
- package/src/api/OfflineAction.ts +14 -1
- package/src/index.ts +3 -0
- package/src/storage/IndexedDBAdapter.ts +47 -0
- package/src/storage/LocalStorageAdapter.ts +38 -0
- package/src/telemetry/tracer.ts +4 -9
- package/src/types.ts +19 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const api = require("@opentelemetry/api");
|
|
4
|
-
const TRACER = api.trace.getTracer(
|
|
4
|
+
const TRACER = api.trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`);
|
|
5
5
|
function getParentContext(options) {
|
|
6
6
|
if (options?.parentContext) {
|
|
7
7
|
const parentSpan = api.trace.wrapSpanContext(options.parentContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracer.cjs","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"tracer.cjs","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport type { Span, SpanContext } from \"@opentelemetry/api\"\n\nconst TRACER = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n\nexport interface SpanAttrs {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface WithSpanOptions {\n parentContext?: SpanContext\n}\n\nfunction getParentContext(options?: WithSpanOptions) {\n if (options?.parentContext) {\n const parentSpan = trace.wrapSpanContext(options.parentContext)\n return trace.setSpan(context.active(), parentSpan)\n }\n\n return context.active()\n}\n\n/**\n * Lightweight span wrapper with error handling.\n * Uses OpenTelemetry API which is no-op when tracing is disabled.\n *\n * By default, creates spans at the current context level (siblings).\n * Use withNestedSpan if you want parent-child relationships.\n */\nexport async function withSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: Span) => Promise<T>,\n options?: WithSpanOptions\n): Promise<T> {\n const parentCtx = getParentContext(options)\n const span = TRACER.startSpan(name, undefined, parentCtx)\n\n // Filter out undefined attributes\n const filteredAttrs: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(attrs)) {\n if (value !== undefined) {\n filteredAttrs[key] = value\n }\n }\n\n span.setAttributes(filteredAttrs)\n\n try {\n const result = await fn(span)\n span.setStatus({ code: SpanStatusCode.OK })\n return result\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n })\n span.recordException(error as Error)\n throw error\n } finally {\n span.end()\n }\n}\n\n/**\n * Like withSpan but propagates context so child spans nest properly.\n * Use this when you want operations inside fn to be child spans.\n */\nexport async function withNestedSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: Span) => Promise<T>,\n options?: WithSpanOptions\n): Promise<T> {\n const parentCtx = getParentContext(options)\n const span = TRACER.startSpan(name, undefined, parentCtx)\n\n // Filter out undefined attributes\n const filteredAttrs: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(attrs)) {\n if (value !== undefined) {\n filteredAttrs[key] = value\n }\n }\n\n span.setAttributes(filteredAttrs)\n\n // Set the span as active context so child spans nest properly\n const ctx = trace.setSpan(parentCtx, span)\n\n try {\n // Execute the function within the span's context\n const result = await context.with(ctx, () => fn(span))\n span.setStatus({ code: SpanStatusCode.OK })\n return result\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n })\n span.recordException(error as Error)\n throw error\n } finally {\n span.end()\n }\n}\n\n/**\n * Creates a synchronous span for non-async operations\n */\nexport function withSyncSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: Span) => T,\n options?: WithSpanOptions\n): T {\n const parentCtx = getParentContext(options)\n const span = TRACER.startSpan(name, undefined, parentCtx)\n\n // Filter out undefined attributes\n const filteredAttrs: Record<string, string | number | boolean> = {}\n for (const [key, value] of Object.entries(attrs)) {\n if (value !== undefined) {\n filteredAttrs[key] = value\n }\n }\n\n span.setAttributes(filteredAttrs)\n\n try {\n const result = fn(span)\n span.setStatus({ code: SpanStatusCode.OK })\n return result\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n })\n span.recordException(error as Error)\n throw error\n } finally {\n span.end()\n }\n}\n\n/**\n * Get the current tracer instance\n */\nexport function getTracer() {\n return TRACER\n}\n"],"names":["trace","context","SpanStatusCode"],"mappings":";;;AAGA,MAAM,SAASA,IAAAA,MAAM,UAAU,kCAAkC,OAAO;AAUxE,SAAS,iBAAiB,SAA2B;AACnD,MAAI,SAAS,eAAe;AAC1B,UAAM,aAAaA,IAAAA,MAAM,gBAAgB,QAAQ,aAAa;AAC9D,WAAOA,IAAAA,MAAM,QAAQC,IAAAA,QAAQ,OAAA,GAAU,UAAU;AAAA,EACnD;AAEA,SAAOA,IAAAA,QAAQ,OAAA;AACjB;AASA,eAAsB,SACpB,MACA,OACA,IACA,SACY;AACZ,QAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAM,OAAO,OAAO,UAAU,MAAM,QAAW,SAAS;AAGxD,QAAM,gBAA2D,CAAA;AACjE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,QAAW;AACvB,oBAAc,GAAG,IAAI;AAAA,IACvB;AAAA,EACF;AAEA,OAAK,cAAc,aAAa;AAEhC,MAAI;AACF,UAAM,SAAS,MAAM,GAAG,IAAI;AAC5B,SAAK,UAAU,EAAE,MAAMC,IAAAA,eAAe,IAAI;AAC1C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,SAAK,UAAU;AAAA,MACb,MAAMA,IAAAA,eAAe;AAAA,MACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAAA,CAC/D;AACD,SAAK,gBAAgB,KAAc;AACnC,UAAM;AAAA,EACR,UAAA;AACE,SAAK,IAAA;AAAA,EACP;AACF;AAMA,eAAsB,eACpB,MACA,OACA,IACA,SACY;AACZ,QAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAM,OAAO,OAAO,UAAU,MAAM,QAAW,SAAS;AAGxD,QAAM,gBAA2D,CAAA;AACjE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,QAAW;AACvB,oBAAc,GAAG,IAAI;AAAA,IACvB;AAAA,EACF;AAEA,OAAK,cAAc,aAAa;AAGhC,QAAM,MAAMF,IAAAA,MAAM,QAAQ,WAAW,IAAI;AAEzC,MAAI;AAEF,UAAM,SAAS,MAAMC,YAAQ,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC;AACrD,SAAK,UAAU,EAAE,MAAMC,IAAAA,eAAe,IAAI;AAC1C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,SAAK,UAAU;AAAA,MACb,MAAMA,IAAAA,eAAe;AAAA,MACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAAA,CAC/D;AACD,SAAK,gBAAgB,KAAc;AACnC,UAAM;AAAA,EACR,UAAA;AACE,SAAK,IAAA;AAAA,EACP;AACF;AAKO,SAAS,aACd,MACA,OACA,IACA,SACG;AACH,QAAM,YAAY,iBAAiB,OAAO;AAC1C,QAAM,OAAO,OAAO,UAAU,MAAM,QAAW,SAAS;AAGxD,QAAM,gBAA2D,CAAA;AACjE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,QAAW;AACvB,oBAAc,GAAG,IAAI;AAAA,IACvB;AAAA,EACF;AAEA,OAAK,cAAc,aAAa;AAEhC,MAAI;AACF,UAAM,SAAS,GAAG,IAAI;AACtB,SAAK,UAAU,EAAE,MAAMA,IAAAA,eAAe,IAAI;AAC1C,WAAO;AAAA,EACT,SAAS,OAAO;AACd,SAAK,UAAU;AAAA,MACb,MAAMA,IAAAA,eAAe;AAAA,MACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAAA,CAC/D;AACD,SAAK,gBAAgB,KAAc;AACnC,UAAM;AAAA,EACR,UAAA;AACE,SAAK,IAAA;AAAA,EACP;AACF;;;;"}
|
package/dist/cjs/types.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.cjs","sources":["../../src/types.ts"],"sourcesContent":["import type {\n Collection,\n MutationFnParams,\n PendingMutation,\n} from \"@tanstack/db\"\n\n// Extended mutation function that includes idempotency key\nexport type OfflineMutationFnParams<\n T extends object = Record<string, unknown>,\n> = MutationFnParams<T> & {\n idempotencyKey: string\n}\n\nexport type OfflineMutationFn<T extends object = Record<string, unknown>> = (\n params: OfflineMutationFnParams<T>\n) => Promise<any>\n\n// Simplified mutation structure for serialization\nexport interface SerializedMutation {\n globalKey: string\n type: string\n modified: any\n original: any\n collectionId: string\n}\n\nexport interface SerializedError {\n name: string\n message: string\n stack?: string\n}\n\nexport interface SerializedSpanContext {\n traceId: string\n spanId: string\n traceFlags: number\n traceState?: string\n}\n\n// In-memory representation with full PendingMutation objects\nexport interface OfflineTransaction {\n id: string\n mutationFnName: string\n mutations: Array<PendingMutation>\n keys: Array<string>\n idempotencyKey: string\n createdAt: Date\n retryCount: number\n nextAttemptAt: number\n lastError?: SerializedError\n metadata?: Record<string, any>\n spanContext?: SerializedSpanContext\n version: 1\n}\n\n// Serialized representation for storage\nexport interface SerializedOfflineTransaction {\n id: string\n mutationFnName: string\n mutations: Array<SerializedMutation>\n keys: Array<string>\n idempotencyKey: string\n createdAt: Date\n retryCount: number\n nextAttemptAt: number\n lastError?: SerializedError\n metadata?: Record<string, any>\n spanContext?: SerializedSpanContext\n version: 1\n}\n\nexport interface OfflineConfig {\n collections: Record<string, Collection>\n mutationFns: Record<string, OfflineMutationFn>\n storage?: StorageAdapter\n maxConcurrency?: number\n jitter?: boolean\n beforeRetry?: (\n transactions: Array<OfflineTransaction>\n ) => Array<OfflineTransaction>\n onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void\n onLeadershipChange?: (isLeader: boolean) => void\n
|
|
1
|
+
{"version":3,"file":"types.cjs","sources":["../../src/types.ts"],"sourcesContent":["import type {\n Collection,\n MutationFnParams,\n PendingMutation,\n} from \"@tanstack/db\"\n\n// Extended mutation function that includes idempotency key\nexport type OfflineMutationFnParams<\n T extends object = Record<string, unknown>,\n> = MutationFnParams<T> & {\n idempotencyKey: string\n}\n\nexport type OfflineMutationFn<T extends object = Record<string, unknown>> = (\n params: OfflineMutationFnParams<T>\n) => Promise<any>\n\n// Simplified mutation structure for serialization\nexport interface SerializedMutation {\n globalKey: string\n type: string\n modified: any\n original: any\n collectionId: string\n}\n\nexport interface SerializedError {\n name: string\n message: string\n stack?: string\n}\n\nexport interface SerializedSpanContext {\n traceId: string\n spanId: string\n traceFlags: number\n traceState?: string\n}\n\n// In-memory representation with full PendingMutation objects\nexport interface OfflineTransaction {\n id: string\n mutationFnName: string\n mutations: Array<PendingMutation>\n keys: Array<string>\n idempotencyKey: string\n createdAt: Date\n retryCount: number\n nextAttemptAt: number\n lastError?: SerializedError\n metadata?: Record<string, any>\n spanContext?: SerializedSpanContext\n version: 1\n}\n\n// Serialized representation for storage\nexport interface SerializedOfflineTransaction {\n id: string\n mutationFnName: string\n mutations: Array<SerializedMutation>\n keys: Array<string>\n idempotencyKey: string\n createdAt: Date\n retryCount: number\n nextAttemptAt: number\n lastError?: SerializedError\n metadata?: Record<string, any>\n spanContext?: SerializedSpanContext\n version: 1\n}\n\n// Storage diagnostics and mode\nexport type OfflineMode = `offline` | `online-only`\n\nexport type StorageDiagnosticCode =\n | `STORAGE_AVAILABLE`\n | `INDEXEDDB_UNAVAILABLE`\n | `LOCALSTORAGE_UNAVAILABLE`\n | `STORAGE_BLOCKED`\n | `QUOTA_EXCEEDED`\n | `UNKNOWN_ERROR`\n\nexport interface StorageDiagnostic {\n code: StorageDiagnosticCode\n mode: OfflineMode\n message: string\n error?: Error\n}\n\nexport interface OfflineConfig {\n collections: Record<string, Collection>\n mutationFns: Record<string, OfflineMutationFn>\n storage?: StorageAdapter\n maxConcurrency?: number\n jitter?: boolean\n beforeRetry?: (\n transactions: Array<OfflineTransaction>\n ) => Array<OfflineTransaction>\n onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void\n onLeadershipChange?: (isLeader: boolean) => void\n onStorageFailure?: (diagnostic: StorageDiagnostic) => void\n leaderElection?: LeaderElection\n}\n\nexport interface StorageAdapter {\n get: (key: string) => Promise<string | null>\n set: (key: string, value: string) => Promise<void>\n delete: (key: string) => Promise<void>\n keys: () => Promise<Array<string>>\n clear: () => Promise<void>\n}\n\nexport interface RetryPolicy {\n calculateDelay: (retryCount: number) => number\n shouldRetry: (error: Error, retryCount: number) => boolean\n}\n\nexport interface LeaderElection {\n requestLeadership: () => Promise<boolean>\n releaseLeadership: () => void\n isLeader: () => boolean\n onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void\n}\n\nexport interface OnlineDetector {\n subscribe: (callback: () => void) => () => void\n notifyOnline: () => void\n}\n\nexport interface CreateOfflineTransactionOptions {\n id?: string\n mutationFnName: string\n autoCommit?: boolean\n idempotencyKey?: string\n metadata?: Record<string, any>\n}\n\nexport interface CreateOfflineActionOptions<T> {\n mutationFnName: string\n onMutate: (variables: T) => void\n}\n\nexport class NonRetriableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = `NonRetriableError`\n }\n}\n"],"names":[],"mappings":";;AA8IO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;"}
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -49,6 +49,14 @@ export interface SerializedOfflineTransaction {
|
|
|
49
49
|
spanContext?: SerializedSpanContext;
|
|
50
50
|
version: 1;
|
|
51
51
|
}
|
|
52
|
+
export type OfflineMode = `offline` | `online-only`;
|
|
53
|
+
export type StorageDiagnosticCode = `STORAGE_AVAILABLE` | `INDEXEDDB_UNAVAILABLE` | `LOCALSTORAGE_UNAVAILABLE` | `STORAGE_BLOCKED` | `QUOTA_EXCEEDED` | `UNKNOWN_ERROR`;
|
|
54
|
+
export interface StorageDiagnostic {
|
|
55
|
+
code: StorageDiagnosticCode;
|
|
56
|
+
mode: OfflineMode;
|
|
57
|
+
message: string;
|
|
58
|
+
error?: Error;
|
|
59
|
+
}
|
|
52
60
|
export interface OfflineConfig {
|
|
53
61
|
collections: Record<string, Collection>;
|
|
54
62
|
mutationFns: Record<string, OfflineMutationFn>;
|
|
@@ -58,11 +66,8 @@ export interface OfflineConfig {
|
|
|
58
66
|
beforeRetry?: (transactions: Array<OfflineTransaction>) => Array<OfflineTransaction>;
|
|
59
67
|
onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void;
|
|
60
68
|
onLeadershipChange?: (isLeader: boolean) => void;
|
|
69
|
+
onStorageFailure?: (diagnostic: StorageDiagnostic) => void;
|
|
61
70
|
leaderElection?: LeaderElection;
|
|
62
|
-
otel?: {
|
|
63
|
-
endpoint: string;
|
|
64
|
-
headers?: Record<string, string>;
|
|
65
|
-
};
|
|
66
71
|
}
|
|
67
72
|
export interface StorageAdapter {
|
|
68
73
|
get: (key: string) => Promise<string | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DefaultOnlineDetector } from './connectivity/OnlineDetector.js';
|
|
2
2
|
import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.js';
|
|
3
|
-
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineTransaction } from './types.js';
|
|
3
|
+
import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineMode, OfflineTransaction, StorageDiagnostic } from './types.js';
|
|
4
4
|
import { Transaction } from '@tanstack/db';
|
|
5
5
|
export declare class OfflineExecutor {
|
|
6
6
|
private config;
|
|
@@ -13,8 +13,17 @@ export declare class OfflineExecutor {
|
|
|
13
13
|
private isLeaderState;
|
|
14
14
|
private unsubscribeOnline;
|
|
15
15
|
private unsubscribeLeadership;
|
|
16
|
+
readonly mode: OfflineMode;
|
|
17
|
+
readonly storageDiagnostic: StorageDiagnostic;
|
|
18
|
+
private initPromise;
|
|
19
|
+
private initResolve;
|
|
20
|
+
private initReject;
|
|
16
21
|
private pendingTransactionPromises;
|
|
17
22
|
constructor(config: OfflineConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Probe storage availability and create appropriate adapter.
|
|
25
|
+
* Returns null if no storage is available (online-only mode).
|
|
26
|
+
*/
|
|
18
27
|
private createStorage;
|
|
19
28
|
private createLeaderElection;
|
|
20
29
|
private setupEventListeners;
|
|
@@ -17,33 +17,85 @@ class OfflineExecutor {
|
|
|
17
17
|
this.unsubscribeLeadership = null;
|
|
18
18
|
this.pendingTransactionPromises = /* @__PURE__ */ new Map();
|
|
19
19
|
this.config = config;
|
|
20
|
-
this.storage = this.createStorage();
|
|
21
|
-
this.outbox = new OutboxManager(this.storage, this.config.collections);
|
|
22
20
|
this.scheduler = new KeyScheduler();
|
|
23
|
-
this.executor = new TransactionExecutor(
|
|
24
|
-
this.scheduler,
|
|
25
|
-
this.outbox,
|
|
26
|
-
this.config,
|
|
27
|
-
this
|
|
28
|
-
);
|
|
29
|
-
this.leaderElection = this.createLeaderElection();
|
|
30
21
|
this.onlineDetector = new DefaultOnlineDetector();
|
|
31
|
-
this.
|
|
22
|
+
this.storage = null;
|
|
23
|
+
this.outbox = null;
|
|
24
|
+
this.executor = null;
|
|
25
|
+
this.leaderElection = null;
|
|
26
|
+
this.mode = `offline`;
|
|
27
|
+
this.storageDiagnostic = {
|
|
28
|
+
code: `STORAGE_AVAILABLE`,
|
|
29
|
+
mode: `offline`,
|
|
30
|
+
message: `Initializing storage...`
|
|
31
|
+
};
|
|
32
|
+
this.initPromise = new Promise((resolve, reject) => {
|
|
33
|
+
this.initResolve = resolve;
|
|
34
|
+
this.initReject = reject;
|
|
35
|
+
});
|
|
32
36
|
this.initialize();
|
|
33
37
|
}
|
|
34
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Probe storage availability and create appropriate adapter.
|
|
40
|
+
* Returns null if no storage is available (online-only mode).
|
|
41
|
+
*/
|
|
42
|
+
async createStorage() {
|
|
35
43
|
if (this.config.storage) {
|
|
36
|
-
return
|
|
44
|
+
return {
|
|
45
|
+
storage: this.config.storage,
|
|
46
|
+
diagnostic: {
|
|
47
|
+
code: `STORAGE_AVAILABLE`,
|
|
48
|
+
mode: `offline`,
|
|
49
|
+
message: `Using custom storage adapter`
|
|
50
|
+
}
|
|
51
|
+
};
|
|
37
52
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
const idbProbe = await IndexedDBAdapter.probe();
|
|
54
|
+
if (idbProbe.available) {
|
|
55
|
+
return {
|
|
56
|
+
storage: new IndexedDBAdapter(),
|
|
57
|
+
diagnostic: {
|
|
58
|
+
code: `STORAGE_AVAILABLE`,
|
|
59
|
+
mode: `offline`,
|
|
60
|
+
message: `Using IndexedDB for offline storage`
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const lsProbe = LocalStorageAdapter.probe();
|
|
65
|
+
if (lsProbe.available) {
|
|
66
|
+
return {
|
|
67
|
+
storage: new LocalStorageAdapter(),
|
|
68
|
+
diagnostic: {
|
|
69
|
+
code: `INDEXEDDB_UNAVAILABLE`,
|
|
70
|
+
mode: `offline`,
|
|
71
|
+
message: `IndexedDB unavailable, using localStorage fallback`,
|
|
72
|
+
error: idbProbe.error
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const isSecurityError = idbProbe.error?.name === `SecurityError` || lsProbe.error?.name === `SecurityError`;
|
|
77
|
+
const isQuotaError = idbProbe.error?.name === `QuotaExceededError` || lsProbe.error?.name === `QuotaExceededError`;
|
|
78
|
+
let code;
|
|
79
|
+
let message;
|
|
80
|
+
if (isSecurityError) {
|
|
81
|
+
code = `STORAGE_BLOCKED`;
|
|
82
|
+
message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`;
|
|
83
|
+
} else if (isQuotaError) {
|
|
84
|
+
code = `QUOTA_EXCEEDED`;
|
|
85
|
+
message = `Storage quota exceeded. Running in online-only mode.`;
|
|
86
|
+
} else {
|
|
87
|
+
code = `UNKNOWN_ERROR`;
|
|
88
|
+
message = `Storage unavailable due to unknown error. Running in online-only mode.`;
|
|
46
89
|
}
|
|
90
|
+
return {
|
|
91
|
+
storage: null,
|
|
92
|
+
diagnostic: {
|
|
93
|
+
code,
|
|
94
|
+
mode: `online-only`,
|
|
95
|
+
message,
|
|
96
|
+
error: idbProbe.error || lsProbe.error
|
|
97
|
+
}
|
|
98
|
+
};
|
|
47
99
|
}
|
|
48
100
|
createLeaderElection() {
|
|
49
101
|
if (this.config.leaderElection) {
|
|
@@ -65,19 +117,21 @@ class OfflineExecutor {
|
|
|
65
117
|
}
|
|
66
118
|
}
|
|
67
119
|
setupEventListeners() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.config.onLeadershipChange
|
|
120
|
+
if (this.leaderElection) {
|
|
121
|
+
this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
|
|
122
|
+
(isLeader) => {
|
|
123
|
+
this.isLeaderState = isLeader;
|
|
124
|
+
if (this.config.onLeadershipChange) {
|
|
125
|
+
this.config.onLeadershipChange(isLeader);
|
|
126
|
+
}
|
|
127
|
+
if (isLeader) {
|
|
128
|
+
this.loadAndReplayTransactions();
|
|
129
|
+
}
|
|
73
130
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
);
|
|
131
|
+
);
|
|
132
|
+
}
|
|
79
133
|
this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
|
|
80
|
-
if (this.isOfflineEnabled) {
|
|
134
|
+
if (this.isOfflineEnabled && this.executor) {
|
|
81
135
|
this.executor.resetRetryDelays();
|
|
82
136
|
this.executor.executeAll().catch((error) => {
|
|
83
137
|
console.warn(
|
|
@@ -91,17 +145,49 @@ class OfflineExecutor {
|
|
|
91
145
|
async initialize() {
|
|
92
146
|
return withSpan(`executor.initialize`, {}, async (span) => {
|
|
93
147
|
try {
|
|
148
|
+
const { storage, diagnostic } = await this.createStorage();
|
|
149
|
+
this.storage = storage;
|
|
150
|
+
this.storageDiagnostic = diagnostic;
|
|
151
|
+
this.mode = diagnostic.mode;
|
|
152
|
+
span.setAttribute(`storage.mode`, diagnostic.mode);
|
|
153
|
+
span.setAttribute(`storage.code`, diagnostic.code);
|
|
154
|
+
if (!storage) {
|
|
155
|
+
if (this.config.onStorageFailure) {
|
|
156
|
+
this.config.onStorageFailure(diagnostic);
|
|
157
|
+
}
|
|
158
|
+
span.setAttribute(`result`, `online-only`);
|
|
159
|
+
this.initResolve();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.outbox = new OutboxManager(storage, this.config.collections);
|
|
163
|
+
this.executor = new TransactionExecutor(
|
|
164
|
+
this.scheduler,
|
|
165
|
+
this.outbox,
|
|
166
|
+
this.config,
|
|
167
|
+
this
|
|
168
|
+
);
|
|
169
|
+
this.leaderElection = this.createLeaderElection();
|
|
94
170
|
const isLeader = await this.leaderElection.requestLeadership();
|
|
95
171
|
span.setAttribute(`isLeader`, isLeader);
|
|
172
|
+
this.setupEventListeners();
|
|
96
173
|
if (isLeader) {
|
|
97
174
|
await this.loadAndReplayTransactions();
|
|
98
175
|
}
|
|
176
|
+
span.setAttribute(`result`, `offline-enabled`);
|
|
177
|
+
this.initResolve();
|
|
99
178
|
} catch (error) {
|
|
100
179
|
console.warn(`Failed to initialize offline executor:`, error);
|
|
180
|
+
span.setAttribute(`result`, `failed`);
|
|
181
|
+
this.initReject(
|
|
182
|
+
error instanceof Error ? error : new Error(String(error))
|
|
183
|
+
);
|
|
101
184
|
}
|
|
102
185
|
});
|
|
103
186
|
}
|
|
104
187
|
async loadAndReplayTransactions() {
|
|
188
|
+
if (!this.executor) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
105
191
|
try {
|
|
106
192
|
await this.executor.loadPendingTransactions();
|
|
107
193
|
await this.executor.executeAll();
|
|
@@ -110,7 +196,7 @@ class OfflineExecutor {
|
|
|
110
196
|
}
|
|
111
197
|
}
|
|
112
198
|
get isOfflineEnabled() {
|
|
113
|
-
return this.isLeaderState;
|
|
199
|
+
return this.mode === `offline` && this.isLeaderState;
|
|
114
200
|
}
|
|
115
201
|
createOfflineTransaction(options) {
|
|
116
202
|
const mutationFn = this.config.mutationFns[options.mutationFnName];
|
|
@@ -161,6 +247,7 @@ class OfflineExecutor {
|
|
|
161
247
|
};
|
|
162
248
|
}
|
|
163
249
|
async persistTransaction(transaction) {
|
|
250
|
+
await this.initPromise;
|
|
164
251
|
return withNestedSpan(
|
|
165
252
|
`executor.persistTransaction`,
|
|
166
253
|
{
|
|
@@ -168,7 +255,7 @@ class OfflineExecutor {
|
|
|
168
255
|
"transaction.mutationFnName": transaction.mutationFnName
|
|
169
256
|
},
|
|
170
257
|
async (span) => {
|
|
171
|
-
if (!this.isOfflineEnabled) {
|
|
258
|
+
if (!this.isOfflineEnabled || !this.outbox || !this.executor) {
|
|
172
259
|
span.setAttribute(`result`, `skipped_not_leader`);
|
|
173
260
|
this.resolveTransaction(transaction.id, void 0);
|
|
174
261
|
return;
|
|
@@ -219,12 +306,21 @@ class OfflineExecutor {
|
|
|
219
306
|
}
|
|
220
307
|
}
|
|
221
308
|
async removeFromOutbox(id) {
|
|
309
|
+
if (!this.outbox) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
222
312
|
await this.outbox.remove(id);
|
|
223
313
|
}
|
|
224
314
|
async peekOutbox() {
|
|
315
|
+
if (!this.outbox) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
225
318
|
return this.outbox.getAll();
|
|
226
319
|
}
|
|
227
320
|
async clearOutbox() {
|
|
321
|
+
if (!this.outbox || !this.executor) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
228
324
|
await this.outbox.clear();
|
|
229
325
|
this.executor.clear();
|
|
230
326
|
}
|
|
@@ -232,9 +328,15 @@ class OfflineExecutor {
|
|
|
232
328
|
this.onlineDetector.notifyOnline();
|
|
233
329
|
}
|
|
234
330
|
getPendingCount() {
|
|
331
|
+
if (!this.executor) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
235
334
|
return this.executor.getPendingCount();
|
|
236
335
|
}
|
|
237
336
|
getRunningCount() {
|
|
337
|
+
if (!this.executor) {
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
238
340
|
return this.executor.getRunningCount();
|
|
239
341
|
}
|
|
240
342
|
getOnlineDetector() {
|
|
@@ -249,11 +351,13 @@ class OfflineExecutor {
|
|
|
249
351
|
this.unsubscribeLeadership();
|
|
250
352
|
this.unsubscribeLeadership = null;
|
|
251
353
|
}
|
|
252
|
-
this.leaderElection
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
354
|
+
if (this.leaderElection) {
|
|
355
|
+
this.leaderElection.releaseLeadership();
|
|
356
|
+
if (`dispose` in this.leaderElection) {
|
|
357
|
+
this.leaderElection.dispose();
|
|
358
|
+
}
|
|
256
359
|
}
|
|
360
|
+
this.onlineDetector.dispose();
|
|
257
361
|
}
|
|
258
362
|
}
|
|
259
363
|
function startOfflineExecutor(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineExecutor.js","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from \"@tanstack/db\"\nimport { IndexedDBAdapter } from \"./storage/IndexedDBAdapter\"\nimport { LocalStorageAdapter } from \"./storage/LocalStorageAdapter\"\n\n// Core components\nimport { OutboxManager } from \"./outbox/OutboxManager\"\nimport { KeyScheduler } from \"./executor/KeyScheduler\"\nimport { TransactionExecutor } from \"./executor/TransactionExecutor\"\n\n// Coordination\nimport { WebLocksLeader } from \"./coordination/WebLocksLeader\"\nimport { BroadcastChannelLeader } from \"./coordination/BroadcastChannelLeader\"\n\n// Connectivity\nimport { DefaultOnlineDetector } from \"./connectivity/OnlineDetector\"\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from \"./api/OfflineTransaction\"\nimport { createOfflineAction } from \"./api/OfflineAction\"\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from \"./telemetry/tracer\"\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineTransaction,\n StorageAdapter,\n} from \"./types\"\nimport type { Transaction } from \"@tanstack/db\"\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n private storage: StorageAdapter\n private outbox: OutboxManager\n private scheduler: KeyScheduler\n private executor: TransactionExecutor\n private leaderElection: LeaderElection\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.storage = this.createStorage()\n this.outbox = new OutboxManager(this.storage, this.config.collections)\n this.scheduler = new KeyScheduler()\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this\n )\n this.leaderElection = this.createLeaderElection()\n this.onlineDetector = new DefaultOnlineDetector()\n\n this.setupEventListeners()\n this.initialize()\n }\n\n private createStorage(): StorageAdapter {\n if (this.config.storage) {\n return this.config.storage\n }\n\n try {\n return new IndexedDBAdapter()\n } catch (error) {\n console.warn(\n `IndexedDB not available, falling back to localStorage:`,\n error\n )\n return new LocalStorageAdapter()\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n }\n )\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n const isLeader = await this.leaderElection.requestLeadership()\n span.setAttribute(`isLeader`, isLeader)\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n }\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n this.leaderElection.releaseLeadership()\n this.onlineDetector.dispose()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["OfflineTransactionAPI","action"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.js","sources":["../../src/OfflineExecutor.ts"],"sourcesContent":["// Storage adapters\nimport { createOptimisticAction, createTransaction } from \"@tanstack/db\"\nimport { IndexedDBAdapter } from \"./storage/IndexedDBAdapter\"\nimport { LocalStorageAdapter } from \"./storage/LocalStorageAdapter\"\n\n// Core components\nimport { OutboxManager } from \"./outbox/OutboxManager\"\nimport { KeyScheduler } from \"./executor/KeyScheduler\"\nimport { TransactionExecutor } from \"./executor/TransactionExecutor\"\n\n// Coordination\nimport { WebLocksLeader } from \"./coordination/WebLocksLeader\"\nimport { BroadcastChannelLeader } from \"./coordination/BroadcastChannelLeader\"\n\n// Connectivity\nimport { DefaultOnlineDetector } from \"./connectivity/OnlineDetector\"\n\n// API\nimport { OfflineTransaction as OfflineTransactionAPI } from \"./api/OfflineTransaction\"\nimport { createOfflineAction } from \"./api/OfflineAction\"\n\n// TanStack DB primitives\n\n// Replay\nimport { withNestedSpan, withSpan } from \"./telemetry/tracer\"\nimport type {\n CreateOfflineActionOptions,\n CreateOfflineTransactionOptions,\n LeaderElection,\n OfflineConfig,\n OfflineMode,\n OfflineTransaction,\n StorageAdapter,\n StorageDiagnostic,\n} from \"./types\"\nimport type { Transaction } from \"@tanstack/db\"\n\nexport class OfflineExecutor {\n private config: OfflineConfig\n\n // @ts-expect-error - Set during async initialization in initialize()\n private storage: StorageAdapter | null\n private outbox: OutboxManager | null\n private scheduler: KeyScheduler\n private executor: TransactionExecutor | null\n private leaderElection: LeaderElection | null\n private onlineDetector: DefaultOnlineDetector\n private isLeaderState = false\n private unsubscribeOnline: (() => void) | null = null\n private unsubscribeLeadership: (() => void) | null = null\n\n // Public diagnostic properties\n public readonly mode: OfflineMode\n public readonly storageDiagnostic: StorageDiagnostic\n\n // Track initialization completion\n private initPromise: Promise<void>\n private initResolve!: () => void\n private initReject!: (error: Error) => void\n\n // Coordination mechanism for blocking transactions\n private pendingTransactionPromises: Map<\n string,\n {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n }\n > = new Map()\n\n constructor(config: OfflineConfig) {\n this.config = config\n this.scheduler = new KeyScheduler()\n this.onlineDetector = new DefaultOnlineDetector()\n\n // Initialize as pending - will be set by async initialization\n this.storage = null\n this.outbox = null\n this.executor = null\n this.leaderElection = null\n\n // Temporary diagnostic - will be updated by async initialization\n this.mode = `offline`\n this.storageDiagnostic = {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Initializing storage...`,\n }\n\n // Create initialization promise\n this.initPromise = new Promise((resolve, reject) => {\n this.initResolve = resolve\n this.initReject = reject\n })\n\n this.initialize()\n }\n\n /**\n * Probe storage availability and create appropriate adapter.\n * Returns null if no storage is available (online-only mode).\n */\n private async createStorage(): Promise<{\n storage: StorageAdapter | null\n diagnostic: StorageDiagnostic\n }> {\n // If user provided custom storage, use it without probing\n if (this.config.storage) {\n return {\n storage: this.config.storage,\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using custom storage adapter`,\n },\n }\n }\n\n // Probe IndexedDB first\n const idbProbe = await IndexedDBAdapter.probe()\n if (idbProbe.available) {\n return {\n storage: new IndexedDBAdapter(),\n diagnostic: {\n code: `STORAGE_AVAILABLE`,\n mode: `offline`,\n message: `Using IndexedDB for offline storage`,\n },\n }\n }\n\n // IndexedDB failed, try localStorage\n const lsProbe = LocalStorageAdapter.probe()\n if (lsProbe.available) {\n return {\n storage: new LocalStorageAdapter(),\n diagnostic: {\n code: `INDEXEDDB_UNAVAILABLE`,\n mode: `offline`,\n message: `IndexedDB unavailable, using localStorage fallback`,\n error: idbProbe.error,\n },\n }\n }\n\n // Both failed - determine the diagnostic code\n const isSecurityError =\n idbProbe.error?.name === `SecurityError` ||\n lsProbe.error?.name === `SecurityError`\n const isQuotaError =\n idbProbe.error?.name === `QuotaExceededError` ||\n lsProbe.error?.name === `QuotaExceededError`\n\n let code: StorageDiagnostic[`code`]\n let message: string\n\n if (isSecurityError) {\n code = `STORAGE_BLOCKED`\n message = `Storage blocked (private mode or security restrictions). Running in online-only mode.`\n } else if (isQuotaError) {\n code = `QUOTA_EXCEEDED`\n message = `Storage quota exceeded. Running in online-only mode.`\n } else {\n code = `UNKNOWN_ERROR`\n message = `Storage unavailable due to unknown error. Running in online-only mode.`\n }\n\n return {\n storage: null,\n diagnostic: {\n code,\n mode: `online-only`,\n message,\n error: idbProbe.error || lsProbe.error,\n },\n }\n }\n\n private createLeaderElection(): LeaderElection {\n if (this.config.leaderElection) {\n return this.config.leaderElection\n }\n\n if (WebLocksLeader.isSupported()) {\n return new WebLocksLeader()\n } else if (BroadcastChannelLeader.isSupported()) {\n return new BroadcastChannelLeader()\n } else {\n // Fallback: always be leader in environments without multi-tab support\n return {\n requestLeadership: () => Promise.resolve(true),\n releaseLeadership: () => {},\n isLeader: () => true,\n onLeadershipChange: () => () => {},\n }\n }\n }\n\n private setupEventListeners(): void {\n // Only set up leader election listeners if we have storage\n if (this.leaderElection) {\n this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(\n (isLeader) => {\n this.isLeaderState = isLeader\n\n if (this.config.onLeadershipChange) {\n this.config.onLeadershipChange(isLeader)\n }\n\n if (isLeader) {\n this.loadAndReplayTransactions()\n }\n }\n )\n }\n\n this.unsubscribeOnline = this.onlineDetector.subscribe(() => {\n if (this.isOfflineEnabled && this.executor) {\n // Reset retry delays so transactions can execute immediately when back online\n this.executor.resetRetryDelays()\n this.executor.executeAll().catch((error) => {\n console.warn(\n `Failed to execute transactions on connectivity change:`,\n error\n )\n })\n }\n })\n }\n\n private async initialize(): Promise<void> {\n return withSpan(`executor.initialize`, {}, async (span) => {\n try {\n // Probe storage and create adapter\n const { storage, diagnostic } = await this.createStorage()\n\n // Cast to writable to set readonly properties\n ;(this as any).storage = storage\n ;(this as any).storageDiagnostic = diagnostic\n ;(this as any).mode = diagnostic.mode\n\n span.setAttribute(`storage.mode`, diagnostic.mode)\n span.setAttribute(`storage.code`, diagnostic.code)\n\n if (!storage) {\n // Online-only mode - notify callback and skip offline setup\n if (this.config.onStorageFailure) {\n this.config.onStorageFailure(diagnostic)\n }\n span.setAttribute(`result`, `online-only`)\n this.initResolve()\n return\n }\n\n // Storage available - set up offline components\n this.outbox = new OutboxManager(storage, this.config.collections)\n this.executor = new TransactionExecutor(\n this.scheduler,\n this.outbox,\n this.config,\n this\n )\n this.leaderElection = this.createLeaderElection()\n\n // Request leadership first\n const isLeader = await this.leaderElection.requestLeadership()\n span.setAttribute(`isLeader`, isLeader)\n\n // Set up event listeners after leadership is established\n // This prevents the callback from being called multiple times\n this.setupEventListeners()\n\n if (isLeader) {\n await this.loadAndReplayTransactions()\n }\n span.setAttribute(`result`, `offline-enabled`)\n this.initResolve()\n } catch (error) {\n console.warn(`Failed to initialize offline executor:`, error)\n span.setAttribute(`result`, `failed`)\n this.initReject(\n error instanceof Error ? error : new Error(String(error))\n )\n }\n })\n }\n\n private async loadAndReplayTransactions(): Promise<void> {\n if (!this.executor) {\n return\n }\n\n try {\n await this.executor.loadPendingTransactions()\n await this.executor.executeAll()\n } catch (error) {\n console.warn(`Failed to load and replay transactions:`, error)\n }\n }\n\n get isOfflineEnabled(): boolean {\n return this.mode === `offline` && this.isLeaderState\n }\n\n createOfflineTransaction(\n options: CreateOfflineTransactionOptions\n ): Transaction | OfflineTransactionAPI {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Check leadership immediately and use the appropriate primitive\n if (!this.isOfflineEnabled) {\n // Non-leader: use createTransaction directly with the resolved mutation function\n // We need to wrap it to add the idempotency key\n return createTransaction({\n autoCommit: options.autoCommit ?? true,\n mutationFn: (params) =>\n mutationFn({\n ...params,\n idempotencyKey: options.idempotencyKey || crypto.randomUUID(),\n }),\n metadata: options.metadata,\n })\n }\n\n // Leader: use OfflineTransaction wrapper for offline persistence\n return new OfflineTransactionAPI(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n }\n\n createOfflineAction<T>(options: CreateOfflineActionOptions<T>) {\n const mutationFn = this.config.mutationFns[options.mutationFnName]\n\n if (!mutationFn) {\n throw new Error(`Unknown mutation function: ${options.mutationFnName}`)\n }\n\n // Return a wrapper that checks leadership status at call time\n return (variables: T) => {\n // Check leadership when action is called, not when it's created\n if (!this.isOfflineEnabled) {\n // Non-leader: use createOptimisticAction directly\n const action = createOptimisticAction({\n mutationFn: (vars, params) =>\n mutationFn({\n ...vars,\n ...params,\n idempotencyKey: crypto.randomUUID(),\n }),\n onMutate: options.onMutate,\n })\n return action(variables)\n }\n\n // Leader: use the offline action wrapper\n const action = createOfflineAction(\n options,\n mutationFn,\n this.persistTransaction.bind(this),\n this\n )\n return action(variables)\n }\n }\n\n private async persistTransaction(\n transaction: OfflineTransaction\n ): Promise<void> {\n // Wait for initialization to complete\n await this.initPromise\n\n return withNestedSpan(\n `executor.persistTransaction`,\n {\n \"transaction.id\": transaction.id,\n \"transaction.mutationFnName\": transaction.mutationFnName,\n },\n async (span) => {\n if (!this.isOfflineEnabled || !this.outbox || !this.executor) {\n span.setAttribute(`result`, `skipped_not_leader`)\n this.resolveTransaction(transaction.id, undefined)\n return\n }\n\n try {\n await this.outbox.add(transaction)\n await this.executor.execute(transaction)\n span.setAttribute(`result`, `persisted`)\n } catch (error) {\n console.error(\n `Failed to persist offline transaction ${transaction.id}:`,\n error\n )\n span.setAttribute(`result`, `failed`)\n throw error\n }\n }\n )\n }\n\n // Method for OfflineTransaction to wait for completion\n async waitForTransactionCompletion(transactionId: string): Promise<any> {\n const existing = this.pendingTransactionPromises.get(transactionId)\n if (existing) {\n return existing.promise\n }\n\n const deferred: {\n promise: Promise<any>\n resolve: (result: any) => void\n reject: (error: Error) => void\n } = {} as any\n\n deferred.promise = new Promise((resolve, reject) => {\n deferred.resolve = resolve\n deferred.reject = reject\n })\n\n this.pendingTransactionPromises.set(transactionId, deferred)\n return deferred.promise\n }\n\n // Method for TransactionExecutor to signal completion\n resolveTransaction(transactionId: string, result: any): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.resolve(result)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n // Method for TransactionExecutor to signal failure\n rejectTransaction(transactionId: string, error: Error): void {\n const deferred = this.pendingTransactionPromises.get(transactionId)\n if (deferred) {\n deferred.reject(error)\n this.pendingTransactionPromises.delete(transactionId)\n }\n }\n\n async removeFromOutbox(id: string): Promise<void> {\n if (!this.outbox) {\n return\n }\n await this.outbox.remove(id)\n }\n\n async peekOutbox(): Promise<Array<OfflineTransaction>> {\n if (!this.outbox) {\n return []\n }\n return this.outbox.getAll()\n }\n\n async clearOutbox(): Promise<void> {\n if (!this.outbox || !this.executor) {\n return\n }\n await this.outbox.clear()\n this.executor.clear()\n }\n\n notifyOnline(): void {\n this.onlineDetector.notifyOnline()\n }\n\n getPendingCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getPendingCount()\n }\n\n getRunningCount(): number {\n if (!this.executor) {\n return 0\n }\n return this.executor.getRunningCount()\n }\n\n getOnlineDetector(): DefaultOnlineDetector {\n return this.onlineDetector\n }\n\n dispose(): void {\n if (this.unsubscribeOnline) {\n this.unsubscribeOnline()\n this.unsubscribeOnline = null\n }\n\n if (this.unsubscribeLeadership) {\n this.unsubscribeLeadership()\n this.unsubscribeLeadership = null\n }\n\n if (this.leaderElection) {\n this.leaderElection.releaseLeadership()\n\n if (`dispose` in this.leaderElection) {\n ;(this.leaderElection as any).dispose()\n }\n }\n\n this.onlineDetector.dispose()\n }\n}\n\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["OfflineTransactionAPI","action"],"mappings":"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"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { OnMutateMustBeSynchronousError } from "@tanstack/db";
|
|
2
3
|
import { OfflineTransaction } from "./OfflineTransaction.js";
|
|
4
|
+
function isPromiseLike(value) {
|
|
5
|
+
return !!value && (typeof value === `object` || typeof value === `function`) && typeof value.then === `function`;
|
|
6
|
+
}
|
|
3
7
|
function createOfflineAction(options, mutationFn, persistTransaction, executor) {
|
|
4
8
|
const { mutationFnName, onMutate } = options;
|
|
5
9
|
console.log(`createOfflineAction 2`, options);
|
|
@@ -15,7 +19,10 @@ function createOfflineAction(options, mutationFn, persistTransaction, executor)
|
|
|
15
19
|
);
|
|
16
20
|
const transaction = offlineTransaction.mutate(() => {
|
|
17
21
|
console.log(`mutate`);
|
|
18
|
-
onMutate(variables);
|
|
22
|
+
const maybePromise = onMutate(variables);
|
|
23
|
+
if (isPromiseLike(maybePromise)) {
|
|
24
|
+
throw new OnMutateMustBeSynchronousError();
|
|
25
|
+
}
|
|
19
26
|
});
|
|
20
27
|
const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`);
|
|
21
28
|
const span = tracer.startSpan(`offlineAction.${mutationFnName}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OfflineAction.js","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport { OfflineTransaction } from \"./OfflineTransaction\"\nimport type { Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n onMutate(variables)\n })\n\n // Immediately commit with span instrumentation\n const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n const span = tracer.startSpan(`offlineAction.${mutationFnName}`)\n const ctx = trace.setSpan(context.active(), span)\n console.log(`starting offlineAction span`, { tracer, span, ctx })\n\n // Execute the commit within the span context\n // The key is to return the promise synchronously from context.with() so context binds to it\n const commitPromise = context.with(ctx, () => {\n // Return the promise synchronously - this is critical for context propagation in browsers\n return (async () => {\n try {\n await transaction.commit()\n span.setStatus({ code: SpanStatusCode.OK })\n span.end()\n console.log(`ended offlineAction span - success`)\n } catch (error) {\n span.recordException(error as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n span.end()\n console.log(`ended offlineAction span - error`)\n }\n })()\n })\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"OfflineAction.js","sources":["../../../src/api/OfflineAction.ts"],"sourcesContent":["import { SpanStatusCode, context, trace } from \"@opentelemetry/api\"\nimport { OnMutateMustBeSynchronousError } from \"@tanstack/db\"\nimport { OfflineTransaction } from \"./OfflineTransaction\"\nimport type { Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineActionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return (\n !!value &&\n (typeof value === `object` || typeof value === `function`) &&\n typeof (value as { then?: unknown }).then === `function`\n )\n}\n\nexport function createOfflineAction<T>(\n options: CreateOfflineActionOptions<T>,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n): (variables: T) => Transaction {\n const { mutationFnName, onMutate } = options\n console.log(`createOfflineAction 2`, options)\n\n return (variables: T): Transaction => {\n const offlineTransaction = new OfflineTransaction(\n {\n mutationFnName,\n autoCommit: false,\n },\n mutationFn,\n persistTransaction,\n executor\n )\n\n const transaction = offlineTransaction.mutate(() => {\n console.log(`mutate`)\n const maybePromise = onMutate(variables) as unknown\n\n if (isPromiseLike(maybePromise)) {\n throw new OnMutateMustBeSynchronousError()\n }\n })\n\n // Immediately commit with span instrumentation\n const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`)\n const span = tracer.startSpan(`offlineAction.${mutationFnName}`)\n const ctx = trace.setSpan(context.active(), span)\n console.log(`starting offlineAction span`, { tracer, span, ctx })\n\n // Execute the commit within the span context\n // The key is to return the promise synchronously from context.with() so context binds to it\n const commitPromise = context.with(ctx, () => {\n // Return the promise synchronously - this is critical for context propagation in browsers\n return (async () => {\n try {\n await transaction.commit()\n span.setStatus({ code: SpanStatusCode.OK })\n span.end()\n console.log(`ended offlineAction span - success`)\n } catch (error) {\n span.recordException(error as Error)\n span.setStatus({ code: SpanStatusCode.ERROR })\n span.end()\n console.log(`ended offlineAction span - error`)\n }\n })()\n })\n\n // Don't await - this is fire-and-forget for optimistic actions\n // But catch to prevent unhandled rejection\n commitPromise.catch(() => {\n // Already handled in try/catch above\n })\n\n return transaction\n }\n}\n"],"names":[],"mappings":";;;AAUA,SAAS,cAAc,OAA+C;AACpE,SACE,CAAC,CAAC,UACD,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,OAAQ,MAA6B,SAAS;AAElD;AAEO,SAAS,oBACd,SACA,YACA,oBACA,UAC+B;AAC/B,QAAM,EAAE,gBAAgB,SAAA,IAAa;AACrC,UAAQ,IAAI,yBAAyB,OAAO;AAE5C,SAAO,CAAC,cAA8B;AACpC,UAAM,qBAAqB,IAAI;AAAA,MAC7B;AAAA,QACE;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAEd;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,UAAM,cAAc,mBAAmB,OAAO,MAAM;AAClD,cAAQ,IAAI,QAAQ;AACpB,YAAM,eAAe,SAAS,SAAS;AAEvC,UAAI,cAAc,YAAY,GAAG;AAC/B,cAAM,IAAI,+BAAA;AAAA,MACZ;AAAA,IACF,CAAC;AAGD,UAAM,SAAS,MAAM,UAAU,kCAAkC,OAAO;AACxE,UAAM,OAAO,OAAO,UAAU,iBAAiB,cAAc,EAAE;AAC/D,UAAM,MAAM,MAAM,QAAQ,QAAQ,OAAA,GAAU,IAAI;AAChD,YAAQ,IAAI,+BAA+B,EAAE,QAAQ,MAAM,KAAK;AAIhE,UAAM,gBAAgB,QAAQ,KAAK,KAAK,MAAM;AAE5C,cAAQ,YAAY;AAClB,YAAI;AACF,gBAAM,YAAY,OAAA;AAClB,eAAK,UAAU,EAAE,MAAM,eAAe,IAAI;AAC1C,eAAK,IAAA;AACL,kBAAQ,IAAI,oCAAoC;AAAA,QAClD,SAAS,OAAO;AACd,eAAK,gBAAgB,KAAc;AACnC,eAAK,UAAU,EAAE,MAAM,eAAe,OAAO;AAC7C,eAAK,IAAA;AACL,kBAAQ,IAAI,kCAAkC;AAAA,QAChD;AAAA,MACF,GAAA;AAAA,IACF,CAAC;AAID,kBAAc,MAAM,MAAM;AAAA,IAE1B,CAAC;AAED,WAAO;AAAA,EACT;AACF;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { OfflineExecutor, startOfflineExecutor } from './OfflineExecutor.js';
|
|
2
|
-
export type { OfflineTransaction, OfflineConfig, StorageAdapter, RetryPolicy, LeaderElection, OnlineDetector, CreateOfflineTransactionOptions, CreateOfflineActionOptions, SerializedError, SerializedMutation, } from './types.js';
|
|
2
|
+
export type { OfflineTransaction, OfflineConfig, OfflineMode, StorageAdapter, StorageDiagnostic, StorageDiagnosticCode, RetryPolicy, LeaderElection, OnlineDetector, CreateOfflineTransactionOptions, CreateOfflineActionOptions, SerializedError, SerializedMutation, } from './types.js';
|
|
3
3
|
export { NonRetriableError } from './types.js';
|
|
4
4
|
export { IndexedDBAdapter } from './storage/IndexedDBAdapter.js';
|
|
5
5
|
export { LocalStorageAdapter } from './storage/LocalStorageAdapter.js';
|
|
@@ -4,6 +4,14 @@ export declare class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
4
4
|
private storeName;
|
|
5
5
|
private db;
|
|
6
6
|
constructor(dbName?: string, storeName?: string);
|
|
7
|
+
/**
|
|
8
|
+
* Probe IndexedDB availability by attempting to open a test database.
|
|
9
|
+
* This catches private mode and other restrictions that block IndexedDB.
|
|
10
|
+
*/
|
|
11
|
+
static probe(): Promise<{
|
|
12
|
+
available: boolean;
|
|
13
|
+
error?: Error;
|
|
14
|
+
}>;
|
|
7
15
|
private openDB;
|
|
8
16
|
private getStore;
|
|
9
17
|
get(key: string): Promise<string | null>;
|
|
@@ -6,6 +6,45 @@ class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
6
6
|
this.dbName = dbName;
|
|
7
7
|
this.storeName = storeName;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Probe IndexedDB availability by attempting to open a test database.
|
|
11
|
+
* This catches private mode and other restrictions that block IndexedDB.
|
|
12
|
+
*/
|
|
13
|
+
static async probe() {
|
|
14
|
+
if (typeof indexedDB === `undefined`) {
|
|
15
|
+
return {
|
|
16
|
+
available: false,
|
|
17
|
+
error: new Error(`IndexedDB is not available in this environment`)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const testDbName = `__offline-tx-probe__`;
|
|
22
|
+
const request = indexedDB.open(testDbName, 1);
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
request.onerror = () => {
|
|
25
|
+
const error = request.error || new Error(`IndexedDB open failed`);
|
|
26
|
+
resolve({ available: false, error });
|
|
27
|
+
};
|
|
28
|
+
request.onsuccess = () => {
|
|
29
|
+
const db = request.result;
|
|
30
|
+
db.close();
|
|
31
|
+
indexedDB.deleteDatabase(testDbName);
|
|
32
|
+
resolve({ available: true });
|
|
33
|
+
};
|
|
34
|
+
request.onblocked = () => {
|
|
35
|
+
resolve({
|
|
36
|
+
available: false,
|
|
37
|
+
error: new Error(`IndexedDB is blocked`)
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
available: false,
|
|
44
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
9
48
|
async openDB() {
|
|
10
49
|
if (this.db) {
|
|
11
50
|
return this.db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from \"./StorageAdapter\"\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.put(value, key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.delete(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.clear()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":";AAEO,MAAM,yBAAyB,mBAAmB;AAAA,EAKvD,YAAY,SAAS,wBAAwB,YAAY,gBAAgB;AACvE,UAAA;AAHF,SAAQ,KAAyB;AAI/B,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAc,SAA+B;AAC3C,QAAI,KAAK,IAAI;AACX,aAAO,KAAK;AAAA,IACd;AAEA,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,UAAU,KAAK,KAAK,QAAQ,CAAC;AAE7C,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,cAAQ,YAAY,MAAM;AACxB,aAAK,KAAK,QAAQ;AAClB,gBAAQ,KAAK,EAAE;AAAA,MACjB;AAEA,cAAQ,kBAAkB,CAAC,UAAU;AACnC,cAAM,KAAM,MAAM,OAA4B;AAC9C,YAAI,CAAC,GAAG,iBAAiB,SAAS,KAAK,SAAS,GAAG;AACjD,aAAG,kBAAkB,KAAK,SAAS;AAAA,QACrC;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,SACZ,OAA2B,YACF;AACzB,UAAM,KAAK,MAAM,KAAK,OAAA;AACtB,UAAM,cAAc,GAAG,YAAY,CAAC,KAAK,SAAS,GAAG,IAAI;AACzD,WAAO,YAAY,YAAY,KAAK,SAAS;AAAA,EAC/C;AAAA,EAEA,MAAM,IAAI,KAAqC;AAC7C,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,SAAS,UAAU;AAC5C,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,UAAU,MAAM,IAAI,GAAG;AAC7B,gBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,gBAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AAAA,MAC1D,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,yBAAyB,KAAK;AAC3C,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAAa,OAA8B;AACnD,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,SAAS,WAAW;AAC7C,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,UAAU,MAAM,IAAI,OAAO,GAAG;AACpC,gBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,gBAAQ,YAAY,MAAM,QAAA;AAAA,MAC5B,CAAC;AAAA,IACH,SAAS,OAAO;AACd,UACE,iBAAiB,gBACjB,MAAM,SAAS,sBACf;AACA,cAAM,IAAI;AAAA,UACR;AAAA,QAAA;AAAA,MAEJ;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,SAAS,WAAW;AAC7C,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,UAAU,MAAM,OAAO,GAAG;AAChC,gBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,gBAAQ,YAAY,MAAM,QAAA;AAAA,MAC5B,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,4BAA4B,KAAK;AAAA,IAChD;AAAA,EACF;AAAA,EAEA,MAAM,OAA+B;AACnC,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,SAAS,UAAU;AAC5C,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,UAAU,MAAM,WAAA;AACtB,gBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,gBAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAuB;AAAA,MACnE,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,0BAA0B,KAAK;AAC5C,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,SAAS,WAAW;AAC7C,aAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAM,UAAU,MAAM,MAAA;AACtB,gBAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAC5C,gBAAQ,YAAY,MAAM,QAAA;AAAA,MAC5B,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,2BAA2B,KAAK;AAAA,IAC/C;AAAA,EACF;AACF;"}
|
|
1
|
+
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from \"./StorageAdapter\"\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.put(value, key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.delete(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.clear()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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"}
|