UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

287 lines (286 loc) • 9.51 kB
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