@tanstack/offline-transactions 1.0.1 → 1.0.3
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/README.md +14 -14
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/api/OfflineAction.cjs.map +1 -1
- package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
- package/dist/cjs/coordination/BroadcastChannelLeader.cjs.map +1 -1
- package/dist/cjs/coordination/LeaderElection.cjs.map +1 -1
- package/dist/cjs/coordination/WebLocksLeader.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/retry/RetryPolicy.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/LocalStorageAdapter.cjs.map +1 -1
- package/dist/cjs/storage/StorageAdapter.cjs.map +1 -1
- package/dist/cjs/telemetry/tracer.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineAction.js.map +1 -1
- package/dist/esm/api/OfflineTransaction.js.map +1 -1
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
- package/dist/esm/coordination/BroadcastChannelLeader.js.map +1 -1
- package/dist/esm/coordination/LeaderElection.js.map +1 -1
- package/dist/esm/coordination/WebLocksLeader.js.map +1 -1
- package/dist/esm/executor/KeyScheduler.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/OutboxManager.js.map +1 -1
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/retry/RetryPolicy.js.map +1 -1
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/storage/LocalStorageAdapter.js.map +1 -1
- package/dist/esm/storage/StorageAdapter.js.map +1 -1
- package/dist/esm/telemetry/tracer.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +10 -13
- package/src/OfflineExecutor.ts +26 -26
- package/src/api/OfflineAction.ts +6 -6
- package/src/api/OfflineTransaction.ts +6 -6
- package/src/connectivity/OnlineDetector.ts +2 -2
- package/src/coordination/BroadcastChannelLeader.ts +1 -1
- package/src/coordination/LeaderElection.ts +1 -1
- package/src/coordination/WebLocksLeader.ts +3 -3
- package/src/executor/KeyScheduler.ts +12 -12
- package/src/executor/TransactionExecutor.ts +22 -22
- package/src/index.ts +16 -16
- package/src/outbox/OutboxManager.ts +17 -17
- package/src/outbox/TransactionSerializer.ts +7 -7
- package/src/retry/NonRetriableError.ts +1 -1
- package/src/retry/RetryPolicy.ts +3 -3
- package/src/storage/IndexedDBAdapter.ts +3 -3
- package/src/storage/LocalStorageAdapter.ts +3 -3
- package/src/storage/StorageAdapter.ts +1 -1
- package/src/telemetry/tracer.ts +3 -3
- package/src/types.ts +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorageAdapter.js","sources":["../../../src/storage/StorageAdapter.ts"],"sourcesContent":["import type { StorageAdapter } from
|
|
1
|
+
{"version":3,"file":"StorageAdapter.js","sources":["../../../src/storage/StorageAdapter.ts"],"sourcesContent":["import type { StorageAdapter } from '../types'\n\nexport abstract class BaseStorageAdapter implements StorageAdapter {\n abstract get(key: string): Promise<string | null>\n abstract set(key: string, value: string): Promise<void>\n abstract delete(key: string): Promise<void>\n abstract keys(): Promise<Array<string>>\n abstract clear(): Promise<void>\n}\n\nexport { type StorageAdapter }\n"],"names":[],"mappings":"AAEO,MAAe,mBAA6C;AAMnE;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracer.js","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["export interface SpanAttrs {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface WithSpanOptions {\n parentContext?: any\n}\n\n// No-op span implementation\nconst noopSpan = {\n setAttribute: () => {},\n setAttributes: () => {},\n setStatus: () => {},\n recordException: () => {},\n end: () => {},\n}\n\n/**\n * Lightweight span wrapper with error handling.\n * No-op implementation - telemetry has been removed.\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: any) => Promise<T>,\n _options?: WithSpanOptions
|
|
1
|
+
{"version":3,"file":"tracer.js","sources":["../../../src/telemetry/tracer.ts"],"sourcesContent":["export interface SpanAttrs {\n [key: string]: string | number | boolean | undefined\n}\n\ninterface WithSpanOptions {\n parentContext?: any\n}\n\n// No-op span implementation\nconst noopSpan = {\n setAttribute: () => {},\n setAttributes: () => {},\n setStatus: () => {},\n recordException: () => {},\n end: () => {},\n}\n\n/**\n * Lightweight span wrapper with error handling.\n * No-op implementation - telemetry has been removed.\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: any) => Promise<T>,\n _options?: WithSpanOptions,\n): Promise<T> {\n return await fn(noopSpan)\n}\n\n/**\n * Like withSpan but propagates context so child spans nest properly.\n * No-op implementation - telemetry has been removed.\n */\nexport async function withNestedSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: any) => Promise<T>,\n _options?: WithSpanOptions,\n): Promise<T> {\n return await fn(noopSpan)\n}\n\n/**\n * Creates a synchronous span for non-async operations\n * No-op implementation - telemetry has been removed.\n */\nexport function withSyncSpan<T>(\n name: string,\n attrs: SpanAttrs,\n fn: (span: any) => T,\n _options?: WithSpanOptions,\n): T {\n return fn(noopSpan)\n}\n\n/**\n * Get the current tracer instance\n * No-op implementation - telemetry has been removed.\n */\nexport function getTracer() {\n return null\n}\n"],"names":[],"mappings":"AASA,MAAM,WAAW;AAAA,EACf,cAAc,MAAM;AAAA,EAAC;AAAA,EACrB,eAAe,MAAM;AAAA,EAAC;AAAA,EACtB,WAAW,MAAM;AAAA,EAAC;AAAA,EAClB,iBAAiB,MAAM;AAAA,EAAC;AAAA,EACxB,KAAK,MAAM;AAAA,EAAC;AACd;AASA,eAAsB,SACpB,MACA,OACA,IACA,UACY;AACZ,SAAO,MAAM,GAAG,QAAQ;AAC1B;AAMA,eAAsB,eACpB,MACA,OACA,IACA,UACY;AACZ,SAAO,MAAM,GAAG,QAAQ;AAC1B;AAMO,SAAS,aACd,MACA,OACA,IACA,UACG;AACH,SAAO,GAAG,QAAQ;AACpB;"}
|
package/dist/esm/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n Collection,\n MutationFnParams,\n PendingMutation,\n} from
|
|
1
|
+
{"version":3,"file":"types.js","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 // eslint-disable-next-line @typescript-eslint/no-explicit-any\n collections: Record<string, Collection<any, any, any, any, any>>\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":"AA+IO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/offline-transactions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Offline-first transaction capabilities for TanStack DB",
|
|
5
5
|
"author": "TanStack",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/TanStack/db.git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/db.git",
|
|
10
10
|
"directory": "packages/offline-transactions"
|
|
11
11
|
},
|
|
12
|
-
"publishConfig": {
|
|
13
|
-
"access": "public"
|
|
14
|
-
},
|
|
15
12
|
"keywords": [
|
|
16
13
|
"tanstack",
|
|
17
14
|
"database",
|
|
@@ -21,7 +18,9 @@
|
|
|
21
18
|
"sync"
|
|
22
19
|
],
|
|
23
20
|
"type": "module",
|
|
24
|
-
"
|
|
21
|
+
"main": "dist/cjs/index.cjs",
|
|
22
|
+
"module": "dist/esm/index.js",
|
|
23
|
+
"types": "dist/esm/index.d.ts",
|
|
25
24
|
"exports": {
|
|
26
25
|
".": {
|
|
27
26
|
"import": {
|
|
@@ -35,20 +34,18 @@
|
|
|
35
34
|
},
|
|
36
35
|
"./package.json": "./package.json"
|
|
37
36
|
},
|
|
38
|
-
"
|
|
39
|
-
"module": "dist/esm/index.js",
|
|
40
|
-
"types": "dist/esm/index.d.ts",
|
|
37
|
+
"sideEffects": false,
|
|
41
38
|
"files": [
|
|
42
39
|
"dist",
|
|
43
40
|
"src"
|
|
44
41
|
],
|
|
45
42
|
"dependencies": {
|
|
46
|
-
"@tanstack/db": "0.5.
|
|
43
|
+
"@tanstack/db": "0.5.13"
|
|
47
44
|
},
|
|
48
45
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^
|
|
50
|
-
"eslint": "^
|
|
51
|
-
"typescript": "^5.
|
|
46
|
+
"@types/node": "^24.6.2",
|
|
47
|
+
"eslint": "^9.39.1",
|
|
48
|
+
"typescript": "^5.9.2",
|
|
52
49
|
"vitest": "^3.2.4"
|
|
53
50
|
},
|
|
54
51
|
"scripts": {
|
package/src/OfflineExecutor.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// Storage adapters
|
|
2
|
-
import { createOptimisticAction, createTransaction } from
|
|
3
|
-
import { IndexedDBAdapter } from
|
|
4
|
-
import { LocalStorageAdapter } from
|
|
2
|
+
import { createOptimisticAction, createTransaction } from '@tanstack/db'
|
|
3
|
+
import { IndexedDBAdapter } from './storage/IndexedDBAdapter'
|
|
4
|
+
import { LocalStorageAdapter } from './storage/LocalStorageAdapter'
|
|
5
5
|
|
|
6
6
|
// Core components
|
|
7
|
-
import { OutboxManager } from
|
|
8
|
-
import { KeyScheduler } from
|
|
9
|
-
import { TransactionExecutor } from
|
|
7
|
+
import { OutboxManager } from './outbox/OutboxManager'
|
|
8
|
+
import { KeyScheduler } from './executor/KeyScheduler'
|
|
9
|
+
import { TransactionExecutor } from './executor/TransactionExecutor'
|
|
10
10
|
|
|
11
11
|
// Coordination
|
|
12
|
-
import { WebLocksLeader } from
|
|
13
|
-
import { BroadcastChannelLeader } from
|
|
12
|
+
import { WebLocksLeader } from './coordination/WebLocksLeader'
|
|
13
|
+
import { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'
|
|
14
14
|
|
|
15
15
|
// Connectivity
|
|
16
|
-
import { DefaultOnlineDetector } from
|
|
16
|
+
import { DefaultOnlineDetector } from './connectivity/OnlineDetector'
|
|
17
17
|
|
|
18
18
|
// API
|
|
19
|
-
import { OfflineTransaction as OfflineTransactionAPI } from
|
|
20
|
-
import { createOfflineAction } from
|
|
19
|
+
import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'
|
|
20
|
+
import { createOfflineAction } from './api/OfflineAction'
|
|
21
21
|
|
|
22
22
|
// TanStack DB primitives
|
|
23
23
|
|
|
24
24
|
// Replay
|
|
25
|
-
import { withNestedSpan, withSpan } from
|
|
25
|
+
import { withNestedSpan, withSpan } from './telemetry/tracer'
|
|
26
26
|
import type {
|
|
27
27
|
CreateOfflineActionOptions,
|
|
28
28
|
CreateOfflineTransactionOptions,
|
|
@@ -32,8 +32,8 @@ import type {
|
|
|
32
32
|
OfflineTransaction,
|
|
33
33
|
StorageAdapter,
|
|
34
34
|
StorageDiagnostic,
|
|
35
|
-
} from
|
|
36
|
-
import type { Transaction } from
|
|
35
|
+
} from './types'
|
|
36
|
+
import type { Transaction } from '@tanstack/db'
|
|
37
37
|
|
|
38
38
|
export class OfflineExecutor {
|
|
39
39
|
private config: OfflineConfig
|
|
@@ -210,7 +210,7 @@ export class OfflineExecutor {
|
|
|
210
210
|
if (isLeader) {
|
|
211
211
|
this.loadAndReplayTransactions()
|
|
212
212
|
}
|
|
213
|
-
}
|
|
213
|
+
},
|
|
214
214
|
)
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -221,7 +221,7 @@ export class OfflineExecutor {
|
|
|
221
221
|
this.executor.executeAll().catch((error) => {
|
|
222
222
|
console.warn(
|
|
223
223
|
`Failed to execute transactions on connectivity change:`,
|
|
224
|
-
error
|
|
224
|
+
error,
|
|
225
225
|
)
|
|
226
226
|
})
|
|
227
227
|
}
|
|
@@ -258,7 +258,7 @@ export class OfflineExecutor {
|
|
|
258
258
|
this.scheduler,
|
|
259
259
|
this.outbox,
|
|
260
260
|
this.config,
|
|
261
|
-
this
|
|
261
|
+
this,
|
|
262
262
|
)
|
|
263
263
|
this.leaderElection = this.createLeaderElection()
|
|
264
264
|
|
|
@@ -279,7 +279,7 @@ export class OfflineExecutor {
|
|
|
279
279
|
console.warn(`Failed to initialize offline executor:`, error)
|
|
280
280
|
span.setAttribute(`result`, `failed`)
|
|
281
281
|
this.initReject(
|
|
282
|
-
error instanceof Error ? error : new Error(String(error))
|
|
282
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
283
283
|
)
|
|
284
284
|
}
|
|
285
285
|
})
|
|
@@ -303,7 +303,7 @@ export class OfflineExecutor {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
createOfflineTransaction(
|
|
306
|
-
options: CreateOfflineTransactionOptions
|
|
306
|
+
options: CreateOfflineTransactionOptions,
|
|
307
307
|
): Transaction | OfflineTransactionAPI {
|
|
308
308
|
const mutationFn = this.config.mutationFns[options.mutationFnName]
|
|
309
309
|
|
|
@@ -331,7 +331,7 @@ export class OfflineExecutor {
|
|
|
331
331
|
options,
|
|
332
332
|
mutationFn,
|
|
333
333
|
this.persistTransaction.bind(this),
|
|
334
|
-
this
|
|
334
|
+
this,
|
|
335
335
|
)
|
|
336
336
|
}
|
|
337
337
|
|
|
@@ -364,14 +364,14 @@ export class OfflineExecutor {
|
|
|
364
364
|
options,
|
|
365
365
|
mutationFn,
|
|
366
366
|
this.persistTransaction.bind(this),
|
|
367
|
-
this
|
|
367
|
+
this,
|
|
368
368
|
)
|
|
369
369
|
return action(variables)
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
|
|
373
373
|
private async persistTransaction(
|
|
374
|
-
transaction: OfflineTransaction
|
|
374
|
+
transaction: OfflineTransaction,
|
|
375
375
|
): Promise<void> {
|
|
376
376
|
// Wait for initialization to complete
|
|
377
377
|
await this.initPromise
|
|
@@ -379,8 +379,8 @@ export class OfflineExecutor {
|
|
|
379
379
|
return withNestedSpan(
|
|
380
380
|
`executor.persistTransaction`,
|
|
381
381
|
{
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
'transaction.id': transaction.id,
|
|
383
|
+
'transaction.mutationFnName': transaction.mutationFnName,
|
|
384
384
|
},
|
|
385
385
|
async (span) => {
|
|
386
386
|
if (!this.isOfflineEnabled || !this.outbox || !this.executor) {
|
|
@@ -396,12 +396,12 @@ export class OfflineExecutor {
|
|
|
396
396
|
} catch (error) {
|
|
397
397
|
console.error(
|
|
398
398
|
`Failed to persist offline transaction ${transaction.id}:`,
|
|
399
|
-
error
|
|
399
|
+
error,
|
|
400
400
|
)
|
|
401
401
|
span.setAttribute(`result`, `failed`)
|
|
402
402
|
throw error
|
|
403
403
|
}
|
|
404
|
-
}
|
|
404
|
+
},
|
|
405
405
|
)
|
|
406
406
|
}
|
|
407
407
|
|
package/src/api/OfflineAction.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { OnMutateMustBeSynchronousError } from
|
|
2
|
-
import { OfflineTransaction } from
|
|
3
|
-
import type { Transaction } from
|
|
1
|
+
import { OnMutateMustBeSynchronousError } from '@tanstack/db'
|
|
2
|
+
import { OfflineTransaction } from './OfflineTransaction'
|
|
3
|
+
import type { Transaction } from '@tanstack/db'
|
|
4
4
|
import type {
|
|
5
5
|
CreateOfflineActionOptions,
|
|
6
6
|
OfflineMutationFn,
|
|
7
7
|
OfflineTransaction as OfflineTransactionType,
|
|
8
|
-
} from
|
|
8
|
+
} from '../types'
|
|
9
9
|
|
|
10
10
|
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
11
11
|
return (
|
|
@@ -19,7 +19,7 @@ export function createOfflineAction<T>(
|
|
|
19
19
|
options: CreateOfflineActionOptions<T>,
|
|
20
20
|
mutationFn: OfflineMutationFn,
|
|
21
21
|
persistTransaction: (tx: OfflineTransactionType) => Promise<void>,
|
|
22
|
-
executor: any
|
|
22
|
+
executor: any,
|
|
23
23
|
): (variables: T) => Transaction {
|
|
24
24
|
const { mutationFnName, onMutate } = options
|
|
25
25
|
console.log(`createOfflineAction 2`, options)
|
|
@@ -32,7 +32,7 @@ export function createOfflineAction<T>(
|
|
|
32
32
|
},
|
|
33
33
|
mutationFn,
|
|
34
34
|
persistTransaction,
|
|
35
|
-
executor
|
|
35
|
+
executor,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
const transaction = offlineTransaction.mutate(() => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createTransaction } from
|
|
2
|
-
import { NonRetriableError } from
|
|
3
|
-
import type { PendingMutation, Transaction } from
|
|
1
|
+
import { createTransaction } from '@tanstack/db'
|
|
2
|
+
import { NonRetriableError } from '../types'
|
|
3
|
+
import type { PendingMutation, Transaction } from '@tanstack/db'
|
|
4
4
|
import type {
|
|
5
5
|
CreateOfflineTransactionOptions,
|
|
6
6
|
OfflineMutationFn,
|
|
7
7
|
OfflineTransaction as OfflineTransactionType,
|
|
8
|
-
} from
|
|
8
|
+
} from '../types'
|
|
9
9
|
|
|
10
10
|
export class OfflineTransaction {
|
|
11
11
|
private offlineId: string
|
|
@@ -21,7 +21,7 @@ export class OfflineTransaction {
|
|
|
21
21
|
options: CreateOfflineTransactionOptions,
|
|
22
22
|
mutationFn: OfflineMutationFn,
|
|
23
23
|
persistTransaction: (tx: OfflineTransactionType) => Promise<void>,
|
|
24
|
-
executor: any
|
|
24
|
+
executor: any,
|
|
25
25
|
) {
|
|
26
26
|
this.offlineId = crypto.randomUUID()
|
|
27
27
|
this.mutationFnName = options.mutationFnName
|
|
@@ -54,7 +54,7 @@ export class OfflineTransaction {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const completionPromise = this.executor.waitForTransactionCompletion(
|
|
57
|
-
this.offlineId
|
|
57
|
+
this.offlineId,
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OnlineDetector } from
|
|
1
|
+
import type { OnlineDetector } from '../types'
|
|
2
2
|
|
|
3
3
|
export class DefaultOnlineDetector implements OnlineDetector {
|
|
4
4
|
private listeners: Set<() => void> = new Set()
|
|
@@ -32,7 +32,7 @@ export class DefaultOnlineDetector implements OnlineDetector {
|
|
|
32
32
|
window.removeEventListener(`online`, this.handleOnline)
|
|
33
33
|
document.removeEventListener(
|
|
34
34
|
`visibilitychange`,
|
|
35
|
-
this.handleVisibilityChange
|
|
35
|
+
this.handleVisibilityChange,
|
|
36
36
|
)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseLeaderElection } from
|
|
1
|
+
import { BaseLeaderElection } from './LeaderElection'
|
|
2
2
|
|
|
3
3
|
export class WebLocksLeader extends BaseLeaderElection {
|
|
4
4
|
private lockName: string
|
|
@@ -28,7 +28,7 @@ export class WebLocksLeader extends BaseLeaderElection {
|
|
|
28
28
|
},
|
|
29
29
|
(lock) => {
|
|
30
30
|
return lock !== null
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
if (!available) {
|
|
@@ -52,7 +52,7 @@ export class WebLocksLeader extends BaseLeaderElection {
|
|
|
52
52
|
}
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
|
-
}
|
|
55
|
+
},
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
return true
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { withSyncSpan } from
|
|
2
|
-
import type { OfflineTransaction } from
|
|
1
|
+
import { withSyncSpan } from '../telemetry/tracer'
|
|
2
|
+
import type { OfflineTransaction } from '../types'
|
|
3
3
|
|
|
4
4
|
export class KeyScheduler {
|
|
5
5
|
private pendingTransactions: Array<OfflineTransaction> = []
|
|
@@ -9,16 +9,16 @@ export class KeyScheduler {
|
|
|
9
9
|
withSyncSpan(
|
|
10
10
|
`scheduler.schedule`,
|
|
11
11
|
{
|
|
12
|
-
|
|
12
|
+
'transaction.id': transaction.id,
|
|
13
13
|
queueLength: this.pendingTransactions.length,
|
|
14
14
|
},
|
|
15
15
|
() => {
|
|
16
16
|
this.pendingTransactions.push(transaction)
|
|
17
17
|
// Sort by creation time to maintain FIFO order
|
|
18
18
|
this.pendingTransactions.sort(
|
|
19
|
-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
19
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
|
20
20
|
)
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -35,7 +35,7 @@ export class KeyScheduler {
|
|
|
35
35
|
|
|
36
36
|
// Find the first transaction that's ready to run
|
|
37
37
|
const readyTransaction = this.pendingTransactions.find((tx) =>
|
|
38
|
-
this.isReadyToRun(tx)
|
|
38
|
+
this.isReadyToRun(tx),
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
if (readyTransaction) {
|
|
@@ -46,7 +46,7 @@ export class KeyScheduler {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
return readyTransaction ? [readyTransaction] : []
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
50
|
)
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -69,7 +69,7 @@ export class KeyScheduler {
|
|
|
69
69
|
|
|
70
70
|
private removeTransaction(transaction: OfflineTransaction): void {
|
|
71
71
|
const index = this.pendingTransactions.findIndex(
|
|
72
|
-
(tx) => tx.id === transaction.id
|
|
72
|
+
(tx) => tx.id === transaction.id,
|
|
73
73
|
)
|
|
74
74
|
if (index >= 0) {
|
|
75
75
|
this.pendingTransactions.splice(index, 1)
|
|
@@ -78,13 +78,13 @@ export class KeyScheduler {
|
|
|
78
78
|
|
|
79
79
|
updateTransaction(transaction: OfflineTransaction): void {
|
|
80
80
|
const index = this.pendingTransactions.findIndex(
|
|
81
|
-
(tx) => tx.id === transaction.id
|
|
81
|
+
(tx) => tx.id === transaction.id,
|
|
82
82
|
)
|
|
83
83
|
if (index >= 0) {
|
|
84
84
|
this.pendingTransactions[index] = transaction
|
|
85
85
|
// Re-sort to maintain FIFO order after update
|
|
86
86
|
this.pendingTransactions.sort(
|
|
87
|
-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
87
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -109,7 +109,7 @@ export class KeyScheduler {
|
|
|
109
109
|
updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {
|
|
110
110
|
for (const updatedTx of updatedTransactions) {
|
|
111
111
|
const index = this.pendingTransactions.findIndex(
|
|
112
|
-
(tx) => tx.id === updatedTx.id
|
|
112
|
+
(tx) => tx.id === updatedTx.id,
|
|
113
113
|
)
|
|
114
114
|
if (index >= 0) {
|
|
115
115
|
this.pendingTransactions[index] = updatedTx
|
|
@@ -117,7 +117,7 @@ export class KeyScheduler {
|
|
|
117
117
|
}
|
|
118
118
|
// Re-sort to maintain FIFO order after updates
|
|
119
119
|
this.pendingTransactions.sort(
|
|
120
|
-
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
120
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DefaultRetryPolicy } from
|
|
2
|
-
import { NonRetriableError } from
|
|
3
|
-
import { withNestedSpan } from
|
|
4
|
-
import type { KeyScheduler } from
|
|
5
|
-
import type { OutboxManager } from
|
|
6
|
-
import type { OfflineConfig, OfflineTransaction } from
|
|
1
|
+
import { DefaultRetryPolicy } from '../retry/RetryPolicy'
|
|
2
|
+
import { NonRetriableError } from '../types'
|
|
3
|
+
import { withNestedSpan } from '../telemetry/tracer'
|
|
4
|
+
import type { KeyScheduler } from './KeyScheduler'
|
|
5
|
+
import type { OutboxManager } from '../outbox/OutboxManager'
|
|
6
|
+
import type { OfflineConfig, OfflineTransaction } from '../types'
|
|
7
7
|
|
|
8
8
|
const HANDLED_EXECUTION_ERROR = Symbol(`HandledExecutionError`)
|
|
9
9
|
|
|
@@ -21,7 +21,7 @@ export class TransactionExecutor {
|
|
|
21
21
|
scheduler: KeyScheduler,
|
|
22
22
|
outbox: OutboxManager,
|
|
23
23
|
config: OfflineConfig,
|
|
24
|
-
offlineExecutor: any
|
|
24
|
+
offlineExecutor: any,
|
|
25
25
|
) {
|
|
26
26
|
this.scheduler = scheduler
|
|
27
27
|
this.outbox = outbox
|
|
@@ -62,7 +62,7 @@ export class TransactionExecutor {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const executions = batch.map((transaction) =>
|
|
65
|
-
this.executeTransaction(transaction)
|
|
65
|
+
this.executeTransaction(transaction),
|
|
66
66
|
)
|
|
67
67
|
await Promise.allSettled(executions)
|
|
68
68
|
}
|
|
@@ -72,16 +72,16 @@ export class TransactionExecutor {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
private async executeTransaction(
|
|
75
|
-
transaction: OfflineTransaction
|
|
75
|
+
transaction: OfflineTransaction,
|
|
76
76
|
): Promise<void> {
|
|
77
77
|
try {
|
|
78
78
|
await withNestedSpan(
|
|
79
79
|
`transaction.execute`,
|
|
80
80
|
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
'transaction.id': transaction.id,
|
|
82
|
+
'transaction.mutationFnName': transaction.mutationFnName,
|
|
83
|
+
'transaction.retryCount': transaction.retryCount,
|
|
84
|
+
'transaction.keyCount': transaction.keys.length,
|
|
85
85
|
},
|
|
86
86
|
async (span) => {
|
|
87
87
|
this.scheduler.markStarted(transaction)
|
|
@@ -108,7 +108,7 @@ export class TransactionExecutor {
|
|
|
108
108
|
;(err as any)[HANDLED_EXECUTION_ERROR] = true
|
|
109
109
|
throw err
|
|
110
110
|
}
|
|
111
|
-
}
|
|
111
|
+
},
|
|
112
112
|
)
|
|
113
113
|
} catch (error) {
|
|
114
114
|
if (
|
|
@@ -151,19 +151,19 @@ export class TransactionExecutor {
|
|
|
151
151
|
|
|
152
152
|
private async handleError(
|
|
153
153
|
transaction: OfflineTransaction,
|
|
154
|
-
error: Error
|
|
154
|
+
error: Error,
|
|
155
155
|
): Promise<void> {
|
|
156
156
|
return withNestedSpan(
|
|
157
157
|
`transaction.handleError`,
|
|
158
158
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
'transaction.id': transaction.id,
|
|
160
|
+
'error.name': error.name,
|
|
161
|
+
'error.message': error.message,
|
|
162
162
|
},
|
|
163
163
|
async (span) => {
|
|
164
164
|
const shouldRetry = this.retryPolicy.shouldRetry(
|
|
165
165
|
error,
|
|
166
|
-
transaction.retryCount
|
|
166
|
+
transaction.retryCount,
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
span.setAttribute(`shouldRetry`, shouldRetry)
|
|
@@ -173,7 +173,7 @@ export class TransactionExecutor {
|
|
|
173
173
|
await this.outbox.remove(transaction.id)
|
|
174
174
|
console.warn(
|
|
175
175
|
`Transaction ${transaction.id} failed permanently:`,
|
|
176
|
-
error
|
|
176
|
+
error,
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
span.setAttribute(`result`, `permanent_failure`)
|
|
@@ -211,7 +211,7 @@ export class TransactionExecutor {
|
|
|
211
211
|
|
|
212
212
|
// Schedule retry timer
|
|
213
213
|
this.scheduleNextRetry()
|
|
214
|
-
}
|
|
214
|
+
},
|
|
215
215
|
)
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -234,7 +234,7 @@ export class TransactionExecutor {
|
|
|
234
234
|
this.scheduleNextRetry()
|
|
235
235
|
|
|
236
236
|
const removedTransactions = transactions.filter(
|
|
237
|
-
(tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id)
|
|
237
|
+
(tx) => !filteredTransactions.some((filtered) => filtered.id === tx.id),
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
if (removedTransactions.length > 0) {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Main API
|
|
2
|
-
export { OfflineExecutor, startOfflineExecutor } from
|
|
2
|
+
export { OfflineExecutor, startOfflineExecutor } from './OfflineExecutor'
|
|
3
3
|
|
|
4
4
|
// Types
|
|
5
5
|
export type {
|
|
@@ -16,35 +16,35 @@ export type {
|
|
|
16
16
|
CreateOfflineActionOptions,
|
|
17
17
|
SerializedError,
|
|
18
18
|
SerializedMutation,
|
|
19
|
-
} from
|
|
19
|
+
} from './types'
|
|
20
20
|
|
|
21
|
-
export { NonRetriableError } from
|
|
21
|
+
export { NonRetriableError } from './types'
|
|
22
22
|
|
|
23
23
|
// Storage adapters
|
|
24
|
-
export { IndexedDBAdapter } from
|
|
25
|
-
export { LocalStorageAdapter } from
|
|
24
|
+
export { IndexedDBAdapter } from './storage/IndexedDBAdapter'
|
|
25
|
+
export { LocalStorageAdapter } from './storage/LocalStorageAdapter'
|
|
26
26
|
|
|
27
27
|
// Retry policies
|
|
28
|
-
export { DefaultRetryPolicy } from
|
|
29
|
-
export { BackoffCalculator } from
|
|
28
|
+
export { DefaultRetryPolicy } from './retry/RetryPolicy'
|
|
29
|
+
export { BackoffCalculator } from './retry/BackoffCalculator'
|
|
30
30
|
|
|
31
31
|
// Coordination
|
|
32
|
-
export { WebLocksLeader } from
|
|
33
|
-
export { BroadcastChannelLeader } from
|
|
32
|
+
export { WebLocksLeader } from './coordination/WebLocksLeader'
|
|
33
|
+
export { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'
|
|
34
34
|
|
|
35
35
|
// Connectivity
|
|
36
|
-
export { DefaultOnlineDetector } from
|
|
36
|
+
export { DefaultOnlineDetector } from './connectivity/OnlineDetector'
|
|
37
37
|
|
|
38
38
|
// API components
|
|
39
|
-
export { OfflineTransaction as OfflineTransactionAPI } from
|
|
40
|
-
export { createOfflineAction } from
|
|
39
|
+
export { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'
|
|
40
|
+
export { createOfflineAction } from './api/OfflineAction'
|
|
41
41
|
|
|
42
42
|
// Outbox management
|
|
43
|
-
export { OutboxManager } from
|
|
44
|
-
export { TransactionSerializer } from
|
|
43
|
+
export { OutboxManager } from './outbox/OutboxManager'
|
|
44
|
+
export { TransactionSerializer } from './outbox/TransactionSerializer'
|
|
45
45
|
|
|
46
46
|
// Execution engine
|
|
47
|
-
export { KeyScheduler } from
|
|
48
|
-
export { TransactionExecutor } from
|
|
47
|
+
export { KeyScheduler } from './executor/KeyScheduler'
|
|
48
|
+
export { TransactionExecutor } from './executor/TransactionExecutor'
|
|
49
49
|
|
|
50
50
|
// Replay
|