@tanstack/offline-transactions 1.0.54 → 1.0.58

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 (64) hide show
  1. package/README.md +137 -23
  2. package/dist/cjs/OfflineExecutor.cjs +28 -11
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/OfflineExecutor.d.cts +1 -0
  5. package/dist/cjs/api/OfflineTransaction.cjs +4 -1
  6. package/dist/cjs/api/OfflineTransaction.cjs.map +1 -1
  7. package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
  8. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  9. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
  10. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
  11. package/dist/cjs/executor/KeyScheduler.cjs +31 -26
  12. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  13. package/dist/cjs/executor/KeyScheduler.d.cts +5 -3
  14. package/dist/cjs/executor/TransactionExecutor.cjs +72 -35
  15. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  16. package/dist/cjs/executor/TransactionExecutor.d.cts +3 -2
  17. package/dist/cjs/outbox/OutboxManager.cjs +66 -24
  18. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  19. package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
  20. package/dist/cjs/outbox/TransactionSerializer.cjs +159 -20
  21. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  22. package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
  23. package/dist/cjs/storage/IndexedDBAdapter.cjs +16 -12
  24. package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
  25. package/dist/cjs/storage/IndexedDBAdapter.d.cts +1 -0
  26. package/dist/cjs/types.cjs.map +1 -1
  27. package/dist/cjs/types.d.cts +3 -0
  28. package/dist/esm/OfflineExecutor.d.ts +1 -0
  29. package/dist/esm/OfflineExecutor.js +28 -11
  30. package/dist/esm/OfflineExecutor.js.map +1 -1
  31. package/dist/esm/api/OfflineTransaction.js +4 -1
  32. package/dist/esm/api/OfflineTransaction.js.map +1 -1
  33. package/dist/esm/connectivity/OnlineDetector.js +1 -1
  34. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  35. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
  36. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
  37. package/dist/esm/executor/KeyScheduler.d.ts +5 -3
  38. package/dist/esm/executor/KeyScheduler.js +31 -26
  39. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  40. package/dist/esm/executor/TransactionExecutor.d.ts +3 -2
  41. package/dist/esm/executor/TransactionExecutor.js +72 -35
  42. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  43. package/dist/esm/outbox/OutboxManager.d.ts +6 -0
  44. package/dist/esm/outbox/OutboxManager.js +67 -25
  45. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  46. package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
  47. package/dist/esm/outbox/TransactionSerializer.js +159 -20
  48. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  49. package/dist/esm/storage/IndexedDBAdapter.d.ts +1 -0
  50. package/dist/esm/storage/IndexedDBAdapter.js +16 -12
  51. package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
  52. package/dist/esm/types.d.ts +3 -0
  53. package/dist/esm/types.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/OfflineExecutor.ts +34 -15
  56. package/src/api/OfflineTransaction.ts +7 -2
  57. package/src/connectivity/OnlineDetector.ts +6 -1
  58. package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
  59. package/src/executor/KeyScheduler.ts +39 -28
  60. package/src/executor/TransactionExecutor.ts +88 -46
  61. package/src/outbox/OutboxManager.ts +82 -28
  62. package/src/outbox/TransactionSerializer.ts +236 -22
  63. package/src/storage/IndexedDBAdapter.ts +23 -12
  64. package/src/types.ts +3 -0
@@ -1,3 +1,56 @@
1
+ const temporalConstructorNames = [
2
+ `Duration`,
3
+ `Instant`,
4
+ `PlainDate`,
5
+ `PlainDateTime`,
6
+ `PlainMonthDay`,
7
+ `PlainTime`,
8
+ `PlainYearMonth`,
9
+ `ZonedDateTime`
10
+ ];
11
+ function getTemporalConstructorName(type) {
12
+ if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return;
13
+ const constructorName = type.slice(
14
+ `Temporal.`.length
15
+ );
16
+ return temporalConstructorNames.includes(constructorName) ? constructorName : void 0;
17
+ }
18
+ function requireTemporalConstructor(name) {
19
+ const constructor = globalThis.Temporal?.[name];
20
+ if (typeof constructor?.from !== `function`)
21
+ throw new MissingTemporalConstructorError(
22
+ `Missing global Temporal.${name} constructor`
23
+ );
24
+ return constructor;
25
+ }
26
+ function serializeTemporalValue(value, name) {
27
+ const constructor = requireTemporalConstructor(name);
28
+ const toString = constructor.prototype?.toString;
29
+ if (typeof toString !== `function` || toString === Object.prototype.toString)
30
+ return;
31
+ try {
32
+ const serialized = toString.call(value);
33
+ return typeof serialized === `string` ? serialized : void 0;
34
+ } catch {
35
+ return;
36
+ }
37
+ }
38
+ function mayBeNativeScalar(value) {
39
+ const prototype = Object.getPrototypeOf(value);
40
+ return prototype !== null && prototype !== Object.prototype && !Array.isArray(value);
41
+ }
42
+ class MissingTemporalConstructorError extends Error {
43
+ }
44
+ function setDataProperty(object, key, value) {
45
+ if (key !== `__proto__`) object[key] = value;
46
+ else
47
+ Object.defineProperty(object, key, {
48
+ value,
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true
52
+ });
53
+ }
1
54
  class TransactionSerializer {
2
55
  constructor(collections) {
3
56
  this.collections = collections;
@@ -9,7 +62,9 @@ class TransactionSerializer {
9
62
  serialize(transaction) {
10
63
  const serialized = {
11
64
  ...transaction,
65
+ valueEncoding: 3,
12
66
  createdAt: transaction.createdAt.toISOString(),
67
+ metadata: this.serializeValue(transaction.metadata, `metadata`),
13
68
  mutations: transaction.mutations.map(
14
69
  (mutation) => this.serializeMutation(mutation)
15
70
  )
@@ -17,7 +72,15 @@ class TransactionSerializer {
17
72
  return JSON.stringify(serialized);
18
73
  }
19
74
  deserialize(data) {
20
- const parsed = JSON.parse(data);
75
+ const {
76
+ valueEncoding,
77
+ ...parsed
78
+ } = JSON.parse(data);
79
+ if (valueEncoding !== void 0 && valueEncoding !== 2 && valueEncoding !== 3) {
80
+ throw new Error(
81
+ `Unsupported transaction value encoding: ${valueEncoding}`
82
+ );
83
+ }
21
84
  const createdAt = new Date(parsed.createdAt);
22
85
  if (isNaN(createdAt.getTime())) {
23
86
  throw new Error(
@@ -27,8 +90,9 @@ class TransactionSerializer {
27
90
  return {
28
91
  ...parsed,
29
92
  createdAt,
93
+ metadata: valueEncoding === 3 ? this.deserializeValue(parsed.metadata, valueEncoding) : parsed.metadata,
30
94
  mutations: parsed.mutations.map(
31
- (mutationData) => this.deserializeMutation(mutationData)
95
+ (mutationData) => this.deserializeMutation(mutationData, valueEncoding)
32
96
  )
33
97
  };
34
98
  }
@@ -49,19 +113,19 @@ class TransactionSerializer {
49
113
  // Store registry key instead of collection.id
50
114
  };
51
115
  }
52
- deserializeMutation(data) {
116
+ deserializeMutation(data, valueEncoding) {
53
117
  const collection = this.collections[data.collectionId];
54
118
  if (!collection) {
55
119
  throw new Error(`Collection with id ${data.collectionId} not found`);
56
120
  }
57
- const modified = this.deserializeValue(data.modified);
121
+ const modified = this.deserializeValue(data.modified, valueEncoding);
58
122
  const key = modified ? collection.getKeyFromItem(modified) : null;
59
123
  return {
60
124
  globalKey: data.globalKey,
61
125
  type: data.type,
62
126
  modified,
63
- original: this.deserializeValue(data.original),
64
- changes: this.deserializeValue(data.changes) ?? {},
127
+ original: this.deserializeValue(data.original, valueEncoding),
128
+ changes: this.deserializeValue(data.changes, valueEncoding) ?? {},
65
129
  collection,
66
130
  // These fields would need to be reconstructed by the executor
67
131
  mutationId: ``,
@@ -74,25 +138,81 @@ class TransactionSerializer {
74
138
  updatedAt: /* @__PURE__ */ new Date()
75
139
  };
76
140
  }
77
- serializeValue(value) {
78
- if (value === null || value === void 0) {
79
- return value;
80
- }
81
- if (value instanceof Date) {
141
+ serializeValue(value, jsonKey, ancestors = /* @__PURE__ */ new WeakSet()) {
142
+ if (value === null || typeof value !== `object`) return value;
143
+ if (jsonKey !== false && value instanceof Date) {
82
144
  return { __type: `Date`, value: value.toISOString() };
83
145
  }
84
- if (typeof value === `object`) {
85
- const result = Array.isArray(value) ? [] : {};
86
- for (const key in value) {
87
- if (Object.prototype.hasOwnProperty.call(value, key)) {
88
- result[key] = this.serializeValue(value[key]);
146
+ const temporalConstructorName = jsonKey !== false && mayBeNativeScalar(value) ? getTemporalConstructorName(value[Symbol.toStringTag]) : void 0;
147
+ if (temporalConstructorName) {
148
+ const temporalValue = serializeTemporalValue(
149
+ value,
150
+ temporalConstructorName
151
+ );
152
+ if (temporalValue !== void 0)
153
+ return {
154
+ __type: `Temporal`,
155
+ type: `Temporal.${temporalConstructorName}`,
156
+ value: temporalValue
157
+ };
158
+ }
159
+ if (ancestors.has(value))
160
+ throw new TypeError(`Converting circular structure to JSON`);
161
+ const toJSON = typeof jsonKey === `string` && value.toJSON;
162
+ if (typeof toJSON === `function`) {
163
+ const replacement = toJSON.call(value, jsonKey);
164
+ if (replacement === value) {
165
+ return this.serializeValue(replacement, false, ancestors);
166
+ }
167
+ ancestors.add(value);
168
+ try {
169
+ return this.serializeValue(replacement, false, ancestors);
170
+ } finally {
171
+ ancestors.delete(value);
172
+ }
173
+ }
174
+ if (jsonKey !== void 0 && (value instanceof Boolean || value instanceof BigInt || value instanceof Number || value instanceof String)) {
175
+ return value.valueOf();
176
+ }
177
+ ancestors.add(value);
178
+ const isArray = Array.isArray(value);
179
+ const result = isArray ? [] : {};
180
+ try {
181
+ if (isArray) {
182
+ const length = value.length;
183
+ for (let index = 0; index < length; index++) {
184
+ const key = String(index);
185
+ setDataProperty(
186
+ result,
187
+ key,
188
+ this.serializeValue(
189
+ value[index],
190
+ jsonKey === void 0 ? void 0 : key,
191
+ ancestors
192
+ )
193
+ );
194
+ }
195
+ } else {
196
+ for (const key of Object.keys(value)) {
197
+ setDataProperty(
198
+ result,
199
+ key,
200
+ this.serializeValue(
201
+ value[key],
202
+ jsonKey === void 0 ? void 0 : key,
203
+ ancestors
204
+ )
205
+ );
89
206
  }
90
207
  }
91
- return result;
208
+ } finally {
209
+ ancestors.delete(value);
92
210
  }
93
- return value;
211
+ if (jsonKey === false && typeof result.toJSON === `function`)
212
+ delete result.toJSON;
213
+ return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`) ? { __type: `Object`, value: result } : result;
94
214
  }
95
- deserializeValue(value) {
215
+ deserializeValue(value, valueEncoding) {
96
216
  if (value === null || value === void 0) {
97
217
  return value;
98
218
  }
@@ -108,11 +228,29 @@ class TransactionSerializer {
108
228
  }
109
229
  return date;
110
230
  }
231
+ if (valueEncoding === 3 && typeof value === `object` && value.__type === `Temporal`) {
232
+ const constructorName = getTemporalConstructorName(value.type);
233
+ if (!constructorName)
234
+ throw new Error(`Corrupted Temporal marker: invalid type field`);
235
+ if (typeof value.value !== `string`)
236
+ throw new Error(`Corrupted Temporal marker: missing value field`);
237
+ return requireTemporalConstructor(constructorName).from(value.value);
238
+ }
111
239
  if (typeof value === `object`) {
240
+ if (valueEncoding !== void 0 && value.__type === `Object`) {
241
+ if (value.value === null || typeof value.value !== `object` || Array.isArray(value.value)) {
242
+ throw new Error(`Corrupted Object marker: expected an object value`);
243
+ }
244
+ value = value.value;
245
+ }
112
246
  const result = Array.isArray(value) ? [] : {};
113
247
  for (const key in value) {
114
248
  if (Object.prototype.hasOwnProperty.call(value, key)) {
115
- result[key] = this.deserializeValue(value[key]);
249
+ setDataProperty(
250
+ result,
251
+ key,
252
+ this.deserializeValue(value[key], valueEncoding)
253
+ );
116
254
  }
117
255
  }
118
256
  return result;
@@ -134,6 +272,7 @@ class TransactionSerializer {
134
272
  }
135
273
  }
136
274
  export {
275
+ MissingTemporalConstructorError,
137
276
  TransactionSerializer
138
277
  };
139
278
  //# sourceMappingURL=TransactionSerializer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nconst temporalConstructorNames = [\n `Duration`,\n `Instant`,\n `PlainDate`,\n `PlainDateTime`,\n `PlainMonthDay`,\n `PlainTime`,\n `PlainYearMonth`,\n `ZonedDateTime`,\n] as const\n\ntype TemporalConstructorName = (typeof temporalConstructorNames)[number]\ntype TemporalConstructor = {\n from: (value: string) => unknown\n prototype?: { toString?: () => string }\n}\n\nfunction getTemporalConstructorName(\n type: unknown,\n): TemporalConstructorName | undefined {\n if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return\n const constructorName = type.slice(\n `Temporal.`.length,\n ) as TemporalConstructorName\n return temporalConstructorNames.includes(constructorName)\n ? constructorName\n : undefined\n}\n\nfunction requireTemporalConstructor(\n name: TemporalConstructorName,\n): TemporalConstructor {\n const constructor = (\n globalThis as {\n Temporal?: Partial<Record<TemporalConstructorName, TemporalConstructor>>\n }\n ).Temporal?.[name]\n if (typeof constructor?.from !== `function`)\n throw new MissingTemporalConstructorError(\n `Missing global Temporal.${name} constructor`,\n )\n return constructor\n}\n\nfunction serializeTemporalValue(\n value: object,\n name: TemporalConstructorName,\n): string | undefined {\n const constructor = requireTemporalConstructor(name)\n const toString = constructor.prototype?.toString\n if (typeof toString !== `function` || toString === Object.prototype.toString)\n return\n try {\n const serialized = toString.call(value)\n return typeof serialized === `string` ? serialized : undefined\n } catch {\n // Temporal prototype methods brand-check their receiver. A matching\n // Symbol.toStringTag without the corresponding internal slots is user data.\n return\n }\n}\n\nfunction mayBeNativeScalar(value: object): boolean {\n const prototype = Object.getPrototypeOf(value)\n return (\n prototype !== null &&\n prototype !== Object.prototype &&\n !Array.isArray(value)\n )\n}\n\nexport class MissingTemporalConstructorError extends Error {}\n\nfunction setDataProperty(\n object: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n if (key !== `__proto__`) object[key] = value\n else\n Object.defineProperty(object, key, {\n value,\n enumerable: true,\n configurable: true,\n writable: true,\n })\n}\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n valueEncoding: 3,\n createdAt: transaction.createdAt.toISOString(),\n metadata: this.serializeValue(transaction.metadata, `metadata`),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Old records retain their Date-marker meaning. New records also escape\n // marker-shaped user objects; the encoding tag is not application metadata.\n const {\n valueEncoding,\n ...parsed\n }: Omit<SerializedOfflineTransaction, `valueEncoding`> & {\n valueEncoding?: unknown\n } = JSON.parse(data)\n if (\n valueEncoding !== undefined &&\n valueEncoding !== 2 &&\n valueEncoding !== 3\n ) {\n throw new Error(\n `Unsupported transaction value encoding: ${valueEncoding}`,\n )\n }\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n metadata:\n valueEncoding === 3\n ? this.deserializeValue(parsed.metadata, valueEncoding)\n : parsed.metadata,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData, valueEncoding),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(\n data: SerializedMutation,\n valueEncoding: 2 | 3 | undefined,\n ): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified, valueEncoding)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original, valueEncoding),\n changes: this.deserializeValue(data.changes, valueEncoding) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(\n value: any,\n jsonKey?: string | false,\n ancestors = new WeakSet<object>(),\n ): any {\n if (value === null || typeof value !== `object`) return value\n\n if (jsonKey !== false && value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n const temporalConstructorName =\n jsonKey !== false && mayBeNativeScalar(value)\n ? getTemporalConstructorName(value[Symbol.toStringTag])\n : undefined\n if (temporalConstructorName) {\n const temporalValue = serializeTemporalValue(\n value,\n temporalConstructorName,\n )\n if (temporalValue !== undefined)\n return {\n __type: `Temporal`,\n type: `Temporal.${temporalConstructorName}`,\n value: temporalValue,\n }\n }\n\n if (ancestors.has(value))\n throw new TypeError(`Converting circular structure to JSON`)\n\n const toJSON = typeof jsonKey === `string` && value.toJSON\n if (typeof toJSON === `function`) {\n const replacement = toJSON.call(value, jsonKey)\n if (replacement === value) {\n return this.serializeValue(replacement, false, ancestors)\n }\n\n ancestors.add(value)\n try {\n return this.serializeValue(replacement, false, ancestors)\n } finally {\n ancestors.delete(value)\n }\n }\n if (\n jsonKey !== undefined &&\n (value instanceof Boolean ||\n value instanceof BigInt ||\n value instanceof Number ||\n value instanceof String)\n ) {\n return value.valueOf()\n }\n\n ancestors.add(value)\n\n const isArray = Array.isArray(value)\n const result: any = isArray ? [] : {}\n try {\n if (isArray) {\n const length = value.length\n for (let index = 0; index < length; index++) {\n const key = String(index)\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[index],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n } else {\n for (const key of Object.keys(value)) {\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[key],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n }\n } finally {\n ancestors.delete(value)\n }\n if (jsonKey === false && typeof result.toJSON === `function`)\n delete result.toJSON\n return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`)\n ? { __type: `Object`, value: result }\n : result\n }\n\n private deserializeValue(value: any, valueEncoding: 2 | 3 | undefined): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (\n valueEncoding === 3 &&\n typeof value === `object` &&\n value.__type === `Temporal`\n ) {\n const constructorName = getTemporalConstructorName(value.type)\n if (!constructorName)\n throw new Error(`Corrupted Temporal marker: invalid type field`)\n if (typeof value.value !== `string`)\n throw new Error(`Corrupted Temporal marker: missing value field`)\n return requireTemporalConstructor(constructorName).from(value.value)\n }\n\n if (typeof value === `object`) {\n // Unwrap once, then decode only the fields: the object's own __type is data.\n if (valueEncoding !== undefined && value.__type === `Object`) {\n if (\n value.value === null ||\n typeof value.value !== `object` ||\n Array.isArray(value.value)\n ) {\n throw new Error(`Corrupted Object marker: expected an object value`)\n }\n value = value.value\n }\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n setDataProperty(\n result,\n key,\n this.deserializeValue(value[key], valueEncoding),\n )\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ export declare class IndexedDBAdapter extends BaseStorageAdapter {
14
14
  }>;
15
15
  private openDB;
16
16
  private getStore;
17
+ private waitForWrite;
17
18
  get(key: string): Promise<string | null>;
18
19
  set(key: string, value: string): Promise<void>;
19
20
  delete(key: string): Promise<void>;
@@ -69,6 +69,16 @@ class IndexedDBAdapter extends BaseStorageAdapter {
69
69
  const transaction = db.transaction([this.storeName], mode);
70
70
  return transaction.objectStore(this.storeName);
71
71
  }
72
+ waitForWrite(store, write) {
73
+ const transaction = store.transaction;
74
+ return new Promise((resolve, reject) => {
75
+ transaction.oncomplete = () => resolve();
76
+ transaction.onabort = () => reject(
77
+ transaction.error ?? new DOMException(`IndexedDB transaction aborted`, `AbortError`)
78
+ );
79
+ write();
80
+ });
81
+ }
72
82
  async get(key) {
73
83
  try {
74
84
  const store = await this.getStore(`readonly`);
@@ -85,10 +95,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
85
95
  async set(key, value) {
86
96
  try {
87
97
  const store = await this.getStore(`readwrite`);
88
- return new Promise((resolve, reject) => {
89
- const request = store.put(value, key);
90
- request.onerror = () => reject(request.error);
91
- request.onsuccess = () => resolve();
98
+ return this.waitForWrite(store, () => {
99
+ store.put(value, key);
92
100
  });
93
101
  } catch (error) {
94
102
  if (error instanceof DOMException && error.name === `QuotaExceededError`) {
@@ -102,10 +110,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
102
110
  async delete(key) {
103
111
  try {
104
112
  const store = await this.getStore(`readwrite`);
105
- return new Promise((resolve, reject) => {
106
- const request = store.delete(key);
107
- request.onerror = () => reject(request.error);
108
- request.onsuccess = () => resolve();
113
+ return this.waitForWrite(store, () => {
114
+ store.delete(key);
109
115
  });
110
116
  } catch (error) {
111
117
  console.warn(`IndexedDB delete failed:`, error);
@@ -127,10 +133,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
127
133
  async clear() {
128
134
  try {
129
135
  const store = await this.getStore(`readwrite`);
130
- return new Promise((resolve, reject) => {
131
- const request = store.clear();
132
- request.onerror = () => reject(request.error);
133
- request.onsuccess = () => resolve();
136
+ return this.waitForWrite(store, () => {
137
+ store.clear();
134
138
  });
135
139
  } catch (error) {
136
140
  console.warn(`IndexedDB clear failed:`, error);
@@ -1 +1 @@
1
- {"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.put(value, key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.delete(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return new Promise((resolve, reject) => {\n const request = store.clear()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n private waitForWrite(\n store: IDBObjectStore,\n write: () => void,\n ): Promise<void> {\n const transaction = store.transaction\n return new Promise((resolve, reject) => {\n // A successful request can still be rolled back by its transaction.\n transaction.oncomplete = () => resolve()\n transaction.onabort = () =>\n reject(\n transaction.error ??\n new DOMException(`IndexedDB transaction aborted`, `AbortError`),\n )\n write()\n })\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.put(value, key)\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.delete(key)\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.clear()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -37,6 +37,8 @@ export interface OfflineTransaction {
37
37
  version: 1;
38
38
  }
39
39
  export interface SerializedOfflineTransaction {
40
+ /** Absent for the original Date-marker format. */
41
+ valueEncoding?: 2 | 3;
40
42
  id: string;
41
43
  mutationFnName: string;
42
44
  mutations: Array<SerializedMutation>;
@@ -94,6 +96,7 @@ export interface LeaderElection {
94
96
  onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void;
95
97
  }
96
98
  export interface TransactionSignaler {
99
+ readonly isOfflineEnabled: boolean;
97
100
  resolveTransaction: (transactionId: string, result: any) => void;
98
101
  rejectTransaction: (transactionId: string, error: Error) => void;
99
102
  registerRestorationTransaction: (offlineTransactionId: string, restorationTransaction: Transaction) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n Collection,\n MutationFnParams,\n PendingMutation,\n Transaction,\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 * The '@tanstack/offline-transactions/react-native' entry point uses ReactNativeOnlineDetector automatically.\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 TransactionSignaler {\n resolveTransaction: (transactionId: string, result: any) => void\n rejectTransaction: (transactionId: string, error: Error) => void\n registerRestorationTransaction: (\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ) => void\n isOnline: () => boolean\n}\n\nexport interface OnlineDetector {\n subscribe: (callback: () => void) => () => void\n notifyOnline: () => void\n isOnline: () => boolean\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":"AAkKO,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 Transaction,\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 /** Absent for the original Date-marker format. */\n valueEncoding?: 2 | 3\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 * The '@tanstack/offline-transactions/react-native' entry point uses ReactNativeOnlineDetector automatically.\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 TransactionSignaler {\n readonly isOfflineEnabled: boolean\n resolveTransaction: (transactionId: string, result: any) => void\n rejectTransaction: (transactionId: string, error: Error) => void\n registerRestorationTransaction: (\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ) => void\n isOnline: () => boolean\n}\n\nexport interface OnlineDetector {\n subscribe: (callback: () => void) => () => void\n notifyOnline: () => void\n isOnline: () => boolean\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":"AAqKO,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.54",
3
+ "version": "1.0.58",
4
4
  "description": "Offline-first transaction capabilities for TanStack DB",
5
5
  "author": "TanStack",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "skills"
53
53
  ],
54
54
  "dependencies": {
55
- "@tanstack/db": "0.9.0"
55
+ "@tanstack/db": "0.11.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@react-native-community/netinfo": ">=11.0.0",
@@ -51,6 +51,7 @@ export class OfflineExecutor {
51
51
  private leaderElection: LeaderElection | null
52
52
  private onlineDetector: OnlineDetector
53
53
  private isLeaderState = false
54
+ private disposed = false
54
55
  private unsubscribeOnline: (() => void) | null = null
55
56
  private unsubscribeLeadership: (() => void) | null = null
56
57
 
@@ -100,7 +101,8 @@ export class OfflineExecutor {
100
101
  this.initResolve = resolve
101
102
  this.initReject = reject
102
103
  })
103
-
104
+ // Handle constructor-started rejection; waitForInit still observes it.
105
+ void this.initPromise.catch(() => {})
104
106
  this.initialize()
105
107
  }
106
108
 
@@ -209,14 +211,19 @@ export class OfflineExecutor {
209
211
  if (this.leaderElection) {
210
212
  this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
211
213
  (isLeader) => {
214
+ // A custom elector may repeat the initial result while replay is active.
215
+ if (this.disposed || isLeader === this.isLeaderState) return
212
216
  this.isLeaderState = isLeader
217
+ if (!isLeader) this.executor?.pause()
213
218
 
214
219
  if (this.config.onLeadershipChange) {
215
220
  this.config.onLeadershipChange(isLeader)
216
221
  }
217
222
 
218
223
  if (isLeader) {
219
- this.loadAndReplayTransactions()
224
+ this.loadAndReplayTransactions().catch((error) => {
225
+ console.warn(`Failed to load and replay transactions:`, error)
226
+ })
220
227
  }
221
228
  },
222
229
  )
@@ -264,6 +271,10 @@ export class OfflineExecutor {
264
271
  try {
265
272
  // Probe storage and create adapter
266
273
  const { storage, diagnostic } = await this.createStorage()
274
+ if (this.disposed) {
275
+ this.initResolve()
276
+ return
277
+ }
267
278
 
268
279
  // Cast to writable to set readonly properties
269
280
  ;(this as any).storage = storage
@@ -295,6 +306,13 @@ export class OfflineExecutor {
295
306
 
296
307
  // Request leadership first
297
308
  const isLeader = await this.leaderElection.requestLeadership()
309
+ // Disposal may have run while leadership was pending.
310
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
311
+ if (this.disposed) {
312
+ this.leaderElection.releaseLeadership()
313
+ this.initResolve()
314
+ return
315
+ }
298
316
  this.isLeaderState = isLeader
299
317
  span.setAttribute(`isLeader`, isLeader)
300
318
 
@@ -327,22 +345,17 @@ export class OfflineExecutor {
327
345
  return
328
346
  }
329
347
 
330
- try {
331
- // Load pending transactions and restore optimistic state
332
- await this.executor.loadPendingTransactions()
348
+ // Startup must observe a failed read instead of reporting an empty outbox.
349
+ await this.executor.loadPendingTransactions()
333
350
 
334
- // Start execution in the background - don't await to avoid blocking initialization
335
- // The transactions will execute and complete asynchronously
336
- this.executor.executeAll().catch((error) => {
337
- console.warn(`Failed to execute transactions:`, error)
338
- })
339
- } catch (error) {
340
- console.warn(`Failed to load and replay transactions:`, error)
341
- }
351
+ // Replay completion is separate from initialization and durable admission.
352
+ this.executor.executeAll().catch((error) => {
353
+ console.warn(`Failed to execute transactions:`, error)
354
+ })
342
355
  }
343
356
 
344
357
  get isOfflineEnabled(): boolean {
345
- return this.mode === `offline` && this.isLeaderState
358
+ return !this.disposed && this.mode === `offline` && this.isLeaderState
346
359
  }
347
360
 
348
361
  /**
@@ -442,7 +455,11 @@ export class OfflineExecutor {
442
455
 
443
456
  try {
444
457
  await this.outbox.add(transaction)
445
- await this.executor.execute(transaction)
458
+ // A shared queue error belongs to its execution, not every caller
459
+ // that durably admitted a transaction. Per-ID signals settle callers.
460
+ this.executor.execute(transaction).catch((error) => {
461
+ console.warn(`Failed to execute transactions:`, error)
462
+ })
446
463
  span.setAttribute(`result`, `persisted`)
447
464
  } catch (error) {
448
465
  console.error(
@@ -596,6 +613,8 @@ export class OfflineExecutor {
596
613
  }
597
614
 
598
615
  dispose(): void {
616
+ this.disposed = true
617
+ this.executor?.pause()
599
618
  for (const collection of Object.values(this.config.collections)) {
600
619
  collection.deferDataRefresh = null
601
620
  }
@@ -58,8 +58,13 @@ export class OfflineTransaction {
58
58
  )
59
59
 
60
60
  try {
61
- await this.persistTransaction(offlineTransaction)
62
- // Now block and wait for the executor to complete the real mutation
61
+ // Persistence also drives the shared queue. This transaction can finish
62
+ // before that queue drains; observe both promises from the outset.
63
+ await Promise.race([
64
+ this.persistTransaction(offlineTransaction),
65
+ completionPromise,
66
+ ])
67
+ // A queue pause (offline or retry) is not transaction completion.
63
68
  await completionPromise
64
69
  } catch (error) {
65
70
  const normalizedError =
@@ -81,7 +81,12 @@ export class WebOnlineDetector implements OnlineDetector {
81
81
 
82
82
  isOnline(): boolean {
83
83
  if (typeof navigator !== `undefined`) {
84
- return navigator.onLine
84
+ // A visible tab may reach the server despite a stale browser hint.
85
+ return (
86
+ navigator.onLine ||
87
+ (typeof document !== `undefined` &&
88
+ document.visibilityState === `visible`)
89
+ )
85
90
  }
86
91
  return true
87
92
  }
@@ -14,7 +14,7 @@ export class ReactNativeOnlineDetector implements OnlineDetector {
14
14
  private netInfoUnsubscribe: (() => void) | null = null
15
15
  private appStateSubscription: NativeEventSubscription | null = null
16
16
  private isListening = false
17
- private wasConnected = true
17
+ private wasConnected = false
18
18
 
19
19
  constructor() {
20
20
  this.startListening()
@@ -27,16 +27,6 @@ export class ReactNativeOnlineDetector implements OnlineDetector {
27
27
 
28
28
  this.isListening = true
29
29
 
30
- if (typeof NetInfo.fetch === `function`) {
31
- void NetInfo.fetch()
32
- .then((state) => {
33
- this.wasConnected = this.toConnectivityState(state)
34
- })
35
- .catch(() => {
36
- // Ignore initial fetch failures and rely on subscription updates.
37
- })
38
- }
39
-
40
30
  // Subscribe to network state changes
41
31
  this.netInfoUnsubscribe = NetInfo.addEventListener((state) => {
42
32
  const isConnected = this.toConnectivityState(state)