@tanstack/offline-transactions 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +219 -0
  2. package/dist/cjs/OfflineExecutor.cjs +266 -0
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -0
  4. package/dist/cjs/OfflineExecutor.d.cts +39 -0
  5. package/dist/cjs/api/OfflineAction.cjs +47 -0
  6. package/dist/cjs/api/OfflineAction.cjs.map +1 -0
  7. package/dist/cjs/api/OfflineAction.d.cts +3 -0
  8. package/dist/cjs/api/OfflineTransaction.cjs +96 -0
  9. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -0
  10. package/dist/cjs/api/OfflineTransaction.d.cts +18 -0
  11. package/dist/cjs/connectivity/OnlineDetector.cjs +73 -0
  12. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -0
  13. package/dist/cjs/connectivity/OnlineDetector.d.cts +15 -0
  14. package/dist/cjs/coordination/BroadcastChannelLeader.cjs +146 -0
  15. package/dist/cjs/coordination/BroadcastChannelLeader.cjs.map +1 -0
  16. package/dist/cjs/coordination/BroadcastChannelLeader.d.cts +26 -0
  17. package/dist/cjs/coordination/LeaderElection.cjs +31 -0
  18. package/dist/cjs/coordination/LeaderElection.cjs.map +1 -0
  19. package/dist/cjs/coordination/LeaderElection.d.cts +10 -0
  20. package/dist/cjs/coordination/WebLocksLeader.cjs +71 -0
  21. package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -0
  22. package/dist/cjs/coordination/WebLocksLeader.d.cts +10 -0
  23. package/dist/cjs/executor/KeyScheduler.cjs +106 -0
  24. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -0
  25. package/dist/cjs/executor/KeyScheduler.d.cts +18 -0
  26. package/dist/cjs/executor/TransactionExecutor.cjs +236 -0
  27. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -0
  28. package/dist/cjs/executor/TransactionExecutor.d.cts +28 -0
  29. package/dist/cjs/index.cjs +34 -0
  30. package/dist/cjs/index.cjs.map +1 -0
  31. package/dist/cjs/index.d.cts +16 -0
  32. package/dist/cjs/outbox/OutboxManager.cjs +114 -0
  33. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -0
  34. package/dist/cjs/outbox/OutboxManager.d.cts +18 -0
  35. package/dist/cjs/outbox/TransactionSerializer.cjs +135 -0
  36. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -0
  37. package/dist/cjs/outbox/TransactionSerializer.d.cts +15 -0
  38. package/dist/cjs/retry/BackoffCalculator.cjs +14 -0
  39. package/dist/cjs/retry/BackoffCalculator.cjs.map +1 -0
  40. package/dist/cjs/retry/BackoffCalculator.d.cts +5 -0
  41. package/dist/cjs/retry/NonRetriableError.d.cts +1 -0
  42. package/dist/cjs/retry/RetryPolicy.cjs +33 -0
  43. package/dist/cjs/retry/RetryPolicy.cjs.map +1 -0
  44. package/dist/cjs/retry/RetryPolicy.d.cts +8 -0
  45. package/dist/cjs/storage/IndexedDBAdapter.cjs +104 -0
  46. package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -0
  47. package/dist/cjs/storage/IndexedDBAdapter.d.cts +14 -0
  48. package/dist/cjs/storage/LocalStorageAdapter.cjs +71 -0
  49. package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -0
  50. package/dist/cjs/storage/LocalStorageAdapter.d.cts +11 -0
  51. package/dist/cjs/storage/StorageAdapter.cjs +6 -0
  52. package/dist/cjs/storage/StorageAdapter.cjs.map +1 -0
  53. package/dist/cjs/storage/StorageAdapter.d.cts +9 -0
  54. package/dist/cjs/telemetry/tracer.cjs +91 -0
  55. package/dist/cjs/telemetry/tracer.cjs.map +1 -0
  56. package/dist/cjs/telemetry/tracer.d.cts +29 -0
  57. package/dist/cjs/types.cjs +10 -0
  58. package/dist/cjs/types.cjs.map +1 -0
  59. package/dist/cjs/types.d.cts +101 -0
  60. package/dist/esm/OfflineExecutor.d.ts +39 -0
  61. package/dist/esm/OfflineExecutor.js +266 -0
  62. package/dist/esm/OfflineExecutor.js.map +1 -0
  63. package/dist/esm/api/OfflineAction.d.ts +3 -0
  64. package/dist/esm/api/OfflineAction.js +47 -0
  65. package/dist/esm/api/OfflineAction.js.map +1 -0
  66. package/dist/esm/api/OfflineTransaction.d.ts +18 -0
  67. package/dist/esm/api/OfflineTransaction.js +96 -0
  68. package/dist/esm/api/OfflineTransaction.js.map +1 -0
  69. package/dist/esm/connectivity/OnlineDetector.d.ts +15 -0
  70. package/dist/esm/connectivity/OnlineDetector.js +73 -0
  71. package/dist/esm/connectivity/OnlineDetector.js.map +1 -0
  72. package/dist/esm/coordination/BroadcastChannelLeader.d.ts +26 -0
  73. package/dist/esm/coordination/BroadcastChannelLeader.js +146 -0
  74. package/dist/esm/coordination/BroadcastChannelLeader.js.map +1 -0
  75. package/dist/esm/coordination/LeaderElection.d.ts +10 -0
  76. package/dist/esm/coordination/LeaderElection.js +31 -0
  77. package/dist/esm/coordination/LeaderElection.js.map +1 -0
  78. package/dist/esm/coordination/WebLocksLeader.d.ts +10 -0
  79. package/dist/esm/coordination/WebLocksLeader.js +71 -0
  80. package/dist/esm/coordination/WebLocksLeader.js.map +1 -0
  81. package/dist/esm/executor/KeyScheduler.d.ts +18 -0
  82. package/dist/esm/executor/KeyScheduler.js +106 -0
  83. package/dist/esm/executor/KeyScheduler.js.map +1 -0
  84. package/dist/esm/executor/TransactionExecutor.d.ts +28 -0
  85. package/dist/esm/executor/TransactionExecutor.js +236 -0
  86. package/dist/esm/executor/TransactionExecutor.js.map +1 -0
  87. package/dist/esm/index.d.ts +16 -0
  88. package/dist/esm/index.js +34 -0
  89. package/dist/esm/index.js.map +1 -0
  90. package/dist/esm/outbox/OutboxManager.d.ts +18 -0
  91. package/dist/esm/outbox/OutboxManager.js +114 -0
  92. package/dist/esm/outbox/OutboxManager.js.map +1 -0
  93. package/dist/esm/outbox/TransactionSerializer.d.ts +15 -0
  94. package/dist/esm/outbox/TransactionSerializer.js +135 -0
  95. package/dist/esm/outbox/TransactionSerializer.js.map +1 -0
  96. package/dist/esm/retry/BackoffCalculator.d.ts +5 -0
  97. package/dist/esm/retry/BackoffCalculator.js +14 -0
  98. package/dist/esm/retry/BackoffCalculator.js.map +1 -0
  99. package/dist/esm/retry/NonRetriableError.d.ts +1 -0
  100. package/dist/esm/retry/RetryPolicy.d.ts +8 -0
  101. package/dist/esm/retry/RetryPolicy.js +33 -0
  102. package/dist/esm/retry/RetryPolicy.js.map +1 -0
  103. package/dist/esm/storage/IndexedDBAdapter.d.ts +14 -0
  104. package/dist/esm/storage/IndexedDBAdapter.js +104 -0
  105. package/dist/esm/storage/IndexedDBAdapter.js.map +1 -0
  106. package/dist/esm/storage/LocalStorageAdapter.d.ts +11 -0
  107. package/dist/esm/storage/LocalStorageAdapter.js +71 -0
  108. package/dist/esm/storage/LocalStorageAdapter.js.map +1 -0
  109. package/dist/esm/storage/StorageAdapter.d.ts +9 -0
  110. package/dist/esm/storage/StorageAdapter.js +6 -0
  111. package/dist/esm/storage/StorageAdapter.js.map +1 -0
  112. package/dist/esm/telemetry/tracer.d.ts +29 -0
  113. package/dist/esm/telemetry/tracer.js +91 -0
  114. package/dist/esm/telemetry/tracer.js.map +1 -0
  115. package/dist/esm/types.d.ts +101 -0
  116. package/dist/esm/types.js +10 -0
  117. package/dist/esm/types.js.map +1 -0
  118. package/package.json +66 -0
  119. package/src/OfflineExecutor.ts +360 -0
  120. package/src/api/OfflineAction.ts +68 -0
  121. package/src/api/OfflineTransaction.ts +134 -0
  122. package/src/connectivity/OnlineDetector.ts +87 -0
  123. package/src/coordination/BroadcastChannelLeader.ts +181 -0
  124. package/src/coordination/LeaderElection.ts +35 -0
  125. package/src/coordination/WebLocksLeader.ts +82 -0
  126. package/src/executor/KeyScheduler.ts +123 -0
  127. package/src/executor/TransactionExecutor.ts +330 -0
  128. package/src/index.ts +47 -0
  129. package/src/outbox/OutboxManager.ts +141 -0
  130. package/src/outbox/TransactionSerializer.ts +163 -0
  131. package/src/retry/BackoffCalculator.ts +13 -0
  132. package/src/retry/NonRetriableError.ts +1 -0
  133. package/src/retry/RetryPolicy.ts +41 -0
  134. package/src/storage/IndexedDBAdapter.ts +119 -0
  135. package/src/storage/LocalStorageAdapter.ts +79 -0
  136. package/src/storage/StorageAdapter.ts +11 -0
  137. package/src/telemetry/tracer.ts +156 -0
  138. package/src/types.ts +133 -0
@@ -0,0 +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 leaderElection?: LeaderElection\n otel?: {\n endpoint: string\n headers?: Record<string, string>\n }\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":";;AA+HO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;"}
@@ -0,0 +1,101 @@
1
+ import { Collection, MutationFnParams, PendingMutation } from '@tanstack/db';
2
+ export type OfflineMutationFnParams<T extends object = Record<string, unknown>> = MutationFnParams<T> & {
3
+ idempotencyKey: string;
4
+ };
5
+ export type OfflineMutationFn<T extends object = Record<string, unknown>> = (params: OfflineMutationFnParams<T>) => Promise<any>;
6
+ export interface SerializedMutation {
7
+ globalKey: string;
8
+ type: string;
9
+ modified: any;
10
+ original: any;
11
+ collectionId: string;
12
+ }
13
+ export interface SerializedError {
14
+ name: string;
15
+ message: string;
16
+ stack?: string;
17
+ }
18
+ export interface SerializedSpanContext {
19
+ traceId: string;
20
+ spanId: string;
21
+ traceFlags: number;
22
+ traceState?: string;
23
+ }
24
+ export interface OfflineTransaction {
25
+ id: string;
26
+ mutationFnName: string;
27
+ mutations: Array<PendingMutation>;
28
+ keys: Array<string>;
29
+ idempotencyKey: string;
30
+ createdAt: Date;
31
+ retryCount: number;
32
+ nextAttemptAt: number;
33
+ lastError?: SerializedError;
34
+ metadata?: Record<string, any>;
35
+ spanContext?: SerializedSpanContext;
36
+ version: 1;
37
+ }
38
+ export interface SerializedOfflineTransaction {
39
+ id: string;
40
+ mutationFnName: string;
41
+ mutations: Array<SerializedMutation>;
42
+ keys: Array<string>;
43
+ idempotencyKey: string;
44
+ createdAt: Date;
45
+ retryCount: number;
46
+ nextAttemptAt: number;
47
+ lastError?: SerializedError;
48
+ metadata?: Record<string, any>;
49
+ spanContext?: SerializedSpanContext;
50
+ version: 1;
51
+ }
52
+ export interface OfflineConfig {
53
+ collections: Record<string, Collection>;
54
+ mutationFns: Record<string, OfflineMutationFn>;
55
+ storage?: StorageAdapter;
56
+ maxConcurrency?: number;
57
+ jitter?: boolean;
58
+ beforeRetry?: (transactions: Array<OfflineTransaction>) => Array<OfflineTransaction>;
59
+ onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void;
60
+ onLeadershipChange?: (isLeader: boolean) => void;
61
+ leaderElection?: LeaderElection;
62
+ otel?: {
63
+ endpoint: string;
64
+ headers?: Record<string, string>;
65
+ };
66
+ }
67
+ export interface StorageAdapter {
68
+ get: (key: string) => Promise<string | null>;
69
+ set: (key: string, value: string) => Promise<void>;
70
+ delete: (key: string) => Promise<void>;
71
+ keys: () => Promise<Array<string>>;
72
+ clear: () => Promise<void>;
73
+ }
74
+ export interface RetryPolicy {
75
+ calculateDelay: (retryCount: number) => number;
76
+ shouldRetry: (error: Error, retryCount: number) => boolean;
77
+ }
78
+ export interface LeaderElection {
79
+ requestLeadership: () => Promise<boolean>;
80
+ releaseLeadership: () => void;
81
+ isLeader: () => boolean;
82
+ onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void;
83
+ }
84
+ export interface OnlineDetector {
85
+ subscribe: (callback: () => void) => () => void;
86
+ notifyOnline: () => void;
87
+ }
88
+ export interface CreateOfflineTransactionOptions {
89
+ id?: string;
90
+ mutationFnName: string;
91
+ autoCommit?: boolean;
92
+ idempotencyKey?: string;
93
+ metadata?: Record<string, any>;
94
+ }
95
+ export interface CreateOfflineActionOptions<T> {
96
+ mutationFnName: string;
97
+ onMutate: (variables: T) => void;
98
+ }
99
+ export declare class NonRetriableError extends Error {
100
+ constructor(message: string);
101
+ }
@@ -0,0 +1,39 @@
1
+ import { DefaultOnlineDetector } from './connectivity/OnlineDetector.js';
2
+ import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction.js';
3
+ import { CreateOfflineActionOptions, CreateOfflineTransactionOptions, OfflineConfig, OfflineTransaction } from './types.js';
4
+ import { Transaction } from '@tanstack/db';
5
+ export declare class OfflineExecutor {
6
+ private config;
7
+ private storage;
8
+ private outbox;
9
+ private scheduler;
10
+ private executor;
11
+ private leaderElection;
12
+ private onlineDetector;
13
+ private isLeaderState;
14
+ private unsubscribeOnline;
15
+ private unsubscribeLeadership;
16
+ private pendingTransactionPromises;
17
+ constructor(config: OfflineConfig);
18
+ private createStorage;
19
+ private createLeaderElection;
20
+ private setupEventListeners;
21
+ private initialize;
22
+ private loadAndReplayTransactions;
23
+ get isOfflineEnabled(): boolean;
24
+ createOfflineTransaction(options: CreateOfflineTransactionOptions): Transaction | OfflineTransactionAPI;
25
+ createOfflineAction<T>(options: CreateOfflineActionOptions<T>): (variables: T) => Transaction<Record<string, unknown>>;
26
+ private persistTransaction;
27
+ waitForTransactionCompletion(transactionId: string): Promise<any>;
28
+ resolveTransaction(transactionId: string, result: any): void;
29
+ rejectTransaction(transactionId: string, error: Error): void;
30
+ removeFromOutbox(id: string): Promise<void>;
31
+ peekOutbox(): Promise<Array<OfflineTransaction>>;
32
+ clearOutbox(): Promise<void>;
33
+ notifyOnline(): void;
34
+ getPendingCount(): number;
35
+ getRunningCount(): number;
36
+ getOnlineDetector(): DefaultOnlineDetector;
37
+ dispose(): void;
38
+ }
39
+ export declare function startOfflineExecutor(config: OfflineConfig): OfflineExecutor;
@@ -0,0 +1,266 @@
1
+ import { createTransaction, createOptimisticAction } from "@tanstack/db";
2
+ import { IndexedDBAdapter } from "./storage/IndexedDBAdapter.js";
3
+ import { LocalStorageAdapter } from "./storage/LocalStorageAdapter.js";
4
+ import { OutboxManager } from "./outbox/OutboxManager.js";
5
+ import { KeyScheduler } from "./executor/KeyScheduler.js";
6
+ import { TransactionExecutor } from "./executor/TransactionExecutor.js";
7
+ import { WebLocksLeader } from "./coordination/WebLocksLeader.js";
8
+ import { BroadcastChannelLeader } from "./coordination/BroadcastChannelLeader.js";
9
+ import { DefaultOnlineDetector } from "./connectivity/OnlineDetector.js";
10
+ import { OfflineTransaction } from "./api/OfflineTransaction.js";
11
+ import { createOfflineAction } from "./api/OfflineAction.js";
12
+ import { withSpan, withNestedSpan } from "./telemetry/tracer.js";
13
+ class OfflineExecutor {
14
+ constructor(config) {
15
+ this.isLeaderState = false;
16
+ this.unsubscribeOnline = null;
17
+ this.unsubscribeLeadership = null;
18
+ this.pendingTransactionPromises = /* @__PURE__ */ new Map();
19
+ this.config = config;
20
+ this.storage = this.createStorage();
21
+ this.outbox = new OutboxManager(this.storage, this.config.collections);
22
+ 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
+ this.onlineDetector = new DefaultOnlineDetector();
31
+ this.setupEventListeners();
32
+ this.initialize();
33
+ }
34
+ createStorage() {
35
+ if (this.config.storage) {
36
+ return this.config.storage;
37
+ }
38
+ try {
39
+ return new IndexedDBAdapter();
40
+ } catch (error) {
41
+ console.warn(
42
+ `IndexedDB not available, falling back to localStorage:`,
43
+ error
44
+ );
45
+ return new LocalStorageAdapter();
46
+ }
47
+ }
48
+ createLeaderElection() {
49
+ if (this.config.leaderElection) {
50
+ return this.config.leaderElection;
51
+ }
52
+ if (WebLocksLeader.isSupported()) {
53
+ return new WebLocksLeader();
54
+ } else if (BroadcastChannelLeader.isSupported()) {
55
+ return new BroadcastChannelLeader();
56
+ } else {
57
+ return {
58
+ requestLeadership: () => Promise.resolve(true),
59
+ releaseLeadership: () => {
60
+ },
61
+ isLeader: () => true,
62
+ onLeadershipChange: () => () => {
63
+ }
64
+ };
65
+ }
66
+ }
67
+ setupEventListeners() {
68
+ this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
69
+ (isLeader) => {
70
+ this.isLeaderState = isLeader;
71
+ if (this.config.onLeadershipChange) {
72
+ this.config.onLeadershipChange(isLeader);
73
+ }
74
+ if (isLeader) {
75
+ this.loadAndReplayTransactions();
76
+ }
77
+ }
78
+ );
79
+ this.unsubscribeOnline = this.onlineDetector.subscribe(() => {
80
+ if (this.isOfflineEnabled) {
81
+ this.executor.resetRetryDelays();
82
+ this.executor.executeAll().catch((error) => {
83
+ console.warn(
84
+ `Failed to execute transactions on connectivity change:`,
85
+ error
86
+ );
87
+ });
88
+ }
89
+ });
90
+ }
91
+ async initialize() {
92
+ return withSpan(`executor.initialize`, {}, async (span) => {
93
+ try {
94
+ const isLeader = await this.leaderElection.requestLeadership();
95
+ span.setAttribute(`isLeader`, isLeader);
96
+ if (isLeader) {
97
+ await this.loadAndReplayTransactions();
98
+ }
99
+ } catch (error) {
100
+ console.warn(`Failed to initialize offline executor:`, error);
101
+ }
102
+ });
103
+ }
104
+ async loadAndReplayTransactions() {
105
+ try {
106
+ await this.executor.loadPendingTransactions();
107
+ await this.executor.executeAll();
108
+ } catch (error) {
109
+ console.warn(`Failed to load and replay transactions:`, error);
110
+ }
111
+ }
112
+ get isOfflineEnabled() {
113
+ return this.isLeaderState;
114
+ }
115
+ createOfflineTransaction(options) {
116
+ const mutationFn = this.config.mutationFns[options.mutationFnName];
117
+ if (!mutationFn) {
118
+ throw new Error(`Unknown mutation function: ${options.mutationFnName}`);
119
+ }
120
+ if (!this.isOfflineEnabled) {
121
+ return createTransaction({
122
+ autoCommit: options.autoCommit ?? true,
123
+ mutationFn: (params) => mutationFn({
124
+ ...params,
125
+ idempotencyKey: options.idempotencyKey || crypto.randomUUID()
126
+ }),
127
+ metadata: options.metadata
128
+ });
129
+ }
130
+ return new OfflineTransaction(
131
+ options,
132
+ mutationFn,
133
+ this.persistTransaction.bind(this),
134
+ this
135
+ );
136
+ }
137
+ createOfflineAction(options) {
138
+ const mutationFn = this.config.mutationFns[options.mutationFnName];
139
+ if (!mutationFn) {
140
+ throw new Error(`Unknown mutation function: ${options.mutationFnName}`);
141
+ }
142
+ return (variables) => {
143
+ if (!this.isOfflineEnabled) {
144
+ const action2 = createOptimisticAction({
145
+ mutationFn: (vars, params) => mutationFn({
146
+ ...vars,
147
+ ...params,
148
+ idempotencyKey: crypto.randomUUID()
149
+ }),
150
+ onMutate: options.onMutate
151
+ });
152
+ return action2(variables);
153
+ }
154
+ const action = createOfflineAction(
155
+ options,
156
+ mutationFn,
157
+ this.persistTransaction.bind(this),
158
+ this
159
+ );
160
+ return action(variables);
161
+ };
162
+ }
163
+ async persistTransaction(transaction) {
164
+ return withNestedSpan(
165
+ `executor.persistTransaction`,
166
+ {
167
+ "transaction.id": transaction.id,
168
+ "transaction.mutationFnName": transaction.mutationFnName
169
+ },
170
+ async (span) => {
171
+ if (!this.isOfflineEnabled) {
172
+ span.setAttribute(`result`, `skipped_not_leader`);
173
+ this.resolveTransaction(transaction.id, void 0);
174
+ return;
175
+ }
176
+ try {
177
+ await this.outbox.add(transaction);
178
+ await this.executor.execute(transaction);
179
+ span.setAttribute(`result`, `persisted`);
180
+ } catch (error) {
181
+ console.error(
182
+ `Failed to persist offline transaction ${transaction.id}:`,
183
+ error
184
+ );
185
+ span.setAttribute(`result`, `failed`);
186
+ throw error;
187
+ }
188
+ }
189
+ );
190
+ }
191
+ // Method for OfflineTransaction to wait for completion
192
+ async waitForTransactionCompletion(transactionId) {
193
+ const existing = this.pendingTransactionPromises.get(transactionId);
194
+ if (existing) {
195
+ return existing.promise;
196
+ }
197
+ const deferred = {};
198
+ deferred.promise = new Promise((resolve, reject) => {
199
+ deferred.resolve = resolve;
200
+ deferred.reject = reject;
201
+ });
202
+ this.pendingTransactionPromises.set(transactionId, deferred);
203
+ return deferred.promise;
204
+ }
205
+ // Method for TransactionExecutor to signal completion
206
+ resolveTransaction(transactionId, result) {
207
+ const deferred = this.pendingTransactionPromises.get(transactionId);
208
+ if (deferred) {
209
+ deferred.resolve(result);
210
+ this.pendingTransactionPromises.delete(transactionId);
211
+ }
212
+ }
213
+ // Method for TransactionExecutor to signal failure
214
+ rejectTransaction(transactionId, error) {
215
+ const deferred = this.pendingTransactionPromises.get(transactionId);
216
+ if (deferred) {
217
+ deferred.reject(error);
218
+ this.pendingTransactionPromises.delete(transactionId);
219
+ }
220
+ }
221
+ async removeFromOutbox(id) {
222
+ await this.outbox.remove(id);
223
+ }
224
+ async peekOutbox() {
225
+ return this.outbox.getAll();
226
+ }
227
+ async clearOutbox() {
228
+ await this.outbox.clear();
229
+ this.executor.clear();
230
+ }
231
+ notifyOnline() {
232
+ this.onlineDetector.notifyOnline();
233
+ }
234
+ getPendingCount() {
235
+ return this.executor.getPendingCount();
236
+ }
237
+ getRunningCount() {
238
+ return this.executor.getRunningCount();
239
+ }
240
+ getOnlineDetector() {
241
+ return this.onlineDetector;
242
+ }
243
+ dispose() {
244
+ if (this.unsubscribeOnline) {
245
+ this.unsubscribeOnline();
246
+ this.unsubscribeOnline = null;
247
+ }
248
+ if (this.unsubscribeLeadership) {
249
+ this.unsubscribeLeadership();
250
+ this.unsubscribeLeadership = null;
251
+ }
252
+ this.leaderElection.releaseLeadership();
253
+ this.onlineDetector.dispose();
254
+ if (`dispose` in this.leaderElection) {
255
+ this.leaderElection.dispose();
256
+ }
257
+ }
258
+ }
259
+ function startOfflineExecutor(config) {
260
+ return new OfflineExecutor(config);
261
+ }
262
+ export {
263
+ OfflineExecutor,
264
+ startOfflineExecutor
265
+ };
266
+ //# sourceMappingURL=OfflineExecutor.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,3 @@
1
+ import { Transaction } from '@tanstack/db';
2
+ import { CreateOfflineActionOptions, OfflineMutationFn, OfflineTransaction as OfflineTransactionType } from '../types.js';
3
+ export declare function createOfflineAction<T>(options: CreateOfflineActionOptions<T>, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any): (variables: T) => Transaction;
@@ -0,0 +1,47 @@
1
+ import { trace, context, SpanStatusCode } from "@opentelemetry/api";
2
+ import { OfflineTransaction } from "./OfflineTransaction.js";
3
+ function createOfflineAction(options, mutationFn, persistTransaction, executor) {
4
+ const { mutationFnName, onMutate } = options;
5
+ console.log(`createOfflineAction 2`, options);
6
+ return (variables) => {
7
+ const offlineTransaction = new OfflineTransaction(
8
+ {
9
+ mutationFnName,
10
+ autoCommit: false
11
+ },
12
+ mutationFn,
13
+ persistTransaction,
14
+ executor
15
+ );
16
+ const transaction = offlineTransaction.mutate(() => {
17
+ console.log(`mutate`);
18
+ onMutate(variables);
19
+ });
20
+ const tracer = trace.getTracer(`@tanstack/offline-transactions`, `0.0.1`);
21
+ const span = tracer.startSpan(`offlineAction.${mutationFnName}`);
22
+ const ctx = trace.setSpan(context.active(), span);
23
+ console.log(`starting offlineAction span`, { tracer, span, ctx });
24
+ const commitPromise = context.with(ctx, () => {
25
+ return (async () => {
26
+ try {
27
+ await transaction.commit();
28
+ span.setStatus({ code: SpanStatusCode.OK });
29
+ span.end();
30
+ console.log(`ended offlineAction span - success`);
31
+ } catch (error) {
32
+ span.recordException(error);
33
+ span.setStatus({ code: SpanStatusCode.ERROR });
34
+ span.end();
35
+ console.log(`ended offlineAction span - error`);
36
+ }
37
+ })();
38
+ });
39
+ commitPromise.catch(() => {
40
+ });
41
+ return transaction;
42
+ };
43
+ }
44
+ export {
45
+ createOfflineAction
46
+ };
47
+ //# sourceMappingURL=OfflineAction.js.map
@@ -0,0 +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":";;AASO,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,eAAS,SAAS;AAAA,IACpB,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;"}
@@ -0,0 +1,18 @@
1
+ import { Transaction } from '@tanstack/db';
2
+ import { CreateOfflineTransactionOptions, OfflineMutationFn, OfflineTransaction as OfflineTransactionType } from '../types.js';
3
+ export declare class OfflineTransaction {
4
+ private offlineId;
5
+ private mutationFnName;
6
+ private autoCommit;
7
+ private idempotencyKey;
8
+ private metadata;
9
+ private transaction;
10
+ private persistTransaction;
11
+ private executor;
12
+ constructor(options: CreateOfflineTransactionOptions, mutationFn: OfflineMutationFn, persistTransaction: (tx: OfflineTransactionType) => Promise<void>, executor: any);
13
+ mutate(callback: () => void): Transaction;
14
+ commit(): Promise<Transaction>;
15
+ rollback(): void;
16
+ private extractKeys;
17
+ get id(): string;
18
+ }
@@ -0,0 +1,96 @@
1
+ import { trace, context } from "@opentelemetry/api";
2
+ import { createTransaction } from "@tanstack/db";
3
+ import { NonRetriableError } from "../types.js";
4
+ class OfflineTransaction {
5
+ // Will be typed properly - reference to OfflineExecutor
6
+ constructor(options, mutationFn, persistTransaction, executor) {
7
+ this.transaction = null;
8
+ this.offlineId = crypto.randomUUID();
9
+ this.mutationFnName = options.mutationFnName;
10
+ this.autoCommit = options.autoCommit ?? true;
11
+ this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID();
12
+ this.metadata = options.metadata ?? {};
13
+ this.persistTransaction = persistTransaction;
14
+ this.executor = executor;
15
+ }
16
+ mutate(callback) {
17
+ this.transaction = createTransaction({
18
+ id: this.offlineId,
19
+ autoCommit: false,
20
+ mutationFn: async () => {
21
+ const activeSpan = trace.getSpan(context.active());
22
+ const spanContext = activeSpan?.spanContext();
23
+ const offlineTransaction = {
24
+ id: this.offlineId,
25
+ mutationFnName: this.mutationFnName,
26
+ mutations: this.transaction.mutations,
27
+ keys: this.extractKeys(this.transaction.mutations),
28
+ idempotencyKey: this.idempotencyKey,
29
+ createdAt: /* @__PURE__ */ new Date(),
30
+ retryCount: 0,
31
+ nextAttemptAt: Date.now(),
32
+ metadata: this.metadata,
33
+ spanContext: spanContext ? {
34
+ traceId: spanContext.traceId,
35
+ spanId: spanContext.spanId,
36
+ traceFlags: spanContext.traceFlags,
37
+ traceState: spanContext.traceState?.serialize()
38
+ } : void 0,
39
+ version: 1
40
+ };
41
+ const completionPromise = this.executor.waitForTransactionCompletion(
42
+ this.offlineId
43
+ );
44
+ try {
45
+ await this.persistTransaction(offlineTransaction);
46
+ await completionPromise;
47
+ } catch (error) {
48
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
49
+ this.executor.rejectTransaction(this.offlineId, normalizedError);
50
+ throw error;
51
+ }
52
+ return;
53
+ },
54
+ metadata: this.metadata
55
+ });
56
+ this.transaction.mutate(() => {
57
+ callback();
58
+ });
59
+ if (this.autoCommit) {
60
+ this.commit().catch((error) => {
61
+ console.error(`Auto-commit failed:`, error);
62
+ throw error;
63
+ });
64
+ }
65
+ return this.transaction;
66
+ }
67
+ async commit() {
68
+ if (!this.transaction) {
69
+ throw new Error(`No mutations to commit. Call mutate() first.`);
70
+ }
71
+ try {
72
+ await this.transaction.commit();
73
+ return this.transaction;
74
+ } catch (error) {
75
+ if (error instanceof NonRetriableError) {
76
+ this.transaction.rollback();
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ rollback() {
82
+ if (this.transaction) {
83
+ this.transaction.rollback();
84
+ }
85
+ }
86
+ extractKeys(mutations) {
87
+ return mutations.map((mutation) => mutation.globalKey);
88
+ }
89
+ get id() {
90
+ return this.offlineId;
91
+ }
92
+ }
93
+ export {
94
+ OfflineTransaction
95
+ };
96
+ //# sourceMappingURL=OfflineTransaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OfflineTransaction.js","sources":["../../../src/api/OfflineTransaction.ts"],"sourcesContent":["import { context, trace } from \"@opentelemetry/api\"\nimport { createTransaction } from \"@tanstack/db\"\nimport { NonRetriableError } from \"../types\"\nimport type { PendingMutation, Transaction } from \"@tanstack/db\"\nimport type {\n CreateOfflineTransactionOptions,\n OfflineMutationFn,\n OfflineTransaction as OfflineTransactionType,\n} from \"../types\"\n\nexport class OfflineTransaction {\n private offlineId: string\n private mutationFnName: string\n private autoCommit: boolean\n private idempotencyKey: string\n private metadata: Record<string, any>\n private transaction: Transaction | null = null\n private persistTransaction: (tx: OfflineTransactionType) => Promise<void>\n private executor: any // Will be typed properly - reference to OfflineExecutor\n\n constructor(\n options: CreateOfflineTransactionOptions,\n mutationFn: OfflineMutationFn,\n persistTransaction: (tx: OfflineTransactionType) => Promise<void>,\n executor: any\n ) {\n this.offlineId = crypto.randomUUID()\n this.mutationFnName = options.mutationFnName\n this.autoCommit = options.autoCommit ?? true\n this.idempotencyKey = options.idempotencyKey ?? crypto.randomUUID()\n this.metadata = options.metadata ?? {}\n this.persistTransaction = persistTransaction\n this.executor = executor\n }\n\n mutate(callback: () => void): Transaction {\n this.transaction = createTransaction({\n id: this.offlineId,\n autoCommit: false,\n mutationFn: async () => {\n // This is the blocking mutationFn that waits for the executor\n // First persist the transaction to the outbox\n const activeSpan = trace.getSpan(context.active())\n const spanContext = activeSpan?.spanContext()\n\n const offlineTransaction: OfflineTransactionType = {\n id: this.offlineId,\n mutationFnName: this.mutationFnName,\n mutations: this.transaction!.mutations,\n keys: this.extractKeys(this.transaction!.mutations),\n idempotencyKey: this.idempotencyKey,\n createdAt: new Date(),\n retryCount: 0,\n nextAttemptAt: Date.now(),\n metadata: this.metadata,\n spanContext: spanContext\n ? {\n traceId: spanContext.traceId,\n spanId: spanContext.spanId,\n traceFlags: spanContext.traceFlags,\n traceState: spanContext.traceState?.serialize(),\n }\n : undefined,\n version: 1,\n }\n\n const completionPromise = this.executor.waitForTransactionCompletion(\n this.offlineId\n )\n\n try {\n await this.persistTransaction(offlineTransaction)\n // Now block and wait for the executor to complete the real mutation\n await completionPromise\n } catch (error) {\n const normalizedError =\n error instanceof Error ? error : new Error(String(error))\n this.executor.rejectTransaction(this.offlineId, normalizedError)\n throw error\n }\n\n return\n },\n metadata: this.metadata,\n })\n\n this.transaction.mutate(() => {\n callback()\n })\n\n if (this.autoCommit) {\n // Auto-commit for direct OfflineTransaction usage\n this.commit().catch((error) => {\n console.error(`Auto-commit failed:`, error)\n throw error\n })\n }\n\n return this.transaction\n }\n\n async commit(): Promise<Transaction> {\n if (!this.transaction) {\n throw new Error(`No mutations to commit. Call mutate() first.`)\n }\n\n try {\n // Commit the TanStack DB transaction\n // This will trigger the mutationFn which handles persistence and waiting\n await this.transaction.commit()\n return this.transaction\n } catch (error) {\n // Only rollback for NonRetriableError - other errors should allow retry\n if (error instanceof NonRetriableError) {\n this.transaction.rollback()\n }\n throw error\n }\n }\n\n rollback(): void {\n if (this.transaction) {\n this.transaction.rollback()\n }\n }\n\n private extractKeys(mutations: Array<PendingMutation>): Array<string> {\n return mutations.map((mutation) => mutation.globalKey)\n }\n\n get id(): string {\n return this.offlineId\n }\n}\n"],"names":[],"mappings":";;;AAUO,MAAM,mBAAmB;AAAA;AAAA,EAU9B,YACE,SACA,YACA,oBACA,UACA;AATF,SAAQ,cAAkC;AAUxC,SAAK,YAAY,OAAO,WAAA;AACxB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,iBAAiB,QAAQ,kBAAkB,OAAO,WAAA;AACvD,SAAK,WAAW,QAAQ,YAAY,CAAA;AACpC,SAAK,qBAAqB;AAC1B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,OAAO,UAAmC;AACxC,SAAK,cAAc,kBAAkB;AAAA,MACnC,IAAI,KAAK;AAAA,MACT,YAAY;AAAA,MACZ,YAAY,YAAY;AAGtB,cAAM,aAAa,MAAM,QAAQ,QAAQ,QAAQ;AACjD,cAAM,cAAc,YAAY,YAAA;AAEhC,cAAM,qBAA6C;AAAA,UACjD,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,WAAW,KAAK,YAAa;AAAA,UAC7B,MAAM,KAAK,YAAY,KAAK,YAAa,SAAS;AAAA,UAClD,gBAAgB,KAAK;AAAA,UACrB,+BAAe,KAAA;AAAA,UACf,YAAY;AAAA,UACZ,eAAe,KAAK,IAAA;AAAA,UACpB,UAAU,KAAK;AAAA,UACf,aAAa,cACT;AAAA,YACE,SAAS,YAAY;AAAA,YACrB,QAAQ,YAAY;AAAA,YACpB,YAAY,YAAY;AAAA,YACxB,YAAY,YAAY,YAAY,UAAA;AAAA,UAAU,IAEhD;AAAA,UACJ,SAAS;AAAA,QAAA;AAGX,cAAM,oBAAoB,KAAK,SAAS;AAAA,UACtC,KAAK;AAAA,QAAA;AAGP,YAAI;AACF,gBAAM,KAAK,mBAAmB,kBAAkB;AAEhD,gBAAM;AAAA,QACR,SAAS,OAAO;AACd,gBAAM,kBACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAC1D,eAAK,SAAS,kBAAkB,KAAK,WAAW,eAAe;AAC/D,gBAAM;AAAA,QACR;AAEA;AAAA,MACF;AAAA,MACA,UAAU,KAAK;AAAA,IAAA,CAChB;AAED,SAAK,YAAY,OAAO,MAAM;AAC5B,eAAA;AAAA,IACF,CAAC;AAED,QAAI,KAAK,YAAY;AAEnB,WAAK,OAAA,EAAS,MAAM,CAAC,UAAU;AAC7B,gBAAQ,MAAM,uBAAuB,KAAK;AAC1C,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,SAA+B;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,8CAA8C;AAAA,IAChE;AAEA,QAAI;AAGF,YAAM,KAAK,YAAY,OAAA;AACvB,aAAO,KAAK;AAAA,IACd,SAAS,OAAO;AAEd,UAAI,iBAAiB,mBAAmB;AACtC,aAAK,YAAY,SAAA;AAAA,MACnB;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY,SAAA;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,YAAY,WAAkD;AACpE,WAAO,UAAU,IAAI,CAAC,aAAa,SAAS,SAAS;AAAA,EACvD;AAAA,EAEA,IAAI,KAAa;AACf,WAAO,KAAK;AAAA,EACd;AACF;"}
@@ -0,0 +1,15 @@
1
+ import { OnlineDetector } from '../types.js';
2
+ export declare class DefaultOnlineDetector implements OnlineDetector {
3
+ private listeners;
4
+ private isListening;
5
+ constructor();
6
+ private startListening;
7
+ private stopListening;
8
+ private handleOnline;
9
+ private handleVisibilityChange;
10
+ private notifyListeners;
11
+ subscribe(callback: () => void): () => void;
12
+ notifyOnline(): void;
13
+ isOnline(): boolean;
14
+ dispose(): void;
15
+ }