@tanstack/offline-transactions 1.0.9 → 1.0.11
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 +41 -1
- package/dist/cjs/OfflineExecutor.cjs +5 -5
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +2 -3
- package/dist/cjs/connectivity/OnlineDetector.cjs +3 -1
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
- package/dist/cjs/connectivity/OnlineDetector.d.cts +11 -1
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +77 -0
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -0
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.d.cts +22 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/outbox/TransactionSerializer.cjs +24 -22
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/react-native/OfflineExecutor.cjs +18 -0
- package/dist/cjs/react-native/OfflineExecutor.cjs.map +1 -0
- package/dist/cjs/react-native/OfflineExecutor.d.cts +14 -0
- package/dist/cjs/react-native/index.cjs +37 -0
- package/dist/cjs/react-native/index.cjs.map +1 -0
- package/dist/cjs/react-native/index.d.cts +3 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +9 -1
- package/dist/esm/OfflineExecutor.d.ts +2 -3
- package/dist/esm/OfflineExecutor.js +6 -6
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/connectivity/OnlineDetector.d.ts +11 -1
- package/dist/esm/connectivity/OnlineDetector.js +4 -2
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
- package/dist/esm/connectivity/ReactNativeOnlineDetector.d.ts +22 -0
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js +77 -0
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/outbox/TransactionSerializer.js +24 -22
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/react-native/OfflineExecutor.d.ts +14 -0
- package/dist/esm/react-native/OfflineExecutor.js +18 -0
- package/dist/esm/react-native/OfflineExecutor.js.map +1 -0
- package/dist/esm/react-native/index.d.ts +3 -0
- package/dist/esm/react-native/index.js +37 -0
- package/dist/esm/react-native/index.js.map +1 -0
- package/dist/esm/types.d.ts +9 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +26 -2
- package/src/OfflineExecutor.ts +5 -4
- package/src/connectivity/OnlineDetector.ts +12 -1
- package/src/connectivity/ReactNativeOnlineDetector.ts +105 -0
- package/src/index.ts +4 -1
- package/src/outbox/TransactionSerializer.ts +27 -27
- package/src/react-native/OfflineExecutor.ts +24 -0
- package/src/react-native/index.ts +45 -0
- package/src/types.ts +9 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OfflineExecutor as BaseOfflineExecutor } from '../OfflineExecutor.js';
|
|
2
|
+
import { OfflineConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* OfflineExecutor configured for React Native environments.
|
|
5
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
6
|
+
*/
|
|
7
|
+
export declare class OfflineExecutor extends BaseOfflineExecutor {
|
|
8
|
+
constructor(config: OfflineConfig);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Start an offline executor configured for React Native environments.
|
|
12
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
13
|
+
*/
|
|
14
|
+
export declare function startOfflineExecutor(config: OfflineConfig): OfflineExecutor;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OfflineExecutor as OfflineExecutor$1 } from "../OfflineExecutor.js";
|
|
2
|
+
import { ReactNativeOnlineDetector } from "../connectivity/ReactNativeOnlineDetector.js";
|
|
3
|
+
class OfflineExecutor extends OfflineExecutor$1 {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super({
|
|
6
|
+
...config,
|
|
7
|
+
onlineDetector: config.onlineDetector ?? new ReactNativeOnlineDetector()
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function startOfflineExecutor(config) {
|
|
12
|
+
return new OfflineExecutor(config);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
OfflineExecutor,
|
|
16
|
+
startOfflineExecutor
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=OfflineExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OfflineExecutor.js","sources":["../../../src/react-native/OfflineExecutor.ts"],"sourcesContent":["import { OfflineExecutor as BaseOfflineExecutor } from '../OfflineExecutor'\nimport { ReactNativeOnlineDetector } from '../connectivity/ReactNativeOnlineDetector'\nimport type { OfflineConfig } from '../types'\n\n/**\n * OfflineExecutor configured for React Native environments.\n * Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.\n */\nexport class OfflineExecutor extends BaseOfflineExecutor {\n constructor(config: OfflineConfig) {\n super({\n ...config,\n onlineDetector: config.onlineDetector ?? new ReactNativeOnlineDetector(),\n })\n }\n}\n\n/**\n * Start an offline executor configured for React Native environments.\n * Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.\n */\nexport function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {\n return new OfflineExecutor(config)\n}\n"],"names":["BaseOfflineExecutor"],"mappings":";;AAQO,MAAM,wBAAwBA,kBAAoB;AAAA,EACvD,YAAY,QAAuB;AACjC,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,gBAAgB,OAAO,kBAAkB,IAAI,0BAAA;AAAA,IAA0B,CACxE;AAAA,EACH;AACF;AAMO,SAAS,qBAAqB,QAAwC;AAC3E,SAAO,IAAI,gBAAgB,MAAM;AACnC;"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type OfflineTransaction, type OfflineConfig, type OfflineMode, type StorageAdapter, type StorageDiagnostic, type StorageDiagnosticCode, type RetryPolicy, type LeaderElection, type OnlineDetector, type CreateOfflineTransactionOptions, type CreateOfflineActionOptions, type SerializedError, type SerializedMutation, NonRetriableError, IndexedDBAdapter, LocalStorageAdapter, DefaultRetryPolicy, BackoffCalculator, WebLocksLeader, BroadcastChannelLeader, WebOnlineDetector, DefaultOnlineDetector, OfflineTransactionAPI, createOfflineAction, OutboxManager, TransactionSerializer, KeyScheduler, TransactionExecutor, } from '../index.js';
|
|
2
|
+
export { ReactNativeOnlineDetector } from '../connectivity/ReactNativeOnlineDetector.js';
|
|
3
|
+
export { OfflineExecutor, startOfflineExecutor } from './OfflineExecutor.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NonRetriableError } from "../types.js";
|
|
2
|
+
import { IndexedDBAdapter } from "../storage/IndexedDBAdapter.js";
|
|
3
|
+
import { LocalStorageAdapter } from "../storage/LocalStorageAdapter.js";
|
|
4
|
+
import { DefaultRetryPolicy } from "../retry/RetryPolicy.js";
|
|
5
|
+
import { BackoffCalculator } from "../retry/BackoffCalculator.js";
|
|
6
|
+
import { WebLocksLeader } from "../coordination/WebLocksLeader.js";
|
|
7
|
+
import { BroadcastChannelLeader } from "../coordination/BroadcastChannelLeader.js";
|
|
8
|
+
import { DefaultOnlineDetector, WebOnlineDetector } from "../connectivity/OnlineDetector.js";
|
|
9
|
+
import { OfflineTransaction } from "../api/OfflineTransaction.js";
|
|
10
|
+
import { createOfflineAction } from "../api/OfflineAction.js";
|
|
11
|
+
import { OutboxManager } from "../outbox/OutboxManager.js";
|
|
12
|
+
import { TransactionSerializer } from "../outbox/TransactionSerializer.js";
|
|
13
|
+
import { KeyScheduler } from "../executor/KeyScheduler.js";
|
|
14
|
+
import { TransactionExecutor } from "../executor/TransactionExecutor.js";
|
|
15
|
+
import { ReactNativeOnlineDetector } from "../connectivity/ReactNativeOnlineDetector.js";
|
|
16
|
+
import { OfflineExecutor, startOfflineExecutor } from "./OfflineExecutor.js";
|
|
17
|
+
export {
|
|
18
|
+
BackoffCalculator,
|
|
19
|
+
BroadcastChannelLeader,
|
|
20
|
+
DefaultOnlineDetector,
|
|
21
|
+
DefaultRetryPolicy,
|
|
22
|
+
IndexedDBAdapter,
|
|
23
|
+
KeyScheduler,
|
|
24
|
+
LocalStorageAdapter,
|
|
25
|
+
NonRetriableError,
|
|
26
|
+
OfflineExecutor,
|
|
27
|
+
OfflineTransaction as OfflineTransactionAPI,
|
|
28
|
+
OutboxManager,
|
|
29
|
+
ReactNativeOnlineDetector,
|
|
30
|
+
TransactionExecutor,
|
|
31
|
+
TransactionSerializer,
|
|
32
|
+
WebLocksLeader,
|
|
33
|
+
WebOnlineDetector,
|
|
34
|
+
createOfflineAction,
|
|
35
|
+
startOfflineExecutor
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface SerializedMutation {
|
|
|
8
8
|
type: string;
|
|
9
9
|
modified: any;
|
|
10
10
|
original: any;
|
|
11
|
+
changes: any;
|
|
11
12
|
collectionId: string;
|
|
12
13
|
}
|
|
13
14
|
export interface SerializedError {
|
|
@@ -41,7 +42,7 @@ export interface SerializedOfflineTransaction {
|
|
|
41
42
|
mutations: Array<SerializedMutation>;
|
|
42
43
|
keys: Array<string>;
|
|
43
44
|
idempotencyKey: string;
|
|
44
|
-
createdAt:
|
|
45
|
+
createdAt: string;
|
|
45
46
|
retryCount: number;
|
|
46
47
|
nextAttemptAt: number;
|
|
47
48
|
lastError?: SerializedError;
|
|
@@ -68,6 +69,12 @@ export interface OfflineConfig {
|
|
|
68
69
|
onLeadershipChange?: (isLeader: boolean) => void;
|
|
69
70
|
onStorageFailure?: (diagnostic: StorageDiagnostic) => void;
|
|
70
71
|
leaderElection?: LeaderElection;
|
|
72
|
+
/**
|
|
73
|
+
* Custom online detector implementation.
|
|
74
|
+
* Defaults to WebOnlineDetector for browser environments.
|
|
75
|
+
* Use ReactNativeOnlineDetector from '@tanstack/offline-transactions/react-native' for RN/Expo.
|
|
76
|
+
*/
|
|
77
|
+
onlineDetector?: OnlineDetector;
|
|
71
78
|
}
|
|
72
79
|
export interface StorageAdapter {
|
|
73
80
|
get: (key: string) => Promise<string | null>;
|
|
@@ -89,6 +96,7 @@ export interface LeaderElection {
|
|
|
89
96
|
export interface OnlineDetector {
|
|
90
97
|
subscribe: (callback: () => void) => () => void;
|
|
91
98
|
notifyOnline: () => void;
|
|
99
|
+
dispose: () => void;
|
|
92
100
|
}
|
|
93
101
|
export interface CreateOfflineTransactionOptions {
|
|
94
102
|
id?: string;
|
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 '@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:
|
|
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 changes: 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: string\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<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 * Custom online detector implementation.\n * Defaults to WebOnlineDetector for browser environments.\n * Use ReactNativeOnlineDetector from '@tanstack/offline-transactions/react-native' for RN/Expo.\n */\n onlineDetector?: OnlineDetector\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 dispose: () => 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":"AAsJO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/offline-transactions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Offline-first transaction capabilities for TanStack DB",
|
|
5
5
|
"author": "TanStack",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
"default": "./dist/cjs/index.cjs"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
+
"./react-native": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/esm/react-native/index.d.ts",
|
|
38
|
+
"default": "./dist/esm/react-native/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/cjs/react-native/index.d.cts",
|
|
42
|
+
"default": "./dist/cjs/react-native/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
35
45
|
"./package.json": "./package.json"
|
|
36
46
|
},
|
|
37
47
|
"sideEffects": false,
|
|
@@ -40,11 +50,25 @@
|
|
|
40
50
|
"src"
|
|
41
51
|
],
|
|
42
52
|
"dependencies": {
|
|
43
|
-
"@tanstack/db": "0.5.
|
|
53
|
+
"@tanstack/db": "0.5.21"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@react-native-community/netinfo": ">=11.0.0",
|
|
57
|
+
"react-native": ">=0.70.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@react-native-community/netinfo": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-native": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
44
66
|
},
|
|
45
67
|
"devDependencies": {
|
|
68
|
+
"@react-native-community/netinfo": "11.4.1",
|
|
46
69
|
"@types/node": "^24.6.2",
|
|
47
70
|
"eslint": "^9.39.2",
|
|
71
|
+
"react-native": "0.79.6",
|
|
48
72
|
"typescript": "^5.9.2",
|
|
49
73
|
"vitest": "^3.2.4"
|
|
50
74
|
},
|
package/src/OfflineExecutor.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { WebLocksLeader } from './coordination/WebLocksLeader'
|
|
|
13
13
|
import { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'
|
|
14
14
|
|
|
15
15
|
// Connectivity
|
|
16
|
-
import {
|
|
16
|
+
import { WebOnlineDetector } from './connectivity/OnlineDetector'
|
|
17
17
|
|
|
18
18
|
// API
|
|
19
19
|
import { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
OfflineConfig,
|
|
31
31
|
OfflineMode,
|
|
32
32
|
OfflineTransaction,
|
|
33
|
+
OnlineDetector,
|
|
33
34
|
StorageAdapter,
|
|
34
35
|
StorageDiagnostic,
|
|
35
36
|
} from './types'
|
|
@@ -44,7 +45,7 @@ export class OfflineExecutor {
|
|
|
44
45
|
private scheduler: KeyScheduler
|
|
45
46
|
private executor: TransactionExecutor | null
|
|
46
47
|
private leaderElection: LeaderElection | null
|
|
47
|
-
private onlineDetector:
|
|
48
|
+
private onlineDetector: OnlineDetector
|
|
48
49
|
private isLeaderState = false
|
|
49
50
|
private unsubscribeOnline: (() => void) | null = null
|
|
50
51
|
private unsubscribeLeadership: (() => void) | null = null
|
|
@@ -71,7 +72,7 @@ export class OfflineExecutor {
|
|
|
71
72
|
constructor(config: OfflineConfig) {
|
|
72
73
|
this.config = config
|
|
73
74
|
this.scheduler = new KeyScheduler()
|
|
74
|
-
this.onlineDetector = new
|
|
75
|
+
this.onlineDetector = config.onlineDetector ?? new WebOnlineDetector()
|
|
75
76
|
|
|
76
77
|
// Initialize as pending - will be set by async initialization
|
|
77
78
|
this.storage = null
|
|
@@ -491,7 +492,7 @@ export class OfflineExecutor {
|
|
|
491
492
|
return this.executor.getRunningCount()
|
|
492
493
|
}
|
|
493
494
|
|
|
494
|
-
getOnlineDetector():
|
|
495
|
+
getOnlineDetector(): OnlineDetector {
|
|
495
496
|
return this.onlineDetector
|
|
496
497
|
}
|
|
497
498
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { OnlineDetector } from '../types'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Web-based online detector that uses browser APIs.
|
|
5
|
+
* Listens for:
|
|
6
|
+
* - `window.online` event for network connectivity changes
|
|
7
|
+
* - `document.visibilitychange` event for tab/window focus changes
|
|
8
|
+
*/
|
|
9
|
+
export class WebOnlineDetector implements OnlineDetector {
|
|
4
10
|
private listeners: Set<() => void> = new Set()
|
|
5
11
|
private isListening = false
|
|
6
12
|
|
|
@@ -85,3 +91,8 @@ export class DefaultOnlineDetector implements OnlineDetector {
|
|
|
85
91
|
this.listeners.clear()
|
|
86
92
|
}
|
|
87
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @deprecated Use `WebOnlineDetector` instead. This alias is kept for backwards compatibility.
|
|
97
|
+
*/
|
|
98
|
+
export const DefaultOnlineDetector = WebOnlineDetector
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import NetInfo from '@react-native-community/netinfo'
|
|
2
|
+
import { AppState } from 'react-native'
|
|
3
|
+
import type { AppStateStatus, NativeEventSubscription } from 'react-native'
|
|
4
|
+
import type { OnlineDetector } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Native online detector that uses RN APIs.
|
|
8
|
+
* Listens for:
|
|
9
|
+
* - Network connectivity changes via `@react-native-community/netinfo`
|
|
10
|
+
* - App state changes (foreground/background) via `AppState`
|
|
11
|
+
*/
|
|
12
|
+
export class ReactNativeOnlineDetector implements OnlineDetector {
|
|
13
|
+
private listeners: Set<() => void> = new Set()
|
|
14
|
+
private netInfoUnsubscribe: (() => void) | null = null
|
|
15
|
+
private appStateSubscription: NativeEventSubscription | null = null
|
|
16
|
+
private isListening = false
|
|
17
|
+
private wasConnected = true
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.startListening()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private startListening(): void {
|
|
24
|
+
if (this.isListening) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.isListening = true
|
|
29
|
+
|
|
30
|
+
// Subscribe to network state changes
|
|
31
|
+
this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {
|
|
32
|
+
const isConnected =
|
|
33
|
+
state.isConnected === true && state.isInternetReachable !== false
|
|
34
|
+
|
|
35
|
+
// Only notify when transitioning to online
|
|
36
|
+
if (isConnected && !this.wasConnected) {
|
|
37
|
+
this.notifyListeners()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.wasConnected = isConnected
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Subscribe to app state changes (foreground/background)
|
|
44
|
+
this.appStateSubscription = AppState.addEventListener(
|
|
45
|
+
`change`,
|
|
46
|
+
this.handleAppStateChange,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private handleAppStateChange = (nextState: AppStateStatus): void => {
|
|
51
|
+
// Notify when app becomes active (foreground)
|
|
52
|
+
if (nextState === `active`) {
|
|
53
|
+
this.notifyListeners()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private stopListening(): void {
|
|
58
|
+
if (!this.isListening) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.isListening = false
|
|
63
|
+
|
|
64
|
+
if (this.netInfoUnsubscribe) {
|
|
65
|
+
this.netInfoUnsubscribe()
|
|
66
|
+
this.netInfoUnsubscribe = null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.appStateSubscription) {
|
|
70
|
+
this.appStateSubscription.remove()
|
|
71
|
+
this.appStateSubscription = null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private notifyListeners(): void {
|
|
76
|
+
for (const listener of this.listeners) {
|
|
77
|
+
try {
|
|
78
|
+
listener()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(`ReactNativeOnlineDetector listener error:`, error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
subscribe(callback: () => void): () => void {
|
|
86
|
+
this.listeners.add(callback)
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
this.listeners.delete(callback)
|
|
90
|
+
|
|
91
|
+
if (this.listeners.size === 0) {
|
|
92
|
+
this.stopListening()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
notifyOnline(): void {
|
|
98
|
+
this.notifyListeners()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dispose(): void {
|
|
102
|
+
this.stopListening()
|
|
103
|
+
this.listeners.clear()
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -33,7 +33,10 @@ export { WebLocksLeader } from './coordination/WebLocksLeader'
|
|
|
33
33
|
export { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'
|
|
34
34
|
|
|
35
35
|
// Connectivity
|
|
36
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
WebOnlineDetector,
|
|
38
|
+
DefaultOnlineDetector,
|
|
39
|
+
} from './connectivity/OnlineDetector'
|
|
37
40
|
|
|
38
41
|
// API components
|
|
39
42
|
export { OfflineTransaction as OfflineTransactionAPI } from './api/OfflineTransaction'
|
|
@@ -7,11 +7,9 @@ import type {
|
|
|
7
7
|
import type { Collection, PendingMutation } from '@tanstack/db'
|
|
8
8
|
|
|
9
9
|
export class TransactionSerializer {
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
10
|
private collections: Record<string, Collection<any, any, any, any, any>>
|
|
12
11
|
private collectionIdToKey: Map<string, string>
|
|
13
12
|
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
13
|
constructor(
|
|
16
14
|
collections: Record<string, Collection<any, any, any, any, any>>,
|
|
17
15
|
) {
|
|
@@ -26,37 +24,29 @@ export class TransactionSerializer {
|
|
|
26
24
|
serialize(transaction: OfflineTransaction): string {
|
|
27
25
|
const serialized: SerializedOfflineTransaction = {
|
|
28
26
|
...transaction,
|
|
29
|
-
createdAt: transaction.createdAt,
|
|
27
|
+
createdAt: transaction.createdAt.toISOString(),
|
|
30
28
|
mutations: transaction.mutations.map((mutation) =>
|
|
31
29
|
this.serializeMutation(mutation),
|
|
32
30
|
),
|
|
33
31
|
}
|
|
34
|
-
|
|
35
|
-
return JSON.stringify(serialized, (key, value) => {
|
|
36
|
-
if (value instanceof Date) {
|
|
37
|
-
return value.toISOString()
|
|
38
|
-
}
|
|
39
|
-
return value
|
|
40
|
-
})
|
|
32
|
+
return JSON.stringify(serialized)
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
deserialize(data: string): OfflineTransaction {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return value
|
|
55
|
-
},
|
|
56
|
-
)
|
|
36
|
+
// Parse without a reviver - let deserializeValue handle dates in mutation data
|
|
37
|
+
// using the { __type: 'Date' } marker system
|
|
38
|
+
const parsed: SerializedOfflineTransaction = JSON.parse(data)
|
|
39
|
+
|
|
40
|
+
const createdAt = new Date(parsed.createdAt)
|
|
41
|
+
if (isNaN(createdAt.getTime())) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Failed to deserialize transaction: invalid createdAt value "${parsed.createdAt}"`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
57
46
|
|
|
58
47
|
return {
|
|
59
48
|
...parsed,
|
|
49
|
+
createdAt,
|
|
60
50
|
mutations: parsed.mutations.map((mutationData) =>
|
|
61
51
|
this.deserializeMutation(mutationData),
|
|
62
52
|
),
|
|
@@ -76,6 +66,7 @@ export class TransactionSerializer {
|
|
|
76
66
|
type: mutation.type,
|
|
77
67
|
modified: this.serializeValue(mutation.modified),
|
|
78
68
|
original: this.serializeValue(mutation.original),
|
|
69
|
+
changes: this.serializeValue(mutation.changes),
|
|
79
70
|
collectionId: registryKey, // Store registry key instead of collection.id
|
|
80
71
|
}
|
|
81
72
|
}
|
|
@@ -93,11 +84,11 @@ export class TransactionSerializer {
|
|
|
93
84
|
type: data.type as any,
|
|
94
85
|
modified: this.deserializeValue(data.modified),
|
|
95
86
|
original: this.deserializeValue(data.original),
|
|
87
|
+
changes: this.deserializeValue(data.changes) ?? {},
|
|
96
88
|
collection,
|
|
97
89
|
// These fields would need to be reconstructed by the executor
|
|
98
90
|
mutationId: ``, // Will be regenerated
|
|
99
91
|
key: null, // Will be extracted from the data
|
|
100
|
-
changes: {}, // Will be recalculated
|
|
101
92
|
metadata: undefined,
|
|
102
93
|
syncMetadata: {},
|
|
103
94
|
optimistic: true,
|
|
@@ -118,7 +109,7 @@ export class TransactionSerializer {
|
|
|
118
109
|
if (typeof value === `object`) {
|
|
119
110
|
const result: any = Array.isArray(value) ? [] : {}
|
|
120
111
|
for (const key in value) {
|
|
121
|
-
if (
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
122
113
|
result[key] = this.serializeValue(value[key])
|
|
123
114
|
}
|
|
124
115
|
}
|
|
@@ -134,13 +125,22 @@ export class TransactionSerializer {
|
|
|
134
125
|
}
|
|
135
126
|
|
|
136
127
|
if (typeof value === `object` && value.__type === `Date`) {
|
|
137
|
-
|
|
128
|
+
if (value.value === undefined || value.value === null) {
|
|
129
|
+
throw new Error(`Corrupted Date marker: missing value field`)
|
|
130
|
+
}
|
|
131
|
+
const date = new Date(value.value)
|
|
132
|
+
if (isNaN(date.getTime())) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Failed to deserialize Date marker: invalid date value "${value.value}"`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
return date
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
if (typeof value === `object`) {
|
|
141
141
|
const result: any = Array.isArray(value) ? [] : {}
|
|
142
142
|
for (const key in value) {
|
|
143
|
-
if (
|
|
143
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
144
144
|
result[key] = this.deserializeValue(value[key])
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { OfflineExecutor as BaseOfflineExecutor } from '../OfflineExecutor'
|
|
2
|
+
import { ReactNativeOnlineDetector } from '../connectivity/ReactNativeOnlineDetector'
|
|
3
|
+
import type { OfflineConfig } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OfflineExecutor configured for React Native environments.
|
|
7
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
8
|
+
*/
|
|
9
|
+
export class OfflineExecutor extends BaseOfflineExecutor {
|
|
10
|
+
constructor(config: OfflineConfig) {
|
|
11
|
+
super({
|
|
12
|
+
...config,
|
|
13
|
+
onlineDetector: config.onlineDetector ?? new ReactNativeOnlineDetector(),
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Start an offline executor configured for React Native environments.
|
|
20
|
+
* Uses ReactNativeOnlineDetector by default instead of WebOnlineDetector.
|
|
21
|
+
*/
|
|
22
|
+
export function startOfflineExecutor(config: OfflineConfig): OfflineExecutor {
|
|
23
|
+
return new OfflineExecutor(config)
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Re-export from main entry (types, utilities, etc.)
|
|
2
|
+
export {
|
|
3
|
+
// Types
|
|
4
|
+
type OfflineTransaction,
|
|
5
|
+
type OfflineConfig,
|
|
6
|
+
type OfflineMode,
|
|
7
|
+
type StorageAdapter,
|
|
8
|
+
type StorageDiagnostic,
|
|
9
|
+
type StorageDiagnosticCode,
|
|
10
|
+
type RetryPolicy,
|
|
11
|
+
type LeaderElection,
|
|
12
|
+
type OnlineDetector,
|
|
13
|
+
type CreateOfflineTransactionOptions,
|
|
14
|
+
type CreateOfflineActionOptions,
|
|
15
|
+
type SerializedError,
|
|
16
|
+
type SerializedMutation,
|
|
17
|
+
NonRetriableError,
|
|
18
|
+
// Storage adapters
|
|
19
|
+
IndexedDBAdapter,
|
|
20
|
+
LocalStorageAdapter,
|
|
21
|
+
// Retry policies
|
|
22
|
+
DefaultRetryPolicy,
|
|
23
|
+
BackoffCalculator,
|
|
24
|
+
// Coordination
|
|
25
|
+
WebLocksLeader,
|
|
26
|
+
BroadcastChannelLeader,
|
|
27
|
+
// Connectivity - export web detector too for flexibility
|
|
28
|
+
WebOnlineDetector,
|
|
29
|
+
DefaultOnlineDetector,
|
|
30
|
+
// API components
|
|
31
|
+
OfflineTransactionAPI,
|
|
32
|
+
createOfflineAction,
|
|
33
|
+
// Outbox management
|
|
34
|
+
OutboxManager,
|
|
35
|
+
TransactionSerializer,
|
|
36
|
+
// Execution engine
|
|
37
|
+
KeyScheduler,
|
|
38
|
+
TransactionExecutor,
|
|
39
|
+
} from '../index'
|
|
40
|
+
|
|
41
|
+
// Export RN-specific detector
|
|
42
|
+
export { ReactNativeOnlineDetector } from '../connectivity/ReactNativeOnlineDetector'
|
|
43
|
+
|
|
44
|
+
// Export RN-configured executor
|
|
45
|
+
export { OfflineExecutor, startOfflineExecutor } from './OfflineExecutor'
|
package/src/types.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface SerializedMutation {
|
|
|
21
21
|
type: string
|
|
22
22
|
modified: any
|
|
23
23
|
original: any
|
|
24
|
+
changes: any
|
|
24
25
|
collectionId: string
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -60,7 +61,7 @@ export interface SerializedOfflineTransaction {
|
|
|
60
61
|
mutations: Array<SerializedMutation>
|
|
61
62
|
keys: Array<string>
|
|
62
63
|
idempotencyKey: string
|
|
63
|
-
createdAt:
|
|
64
|
+
createdAt: string
|
|
64
65
|
retryCount: number
|
|
65
66
|
nextAttemptAt: number
|
|
66
67
|
lastError?: SerializedError
|
|
@@ -88,7 +89,6 @@ export interface StorageDiagnostic {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export interface OfflineConfig {
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
92
|
collections: Record<string, Collection<any, any, any, any, any>>
|
|
93
93
|
mutationFns: Record<string, OfflineMutationFn>
|
|
94
94
|
storage?: StorageAdapter
|
|
@@ -101,6 +101,12 @@ export interface OfflineConfig {
|
|
|
101
101
|
onLeadershipChange?: (isLeader: boolean) => void
|
|
102
102
|
onStorageFailure?: (diagnostic: StorageDiagnostic) => void
|
|
103
103
|
leaderElection?: LeaderElection
|
|
104
|
+
/**
|
|
105
|
+
* Custom online detector implementation.
|
|
106
|
+
* Defaults to WebOnlineDetector for browser environments.
|
|
107
|
+
* Use ReactNativeOnlineDetector from '@tanstack/offline-transactions/react-native' for RN/Expo.
|
|
108
|
+
*/
|
|
109
|
+
onlineDetector?: OnlineDetector
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export interface StorageAdapter {
|
|
@@ -126,6 +132,7 @@ export interface LeaderElection {
|
|
|
126
132
|
export interface OnlineDetector {
|
|
127
133
|
subscribe: (callback: () => void) => () => void
|
|
128
134
|
notifyOnline: () => void
|
|
135
|
+
dispose: () => void
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
export interface CreateOfflineTransactionOptions {
|