@baqhub/sdk-react
Version:
The official React SDK for the BAQ federated app platform.
196 lines (195 loc) • 7.66 kB
JavaScript
;
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);
}