@tanstack/offline-transactions
Version:
Offline-first transaction capabilities for TanStack DB
287 lines (286 loc) • 9.51 kB
JavaScript
const temporalConstructorNames = [
`Duration`,
`Instant`,
`PlainDate`,
`PlainDateTime`,
`PlainMonthDay`,
`PlainTime`,
`PlainYearMonth`,
`ZonedDateTime`
];
function getTemporalConstructorName(type) {
if (typeof type !== `string` || !type.startsWith(`Temporal.`)) return;
const constructorName = type.slice(
`Temporal.`.length
);
return temporalConstructorNames.includes(constructorName) ? constructorName : void 0;
}
function requireTemporalConstructor(name) {
const constructor = globalThis.Temporal?.[name];
if (typeof constructor?.from !== `function`)
throw new MissingTemporalConstructorError(
`Missing global Temporal.${name} constructor`
);
return constructor;
}
function serializeTemporalValue(value, name) {
const constructor = requireTemporalConstructor(name);
const toString = constructor.prototype?.toString;
if (typeof toString !== `function` || toString === Object.prototype.toString)
return;
try {
const serialized = toString.call(value);
return typeof serialized === `string` ? serialized : void 0;
} catch {
return;
}
}
function mayBeNativeScalar(value) {
const prototype = Object.getPrototypeOf(value);
return prototype !== null && prototype !== Object.prototype && !Array.isArray(value);
}
class MissingTemporalConstructorError extends Error {
}
function setDataProperty(object, key, value) {
if (key !== `__proto__`) object[key] = value;
else
Object.defineProperty(object, key, {
value,
enumerable: true,
configurable: true,
writable: true
});
}
function assertOutboxPhase(phase) {
if (phase !== void 0 && phase !== `deletion-pending` && phase !== `rejection-pending`)
throw new Error(`Unsupported transaction outbox phase`);
}
class TransactionSerializer {
constructor(collections) {
this.collections = collections;
this.collectionIdToKey = /* @__PURE__ */ new Map();
for (const [key, collection] of Object.entries(collections)) {
this.collectionIdToKey.set(collection.id, key);
}
}
serialize(transaction) {
assertOutboxPhase(transaction.outboxPhase);
const serialized = {
...transaction,
valueEncoding: 3,
createdAt: transaction.createdAt.toISOString(),
metadata: this.serializeValue(transaction.metadata, `metadata`),
mutations: transaction.mutations.map(
(mutation) => this.serializeMutation(mutation)
)
};
return JSON.stringify(serialized);
}
deserialize(data) {
const {
valueEncoding,
outboxPhase,
...parsed
} = JSON.parse(data);
if (valueEncoding !== void 0 && valueEncoding !== 2 && valueEncoding !== 3) {
throw new Error(
`Unsupported transaction value encoding: ${valueEncoding}`
);
}
assertOutboxPhase(outboxPhase);
const createdAt = new Date(parsed.createdAt);
if (isNaN(createdAt.getTime())) {
throw new Error(
`Failed to deserialize transaction: invalid createdAt value "${parsed.createdAt}"`
);
}
return {
...parsed,
...outboxPhase === void 0 ? {} : { outboxPhase },
createdAt,
metadata: valueEncoding === 3 ? this.deserializeValue(parsed.metadata, valueEncoding) : parsed.metadata,
mutations: parsed.mutations.map(
(mutationData) => this.deserializeMutation(mutationData, valueEncoding)
)
};
}
serializeMutation(mutation) {
const registryKey = this.collectionIdToKey.get(mutation.collection.id);
if (!registryKey) {
throw new Error(
`Collection with id ${mutation.collection.id} not found in registry`
);
}
return {
globalKey: mutation.globalKey,
type: mutation.type,
modified: this.serializeValue(mutation.modified),
original: this.serializeValue(mutation.original),
changes: this.serializeValue(mutation.changes),
collectionId: registryKey
// Store registry key instead of collection.id
};
}
deserializeMutation(data, valueEncoding) {
const collection = this.collections[data.collectionId];
if (!collection) {
throw new Error(`Collection with id ${data.collectionId} not found`);
}
const modified = this.deserializeValue(data.modified, valueEncoding);
const key = modified ? collection.getKeyFromItem(modified) : null;
return {
globalKey: data.globalKey,
type: data.type,
modified,
original: this.deserializeValue(data.original, valueEncoding),
changes: this.deserializeValue(data.changes, valueEncoding) ?? {},
collection,
// These fields would need to be reconstructed by the executor
mutationId: ``,
// Will be regenerated
key,
metadata: void 0,
syncMetadata: {},
optimistic: true,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
};
}
serializeValue(value, jsonKey, ancestors = /* @__PURE__ */ new WeakSet()) {
if (value === null || typeof value !== `object`) return value;
if (jsonKey !== false && value instanceof Date) {
return { __type: `Date`, value: value.toISOString() };
}
const temporalConstructorName = jsonKey !== false && mayBeNativeScalar(value) ? getTemporalConstructorName(value[Symbol.toStringTag]) : void 0;
if (temporalConstructorName) {
const temporalValue = serializeTemporalValue(
value,
temporalConstructorName
);
if (temporalValue !== void 0)
return {
__type: `Temporal`,
type: `Temporal.${temporalConstructorName}`,
value: temporalValue
};
}
if (ancestors.has(value))
throw new TypeError(`Converting circular structure to JSON`);
const toJSON = typeof jsonKey === `string` && value.toJSON;
if (typeof toJSON === `function`) {
const replacement = toJSON.call(value, jsonKey);
if (replacement === value) {
return this.serializeValue(replacement, false, ancestors);
}
ancestors.add(value);
try {
return this.serializeValue(replacement, false, ancestors);
} finally {
ancestors.delete(value);
}
}
if (jsonKey !== void 0 && (value instanceof Boolean || value instanceof BigInt || value instanceof Number || value instanceof String)) {
return value.valueOf();
}
ancestors.add(value);
const isArray = Array.isArray(value);
const result = isArray ? [] : {};
try {
if (isArray) {
const length = value.length;
for (let index = 0; index < length; index++) {
const key = String(index);
setDataProperty(
result,
key,
this.serializeValue(
value[index],
jsonKey === void 0 ? void 0 : key,
ancestors
)
);
}
} else {
for (const key of Object.keys(value)) {
setDataProperty(
result,
key,
this.serializeValue(
value[key],
jsonKey === void 0 ? void 0 : key,
ancestors
)
);
}
}
} finally {
ancestors.delete(value);
}
if (jsonKey === false && typeof result.toJSON === `function`)
delete result.toJSON;
return !isArray && Object.prototype.hasOwnProperty.call(value, `__type`) ? { __type: `Object`, value: result } : result;
}
deserializeValue(value, valueEncoding) {
if (value === null || value === void 0) {
return value;
}
if (typeof value === `object` && value.__type === `Date`) {
if (value.value === void 0 || value.value === null) {
throw new Error(`Corrupted Date marker: missing value field`);
}
const date = new Date(value.value);
if (isNaN(date.getTime())) {
throw new Error(
`Failed to deserialize Date marker: invalid date value "${value.value}"`
);
}
return date;
}
if (valueEncoding === 3 && typeof value === `object` && value.__type === `Temporal`) {
const constructorName = getTemporalConstructorName(value.type);
if (!constructorName)
throw new Error(`Corrupted Temporal marker: invalid type field`);
if (typeof value.value !== `string`)
throw new Error(`Corrupted Temporal marker: missing value field`);
return requireTemporalConstructor(constructorName).from(value.value);
}
if (typeof value === `object`) {
if (valueEncoding !== void 0 && value.__type === `Object`) {
if (value.value === null || typeof value.value !== `object` || Array.isArray(value.value)) {
throw new Error(`Corrupted Object marker: expected an object value`);
}
value = value.value;
}
const result = Array.isArray(value) ? [] : {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
setDataProperty(
result,
key,
this.deserializeValue(value[key], valueEncoding)
);
}
}
return result;
}
return value;
}
serializeError(error) {
return {
name: error.name,
message: error.message,
stack: error.stack
};
}
deserializeError(data) {
const error = new Error(data.message);
error.name = data.name;
error.stack = data.stack;
return error;
}
}
export {
MissingTemporalConstructorError,
TransactionSerializer
};
//# sourceMappingURL=TransactionSerializer.js.map