@tanstack/offline-transactions 1.0.54 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -23
- package/dist/cjs/OfflineExecutor.cjs +28 -11
- package/dist/cjs/OfflineExecutor.cjs.map +1 -1
- package/dist/cjs/OfflineExecutor.d.cts +1 -0
- package/dist/cjs/api/OfflineTransaction.cjs +4 -1
- package/dist/cjs/api/OfflineTransaction.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 +31 -26
- package/dist/cjs/executor/KeyScheduler.cjs.map +1 -1
- package/dist/cjs/executor/KeyScheduler.d.cts +5 -3
- package/dist/cjs/executor/TransactionExecutor.cjs +72 -35
- package/dist/cjs/executor/TransactionExecutor.cjs.map +1 -1
- package/dist/cjs/executor/TransactionExecutor.d.cts +3 -2
- 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 +159 -20
- package/dist/cjs/outbox/TransactionSerializer.cjs.map +1 -1
- package/dist/cjs/outbox/TransactionSerializer.d.cts +2 -0
- package/dist/cjs/storage/IndexedDBAdapter.cjs +16 -12
- package/dist/cjs/storage/IndexedDBAdapter.cjs.map +1 -1
- package/dist/cjs/storage/IndexedDBAdapter.d.cts +1 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/cjs/types.d.cts +3 -0
- package/dist/esm/OfflineExecutor.d.ts +1 -0
- package/dist/esm/OfflineExecutor.js +28 -11
- package/dist/esm/OfflineExecutor.js.map +1 -1
- package/dist/esm/api/OfflineTransaction.js +4 -1
- package/dist/esm/api/OfflineTransaction.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 +5 -3
- package/dist/esm/executor/KeyScheduler.js +31 -26
- package/dist/esm/executor/KeyScheduler.js.map +1 -1
- package/dist/esm/executor/TransactionExecutor.d.ts +3 -2
- package/dist/esm/executor/TransactionExecutor.js +72 -35
- 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 +159 -20
- package/dist/esm/outbox/TransactionSerializer.js.map +1 -1
- package/dist/esm/storage/IndexedDBAdapter.d.ts +1 -0
- package/dist/esm/storage/IndexedDBAdapter.js +16 -12
- package/dist/esm/storage/IndexedDBAdapter.js.map +1 -1
- package/dist/esm/types.d.ts +3 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +2 -2
- package/src/OfflineExecutor.ts +34 -15
- package/src/api/OfflineTransaction.ts +7 -2
- package/src/connectivity/OnlineDetector.ts +6 -1
- package/src/connectivity/ReactNativeOnlineDetector.ts +1 -11
- package/src/executor/KeyScheduler.ts +39 -28
- package/src/executor/TransactionExecutor.ts +88 -46
- package/src/outbox/OutboxManager.ts +82 -28
- package/src/outbox/TransactionSerializer.ts +236 -22
- package/src/storage/IndexedDBAdapter.ts +23 -12
- package/src/types.ts +3 -0
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
const temporalConstructorNames = [
|
|
2
|
+
`Duration`,
|
|
3
|
+
`Instant`,
|
|
4
|
+
`PlainDate`,
|
|
5
|
+
`PlainDateTime`,
|
|
6
|
+
`PlainMonthDay`,
|
|
7
|
+
`PlainTime`,
|
|
8
|
+
`PlainYearMonth`,
|
|
9
|
+
`ZonedDateTime`
|
|
10
|
+
];
|
|
11
|
+
function getTemporalConstructorName(type) {
|
|
12
|
+
if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return;
|
|
13
|
+
const constructorName = type.slice(
|
|
14
|
+
`Temporal.`.length
|
|
15
|
+
);
|
|
16
|
+
return temporalConstructorNames.includes(constructorName) ? constructorName : void 0;
|
|
17
|
+
}
|
|
18
|
+
function requireTemporalConstructor(name) {
|
|
19
|
+
const constructor = globalThis.Temporal?.[name];
|
|
20
|
+
if (typeof constructor?.from !== `function`)
|
|
21
|
+
throw new MissingTemporalConstructorError(
|
|
22
|
+
`Missing global Temporal.${name} constructor`
|
|
23
|
+
);
|
|
24
|
+
return constructor;
|
|
25
|
+
}
|
|
26
|
+
function serializeTemporalValue(value, name) {
|
|
27
|
+
const constructor = requireTemporalConstructor(name);
|
|
28
|
+
const toString = constructor.prototype?.toString;
|
|
29
|
+
if (typeof toString !== `function` || toString === Object.prototype.toString)
|
|
30
|
+
return;
|
|
31
|
+
try {
|
|
32
|
+
const serialized = toString.call(value);
|
|
33
|
+
return typeof serialized === `string` ? serialized : void 0;
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function mayBeNativeScalar(value) {
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype !== null && prototype !== Object.prototype && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
class MissingTemporalConstructorError extends Error {
|
|
43
|
+
}
|
|
44
|
+
function setDataProperty(object, key, value) {
|
|
45
|
+
if (key !== `__proto__`) object[key] = value;
|
|
46
|
+
else
|
|
47
|
+
Object.defineProperty(object, key, {
|
|
48
|
+
value,
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true
|
|
52
|
+
});
|
|
53
|
+
}
|
|
1
54
|
class TransactionSerializer {
|
|
2
55
|
constructor(collections) {
|
|
3
56
|
this.collections = collections;
|
|
@@ -9,7 +62,9 @@ class TransactionSerializer {
|
|
|
9
62
|
serialize(transaction) {
|
|
10
63
|
const serialized = {
|
|
11
64
|
...transaction,
|
|
65
|
+
valueEncoding: 3,
|
|
12
66
|
createdAt: transaction.createdAt.toISOString(),
|
|
67
|
+
metadata: this.serializeValue(transaction.metadata, `metadata`),
|
|
13
68
|
mutations: transaction.mutations.map(
|
|
14
69
|
(mutation) => this.serializeMutation(mutation)
|
|
15
70
|
)
|
|
@@ -17,7 +72,15 @@ class TransactionSerializer {
|
|
|
17
72
|
return JSON.stringify(serialized);
|
|
18
73
|
}
|
|
19
74
|
deserialize(data) {
|
|
20
|
-
const
|
|
75
|
+
const {
|
|
76
|
+
valueEncoding,
|
|
77
|
+
...parsed
|
|
78
|
+
} = JSON.parse(data);
|
|
79
|
+
if (valueEncoding !== void 0 && valueEncoding !== 2 && valueEncoding !== 3) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Unsupported transaction value encoding: ${valueEncoding}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
21
84
|
const createdAt = new Date(parsed.createdAt);
|
|
22
85
|
if (isNaN(createdAt.getTime())) {
|
|
23
86
|
throw new Error(
|
|
@@ -27,8 +90,9 @@ class TransactionSerializer {
|
|
|
27
90
|
return {
|
|
28
91
|
...parsed,
|
|
29
92
|
createdAt,
|
|
93
|
+
metadata: valueEncoding === 3 ? this.deserializeValue(parsed.metadata, valueEncoding) : parsed.metadata,
|
|
30
94
|
mutations: parsed.mutations.map(
|
|
31
|
-
(mutationData) => this.deserializeMutation(mutationData)
|
|
95
|
+
(mutationData) => this.deserializeMutation(mutationData, valueEncoding)
|
|
32
96
|
)
|
|
33
97
|
};
|
|
34
98
|
}
|
|
@@ -49,19 +113,19 @@ class TransactionSerializer {
|
|
|
49
113
|
// Store registry key instead of collection.id
|
|
50
114
|
};
|
|
51
115
|
}
|
|
52
|
-
deserializeMutation(data) {
|
|
116
|
+
deserializeMutation(data, valueEncoding) {
|
|
53
117
|
const collection = this.collections[data.collectionId];
|
|
54
118
|
if (!collection) {
|
|
55
119
|
throw new Error(`Collection with id ${data.collectionId} not found`);
|
|
56
120
|
}
|
|
57
|
-
const modified = this.deserializeValue(data.modified);
|
|
121
|
+
const modified = this.deserializeValue(data.modified, valueEncoding);
|
|
58
122
|
const key = modified ? collection.getKeyFromItem(modified) : null;
|
|
59
123
|
return {
|
|
60
124
|
globalKey: data.globalKey,
|
|
61
125
|
type: data.type,
|
|
62
126
|
modified,
|
|
63
|
-
original: this.deserializeValue(data.original),
|
|
64
|
-
changes: this.deserializeValue(data.changes) ?? {},
|
|
127
|
+
original: this.deserializeValue(data.original, valueEncoding),
|
|
128
|
+
changes: this.deserializeValue(data.changes, valueEncoding) ?? {},
|
|
65
129
|
collection,
|
|
66
130
|
// These fields would need to be reconstructed by the executor
|
|
67
131
|
mutationId: ``,
|
|
@@ -74,25 +138,81 @@ class TransactionSerializer {
|
|
|
74
138
|
updatedAt: /* @__PURE__ */ new Date()
|
|
75
139
|
};
|
|
76
140
|
}
|
|
77
|
-
serializeValue(value) {
|
|
78
|
-
if (value === null || value
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (value instanceof Date) {
|
|
141
|
+
serializeValue(value, jsonKey, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
142
|
+
if (value === null || typeof value !== `object`) return value;
|
|
143
|
+
if (jsonKey !== false && value instanceof Date) {
|
|
82
144
|
return { __type: `Date`, value: value.toISOString() };
|
|
83
145
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
146
|
+
const temporalConstructorName = jsonKey !== false && mayBeNativeScalar(value) ? getTemporalConstructorName(value[Symbol.toStringTag]) : void 0;
|
|
147
|
+
if (temporalConstructorName) {
|
|
148
|
+
const temporalValue = serializeTemporalValue(
|
|
149
|
+
value,
|
|
150
|
+
temporalConstructorName
|
|
151
|
+
);
|
|
152
|
+
if (temporalValue !== void 0)
|
|
153
|
+
return {
|
|
154
|
+
__type: `Temporal`,
|
|
155
|
+
type: `Temporal.${temporalConstructorName}`,
|
|
156
|
+
value: temporalValue
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (ancestors.has(value))
|
|
160
|
+
throw new TypeError(`Converting circular structure to JSON`);
|
|
161
|
+
const toJSON = typeof jsonKey === `string` && value.toJSON;
|
|
162
|
+
if (typeof toJSON === `function`) {
|
|
163
|
+
const replacement = toJSON.call(value, jsonKey);
|
|
164
|
+
if (replacement === value) {
|
|
165
|
+
return this.serializeValue(replacement, false, ancestors);
|
|
166
|
+
}
|
|
167
|
+
ancestors.add(value);
|
|
168
|
+
try {
|
|
169
|
+
return this.serializeValue(replacement, false, ancestors);
|
|
170
|
+
} finally {
|
|
171
|
+
ancestors.delete(value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (jsonKey !== void 0 && (value instanceof Boolean || value instanceof BigInt || value instanceof Number || value instanceof String)) {
|
|
175
|
+
return value.valueOf();
|
|
176
|
+
}
|
|
177
|
+
ancestors.add(value);
|
|
178
|
+
const isArray = Array.isArray(value);
|
|
179
|
+
const result = isArray ? [] : {};
|
|
180
|
+
try {
|
|
181
|
+
if (isArray) {
|
|
182
|
+
const length = value.length;
|
|
183
|
+
for (let index = 0; index < length; index++) {
|
|
184
|
+
const key = String(index);
|
|
185
|
+
setDataProperty(
|
|
186
|
+
result,
|
|
187
|
+
key,
|
|
188
|
+
this.serializeValue(
|
|
189
|
+
value[index],
|
|
190
|
+
jsonKey === void 0 ? void 0 : key,
|
|
191
|
+
ancestors
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
for (const key of Object.keys(value)) {
|
|
197
|
+
setDataProperty(
|
|
198
|
+
result,
|
|
199
|
+
key,
|
|
200
|
+
this.serializeValue(
|
|
201
|
+
value[key],
|
|
202
|
+
jsonKey === void 0 ? void 0 : key,
|
|
203
|
+
ancestors
|
|
204
|
+
)
|
|
205
|
+
);
|
|
89
206
|
}
|
|
90
207
|
}
|
|
91
|
-
|
|
208
|
+
} finally {
|
|
209
|
+
ancestors.delete(value);
|
|
92
210
|
}
|
|
93
|
-
|
|
211
|
+
if (jsonKey === false && typeof result.toJSON === `function`)
|
|
212
|
+
delete result.toJSON;
|
|
213
|
+
return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`) ? { __type: `Object`, value: result } : result;
|
|
94
214
|
}
|
|
95
|
-
deserializeValue(value) {
|
|
215
|
+
deserializeValue(value, valueEncoding) {
|
|
96
216
|
if (value === null || value === void 0) {
|
|
97
217
|
return value;
|
|
98
218
|
}
|
|
@@ -108,11 +228,29 @@ class TransactionSerializer {
|
|
|
108
228
|
}
|
|
109
229
|
return date;
|
|
110
230
|
}
|
|
231
|
+
if (valueEncoding === 3 && typeof value === `object` && value.__type === `Temporal`) {
|
|
232
|
+
const constructorName = getTemporalConstructorName(value.type);
|
|
233
|
+
if (!constructorName)
|
|
234
|
+
throw new Error(`Corrupted Temporal marker: invalid type field`);
|
|
235
|
+
if (typeof value.value !== `string`)
|
|
236
|
+
throw new Error(`Corrupted Temporal marker: missing value field`);
|
|
237
|
+
return requireTemporalConstructor(constructorName).from(value.value);
|
|
238
|
+
}
|
|
111
239
|
if (typeof value === `object`) {
|
|
240
|
+
if (valueEncoding !== void 0 && value.__type === `Object`) {
|
|
241
|
+
if (value.value === null || typeof value.value !== `object` || Array.isArray(value.value)) {
|
|
242
|
+
throw new Error(`Corrupted Object marker: expected an object value`);
|
|
243
|
+
}
|
|
244
|
+
value = value.value;
|
|
245
|
+
}
|
|
112
246
|
const result = Array.isArray(value) ? [] : {};
|
|
113
247
|
for (const key in value) {
|
|
114
248
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
115
|
-
|
|
249
|
+
setDataProperty(
|
|
250
|
+
result,
|
|
251
|
+
key,
|
|
252
|
+
this.deserializeValue(value[key], valueEncoding)
|
|
253
|
+
);
|
|
116
254
|
}
|
|
117
255
|
}
|
|
118
256
|
return result;
|
|
@@ -134,6 +272,7 @@ class TransactionSerializer {
|
|
|
134
272
|
}
|
|
135
273
|
}
|
|
136
274
|
export {
|
|
275
|
+
MissingTemporalConstructorError,
|
|
137
276
|
TransactionSerializer
|
|
138
277
|
};
|
|
139
278
|
//# sourceMappingURL=TransactionSerializer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n createdAt: transaction.createdAt.toISOString(),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Parse without a reviver - let deserializeValue handle dates in mutation data\n // using the { __type: 'Date' } marker system\n const parsed: SerializedOfflineTransaction = JSON.parse(data)\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(data: SerializedMutation): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original),\n changes: this.deserializeValue(data.changes) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.serializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n private deserializeValue(value: any): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (typeof value === `object`) {\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n result[key] = this.deserializeValue(value[key])\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TransactionSerializer.js","sources":["../../../src/outbox/TransactionSerializer.ts"],"sourcesContent":["import type {\n OfflineTransaction,\n SerializedError,\n SerializedMutation,\n SerializedOfflineTransaction,\n} from '../types'\nimport type { Collection, PendingMutation } from '@tanstack/db'\n\nconst temporalConstructorNames = [\n `Duration`,\n `Instant`,\n `PlainDate`,\n `PlainDateTime`,\n `PlainMonthDay`,\n `PlainTime`,\n `PlainYearMonth`,\n `ZonedDateTime`,\n] as const\n\ntype TemporalConstructorName = (typeof temporalConstructorNames)[number]\ntype TemporalConstructor = {\n from: (value: string) => unknown\n prototype?: { toString?: () => string }\n}\n\nfunction getTemporalConstructorName(\n type: unknown,\n): TemporalConstructorName | undefined {\n if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return\n const constructorName = type.slice(\n `Temporal.`.length,\n ) as TemporalConstructorName\n return temporalConstructorNames.includes(constructorName)\n ? constructorName\n : undefined\n}\n\nfunction requireTemporalConstructor(\n name: TemporalConstructorName,\n): TemporalConstructor {\n const constructor = (\n globalThis as {\n Temporal?: Partial<Record<TemporalConstructorName, TemporalConstructor>>\n }\n ).Temporal?.[name]\n if (typeof constructor?.from !== `function`)\n throw new MissingTemporalConstructorError(\n `Missing global Temporal.${name} constructor`,\n )\n return constructor\n}\n\nfunction serializeTemporalValue(\n value: object,\n name: TemporalConstructorName,\n): string | undefined {\n const constructor = requireTemporalConstructor(name)\n const toString = constructor.prototype?.toString\n if (typeof toString !== `function` || toString === Object.prototype.toString)\n return\n try {\n const serialized = toString.call(value)\n return typeof serialized === `string` ? serialized : undefined\n } catch {\n // Temporal prototype methods brand-check their receiver. A matching\n // Symbol.toStringTag without the corresponding internal slots is user data.\n return\n }\n}\n\nfunction mayBeNativeScalar(value: object): boolean {\n const prototype = Object.getPrototypeOf(value)\n return (\n prototype !== null &&\n prototype !== Object.prototype &&\n !Array.isArray(value)\n )\n}\n\nexport class MissingTemporalConstructorError extends Error {}\n\nfunction setDataProperty(\n object: Record<string, unknown>,\n key: string,\n value: unknown,\n): void {\n if (key !== `__proto__`) object[key] = value\n else\n Object.defineProperty(object, key, {\n value,\n enumerable: true,\n configurable: true,\n writable: true,\n })\n}\n\nexport class TransactionSerializer {\n private collections: Record<string, Collection<any, any, any, any, any>>\n private collectionIdToKey: Map<string, string>\n\n constructor(\n collections: Record<string, Collection<any, any, any, any, any>>,\n ) {\n this.collections = collections\n // Create reverse lookup from collection.id to registry key\n this.collectionIdToKey = new Map()\n for (const [key, collection] of Object.entries(collections)) {\n this.collectionIdToKey.set(collection.id, key)\n }\n }\n\n serialize(transaction: OfflineTransaction): string {\n const serialized: SerializedOfflineTransaction = {\n ...transaction,\n valueEncoding: 3,\n createdAt: transaction.createdAt.toISOString(),\n metadata: this.serializeValue(transaction.metadata, `metadata`),\n mutations: transaction.mutations.map((mutation) =>\n this.serializeMutation(mutation),\n ),\n }\n return JSON.stringify(serialized)\n }\n\n deserialize(data: string): OfflineTransaction {\n // Old records retain their Date-marker meaning. New records also escape\n // marker-shaped user objects; the encoding tag is not application metadata.\n const {\n valueEncoding,\n ...parsed\n }: Omit<SerializedOfflineTransaction, `valueEncoding`> & {\n valueEncoding?: unknown\n } = JSON.parse(data)\n if (\n valueEncoding !== undefined &&\n valueEncoding !== 2 &&\n valueEncoding !== 3\n ) {\n throw new Error(\n `Unsupported transaction value encoding: ${valueEncoding}`,\n )\n }\n\n const createdAt = new Date(parsed.createdAt)\n if (isNaN(createdAt.getTime())) {\n throw new Error(\n `Failed to deserialize transaction: invalid createdAt value \"${parsed.createdAt}\"`,\n )\n }\n\n return {\n ...parsed,\n createdAt,\n metadata:\n valueEncoding === 3\n ? this.deserializeValue(parsed.metadata, valueEncoding)\n : parsed.metadata,\n mutations: parsed.mutations.map((mutationData) =>\n this.deserializeMutation(mutationData, valueEncoding),\n ),\n }\n }\n\n private serializeMutation(mutation: PendingMutation): SerializedMutation {\n const registryKey = this.collectionIdToKey.get(mutation.collection.id)\n if (!registryKey) {\n throw new Error(\n `Collection with id ${mutation.collection.id} not found in registry`,\n )\n }\n\n return {\n globalKey: mutation.globalKey,\n type: mutation.type,\n modified: this.serializeValue(mutation.modified),\n original: this.serializeValue(mutation.original),\n changes: this.serializeValue(mutation.changes),\n collectionId: registryKey, // Store registry key instead of collection.id\n }\n }\n\n private deserializeMutation(\n data: SerializedMutation,\n valueEncoding: 2 | 3 | undefined,\n ): PendingMutation {\n const collection = this.collections[data.collectionId]\n if (!collection) {\n throw new Error(`Collection with id ${data.collectionId} not found`)\n }\n\n const modified = this.deserializeValue(data.modified, valueEncoding)\n\n // Extract the key from the modified data using the collection's getKey function\n // This is needed for optimistic state restoration to work correctly\n const key = modified ? collection.getKeyFromItem(modified) : null\n\n // Create a partial PendingMutation - we can't fully reconstruct it but\n // we provide what we can. The executor will need to handle the rest.\n return {\n globalKey: data.globalKey,\n type: data.type as any,\n modified,\n original: this.deserializeValue(data.original, valueEncoding),\n changes: this.deserializeValue(data.changes, valueEncoding) ?? {},\n collection,\n // These fields would need to be reconstructed by the executor\n mutationId: ``, // Will be regenerated\n key,\n metadata: undefined,\n syncMetadata: {},\n optimistic: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n } as PendingMutation\n }\n\n private serializeValue(\n value: any,\n jsonKey?: string | false,\n ancestors = new WeakSet<object>(),\n ): any {\n if (value === null || typeof value !== `object`) return value\n\n if (jsonKey !== false && value instanceof Date) {\n return { __type: `Date`, value: value.toISOString() }\n }\n\n const temporalConstructorName =\n jsonKey !== false && mayBeNativeScalar(value)\n ? getTemporalConstructorName(value[Symbol.toStringTag])\n : undefined\n if (temporalConstructorName) {\n const temporalValue = serializeTemporalValue(\n value,\n temporalConstructorName,\n )\n if (temporalValue !== undefined)\n return {\n __type: `Temporal`,\n type: `Temporal.${temporalConstructorName}`,\n value: temporalValue,\n }\n }\n\n if (ancestors.has(value))\n throw new TypeError(`Converting circular structure to JSON`)\n\n const toJSON = typeof jsonKey === `string` && value.toJSON\n if (typeof toJSON === `function`) {\n const replacement = toJSON.call(value, jsonKey)\n if (replacement === value) {\n return this.serializeValue(replacement, false, ancestors)\n }\n\n ancestors.add(value)\n try {\n return this.serializeValue(replacement, false, ancestors)\n } finally {\n ancestors.delete(value)\n }\n }\n if (\n jsonKey !== undefined &&\n (value instanceof Boolean ||\n value instanceof BigInt ||\n value instanceof Number ||\n value instanceof String)\n ) {\n return value.valueOf()\n }\n\n ancestors.add(value)\n\n const isArray = Array.isArray(value)\n const result: any = isArray ? [] : {}\n try {\n if (isArray) {\n const length = value.length\n for (let index = 0; index < length; index++) {\n const key = String(index)\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[index],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n } else {\n for (const key of Object.keys(value)) {\n setDataProperty(\n result,\n key,\n this.serializeValue(\n value[key],\n jsonKey === undefined ? undefined : key,\n ancestors,\n ),\n )\n }\n }\n } finally {\n ancestors.delete(value)\n }\n if (jsonKey === false && typeof result.toJSON === `function`)\n delete result.toJSON\n return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`)\n ? { __type: `Object`, value: result }\n : result\n }\n\n private deserializeValue(value: any, valueEncoding: 2 | 3 | undefined): any {\n if (value === null || value === undefined) {\n return value\n }\n\n if (typeof value === `object` && value.__type === `Date`) {\n if (value.value === undefined || value.value === null) {\n throw new Error(`Corrupted Date marker: missing value field`)\n }\n const date = new Date(value.value)\n if (isNaN(date.getTime())) {\n throw new Error(\n `Failed to deserialize Date marker: invalid date value \"${value.value}\"`,\n )\n }\n return date\n }\n\n if (\n valueEncoding === 3 &&\n typeof value === `object` &&\n value.__type === `Temporal`\n ) {\n const constructorName = getTemporalConstructorName(value.type)\n if (!constructorName)\n throw new Error(`Corrupted Temporal marker: invalid type field`)\n if (typeof value.value !== `string`)\n throw new Error(`Corrupted Temporal marker: missing value field`)\n return requireTemporalConstructor(constructorName).from(value.value)\n }\n\n if (typeof value === `object`) {\n // Unwrap once, then decode only the fields: the object's own __type is data.\n if (valueEncoding !== undefined && value.__type === `Object`) {\n if (\n value.value === null ||\n typeof value.value !== `object` ||\n Array.isArray(value.value)\n ) {\n throw new Error(`Corrupted Object marker: expected an object value`)\n }\n value = value.value\n }\n const result: any = Array.isArray(value) ? [] : {}\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n setDataProperty(\n result,\n key,\n this.deserializeValue(value[key], valueEncoding),\n )\n }\n }\n return result\n }\n\n return value\n }\n\n serializeError(error: Error): SerializedError {\n return {\n name: error.name,\n message: error.message,\n stack: error.stack,\n }\n }\n\n deserializeError(data: SerializedError): Error {\n const error = new Error(data.message)\n error.name = data.name\n error.stack = data.stack\n return error\n }\n}\n"],"names":[],"mappings":"AAQA,MAAM,2BAA2B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,SAAS,2BACP,MACqC;AACrC,MAAI,OAAO,SAAS,YAAY,CAAC,KAAK,WAAW,WAAW,EAAG;AAC/D,QAAM,kBAAkB,KAAK;AAAA,IAC3B,YAAY;AAAA,EAAA;AAEd,SAAO,yBAAyB,SAAS,eAAe,IACpD,kBACA;AACN;AAEA,SAAS,2BACP,MACqB;AACrB,QAAM,cACJ,WAGA,WAAW,IAAI;AACjB,MAAI,OAAO,aAAa,SAAS;AAC/B,UAAM,IAAI;AAAA,MACR,2BAA2B,IAAI;AAAA,IAAA;AAEnC,SAAO;AACT;AAEA,SAAS,uBACP,OACA,MACoB;AACpB,QAAM,cAAc,2BAA2B,IAAI;AACnD,QAAM,WAAW,YAAY,WAAW;AACxC,MAAI,OAAO,aAAa,cAAc,aAAa,OAAO,UAAU;AAClE;AACF,MAAI;AACF,UAAM,aAAa,SAAS,KAAK,KAAK;AACtC,WAAO,OAAO,eAAe,WAAW,aAAa;AAAA,EACvD,QAAQ;AAGN;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,OAAwB;AACjD,QAAM,YAAY,OAAO,eAAe,KAAK;AAC7C,SACE,cAAc,QACd,cAAc,OAAO,aACrB,CAAC,MAAM,QAAQ,KAAK;AAExB;AAEO,MAAM,wCAAwC,MAAM;AAAC;AAE5D,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,UAAU,KAAK,eAAe,YAAY,UAAU,UAAU;AAAA,MAC9D,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,QACE,kBAAkB,UAClB,kBAAkB,KAClB,kBAAkB,GAClB;AACA,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,UACE,kBAAkB,IACd,KAAK,iBAAiB,OAAO,UAAU,aAAa,IACpD,OAAO;AAAA,MACb,WAAW,OAAO,UAAU;AAAA,QAAI,CAAC,iBAC/B,KAAK,oBAAoB,cAAc,aAAa;AAAA,MAAA;AAAA,IACtD;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,eACiB;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,aAAa;AAInE,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,aAAa;AAAA,MAC5D,SAAS,KAAK,iBAAiB,KAAK,SAAS,aAAa,KAAK,CAAA;AAAA,MAC/D;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,eACN,OACA,SACA,YAAY,oBAAI,WACX;AACL,QAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAExD,QAAI,YAAY,SAAS,iBAAiB,MAAM;AAC9C,aAAO,EAAE,QAAQ,QAAQ,OAAO,MAAM,cAAY;AAAA,IACpD;AAEA,UAAM,0BACJ,YAAY,SAAS,kBAAkB,KAAK,IACxC,2BAA2B,MAAM,OAAO,WAAW,CAAC,IACpD;AACN,QAAI,yBAAyB;AAC3B,YAAM,gBAAgB;AAAA,QACpB;AAAA,QACA;AAAA,MAAA;AAEF,UAAI,kBAAkB;AACpB,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,MAAM,YAAY,uBAAuB;AAAA,UACzC,OAAO;AAAA,QAAA;AAAA,IAEb;AAEA,QAAI,UAAU,IAAI,KAAK;AACrB,YAAM,IAAI,UAAU,uCAAuC;AAE7D,UAAM,SAAS,OAAO,YAAY,YAAY,MAAM;AACpD,QAAI,OAAO,WAAW,YAAY;AAChC,YAAM,cAAc,OAAO,KAAK,OAAO,OAAO;AAC9C,UAAI,gBAAgB,OAAO;AACzB,eAAO,KAAK,eAAe,aAAa,OAAO,SAAS;AAAA,MAC1D;AAEA,gBAAU,IAAI,KAAK;AACnB,UAAI;AACF,eAAO,KAAK,eAAe,aAAa,OAAO,SAAS;AAAA,MAC1D,UAAA;AACE,kBAAU,OAAO,KAAK;AAAA,MACxB;AAAA,IACF;AACA,QACE,YAAY,WACX,iBAAiB,WAChB,iBAAiB,UACjB,iBAAiB,UACjB,iBAAiB,SACnB;AACA,aAAO,MAAM,QAAA;AAAA,IACf;AAEA,cAAU,IAAI,KAAK;AAEnB,UAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAM,SAAc,UAAU,CAAA,IAAK,CAAA;AACnC,QAAI;AACF,UAAI,SAAS;AACX,cAAM,SAAS,MAAM;AACrB,iBAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS;AAC3C,gBAAM,MAAM,OAAO,KAAK;AACxB;AAAA,YACE;AAAA,YACA;AAAA,YACA,KAAK;AAAA,cACH,MAAM,KAAK;AAAA,cACX,YAAY,SAAY,SAAY;AAAA,cACpC;AAAA,YAAA;AAAA,UACF;AAAA,QAEJ;AAAA,MACF,OAAO;AACL,mBAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC;AAAA,YACE;AAAA,YACA;AAAA,YACA,KAAK;AAAA,cACH,MAAM,GAAG;AAAA,cACT,YAAY,SAAY,SAAY;AAAA,cACpC;AAAA,YAAA;AAAA,UACF;AAAA,QAEJ;AAAA,MACF;AAAA,IACF,UAAA;AACE,gBAAU,OAAO,KAAK;AAAA,IACxB;AACA,QAAI,YAAY,SAAS,OAAO,OAAO,WAAW;AAChD,aAAO,OAAO;AAChB,WAAO,CAAC,WAAW,OAAO,UAAU,eAAe,KAAK,OAAO,QAAQ,IACnE,EAAE,QAAQ,UAAU,OAAO,WAC3B;AAAA,EACN;AAAA,EAEQ,iBAAiB,OAAY,eAAuC;AAC1E,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,QACE,kBAAkB,KAClB,OAAO,UAAU,YACjB,MAAM,WAAW,YACjB;AACA,YAAM,kBAAkB,2BAA2B,MAAM,IAAI;AAC7D,UAAI,CAAC;AACH,cAAM,IAAI,MAAM,+CAA+C;AACjE,UAAI,OAAO,MAAM,UAAU;AACzB,cAAM,IAAI,MAAM,gDAAgD;AAClE,aAAO,2BAA2B,eAAe,EAAE,KAAK,MAAM,KAAK;AAAA,IACrE;AAEA,QAAI,OAAO,UAAU,UAAU;AAE7B,UAAI,kBAAkB,UAAa,MAAM,WAAW,UAAU;AAC5D,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,aAAa;AAAA,UAAA;AAAA,QAEnD;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;"}
|
|
@@ -14,6 +14,7 @@ export declare class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
14
14
|
}>;
|
|
15
15
|
private openDB;
|
|
16
16
|
private getStore;
|
|
17
|
+
private waitForWrite;
|
|
17
18
|
get(key: string): Promise<string | null>;
|
|
18
19
|
set(key: string, value: string): Promise<void>;
|
|
19
20
|
delete(key: string): Promise<void>;
|
|
@@ -69,6 +69,16 @@ class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
69
69
|
const transaction = db.transaction([this.storeName], mode);
|
|
70
70
|
return transaction.objectStore(this.storeName);
|
|
71
71
|
}
|
|
72
|
+
waitForWrite(store, write) {
|
|
73
|
+
const transaction = store.transaction;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
transaction.oncomplete = () => resolve();
|
|
76
|
+
transaction.onabort = () => reject(
|
|
77
|
+
transaction.error ?? new DOMException(`IndexedDB transaction aborted`, `AbortError`)
|
|
78
|
+
);
|
|
79
|
+
write();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
72
82
|
async get(key) {
|
|
73
83
|
try {
|
|
74
84
|
const store = await this.getStore(`readonly`);
|
|
@@ -85,10 +95,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
85
95
|
async set(key, value) {
|
|
86
96
|
try {
|
|
87
97
|
const store = await this.getStore(`readwrite`);
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
request.onerror = () => reject(request.error);
|
|
91
|
-
request.onsuccess = () => resolve();
|
|
98
|
+
return this.waitForWrite(store, () => {
|
|
99
|
+
store.put(value, key);
|
|
92
100
|
});
|
|
93
101
|
} catch (error) {
|
|
94
102
|
if (error instanceof DOMException && error.name === `QuotaExceededError`) {
|
|
@@ -102,10 +110,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
102
110
|
async delete(key) {
|
|
103
111
|
try {
|
|
104
112
|
const store = await this.getStore(`readwrite`);
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
request.onerror = () => reject(request.error);
|
|
108
|
-
request.onsuccess = () => resolve();
|
|
113
|
+
return this.waitForWrite(store, () => {
|
|
114
|
+
store.delete(key);
|
|
109
115
|
});
|
|
110
116
|
} catch (error) {
|
|
111
117
|
console.warn(`IndexedDB delete failed:`, error);
|
|
@@ -127,10 +133,8 @@ class IndexedDBAdapter extends BaseStorageAdapter {
|
|
|
127
133
|
async clear() {
|
|
128
134
|
try {
|
|
129
135
|
const store = await this.getStore(`readwrite`);
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
request.onerror = () => reject(request.error);
|
|
133
|
-
request.onsuccess = () => resolve();
|
|
136
|
+
return this.waitForWrite(store, () => {
|
|
137
|
+
store.clear();
|
|
134
138
|
});
|
|
135
139
|
} catch (error) {
|
|
136
140
|
console.warn(`IndexedDB clear failed:`, error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return
|
|
1
|
+
{"version":3,"file":"IndexedDBAdapter.js","sources":["../../../src/storage/IndexedDBAdapter.ts"],"sourcesContent":["import { BaseStorageAdapter } from './StorageAdapter'\n\nexport class IndexedDBAdapter extends BaseStorageAdapter {\n private dbName: string\n private storeName: string\n private db: IDBDatabase | null = null\n\n constructor(dbName = `offline-transactions`, storeName = `transactions`) {\n super()\n this.dbName = dbName\n this.storeName = storeName\n }\n\n /**\n * Probe IndexedDB availability by attempting to open a test database.\n * This catches private mode and other restrictions that block IndexedDB.\n */\n static async probe(): Promise<{ available: boolean; error?: Error }> {\n // Check if IndexedDB exists\n if (typeof indexedDB === `undefined`) {\n return {\n available: false,\n error: new Error(`IndexedDB is not available in this environment`),\n }\n }\n\n // Try to actually open a test database to verify it works\n try {\n const testDbName = `__offline-tx-probe__`\n const request = indexedDB.open(testDbName, 1)\n\n return new Promise((resolve) => {\n request.onerror = () => {\n const error = request.error || new Error(`IndexedDB open failed`)\n resolve({ available: false, error })\n }\n\n request.onsuccess = () => {\n // Clean up test database\n const db = request.result\n db.close()\n indexedDB.deleteDatabase(testDbName)\n resolve({ available: true })\n }\n\n request.onblocked = () => {\n resolve({\n available: false,\n error: new Error(`IndexedDB is blocked`),\n })\n }\n })\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n }\n\n private async openDB(): Promise<IDBDatabase> {\n if (this.db) {\n return this.db\n }\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(this.dbName, 1)\n\n request.onerror = () => reject(request.error)\n request.onsuccess = () => {\n this.db = request.result\n resolve(this.db)\n }\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result\n if (!db.objectStoreNames.contains(this.storeName)) {\n db.createObjectStore(this.storeName)\n }\n }\n })\n }\n\n private async getStore(\n mode: IDBTransactionMode = `readonly`,\n ): Promise<IDBObjectStore> {\n const db = await this.openDB()\n const transaction = db.transaction([this.storeName], mode)\n return transaction.objectStore(this.storeName)\n }\n\n private waitForWrite(\n store: IDBObjectStore,\n write: () => void,\n ): Promise<void> {\n const transaction = store.transaction\n return new Promise((resolve, reject) => {\n // A successful request can still be rolled back by its transaction.\n transaction.oncomplete = () => resolve()\n transaction.onabort = () =>\n reject(\n transaction.error ??\n new DOMException(`IndexedDB transaction aborted`, `AbortError`),\n )\n write()\n })\n }\n\n async get(key: string): Promise<string | null> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.get(key)\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result ?? null)\n })\n } catch (error) {\n console.warn(`IndexedDB get failed:`, error)\n return null\n }\n }\n\n async set(key: string, value: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.put(value, key)\n })\n } catch (error) {\n if (\n error instanceof DOMException &&\n error.name === `QuotaExceededError`\n ) {\n throw new Error(\n `Storage quota exceeded. Consider clearing old transactions.`,\n )\n }\n throw error\n }\n }\n\n async delete(key: string): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.delete(key)\n })\n } catch (error) {\n console.warn(`IndexedDB delete failed:`, error)\n }\n }\n\n async keys(): Promise<Array<string>> {\n try {\n const store = await this.getStore(`readonly`)\n return new Promise((resolve, reject) => {\n const request = store.getAllKeys()\n request.onerror = () => reject(request.error)\n request.onsuccess = () => resolve(request.result as Array<string>)\n })\n } catch (error) {\n console.warn(`IndexedDB keys failed:`, error)\n return []\n }\n }\n\n async clear(): Promise<void> {\n try {\n const store = await this.getStore(`readwrite`)\n return this.waitForWrite(store, () => {\n store.clear()\n })\n } catch (error) {\n console.warn(`IndexedDB clear failed:`, error)\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface OfflineTransaction {
|
|
|
37
37
|
version: 1;
|
|
38
38
|
}
|
|
39
39
|
export interface SerializedOfflineTransaction {
|
|
40
|
+
/** Absent for the original Date-marker format. */
|
|
41
|
+
valueEncoding?: 2 | 3;
|
|
40
42
|
id: string;
|
|
41
43
|
mutationFnName: string;
|
|
42
44
|
mutations: Array<SerializedMutation>;
|
|
@@ -94,6 +96,7 @@ export interface LeaderElection {
|
|
|
94
96
|
onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void;
|
|
95
97
|
}
|
|
96
98
|
export interface TransactionSignaler {
|
|
99
|
+
readonly isOfflineEnabled: boolean;
|
|
97
100
|
resolveTransaction: (transactionId: string, result: any) => void;
|
|
98
101
|
rejectTransaction: (transactionId: string, error: Error) => void;
|
|
99
102
|
registerRestorationTransaction: (offlineTransactionId: string, restorationTransaction: Transaction) => void;
|
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 id: string\n mutationFnName: string\n mutations: Array<SerializedMutation>\n keys: Array<string>\n idempotencyKey: string\n createdAt: string\n retryCount: number\n nextAttemptAt: number\n lastError?: SerializedError\n metadata?: Record<string, any>\n spanContext?: SerializedSpanContext\n version: 1\n}\n\n// Storage diagnostics and mode\nexport type OfflineMode = `offline` | `online-only`\n\nexport type StorageDiagnosticCode =\n | `STORAGE_AVAILABLE`\n | `INDEXEDDB_UNAVAILABLE`\n | `LOCALSTORAGE_UNAVAILABLE`\n | `STORAGE_BLOCKED`\n | `QUOTA_EXCEEDED`\n | `UNKNOWN_ERROR`\n\nexport interface StorageDiagnostic {\n code: StorageDiagnosticCode\n mode: OfflineMode\n message: string\n error?: Error\n}\n\nexport interface OfflineConfig {\n collections: Record<string, Collection<any, any, any, any, any>>\n mutationFns: Record<string, OfflineMutationFn>\n storage?: StorageAdapter\n maxConcurrency?: number\n jitter?: boolean\n beforeRetry?: (\n transactions: Array<OfflineTransaction>,\n ) => Array<OfflineTransaction>\n onUnknownMutationFn?: (name: string, tx: OfflineTransaction) => void\n onLeadershipChange?: (isLeader: boolean) => void\n onStorageFailure?: (diagnostic: StorageDiagnostic) => void\n leaderElection?: LeaderElection\n /**\n * Custom online detector implementation.\n * Defaults to WebOnlineDetector for browser environments.\n * The '@tanstack/offline-transactions/react-native' entry point uses ReactNativeOnlineDetector automatically.\n */\n onlineDetector?: OnlineDetector\n}\n\nexport interface StorageAdapter {\n get: (key: string) => Promise<string | null>\n set: (key: string, value: string) => Promise<void>\n delete: (key: string) => Promise<void>\n keys: () => Promise<Array<string>>\n clear: () => Promise<void>\n}\n\nexport interface RetryPolicy {\n calculateDelay: (retryCount: number) => number\n shouldRetry: (error: Error, retryCount: number) => boolean\n}\n\nexport interface LeaderElection {\n requestLeadership: () => Promise<boolean>\n releaseLeadership: () => void\n isLeader: () => boolean\n onLeadershipChange: (callback: (isLeader: boolean) => void) => () => void\n}\n\nexport interface TransactionSignaler {\n resolveTransaction: (transactionId: string, result: any) => void\n rejectTransaction: (transactionId: string, error: Error) => void\n registerRestorationTransaction: (\n offlineTransactionId: string,\n restorationTransaction: Transaction,\n ) => void\n isOnline: () => boolean\n}\n\nexport interface OnlineDetector {\n subscribe: (callback: () => void) => () => void\n notifyOnline: () => void\n isOnline: () => boolean\n dispose: () => void\n}\n\nexport interface CreateOfflineTransactionOptions {\n id?: string\n mutationFnName: string\n autoCommit?: boolean\n idempotencyKey?: string\n metadata?: Record<string, any>\n}\n\nexport interface CreateOfflineActionOptions<T> {\n mutationFnName: string\n onMutate: (variables: T) => void\n}\n\nexport class NonRetriableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = `NonRetriableError`\n }\n}\n"],"names":[],"mappings":"
|
|
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
|
@@ -51,6 +51,7 @@ export class OfflineExecutor {
|
|
|
51
51
|
private leaderElection: LeaderElection | null
|
|
52
52
|
private onlineDetector: OnlineDetector
|
|
53
53
|
private isLeaderState = false
|
|
54
|
+
private disposed = false
|
|
54
55
|
private unsubscribeOnline: (() => void) | null = null
|
|
55
56
|
private unsubscribeLeadership: (() => void) | null = null
|
|
56
57
|
|
|
@@ -100,7 +101,8 @@ export class OfflineExecutor {
|
|
|
100
101
|
this.initResolve = resolve
|
|
101
102
|
this.initReject = reject
|
|
102
103
|
})
|
|
103
|
-
|
|
104
|
+
// Handle constructor-started rejection; waitForInit still observes it.
|
|
105
|
+
void this.initPromise.catch(() => {})
|
|
104
106
|
this.initialize()
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -209,14 +211,19 @@ export class OfflineExecutor {
|
|
|
209
211
|
if (this.leaderElection) {
|
|
210
212
|
this.unsubscribeLeadership = this.leaderElection.onLeadershipChange(
|
|
211
213
|
(isLeader) => {
|
|
214
|
+
// A custom elector may repeat the initial result while replay is active.
|
|
215
|
+
if (this.disposed || isLeader === this.isLeaderState) return
|
|
212
216
|
this.isLeaderState = isLeader
|
|
217
|
+
if (!isLeader) this.executor?.pause()
|
|
213
218
|
|
|
214
219
|
if (this.config.onLeadershipChange) {
|
|
215
220
|
this.config.onLeadershipChange(isLeader)
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
if (isLeader) {
|
|
219
|
-
this.loadAndReplayTransactions()
|
|
224
|
+
this.loadAndReplayTransactions().catch((error) => {
|
|
225
|
+
console.warn(`Failed to load and replay transactions:`, error)
|
|
226
|
+
})
|
|
220
227
|
}
|
|
221
228
|
},
|
|
222
229
|
)
|
|
@@ -264,6 +271,10 @@ export class OfflineExecutor {
|
|
|
264
271
|
try {
|
|
265
272
|
// Probe storage and create adapter
|
|
266
273
|
const { storage, diagnostic } = await this.createStorage()
|
|
274
|
+
if (this.disposed) {
|
|
275
|
+
this.initResolve()
|
|
276
|
+
return
|
|
277
|
+
}
|
|
267
278
|
|
|
268
279
|
// Cast to writable to set readonly properties
|
|
269
280
|
;(this as any).storage = storage
|
|
@@ -295,6 +306,13 @@ export class OfflineExecutor {
|
|
|
295
306
|
|
|
296
307
|
// Request leadership first
|
|
297
308
|
const isLeader = await this.leaderElection.requestLeadership()
|
|
309
|
+
// Disposal may have run while leadership was pending.
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
311
|
+
if (this.disposed) {
|
|
312
|
+
this.leaderElection.releaseLeadership()
|
|
313
|
+
this.initResolve()
|
|
314
|
+
return
|
|
315
|
+
}
|
|
298
316
|
this.isLeaderState = isLeader
|
|
299
317
|
span.setAttribute(`isLeader`, isLeader)
|
|
300
318
|
|
|
@@ -327,22 +345,17 @@ export class OfflineExecutor {
|
|
|
327
345
|
return
|
|
328
346
|
}
|
|
329
347
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await this.executor.loadPendingTransactions()
|
|
348
|
+
// Startup must observe a failed read instead of reporting an empty outbox.
|
|
349
|
+
await this.executor.loadPendingTransactions()
|
|
333
350
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
})
|
|
339
|
-
} catch (error) {
|
|
340
|
-
console.warn(`Failed to load and replay transactions:`, error)
|
|
341
|
-
}
|
|
351
|
+
// Replay completion is separate from initialization and durable admission.
|
|
352
|
+
this.executor.executeAll().catch((error) => {
|
|
353
|
+
console.warn(`Failed to execute transactions:`, error)
|
|
354
|
+
})
|
|
342
355
|
}
|
|
343
356
|
|
|
344
357
|
get isOfflineEnabled(): boolean {
|
|
345
|
-
return this.mode === `offline` && this.isLeaderState
|
|
358
|
+
return !this.disposed && this.mode === `offline` && this.isLeaderState
|
|
346
359
|
}
|
|
347
360
|
|
|
348
361
|
/**
|
|
@@ -442,7 +455,11 @@ export class OfflineExecutor {
|
|
|
442
455
|
|
|
443
456
|
try {
|
|
444
457
|
await this.outbox.add(transaction)
|
|
445
|
-
|
|
458
|
+
// A shared queue error belongs to its execution, not every caller
|
|
459
|
+
// that durably admitted a transaction. Per-ID signals settle callers.
|
|
460
|
+
this.executor.execute(transaction).catch((error) => {
|
|
461
|
+
console.warn(`Failed to execute transactions:`, error)
|
|
462
|
+
})
|
|
446
463
|
span.setAttribute(`result`, `persisted`)
|
|
447
464
|
} catch (error) {
|
|
448
465
|
console.error(
|
|
@@ -596,6 +613,8 @@ export class OfflineExecutor {
|
|
|
596
613
|
}
|
|
597
614
|
|
|
598
615
|
dispose(): void {
|
|
616
|
+
this.disposed = true
|
|
617
|
+
this.executor?.pause()
|
|
599
618
|
for (const collection of Object.values(this.config.collections)) {
|
|
600
619
|
collection.deferDataRefresh = null
|
|
601
620
|
}
|
|
@@ -58,8 +58,13 @@ export class OfflineTransaction {
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
try {
|
|
61
|
-
|
|
62
|
-
//
|
|
61
|
+
// Persistence also drives the shared queue. This transaction can finish
|
|
62
|
+
// before that queue drains; observe both promises from the outset.
|
|
63
|
+
await Promise.race([
|
|
64
|
+
this.persistTransaction(offlineTransaction),
|
|
65
|
+
completionPromise,
|
|
66
|
+
])
|
|
67
|
+
// A queue pause (offline or retry) is not transaction completion.
|
|
63
68
|
await completionPromise
|
|
64
69
|
} catch (error) {
|
|
65
70
|
const normalizedError =
|
|
@@ -81,7 +81,12 @@ export class WebOnlineDetector implements OnlineDetector {
|
|
|
81
81
|
|
|
82
82
|
isOnline(): boolean {
|
|
83
83
|
if (typeof navigator !== `undefined`) {
|
|
84
|
-
|
|
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)
|