@tanstack/offline-transactions 1.0.56 → 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 (48) hide show
  1. package/README.md +137 -23
  2. package/dist/cjs/OfflineExecutor.cjs +2 -0
  3. package/dist/cjs/OfflineExecutor.cjs.map +1 -1
  4. package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
  5. package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
  6. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
  7. package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
  8. package/dist/cjs/executor/KeyScheduler.cjs +24 -25
  9. package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
  10. package/dist/cjs/executor/KeyScheduler.d.cts +4 -2
  11. package/dist/cjs/executor/TransactionExecutor.cjs +60 -30
  12. package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
  13. package/dist/cjs/outbox/OutboxManager.cjs +66 -24
  14. package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
  15. package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
  16. package/dist/cjs/outbox/TransactionSerializer.cjs +132 -22
  17. package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
  18. package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
  19. package/dist/cjs/types.cjs.map +1 -1
  20. package/dist/cjs/types.d.cts +1 -1
  21. package/dist/esm/OfflineExecutor.js +2 -0
  22. package/dist/esm/OfflineExecutor.js.map +1 -1
  23. package/dist/esm/connectivity/OnlineDetector.js +1 -1
  24. package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
  25. package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
  26. package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
  27. package/dist/esm/executor/KeyScheduler.d.ts +4 -2
  28. package/dist/esm/executor/KeyScheduler.js +24 -25
  29. package/dist/esm/executor/KeyScheduler.js.map +1 -1
  30. package/dist/esm/executor/TransactionExecutor.js +60 -30
  31. package/dist/esm/executor/TransactionExecutor.js.map +1 -1
  32. package/dist/esm/outbox/OutboxManager.d.ts +6 -0
  33. package/dist/esm/outbox/OutboxManager.js +67 -25
  34. package/dist/esm/outbox/OutboxManager.js.map +1 -1
  35. package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
  36. package/dist/esm/outbox/TransactionSerializer.js +132 -22
  37. package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
  38. package/dist/esm/types.d.ts +1 -1
  39. package/dist/esm/types.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/OfflineExecutor.ts +2 -1
  42. package/src/connectivity/OnlineDetector.ts +6 -1
  43. package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
  44. package/src/executor/KeyScheduler.ts +29 -26
  45. package/src/executor/TransactionExecutor.ts +71 -43
  46. package/src/outbox/OutboxManager.ts +82 -28
  47. package/src/outbox/TransactionSerializer.ts +192 -25
  48. package/src/types.ts +1 -1
@@ -1,3 +1,46 @@
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
+ }
1
44
  function setDataProperty(object, key, value) {
2
45
  if (key !== `__proto__`) object[key] = value;
3
46
  else
@@ -19,8 +62,9 @@ class TransactionSerializer {
19
62
  serialize(transaction) {
20
63
  const serialized = {
21
64
  ...transaction,
22
- valueEncoding: 2,
65
+ valueEncoding: 3,
23
66
  createdAt: transaction.createdAt.toISOString(),
67
+ metadata: this.serializeValue(transaction.metadata, `metadata`),
24
68
  mutations: transaction.mutations.map(
25
69
  (mutation) => this.serializeMutation(mutation)
26
70
  )
@@ -32,7 +76,7 @@ class TransactionSerializer {
32
76
  valueEncoding,
33
77
  ...parsed
34
78
  } = JSON.parse(data);
35
- if (valueEncoding !== void 0 && valueEncoding !== 2) {
79
+ if (valueEncoding !== void 0 && valueEncoding !== 2 && valueEncoding !== 3) {
36
80
  throw new Error(
37
81
  `Unsupported transaction value encoding: ${valueEncoding}`
38
82
  );
@@ -46,8 +90,9 @@ class TransactionSerializer {
46
90
  return {
47
91
  ...parsed,
48
92
  createdAt,
93
+ metadata: valueEncoding === 3 ? this.deserializeValue(parsed.metadata, valueEncoding) : parsed.metadata,
49
94
  mutations: parsed.mutations.map(
50
- (mutationData) => this.deserializeMutation(mutationData, valueEncoding === 2)
95
+ (mutationData) => this.deserializeMutation(mutationData, valueEncoding)
51
96
  )
52
97
  };
53
98
  }
@@ -68,19 +113,19 @@ class TransactionSerializer {
68
113
  // Store registry key instead of collection.id
69
114
  };
70
115
  }
71
- deserializeMutation(data, escapedObjects) {
116
+ deserializeMutation(data, valueEncoding) {
72
117
  const collection = this.collections[data.collectionId];
73
118
  if (!collection) {
74
119
  throw new Error(`Collection with id ${data.collectionId} not found`);
75
120
  }
76
- const modified = this.deserializeValue(data.modified, escapedObjects);
121
+ const modified = this.deserializeValue(data.modified, valueEncoding);
77
122
  const key = modified ? collection.getKeyFromItem(modified) : null;
78
123
  return {
79
124
  globalKey: data.globalKey,
80
125
  type: data.type,
81
126
  modified,
82
- original: this.deserializeValue(data.original, escapedObjects),
83
- changes: this.deserializeValue(data.changes, escapedObjects) ?? {},
127
+ original: this.deserializeValue(data.original, valueEncoding),
128
+ changes: this.deserializeValue(data.changes, valueEncoding) ?? {},
84
129
  collection,
85
130
  // These fields would need to be reconstructed by the executor
86
131
  mutationId: ``,
@@ -93,25 +138,81 @@ class TransactionSerializer {
93
138
  updatedAt: /* @__PURE__ */ new Date()
94
139
  };
95
140
  }
96
- serializeValue(value) {
97
- if (value === null || value === void 0) {
98
- return value;
99
- }
100
- 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) {
101
144
  return { __type: `Date`, value: value.toISOString() };
102
145
  }
103
- if (typeof value === `object`) {
104
- const result = Array.isArray(value) ? [] : {};
105
- for (const key in value) {
106
- if (Object.prototype.hasOwnProperty.call(value, key)) {
107
- setDataProperty(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
+ );
108
206
  }
109
207
  }
110
- return !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, `__type`) ? { __type: `Object`, value: result } : result;
208
+ } finally {
209
+ ancestors.delete(value);
111
210
  }
112
- 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;
113
214
  }
114
- deserializeValue(value, escapedObjects) {
215
+ deserializeValue(value, valueEncoding) {
115
216
  if (value === null || value === void 0) {
116
217
  return value;
117
218
  }
@@ -127,8 +228,16 @@ class TransactionSerializer {
127
228
  }
128
229
  return date;
129
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
+ }
130
239
  if (typeof value === `object`) {
131
- if (escapedObjects && value.__type === `Object`) {
240
+ if (valueEncoding !== void 0 && value.__type === `Object`) {
132
241
  if (value.value === null || typeof value.value !== `object` || Array.isArray(value.value)) {
133
242
  throw new Error(`Corrupted Object marker: expected an object value`);
134
243
  }
@@ -140,7 +249,7 @@ class TransactionSerializer {
140
249
  setDataProperty(
141
250
  result,
142
251
  key,
143
- this.deserializeValue(value[key], escapedObjects)
252
+ this.deserializeValue(value[key], valueEncoding)
144
253
  );
145
254
  }
146
255
  }
@@ -163,6 +272,7 @@ class TransactionSerializer {
163
272
  }
164
273
  }
165
274
  export {
275
+ MissingTemporalConstructorError,
166
276
  TransactionSerializer
167
277
  };
168
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\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: 2,\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 // 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 (valueEncoding !== undefined && valueEncoding !== 2) {\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 mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData, valueEncoding === 2),\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 escapedObjects: boolean,\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, escapedObjects)\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, escapedObjects),\n changes: this.deserializeValue(data.changes, escapedObjects) ?? {},\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 setDataProperty(result, key, this.serializeValue(value[key]))\n }\n }\n return !Array.isArray(value) &&\n Object.prototype.hasOwnProperty.call(value, `__type`)\n ? { __type: `Object`, value: result }\n : result\n }\n\n return value\n }\n\n private deserializeValue(value: any, escapedObjects: boolean): 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 // Unwrap once, then decode only the fields: the object's own __type is data.\n if (escapedObjects && 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], escapedObjects),\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":"AAQA,SAAS,gBACP,QACA,KACA,OACM;AACN,MAAI,QAAQ,YAAa,QAAO,GAAG,IAAI;AAAA;AAErC,WAAO,eAAe,QAAQ,KAAK;AAAA,MACjC;AAAA,MACA,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,UAAU;AAAA,IAAA,CACX;AACL;AAEO,MAAM,sBAAsB;AAAA,EAIjC,YACE,aACA;AACA,SAAK,cAAc;AAEnB,SAAK,wCAAwB,IAAA;AAC7B,eAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC3D,WAAK,kBAAkB,IAAI,WAAW,IAAI,GAAG;AAAA,IAC/C;AAAA,EACF;AAAA,EAEA,UAAU,aAAyC;AACjD,UAAM,aAA2C;AAAA,MAC/C,GAAG;AAAA,MACH,eAAe;AAAA,MACf,WAAW,YAAY,UAAU,YAAA;AAAA,MACjC,WAAW,YAAY,UAAU;AAAA,QAAI,CAAC,aACpC,KAAK,kBAAkB,QAAQ;AAAA,MAAA;AAAA,IACjC;AAEF,WAAO,KAAK,UAAU,UAAU;AAAA,EAClC;AAAA,EAEA,YAAY,MAAkC;AAG5C,UAAM;AAAA,MACJ;AAAA,MACA,GAAG;AAAA,IAAA,IAGD,KAAK,MAAM,IAAI;AACnB,QAAI,kBAAkB,UAAa,kBAAkB,GAAG;AACtD,YAAM,IAAI;AAAA,QACR,2CAA2C,aAAa;AAAA,MAAA;AAAA,IAE5D;AAEA,UAAM,YAAY,IAAI,KAAK,OAAO,SAAS;AAC3C,QAAI,MAAM,UAAU,QAAA,CAAS,GAAG;AAC9B,YAAM,IAAI;AAAA,QACR,+DAA+D,OAAO,SAAS;AAAA,MAAA;AAAA,IAEnF;AAEA,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,MACA,WAAW,OAAO,UAAU;AAAA,QAAI,CAAC,iBAC/B,KAAK,oBAAoB,cAAc,kBAAkB,CAAC;AAAA,MAAA;AAAA,IAC5D;AAAA,EAEJ;AAAA,EAEQ,kBAAkB,UAA+C;AACvE,UAAM,cAAc,KAAK,kBAAkB,IAAI,SAAS,WAAW,EAAE;AACrE,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI;AAAA,QACR,sBAAsB,SAAS,WAAW,EAAE;AAAA,MAAA;AAAA,IAEhD;AAEA,WAAO;AAAA,MACL,WAAW,SAAS;AAAA,MACpB,MAAM,SAAS;AAAA,MACf,UAAU,KAAK,eAAe,SAAS,QAAQ;AAAA,MAC/C,UAAU,KAAK,eAAe,SAAS,QAAQ;AAAA,MAC/C,SAAS,KAAK,eAAe,SAAS,OAAO;AAAA,MAC7C,cAAc;AAAA;AAAA,IAAA;AAAA,EAElB;AAAA,EAEQ,oBACN,MACA,gBACiB;AACjB,UAAM,aAAa,KAAK,YAAY,KAAK,YAAY;AACrD,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,sBAAsB,KAAK,YAAY,YAAY;AAAA,IACrE;AAEA,UAAM,WAAW,KAAK,iBAAiB,KAAK,UAAU,cAAc;AAIpE,UAAM,MAAM,WAAW,WAAW,eAAe,QAAQ,IAAI;AAI7D,WAAO;AAAA,MACL,WAAW,KAAK;AAAA,MAChB,MAAM,KAAK;AAAA,MACX;AAAA,MACA,UAAU,KAAK,iBAAiB,KAAK,UAAU,cAAc;AAAA,MAC7D,SAAS,KAAK,iBAAiB,KAAK,SAAS,cAAc,KAAK,CAAA;AAAA,MAChE;AAAA;AAAA,MAEA,YAAY;AAAA;AAAA,MACZ;AAAA,MACA,UAAU;AAAA,MACV,cAAc,CAAA;AAAA,MACd,YAAY;AAAA,MACZ,+BAAe,KAAA;AAAA,MACf,+BAAe,KAAA;AAAA,IAAK;AAAA,EAExB;AAAA,EAEQ,eAAe,OAAiB;AACtC,QAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,aAAO;AAAA,IACT;AAEA,QAAI,iBAAiB,MAAM;AACzB,aAAO,EAAE,QAAQ,QAAQ,OAAO,MAAM,cAAY;AAAA,IACpD;AAEA,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,SAAc,MAAM,QAAQ,KAAK,IAAI,CAAA,IAAK,CAAA;AAChD,iBAAW,OAAO,OAAO;AACvB,YAAI,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG;AACpD,0BAAgB,QAAQ,KAAK,KAAK,eAAe,MAAM,GAAG,CAAC,CAAC;AAAA,QAC9D;AAAA,MACF;AACA,aAAO,CAAC,MAAM,QAAQ,KAAK,KACzB,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ,IAClD,EAAE,QAAQ,UAAU,OAAO,WAC3B;AAAA,IACN;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,iBAAiB,OAAY,gBAA8B;AACjE,QAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,UAAU,YAAY,MAAM,WAAW,QAAQ;AACxD,UAAI,MAAM,UAAU,UAAa,MAAM,UAAU,MAAM;AACrD,cAAM,IAAI,MAAM,4CAA4C;AAAA,MAC9D;AACA,YAAM,OAAO,IAAI,KAAK,MAAM,KAAK;AACjC,UAAI,MAAM,KAAK,QAAA,CAAS,GAAG;AACzB,cAAM,IAAI;AAAA,UACR,0DAA0D,MAAM,KAAK;AAAA,QAAA;AAAA,MAEzE;AACA,aAAO;AAAA,IACT;AAEA,QAAI,OAAO,UAAU,UAAU;AAE7B,UAAI,kBAAkB,MAAM,WAAW,UAAU;AAC/C,YACE,MAAM,UAAU,QAChB,OAAO,MAAM,UAAU,YACvB,MAAM,QAAQ,MAAM,KAAK,GACzB;AACA,gBAAM,IAAI,MAAM,mDAAmD;AAAA,QACrE;AACA,gBAAQ,MAAM;AAAA,MAChB;AACA,YAAM,SAAc,MAAM,QAAQ,KAAK,IAAI,CAAA,IAAK,CAAA;AAChD,iBAAW,OAAO,OAAO;AACvB,YAAI,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG;AACpD;AAAA,YACE;AAAA,YACA;AAAA,YACA,KAAK,iBAAiB,MAAM,GAAG,GAAG,cAAc;AAAA,UAAA;AAAA,QAEpD;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,OAA+B;AAC5C,WAAO;AAAA,MACL,MAAM,MAAM;AAAA,MACZ,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,IAAA;AAAA,EAEjB;AAAA,EAEA,iBAAiB,MAA8B;AAC7C,UAAM,QAAQ,IAAI,MAAM,KAAK,OAAO;AACpC,UAAM,OAAO,KAAK;AAClB,UAAM,QAAQ,KAAK;AACnB,WAAO;AAAA,EACT;AACF;"}
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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"}
@@ -38,7 +38,7 @@ export interface OfflineTransaction {
38
38
  }
39
39
  export interface SerializedOfflineTransaction {
40
40
  /** Absent for the original Date-marker format. */
41
- valueEncoding?: 2;
41
+ valueEncoding?: 2 | 3;
42
42
  id: string;
43
43
  mutationFnName: string;
44
44
  mutations: Array<SerializedMutation>;
@@ -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 /** Absent for the original Date-marker format. */\n valueEncoding?: 2\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;"}
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.56",
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.2"
55
+ "@tanstack/db": "0.11.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@react-native-community/netinfo": ">=11.0.0",
@@ -101,7 +101,8 @@ export class OfflineExecutor {
101
101
  this.initResolve = resolve
102
102
  this.initReject = reject
103
103
  })
104
-
104
+ // Handle constructor-started rejection; waitForInit still observes it.
105
+ void this.initPromise.catch(() => {})
105
106
  this.initialize()
106
107
  }
107
108
 
@@ -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)
@@ -3,7 +3,7 @@ import type { OfflineTransaction } from '../types'
3
3
 
4
4
  export class KeyScheduler {
5
5
  private pendingTransactions: Array<OfflineTransaction> = []
6
- private isRunning = false
6
+ private activeTransactionId: string | undefined
7
7
 
8
8
  schedule(transaction: OfflineTransaction): boolean {
9
9
  return withSyncSpan(
@@ -35,7 +35,10 @@ export class KeyScheduler {
35
35
  `scheduler.getNext`,
36
36
  { pendingCount: this.pendingTransactions.length },
37
37
  (span) => {
38
- if (this.isRunning || this.pendingTransactions.length === 0) {
38
+ if (
39
+ this.activeTransactionId !== undefined ||
40
+ this.pendingTransactions.length === 0
41
+ ) {
39
42
  span.setAttribute(`result`, `empty`)
40
43
  return undefined
41
44
  }
@@ -59,17 +62,17 @@ export class KeyScheduler {
59
62
  return Date.now() >= transaction.nextAttemptAt
60
63
  }
61
64
 
62
- markStarted(_transaction: OfflineTransaction): void {
63
- this.isRunning = true
65
+ markStarted(transaction: OfflineTransaction): void {
66
+ this.activeTransactionId = transaction.id
64
67
  }
65
68
 
66
69
  markCompleted(transaction: OfflineTransaction): void {
67
70
  this.removeTransaction(transaction)
68
- this.isRunning = false
71
+ this.activeTransactionId = undefined
69
72
  }
70
73
 
71
74
  markFailed(_transaction: OfflineTransaction): void {
72
- this.isRunning = false
75
+ this.activeTransactionId = undefined
73
76
  }
74
77
 
75
78
  private removeTransaction(transaction: OfflineTransaction): void {
@@ -82,16 +85,7 @@ export class KeyScheduler {
82
85
  }
83
86
 
84
87
  updateTransaction(transaction: OfflineTransaction): void {
85
- const index = this.pendingTransactions.findIndex(
86
- (tx) => tx.id === transaction.id,
87
- )
88
- if (index >= 0) {
89
- this.pendingTransactions[index] = transaction
90
- // Re-sort to maintain FIFO order after update
91
- this.pendingTransactions.sort(
92
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
93
- )
94
- }
88
+ this.updateTransactions([transaction])
95
89
  }
96
90
 
97
91
  getPendingCount(): number {
@@ -99,12 +93,23 @@ export class KeyScheduler {
99
93
  }
100
94
 
101
95
  getRunningCount(): number {
102
- return this.isRunning ? 1 : 0
96
+ return this.activeTransactionId === undefined ? 0 : 1
103
97
  }
104
98
 
105
99
  clear(): void {
106
100
  this.pendingTransactions = []
107
- this.isRunning = false
101
+ this.activeTransactionId = undefined
102
+ }
103
+
104
+ /** @internal Reconcile one replay snapshot without canceling issued work. */
105
+ removePendingTransactions(transactionIds: Iterable<string>): Array<string> {
106
+ const ids = new Set(transactionIds)
107
+ if (this.activeTransactionId !== undefined)
108
+ ids.delete(this.activeTransactionId)
109
+ this.pendingTransactions = this.pendingTransactions.filter(
110
+ ({ id }) => !ids.has(id),
111
+ )
112
+ return [...ids]
108
113
  }
109
114
 
110
115
  getAllPendingTransactions(): Array<OfflineTransaction> {
@@ -112,14 +117,12 @@ export class KeyScheduler {
112
117
  }
113
118
 
114
119
  updateTransactions(updatedTransactions: Array<OfflineTransaction>): void {
115
- for (const updatedTx of updatedTransactions) {
116
- const index = this.pendingTransactions.findIndex(
117
- (tx) => tx.id === updatedTx.id,
118
- )
119
- if (index >= 0) {
120
- this.pendingTransactions[index] = updatedTx
121
- }
122
- }
120
+ const updatedById = new Map(
121
+ updatedTransactions.map((transaction) => [transaction.id, transaction]),
122
+ )
123
+ this.pendingTransactions = this.pendingTransactions.map(
124
+ (transaction) => updatedById.get(transaction.id) ?? transaction,
125
+ )
123
126
  // Re-sort to maintain FIFO order after updates
124
127
  this.pendingTransactions.sort(
125
128
  (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),