UNPKG

@baqhub/sdk-react

Version:

The official React SDK for the BAQ federated app platform.

196 lines (195 loc) 7.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyUpdates = applyUpdates; exports.applyProxyUpdates = applyProxyUpdates; exports.performMutationRequest = performMutationRequest; const sdk_1 = require("@baqhub/sdk"); function updateState(state, key, update) { // Not a standing record: normal state update. if ("noContent" in update || !sdk_1.Record.hasType(update, sdk_1.StandingRecord)) { return { ...state, [key]: update }; } // Otherwise, filter out all the "notification_unknown" records. const isPublisherUnknownRecord = (record) => { if ("noContent" in record) { return true; } if (record.source !== sdk_1.RecordSource.NOTIFICATION_UNKNOWN) { return true; } if (record.author.entity !== update.content.publisher.entity) { return true; } return false; }; const filteredRecords = Object.values(state).filter(isPublisherUnknownRecord); return [...filteredRecords, update].reduce((result, record) => { result[sdk_1.Record.toKey(record)] = record; return result; }, {}); } function applyUpdates(initialState, initialMutations, updates) { return updates.reduce((result, update) => { const { state, mutations } = result; const key = sdk_1.Record.toKey(update); const existing = state[key]; const existingVCA = existing?.version?.createdAt || new Date(0); const versionCreatedAt = update.version?.createdAt || new Date(0); if (update.source === "proxy") { throw new Error("Unexpected proxy record"); } if (existingVCA > versionCreatedAt) { return result; } const newState = updateState(state, key, update); if (update.mode === sdk_1.RecordMode.SYNCED && !update.version?.hash) { const mutation = { state, record: update, followingUpdates: [], }; return { state: newState, mutations: [...mutations, mutation], }; } const lastMutation = mutations[mutations.length - 1]; if (!lastMutation) { return { state: newState, mutations, }; } return { state: newState, mutations: [ ...mutations.slice(0, -1), { ...lastMutation, followingUpdates: [...lastMutation.followingUpdates, update], }, ], }; }, { state: initialState, mutations: initialMutations, }); } function applyProxyUpdates(initialState, initialMutations, updates) { return updates.reduce((result, update) => { const { state, mutations } = result; const key = sdk_1.Record.toKey(update); const existing = state[key]; const existingVCA = existing?.version?.createdAt || new Date(0); const versionCreatedAt = update.version?.createdAt || new Date(0); if (existingVCA > versionCreatedAt) { return result; } const newState = { ...state, [key]: update }; return { state: newState, mutations, }; }, { state: initialState, mutations: initialMutations, }); } async function performMutationRequest(model, eventModel, entity, client, mutation, signal) { const performRequest = async ({ state, record, followingUpdates }, attempt) => { const key = sdk_1.Record.toKey(record); const existing = state[key]; const newRecord = (() => { // Create record. if ((!existing || existing.mode !== sdk_1.RecordMode.SYNCED) && "content" in record) { return record; } // Update / Delete. if (!existing || !existing.version) { throw new Error("Unexpected record update."); } const versionCreatedAt = existing.version.createdAt; if (record.version && record.version.createdAt <= versionCreatedAt) { throw new Error("CreatedAt needs to be more recent."); } // Make sure the new date is higher. // TODO: bound this and keep local server offset. const now = new Date(); const newVersionCreatedAt = versionCreatedAt > now ? new Date(versionCreatedAt.getTime() + 1) : now; // Update record or delete record. const newVersion = { author: { entity }, hash: undefined, createdAt: newVersionCreatedAt, receivedAt: undefined, ...record.version, parentHash: existing.version.hash, }; return { ...record, version: newVersion, }; })(); try { const { record, linkedRecords } = await client.postRecord(model, eventModel, newRecord, signal); return applyUpdates(state, [], [record, ...linkedRecords, ...followingUpdates]); } catch (err) { // Permanent error OR Conflict on create: revert. if (sdk_1.Http.isError(err, [ sdk_1.HttpStatusCode.BAD_REQUEST, sdk_1.HttpStatusCode.NOT_FOUND, ]) || ((!newRecord.version?.parentHash || attempt > 3) && sdk_1.Http.isError(err, [sdk_1.HttpStatusCode.CONFLICT]))) { return applyUpdates(state, [], followingUpdates); } // Conflict on update, fetch latest version and resolve. if (sdk_1.Http.isError(err, [sdk_1.HttpStatusCode.CONFLICT])) { return fetchLatestAndUpdate(mutation, newRecord, attempt); } throw err; } }; const fetchLatestAndUpdate = async ({ state, record, followingUpdates }, newRecord, attempt) => { try { const { record: latest, linkedRecords } = await client.getRecord(model, eventModel, record.author.entity, record.id, { query: { includeDeleted: true }, signal, }); if (!latest.version || !newRecord.version) { (0, sdk_1.never)(); } // We may want to use the latest version instead of updating. // TODO: // - Improve same record detection logic. // - Compare content while ignoring missing undefined properties. if ( // Updated record. latest.version.createdAt >= newRecord.version.createdAt || // Deleted record. "noContent" in latest) { return applyUpdates(state, [], [latest, ...linkedRecords, ...followingUpdates]); } // TODO: Call a "onConflict" handler to expose resolution. const newMutation = { state: applyUpdates(state, [], [latest]).state, record, followingUpdates, }; return performRequest(newMutation, attempt + 1); } catch (err) { // Permanent error. if (sdk_1.Http.isError(err, [ sdk_1.HttpStatusCode.BAD_REQUEST, sdk_1.HttpStatusCode.NOT_FOUND, ])) { return applyUpdates(state, [], followingUpdates); } throw err; } }; return performRequest(mutation, 1); }