@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.
- package/README.md +137 -23
- package/dist/cjs/OfflineExecutor.cjs +2 -0
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/connectivity/OnlineDetector.cjs +1 -1
- package/dist/cjs/connectivity/OnlineDetector.cjs.map +1 -1
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs +1 -7
- package/dist/cjs/connectivity/ReactNativeOnlineDetector.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.cjs +24 -25
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.d.cts +4 -2
- package/dist/cjs/executor/TransactionExecutor.cjs +60 -30
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.cjs +66 -24
- package/dist/cjs/outbox/OutboxManager.cjs.map +1 -1
- package/dist/cjs/outbox/OutboxManager.d.cts +6 -0
- package/dist/cjs/outbox/TransactionSerializer.cjs +132 -22
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +1 -1
- package/dist/esm/OfflineExecutor.js +2 -0
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/connectivity/OnlineDetector.js +1 -1
- package/dist/esm/connectivity/OnlineDetector.js.map +1 -1
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js +1 -7
- package/dist/esm/connectivity/ReactNativeOnlineDetector.js.map +1 -1
- package/dist/esm/executor/KeyScheduler.d.ts +4 -2
- package/dist/esm/executor/KeyScheduler.js +24 -25
- package/dist/esm/executor/KeyScheduler.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.js +60 -30
- package/dist/esm/executor/TransactionExecutor.js.map +1 -1
- package/dist/esm/outbox/OutboxManager.d.ts +6 -0
- package/dist/esm/outbox/OutboxManager.js +67 -25
- package/dist/esm/outbox/OutboxManager.js.map +1 -1
- package/dist/esm/outbox/TransactionSerializer.d.ts +2 -0
- package/dist/esm/outbox/TransactionSerializer.js +132 -22
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/types.d.ts +1 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/OfflineExecutor.ts +2 -1
- package/src/connectivity/OnlineDetector.ts +6 -1
- package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
- package/src/executor/KeyScheduler.ts +29 -26
- package/src/executor/TransactionExecutor.ts +71 -43
- package/src/outbox/OutboxManager.ts +82 -28
- package/src/outbox/TransactionSerializer.ts +192 -25
- 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:
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
83
|
-
changes: this.deserializeValue(data.changes,
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
208
|
+
} finally {
|
|
209
|
+
ancestors.delete(value);
|
|
111
210
|
}
|
|
112
|
-
|
|
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,
|
|
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 (
|
|
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],
|
|
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":"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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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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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>;
|
package/dist/esm/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n Collection,\n MutationFnParams,\n PendingMutation,\n 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.
|
|
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.
|
|
55
|
+
"@tanstack/db": "0.11.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@react-native-community/netinfo": ">=11.0.0",
|
package/src/OfflineExecutor.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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(
|
|
63
|
-
this.
|
|
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.
|
|
71
|
+
this.activeTransactionId = undefined
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
markFailed(_transaction: OfflineTransaction): void {
|
|
72
|
-
this.
|
|
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
|
-
|
|
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.
|
|
96
|
+
return this.activeTransactionId === undefined ? 0 : 1
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
clear(): void {
|
|
106
100
|
this.pendingTransactions = []
|
|
107
|
-
this.
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(),
|