Add Automerge CRDT datatype helpers

This commit is contained in:
Timothy J. Aveni
2026-07-12 19:57:27 -07:00
parent b018a0630e
commit 9e44c722e5
6 changed files with 179 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
import * as Automerge from "@automerge/automerge";
export type CaminoCrdtEncoding = "base64";
export type CaminoCrdtEnvelope = {
$caminoCrdtType: string;
$caminoCrdtEncoding: CaminoCrdtEncoding;
$caminoCrdtPayload: string;
};
type AutomergeFieldDoc = {
value: unknown;
};
const bytesToBase64 = (value: Uint8Array) =>
Buffer.from(value).toString("base64");
const base64ToBytes = (value: string) => Buffer.from(value, "base64");
export const isCaminoCrdtEnvelope = (
value: unknown,
): value is CaminoCrdtEnvelope =>
Boolean(value) &&
typeof value === "object" &&
!Array.isArray(value) &&
typeof (value as { $caminoCrdtType?: unknown }).$caminoCrdtType ===
"string" &&
typeof (value as { $caminoCrdtPayload?: unknown }).$caminoCrdtPayload ===
"string" &&
((value as { $caminoCrdtEncoding?: unknown }).$caminoCrdtEncoding ===
undefined ||
(value as { $caminoCrdtEncoding?: unknown }).$caminoCrdtEncoding ===
"base64");
const assertEnvelopeType = (value: CaminoCrdtEnvelope, type: string) => {
if (value.$caminoCrdtType !== type) {
throw new Error(
`CRDT type mismatch: expected ${type}, got ${value.$caminoCrdtType}`,
);
}
};
export const loadAutomergeFieldDoc = (
value: CaminoCrdtEnvelope,
type: string,
): Automerge.Doc<AutomergeFieldDoc> => {
assertEnvelopeType(value, type);
return Automerge.load<AutomergeFieldDoc>(
base64ToBytes(value.$caminoCrdtPayload),
);
};
export const saveAutomergeFieldDoc = (
doc: Automerge.Doc<AutomergeFieldDoc>,
type: string,
): CaminoCrdtEnvelope => ({
$caminoCrdtType: type,
$caminoCrdtEncoding: "base64",
$caminoCrdtPayload: bytesToBase64(Automerge.save(doc)),
});
export const createAutomergeFieldValue = (
value: unknown,
type: string,
): CaminoCrdtEnvelope =>
saveAutomergeFieldDoc(Automerge.from<AutomergeFieldDoc>({ value }), type);
export const normalizeAutomergeFieldValue = (
value: unknown,
type: string,
): CaminoCrdtEnvelope => {
if (isCaminoCrdtEnvelope(value)) {
return saveAutomergeFieldDoc(loadAutomergeFieldDoc(value, type), type);
}
return createAutomergeFieldValue(value, type);
};
export const materializeAutomergeFieldValue = (
value: CaminoCrdtEnvelope,
type: string,
) => loadAutomergeFieldDoc(value, type).value;
export type MergeAutomergeFieldValueParams = {
existing?: unknown;
incoming: unknown;
type: string;
};
export type MergeAutomergeFieldValueResult = {
stored: CaminoCrdtEnvelope;
materialized: unknown;
};
export const mergeAutomergeFieldValues = ({
existing,
incoming,
type,
}: MergeAutomergeFieldValueParams): MergeAutomergeFieldValueResult => {
const incomingDoc = loadAutomergeFieldDoc(
normalizeAutomergeFieldValue(incoming, type),
type,
);
const mergedDoc =
existing === undefined || existing === null
? incomingDoc
: Automerge.merge(
loadAutomergeFieldDoc(normalizeAutomergeFieldValue(existing, type), type),
incomingDoc,
);
const stored = saveAutomergeFieldDoc(mergedDoc, type);
return {
stored,
materialized: mergedDoc.value,
};
};
+1
View File
@@ -1 +1,2 @@
export * from "./field-semantics.js";
export * from "./crdt-automerge.js";