Compare commits

..

14 Commits

Author SHA1 Message Date
Quixos Subtree Publisher e061e29070 Publish camino-datatypes from 5ae087c6c8f9c945d116858341ec882ab4c72636 2026-07-13 04:14:05 +00:00
Timothy J. Aveni fceb49bb2c Refresh Camino datatypes pins 2026-07-12 21:14:05 -07:00
Quixos Subtree Publisher 7a622dd028 Publish camino-datatypes from 1ed3d2fd04afba64ffda2125175704a6fdb4f2ad 2026-07-13 04:04:20 +00:00
Timothy J. Aveni cef58a7d48 Make Todo notes use CRDT docs directly 2026-07-12 21:04:20 -07:00
Quixos Subtree Publisher 709990b5f2 Publish camino-datatypes from 3a81f75f5da1be812d8bbc4dec9180c8d2b9fd0e 2026-07-13 03:44:22 +00:00
Timothy J. Aveni a9a6ef2cf4 Bump Camino package helper pins 2026-07-12 20:44:22 -07:00
Quixos Subtree Publisher fa50782559 Publish camino-datatypes from 4e9cbccd3882cded26ec198c0299435a8bfa6f99 2026-07-13 03:15:51 +00:00
Timothy J. Aveni e72b3c5577 Encode Automerge docs for Camino CRDT writes 2026-07-12 20:15:51 -07:00
Quixos Subtree Publisher da3e43d059 Publish camino-datatypes from a38d55b0e88bf087a55f4db515b769cfbb3c6545 2026-07-13 03:05:50 +00:00
Timothy J. Aveni 864f8f5de6 Package all Camino datatypes files 2026-07-12 20:05:50 -07:00
Quixos Subtree Publisher 01fdee1873 Publish camino-datatypes from 8022af2322e24d82212ec92cbe7583eb9d554310 2026-07-13 03:03:59 +00:00
Timothy J. Aveni 016f471c81 Include built Camino datatypes artifacts 2026-07-12 20:03:59 -07:00
Quixos Subtree Publisher 06072c2143 Publish camino-datatypes from 9c51b9f412ef04d0f8b4a9f8f533956ef027b47f 2026-07-13 03:01:20 +00:00
Timothy J. Aveni 05f9add1eb Package Camino datatypes as built JS 2026-07-12 20:01:20 -07:00
16 changed files with 361 additions and 20 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
dist/
dist/*
!dist/src/
!dist/src/**
node_modules/
.yarn/install-state.gz
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git",
"sourceCommit": "7c91cadcce482cce86e16d724fc6678d6d783d88",
"sourceCommit": "5ae087c6c8f9c945d116858341ec882ab4c72636",
"sourcePath": "quixos-instance/packages/camino-datatypes",
"exportName": "camino-datatypes",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/camino-datatypes.git"
+32
View File
@@ -0,0 +1,32 @@
import * as Automerge from "@automerge/automerge";
export type CaminoCrdtEncoding = "base64";
export type CaminoCrdtEnvelope = {
$caminoCrdtType: string;
$caminoCrdtEncoding: CaminoCrdtEncoding;
$caminoCrdtPayload: string;
};
type AutomergeFieldDoc = {
value: unknown;
};
export type CaminoAutomergeDoc<T = unknown> = Automerge.Doc<{
value: T;
}>;
export declare const isCaminoCrdtEnvelope: (value: unknown) => value is CaminoCrdtEnvelope;
export declare const loadAutomergeFieldDoc: (value: CaminoCrdtEnvelope, type: string) => Automerge.Doc<AutomergeFieldDoc>;
export declare const saveAutomergeFieldDoc: (doc: Automerge.Doc<AutomergeFieldDoc>, type: string) => CaminoCrdtEnvelope;
export declare const createAutomergeFieldValue: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const isAutomergeFieldDoc: (value: unknown) => value is Automerge.Doc<AutomergeFieldDoc>;
export declare const encodeAutomergeFieldWrite: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const normalizeAutomergeFieldValue: (value: unknown, type: string) => CaminoCrdtEnvelope;
export declare const materializeAutomergeFieldValue: (value: CaminoCrdtEnvelope, type: string) => unknown;
export type MergeAutomergeFieldValueParams = {
existing?: unknown;
incoming: unknown;
type: string;
};
export type MergeAutomergeFieldValueResult = {
stored: CaminoCrdtEnvelope;
materialized: unknown;
};
export declare const mergeAutomergeFieldValues: ({ existing, incoming, type, }: MergeAutomergeFieldValueParams) => MergeAutomergeFieldValueResult;
export {};
+83
View File
@@ -0,0 +1,83 @@
import * as Automerge from "@automerge/automerge";
const bytesToBase64 = (value) => {
if (typeof Buffer !== "undefined") {
return Buffer.from(value).toString("base64");
}
let binary = "";
for (const byte of value) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
};
const base64ToBytes = (value) => {
if (typeof Buffer !== "undefined") {
return Buffer.from(value, "base64");
}
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes;
};
export const isCaminoCrdtEnvelope = (value) => Boolean(value) &&
typeof value === "object" &&
!Array.isArray(value) &&
typeof value.$caminoCrdtType ===
"string" &&
typeof value.$caminoCrdtPayload ===
"string" &&
(value.$caminoCrdtEncoding ===
undefined ||
value.$caminoCrdtEncoding ===
"base64");
const assertEnvelopeType = (value, type) => {
if (value.$caminoCrdtType !== type) {
throw new Error(`CRDT type mismatch: expected ${type}, got ${value.$caminoCrdtType}`);
}
};
export const loadAutomergeFieldDoc = (value, type) => {
assertEnvelopeType(value, type);
return Automerge.load(base64ToBytes(value.$caminoCrdtPayload));
};
export const saveAutomergeFieldDoc = (doc, type) => ({
$caminoCrdtType: type,
$caminoCrdtEncoding: "base64",
$caminoCrdtPayload: bytesToBase64(Automerge.save(doc)),
});
export const createAutomergeFieldValue = (value, type) => saveAutomergeFieldDoc(Automerge.from({ value }), type);
export const isAutomergeFieldDoc = (value) => {
if (!value || typeof value !== "object") {
return false;
}
try {
Automerge.getHeads(value);
return true;
}
catch {
return false;
}
};
export const encodeAutomergeFieldWrite = (value, type) => {
if (isCaminoCrdtEnvelope(value)) {
return saveAutomergeFieldDoc(loadAutomergeFieldDoc(value, type), type);
}
if (isAutomergeFieldDoc(value)) {
return saveAutomergeFieldDoc(value, type);
}
throw new Error("CRDT field writes must use an Automerge document or Camino CRDT envelope");
};
export const normalizeAutomergeFieldValue = (value, type) => encodeAutomergeFieldWrite(value, type);
export const materializeAutomergeFieldValue = (value, type) => loadAutomergeFieldDoc(value, type).value;
export const mergeAutomergeFieldValues = ({ existing, incoming, type, }) => {
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,
};
};
//# sourceMappingURL=crdt-automerge.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"crdt-automerge.js","sourceRoot":"","sources":["../../src/crdt-automerge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAC;AAkBlD,MAAM,aAAa,GAAG,CAAC,KAAiB,EAAE,EAAE;IAC1C,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC/C,CAAC;IACD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAa,EAAE,EAAE;IACtC,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,KAAc,EACe,EAAE,CAC/B,OAAO,CAAC,KAAK,CAAC;IACd,OAAO,KAAK,KAAK,QAAQ;IACzB,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACrB,OAAQ,KAAuC,CAAC,eAAe;QAC7D,QAAQ;IACV,OAAQ,KAA0C,CAAC,kBAAkB;QACnE,QAAQ;IACV,CAAE,KAA2C,CAAC,mBAAmB;QAC/D,SAAS;QACR,KAA2C,CAAC,mBAAmB;YAC9D,QAAQ,CAAC,CAAC;AAEhB,MAAM,kBAAkB,GAAG,CAAC,KAAyB,EAAE,IAAY,EAAE,EAAE;IACrE,IAAI,KAAK,CAAC,eAAe,KAAK,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,SAAS,KAAK,CAAC,eAAe,EAAE,CACrE,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,KAAyB,EACzB,IAAY,EACsB,EAAE;IACpC,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChC,OAAO,SAAS,CAAC,IAAI,CACnB,aAAa,CAAC,KAAK,CAAC,kBAAkB,CAAC,CACxC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,GAAqC,EACrC,IAAY,EACQ,EAAE,CAAC,CAAC;IACxB,eAAe,EAAE,IAAI;IACrB,mBAAmB,EAAE,QAAQ;IAC7B,kBAAkB,EAAE,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;CACvD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,KAAc,EACd,IAAY,EACQ,EAAE,CACtB,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAoB,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;AAE5E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,KAAc,EAC6B,EAAE;IAC7C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,CAAC,KAAyC,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,KAAc,EACd,IAAY,EACQ,EAAE;IACtB,IAAI,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,qBAAqB,CAAC,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,IAAI,KAAK,CACb,0EAA0E,CAC3E,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAC1C,KAAc,EACd,IAAY,EACQ,EAAE,CAAC,yBAAyB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAC5C,KAAyB,EACzB,IAAY,EACZ,EAAE,CAAC,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC;AAa9C,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,EACxC,QAAQ,EACR,QAAQ,EACR,IAAI,GAC2B,EAAkC,EAAE;IACnE,MAAM,WAAW,GAAG,qBAAqB,CACvC,4BAA4B,CAAC,QAAQ,EAAE,IAAI,CAAC,EAC5C,IAAI,CACL,CAAC;IACF,MAAM,SAAS,GACb,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI;QACzC,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,SAAS,CAAC,KAAK,CACb,qBAAqB,CAAC,4BAA4B,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EACzE,WAAW,CACZ,CAAC;IACR,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACtD,OAAO;QACL,MAAM;QACN,YAAY,EAAE,SAAS,CAAC,KAAK;KAC9B,CAAC;AACJ,CAAC,CAAC"}
+38
View File
@@ -0,0 +1,38 @@
export type UnknownFieldClock = {
kind: "unknown";
};
export type RevisionFieldClock = {
kind: "revision";
revision: bigint;
};
export type FieldClock = UnknownFieldClock | RevisionFieldClock;
export type CanonicalFieldValue<T> = {
value: T;
clock: FieldClock;
};
export type PendingFieldWrite<T> = {
clientMutationId: string;
sequence: number;
value: T;
};
export type RegisterFieldState<T> = {
canonical: CanonicalFieldValue<T>;
visible: T;
pending: PendingFieldWrite<T>[];
nextPendingSequence: number;
};
export type FieldApplyResult = {
accepted: boolean;
canonicalChanged: boolean;
pendingChanged: boolean;
visibleChanged: boolean;
};
export declare const unknownFieldClock: () => FieldClock;
export declare const revisionFieldClock: (revision: string | number | bigint | undefined | null) => FieldClock;
export declare const fieldClockToRevisionString: (clock: FieldClock) => string;
export declare const compareFieldClocks: (left: FieldClock, right: FieldClock) => "older" | "same" | "newer" | "unknown";
export declare const sameFieldClock: (left: FieldClock, right: FieldClock) => boolean;
export declare const createRegisterFieldState: <T>(initial: CanonicalFieldValue<T>) => RegisterFieldState<T>;
export declare const applyCanonicalFieldValue: <T>(state: RegisterFieldState<T>, incoming: CanonicalFieldValue<T>, clientMutationId?: string) => FieldApplyResult;
export declare const beginLocalFieldWrite: <T>(state: RegisterFieldState<T>, clientMutationId: string, value: T) => FieldApplyResult;
export declare const failLocalFieldWrite: <T>(state: RegisterFieldState<T>, clientMutationId: string) => FieldApplyResult;
+104
View File
@@ -0,0 +1,104 @@
export const unknownFieldClock = () => ({ kind: "unknown" });
export const revisionFieldClock = (revision) => {
if (typeof revision === "bigint") {
return { kind: "revision", revision };
}
if (typeof revision === "number" && Number.isInteger(revision) && revision >= 0) {
return { kind: "revision", revision: BigInt(revision) };
}
if (typeof revision === "string" && /^\d+$/.test(revision)) {
return { kind: "revision", revision: BigInt(revision) };
}
return unknownFieldClock();
};
export const fieldClockToRevisionString = (clock) => clock.kind === "revision" ? clock.revision.toString() : "";
export const compareFieldClocks = (left, right) => {
if (left.kind === "revision" && right.kind === "revision") {
if (left.revision < right.revision) {
return "older";
}
if (left.revision > right.revision) {
return "newer";
}
return "same";
}
if (left.kind === "unknown" && right.kind === "unknown") {
return "same";
}
return "unknown";
};
export const sameFieldClock = (left, right) => compareFieldClocks(left, right) === "same";
const shouldAcceptCanonical = (incoming, current) => {
const order = compareFieldClocks(incoming, current);
if (order === "older") {
return false;
}
if (order === "unknown" && current.kind !== "unknown") {
return false;
}
return true;
};
const latestPendingValue = (state) => state.pending.at(-1)?.value;
const recomputeVisible = (state) => {
const pendingValue = latestPendingValue(state);
return pendingValue === undefined ? state.canonical.value : pendingValue;
};
export const createRegisterFieldState = (initial) => ({
canonical: initial,
visible: initial.value,
pending: [],
nextPendingSequence: 1,
});
export const applyCanonicalFieldValue = (state, incoming, clientMutationId) => {
const visibleBefore = state.visible;
const pendingBefore = state.pending.length;
if (!shouldAcceptCanonical(incoming.clock, state.canonical.clock)) {
return {
accepted: false,
canonicalChanged: false,
pendingChanged: false,
visibleChanged: false,
};
}
const canonicalChanged = !Object.is(state.canonical.value, incoming.value) ||
!sameFieldClock(state.canonical.clock, incoming.clock);
state.canonical = incoming;
if (clientMutationId) {
state.pending = state.pending.filter((entry) => entry.clientMutationId !== clientMutationId);
}
state.visible = recomputeVisible(state);
return {
accepted: true,
canonicalChanged,
pendingChanged: state.pending.length !== pendingBefore,
visibleChanged: !Object.is(state.visible, visibleBefore),
};
};
export const beginLocalFieldWrite = (state, clientMutationId, value) => {
const visibleBefore = state.visible;
state.pending.push({
clientMutationId,
sequence: state.nextPendingSequence++,
value,
});
state.visible = value;
return {
accepted: true,
canonicalChanged: false,
pendingChanged: true,
visibleChanged: !Object.is(state.visible, visibleBefore),
};
};
export const failLocalFieldWrite = (state, clientMutationId) => {
const visibleBefore = state.visible;
const pendingBefore = state.pending.length;
state.pending = state.pending.filter((entry) => entry.clientMutationId !== clientMutationId);
state.visible = recomputeVisible(state);
return {
accepted: true,
canonicalChanged: false,
pendingChanged: state.pending.length !== pendingBefore,
visibleChanged: !Object.is(state.visible, visibleBefore),
};
};
//# sourceMappingURL=field-semantics.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"field-semantics.js","sourceRoot":"","sources":["../../src/field-semantics.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export * from "./field-semantics.js";
export * from "./crdt-automerge.js";
+3
View File
@@ -0,0 +1,3 @@
export * from "./field-semantics.js";
export * from "./crdt-automerge.js";
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC"}
Generated
+5 -5
View File
@@ -37,17 +37,17 @@
"quixosNixHelpers": {
"flake": false,
"locked": {
"lastModified": 1783886666,
"narHash": "sha256-y52TlYwH9RBzzyhpkY/eB21t85BE84idpsg8618MIHg=",
"lastModified": 1783914102,
"narHash": "sha256-A9EnXxKaODNrSUpw1IlUBR8fe4px5I+wNJFUFGbZY7c=",
"ref": "refs/heads/exported",
"rev": "81e55657d9ce0ba092cffd69ee15179ea02aa598",
"revCount": 2,
"rev": "1f0c39b01501d646fe97dfc6e5777ccab0bde2f1",
"revCount": 6,
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
},
"original": {
"ref": "refs/heads/exported",
"rev": "81e55657d9ce0ba092cffd69ee15179ea02aa598",
"rev": "1f0c39b01501d646fe97dfc6e5777ccab0bde2f1",
"type": "git",
"url": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git"
}
+1 -1
View File
@@ -5,7 +5,7 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
quixosNixHelpers = {
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/heads/exported&rev=81e55657d9ce0ba092cffd69ee15179ea02aa598";
url = "git+https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git?ref=refs/heads/exported&rev=1f0c39b01501d646fe97dfc6e5777ccab0bde2f1";
flake = false;
};
};
+7 -6
View File
@@ -6,20 +6,21 @@
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./field-semantics": {
"types": "./src/field-semantics.ts",
"default": "./src/field-semantics.ts"
"types": "./dist/src/field-semantics.d.ts",
"default": "./dist/src/field-semantics.js"
},
"./crdt-automerge": {
"types": "./src/crdt-automerge.ts",
"default": "./src/crdt-automerge.ts"
"types": "./dist/src/crdt-automerge.d.ts",
"default": "./dist/src/crdt-automerge.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"prepare": "yarn build",
"test": "yarn build && node --test dist/test/*.test.js",
"typecheck": "tsc --noEmit"
},
+51 -5
View File
@@ -12,10 +12,32 @@ type AutomergeFieldDoc = {
value: unknown;
};
const bytesToBase64 = (value: Uint8Array) =>
Buffer.from(value).toString("base64");
export type CaminoAutomergeDoc<T = unknown> = Automerge.Doc<{
value: T;
}>;
const base64ToBytes = (value: string) => Buffer.from(value, "base64");
const bytesToBase64 = (value: Uint8Array) => {
if (typeof Buffer !== "undefined") {
return Buffer.from(value).toString("base64");
}
let binary = "";
for (const byte of value) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
};
const base64ToBytes = (value: string) => {
if (typeof Buffer !== "undefined") {
return Buffer.from(value, "base64");
}
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes;
};
export const isCaminoCrdtEnvelope = (
value: unknown,
@@ -65,16 +87,40 @@ export const createAutomergeFieldValue = (
): CaminoCrdtEnvelope =>
saveAutomergeFieldDoc(Automerge.from<AutomergeFieldDoc>({ value }), type);
export const normalizeAutomergeFieldValue = (
export const isAutomergeFieldDoc = (
value: unknown,
): value is Automerge.Doc<AutomergeFieldDoc> => {
if (!value || typeof value !== "object") {
return false;
}
try {
Automerge.getHeads(value as Automerge.Doc<AutomergeFieldDoc>);
return true;
} catch {
return false;
}
};
export const encodeAutomergeFieldWrite = (
value: unknown,
type: string,
): CaminoCrdtEnvelope => {
if (isCaminoCrdtEnvelope(value)) {
return saveAutomergeFieldDoc(loadAutomergeFieldDoc(value, type), type);
}
return createAutomergeFieldValue(value, type);
if (isAutomergeFieldDoc(value)) {
return saveAutomergeFieldDoc(value, type);
}
throw new Error(
"CRDT field writes must use an Automerge document or Camino CRDT envelope",
);
};
export const normalizeAutomergeFieldValue = (
value: unknown,
type: string,
): CaminoCrdtEnvelope => encodeAutomergeFieldWrite(value, type);
export const materializeAutomergeFieldValue = (
value: CaminoCrdtEnvelope,
type: string,
+28 -1
View File
@@ -3,6 +3,8 @@ import test from "node:test";
import * as Automerge from "@automerge/automerge";
import {
createAutomergeFieldValue,
encodeAutomergeFieldWrite,
isAutomergeFieldDoc,
loadAutomergeFieldDoc,
materializeAutomergeFieldValue,
mergeAutomergeFieldValues,
@@ -11,7 +13,7 @@ import {
const type = "camino.CrdtJson";
test("Automerge CRDT fields lift plain values into stored envelopes", () => {
test("Automerge CRDT fields explicitly create stored envelopes", () => {
const stored = createAutomergeFieldValue({ title: "draft" }, type);
assert.equal(stored.$caminoCrdtType, type);
@@ -21,6 +23,31 @@ test("Automerge CRDT fields lift plain values into stored envelopes", () => {
});
});
test("Automerge CRDT field writes reject plain values", () => {
assert.throws(
() => encodeAutomergeFieldWrite({ title: "draft" }, type),
/CRDT field writes must use an Automerge document/,
);
});
test("Automerge CRDT fields encode rehydrated docs for writes", () => {
const base = loadAutomergeFieldDoc(
createAutomergeFieldValue({ title: "draft" }, type),
type,
);
const changed = Automerge.change(base, (doc) => {
(doc.value as { title: string }).title = "edited";
});
assert.equal(isAutomergeFieldDoc(changed), true);
const stored = encodeAutomergeFieldWrite(changed, type);
assert.deepEqual(materializeAutomergeFieldValue(stored, type), {
title: "edited",
});
});
test("Automerge CRDT fields merge divergent saved documents", () => {
const base = loadAutomergeFieldDoc(
createAutomergeFieldValue({ left: false, right: false }, type),