@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.
Files changed (53) hide show
  1. package/README.md +41 -1
  2. package/dist/cjs/OfflineExecutor.cjs +5 -5
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +2 -3
  5. package/dist/cjs/connectivity/OnlineDetector.cjs +3 -1
  6. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  7. package/dist/cjs/connectivity/OnlineDetector.d.cts +11 -1
  8. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +77 -0
  9. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -0
  10. package/dist/cjs/connectivity/ReactNativeOnlineDetector.d.cts +22 -0
  11. package/dist/cjs/index.cjs +1 -0
  12. package/dist/cjs/index.cjs.map +1 -1
  13. package/dist/cjs/index.d.cts +1 -1
  14. package/dist/cjs/outbox/TransactionSerializer.cjs +24 -22
  15. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  16. package/dist/cjs/react-native/OfflineExecutor.cjs +18 -0
  17. package/dist/cjs/react-native/OfflineExecutor.cjs.map +1 -0
  18. package/dist/cjs/react-native/OfflineExecutor.d.cts +14 -0
  19. package/dist/cjs/react-native/index.cjs +37 -0
  20. package/dist/cjs/react-native/index.cjs.map +1 -0
  21. package/dist/cjs/react-native/index.d.cts +3 -0
  22. package/dist/cjs/types.cjs.map +1 -1
  23. package/dist/cjs/types.d.cts +9 -1
  24. package/dist/esm/OfflineExecutor.d.ts +2 -3
  25. package/dist/esm/OfflineExecutor.js +6 -6
  26. package/dist/esm/OfflineExecutor.js.map +1 -1
  27. package/dist/esm/connectivity/OnlineDetector.d.ts +11 -1
  28. package/dist/esm/connectivity/OnlineDetector.js +4 -2
  29. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  30. package/dist/esm/connectivity/ReactNativeOnlineDetector.d.ts +22 -0
  31. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +77 -0
  32. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -0
  33. package/dist/esm/index.d.ts +1 -1
  34. package/dist/esm/index.js +2 -1
  35. package/dist/esm/outbox/TransactionSerializer.js +24 -22
  36. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  37. package/dist/esm/react-native/OfflineExecutor.d.ts +14 -0
  38. package/dist/esm/react-native/OfflineExecutor.js +18 -0
  39. package/dist/esm/react-native/OfflineExecutor.js.map +1 -0
  40. package/dist/esm/react-native/index.d.ts +3 -0
  41. package/dist/esm/react-native/index.js +37 -0
  42. package/dist/esm/react-native/index.js.map +1 -0
  43. package/dist/esm/types.d.ts +9 -1
  44. package/dist/esm/types.js.map +1 -1
  45. package/package.json +26 -2
  46. package/src/OfflineExecutor.ts +5 -4
  47. package/src/connectivity/OnlineDetector.ts +12 -1
  48. package/src/connectivity/ReactNativeOnlineDetector.ts +105 -0
  49. package/src/index.ts +4 -1
  50. package/src/outbox/TransactionSerializer.ts +27 -27
  51. package/src/react-native/OfflineExecutor.ts +24 -0
  52. package/src/react-native/index.ts +45 -0
  53. 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":";;;;;;;;;;;;;;;;"}
@@ -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: Date;
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;
@@ -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: 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;"}
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.9",
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.19"
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
  },
@@ -13,7 +13,7 @@ import { WebLocksLeader } from './coordination/WebLocksLeader'
13
13
  import { BroadcastChannelLeader } from './coordination/BroadcastChannelLeader'
14
14
 
15
15
  // Connectivity
16
- import { DefaultOnlineDetector } from './connectivity/OnlineDetector'
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: DefaultOnlineDetector
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 DefaultOnlineDetector()
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(): DefaultOnlineDetector {
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
- export class DefaultOnlineDetector implements OnlineDetector {
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 { DefaultOnlineDetector } from './connectivity/OnlineDetector'
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
- // Convert the whole object to JSON, handling dates
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
- const parsed: SerializedOfflineTransaction = JSON.parse(
45
- data,
46
- (key, value) => {
47
- // Parse ISO date strings back to Date objects
48
- if (
49
- typeof value === `string` &&
50
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)
51
- ) {
52
- return new Date(value)
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 (value.hasOwnProperty(key)) {
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
- return new Date(value.value)
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 (value.hasOwnProperty(key)) {
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: Date
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 {