@korajs/merge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +718 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/strategies/lww.ts","../src/strategies/add-wins-set.ts","../src/strategies/yjs-richtext.ts","../src/engine/field-merger.ts","../src/constraints/constraint-checker.ts","../src/constraints/resolvers.ts","../src/engine/merge-engine.ts"],"sourcesContent":["// @korajs/merge — public API\n// Every export here is a public API commitment. Be explicit.\n\n// === Types ===\nexport type {\n\tConstraintContext,\n\tConstraintViolation,\n\tFieldMergeResult,\n\tMergeInput,\n\tMergeResult,\n} from './types'\n\n// === Strategies ===\nexport { lastWriteWins, type LWWResult } from './strategies/lww'\nexport { addWinsSet } from './strategies/add-wins-set'\nexport { mergeRichtext, richtextToString, stringToRichtextUpdate } from './strategies/yjs-richtext'\n\n// === Field Merger ===\nexport { mergeField } from './engine/field-merger'\n\n// === Constraint Checking ===\nexport { checkConstraints } from './constraints/constraint-checker'\nexport { resolveConstraintViolation, type ConstraintResolution } from './constraints/resolvers'\n\n// === Merge Engine ===\nexport { MergeEngine } from './engine/merge-engine'\n","import { HybridLogicalClock } from '@korajs/core'\nimport type { HLCTimestamp } from '@korajs/core'\n\n/**\n * Result of a Last-Write-Wins comparison.\n */\nexport interface LWWResult {\n\t/** The winning value */\n\tvalue: unknown\n\t/** Which side won */\n\twinner: 'local' | 'remote'\n}\n\n/**\n * Last-Write-Wins merge strategy using HLC timestamps.\n *\n * Compares two values by their HLC timestamps and returns the value with the\n * later timestamp. The HLC total order guarantees a deterministic winner even\n * when wall-clock times and logical counters are identical (nodeId tiebreaker).\n *\n * @param localValue - The local field value\n * @param remoteValue - The remote field value\n * @param localTimestamp - HLC timestamp of the local operation\n * @param remoteTimestamp - HLC timestamp of the remote operation\n * @returns The winning value and which side won\n */\nexport function lastWriteWins(\n\tlocalValue: unknown,\n\tremoteValue: unknown,\n\tlocalTimestamp: HLCTimestamp,\n\tremoteTimestamp: HLCTimestamp,\n): LWWResult {\n\tconst comparison = HybridLogicalClock.compare(localTimestamp, remoteTimestamp)\n\t// HLC total order guarantees comparison is never 0 for different nodeIds.\n\t// If comparison >= 0, local wins (local is later or same node).\n\tif (comparison >= 0) {\n\t\treturn { value: localValue, winner: 'local' }\n\t}\n\treturn { value: remoteValue, winner: 'remote' }\n}\n","/**\n * Add-wins set merge strategy for array fields.\n *\n * When two sides concurrently modify an array, this strategy preserves all\n * additions from both sides. An element is only removed from the result if\n * BOTH sides independently removed it. This prevents data loss: if one side\n * adds an element while another removes a different element, both changes\n * are preserved.\n *\n * Algorithm:\n * added_local = local - base\n * added_remote = remote - base\n * removed_local = base - local\n * removed_remote = base - remote\n * result = (base ∪ added_local ∪ added_remote) - (removed_local ∩ removed_remote)\n *\n * Uses JSON.stringify for element comparison to handle primitives and objects.\n *\n * @param localArray - The local array after local modifications\n * @param remoteArray - The remote array after remote modifications\n * @param baseArray - The array state before either modification\n * @returns The merged array\n */\nexport function addWinsSet(\n\tlocalArray: unknown[],\n\tremoteArray: unknown[],\n\tbaseArray: unknown[],\n): unknown[] {\n\tconst serialize = (v: unknown): string => JSON.stringify(v)\n\n\tconst baseSet = new Set(baseArray.map(serialize))\n\tconst localSet = new Set(localArray.map(serialize))\n\tconst remoteSet = new Set(remoteArray.map(serialize))\n\n\t// Elements added by each side (present in their set but not in base)\n\tconst addedLocal = new Set<string>()\n\tfor (const s of localSet) {\n\t\tif (!baseSet.has(s)) {\n\t\t\taddedLocal.add(s)\n\t\t}\n\t}\n\n\tconst addedRemote = new Set<string>()\n\tfor (const s of remoteSet) {\n\t\tif (!baseSet.has(s)) {\n\t\t\taddedRemote.add(s)\n\t\t}\n\t}\n\n\t// Elements removed by each side (present in base but not in their set)\n\tconst removedLocal = new Set<string>()\n\tfor (const s of baseSet) {\n\t\tif (!localSet.has(s)) {\n\t\t\tremovedLocal.add(s)\n\t\t}\n\t}\n\n\tconst removedRemote = new Set<string>()\n\tfor (const s of baseSet) {\n\t\tif (!remoteSet.has(s)) {\n\t\t\tremovedRemote.add(s)\n\t\t}\n\t}\n\n\t// An element is truly removed only if BOTH sides removed it\n\tconst removedByBoth = new Set<string>()\n\tfor (const s of removedLocal) {\n\t\tif (removedRemote.has(s)) {\n\t\t\tremovedByBoth.add(s)\n\t\t}\n\t}\n\n\t// Result = (base ∪ added_local ∪ added_remote) - removed_by_both\n\t// Maintain order: base elements first (preserving order), then local adds, then remote adds\n\tconst resultSerialized = new Set<string>()\n\tconst result: unknown[] = []\n\n\tconst addIfNew = (serialized: string, value: unknown): void => {\n\t\tif (!resultSerialized.has(serialized) && !removedByBoth.has(serialized)) {\n\t\t\tresultSerialized.add(serialized)\n\t\t\tresult.push(value)\n\t\t}\n\t}\n\n\t// Base elements (in original order, minus those removed by both)\n\tfor (const item of baseArray) {\n\t\taddIfNew(serialize(item), item)\n\t}\n\n\t// Local additions (in order they appear in local array)\n\tfor (const item of localArray) {\n\t\tconst s = serialize(item)\n\t\tif (addedLocal.has(s)) {\n\t\t\taddIfNew(s, item)\n\t\t}\n\t}\n\n\t// Remote additions (in order they appear in remote array)\n\tfor (const item of remoteArray) {\n\t\tconst s = serialize(item)\n\t\tif (addedRemote.has(s)) {\n\t\t\taddIfNew(s, item)\n\t\t}\n\t}\n\n\treturn result\n}\n","import * as Y from 'yjs'\n\nexport type RichtextValue = string | Uint8Array | ArrayBuffer | null | undefined\n\nconst TEXT_KEY = 'content'\n\n/**\n * Merges richtext values using Yjs CRDT updates.\n */\nexport function mergeRichtext(\n\tlocalValue: RichtextValue,\n\tremoteValue: RichtextValue,\n\tbaseValue: RichtextValue,\n): Uint8Array {\n\tconst mergedDoc = new Y.Doc()\n\n\tY.applyUpdate(mergedDoc, toYjsUpdate(baseValue))\n\tY.applyUpdate(mergedDoc, toYjsUpdate(localValue))\n\tY.applyUpdate(mergedDoc, toYjsUpdate(remoteValue))\n\n\treturn Y.encodeStateAsUpdate(mergedDoc)\n}\n\n/**\n * Converts a richtext state update to plain text.\n */\nexport function richtextToString(value: RichtextValue): string {\n\tconst doc = new Y.Doc()\n\tY.applyUpdate(doc, toYjsUpdate(value))\n\treturn doc.getText(TEXT_KEY).toString()\n}\n\n/**\n * Converts a plain string to a Yjs state update.\n */\nexport function stringToRichtextUpdate(value: string): Uint8Array {\n\tconst doc = new Y.Doc()\n\tdoc.getText(TEXT_KEY).insert(0, value)\n\treturn Y.encodeStateAsUpdate(doc)\n}\n\nfunction toYjsUpdate(value: RichtextValue): Uint8Array {\n\tif (value === null || value === undefined) {\n\t\treturn Y.encodeStateAsUpdate(new Y.Doc())\n\t}\n\n\tif (typeof value === 'string') {\n\t\treturn stringToRichtextUpdate(value)\n\t}\n\n\tif (value instanceof Uint8Array) {\n\t\treturn value\n\t}\n\n\tif (value instanceof ArrayBuffer) {\n\t\treturn new Uint8Array(value)\n\t}\n\n\tthrow new Error('Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.')\n}\n","import type { CustomResolver, FieldDescriptor, HLCTimestamp, Operation } from '@korajs/core'\nimport type { MergeTrace } from '@korajs/core'\nimport { addWinsSet } from '../strategies/add-wins-set'\nimport { lastWriteWins } from '../strategies/lww'\nimport { mergeRichtext } from '../strategies/yjs-richtext'\nimport type { RichtextValue } from '../strategies/yjs-richtext'\nimport type { FieldMergeResult } from '../types'\n\n/**\n * Merges a single field from two concurrent operations.\n *\n * Dispatches to the appropriate strategy based on field kind:\n * - string, number, boolean, enum, timestamp → LWW (Last-Write-Wins via HLC)\n * - array → add-wins set (union of additions, only mutual removals)\n * - richtext → Yjs CRDT merge\n *\n * If a custom resolver (Tier 3) is provided, it overrides the default strategy.\n *\n * Handles non-conflict cases where only one side modified the field:\n * - Only local changed → take local value\n * - Only remote changed → take remote value\n * - Neither changed → keep base value\n *\n * @param fieldName - Name of the field being merged\n * @param localOp - The local operation\n * @param remoteOp - The remote operation\n * @param baseState - Full record state before either operation\n * @param fieldDescriptor - Schema descriptor for this field\n * @param resolver - Optional Tier 3 custom resolver for this field\n * @returns The merged field value and a trace for DevTools\n */\nexport function mergeField(\n\tfieldName: string,\n\tlocalOp: Operation,\n\tremoteOp: Operation,\n\tbaseState: Record<string, unknown>,\n\tfieldDescriptor: FieldDescriptor,\n\tresolver?: CustomResolver,\n): FieldMergeResult {\n\tconst startTime = Date.now()\n\n\tconst localData = localOp.data ?? {}\n\tconst remoteData = remoteOp.data ?? {}\n\tconst localPrevious = localOp.previousData ?? {}\n\tconst remotePrevious = remoteOp.previousData ?? {}\n\n\tconst localChanged = fieldName in localData\n\tconst remoteChanged = fieldName in remoteData\n\tconst baseValue = baseState[fieldName]\n\n\t// Non-conflict: only one side changed\n\tif (localChanged && !remoteChanged) {\n\t\treturn createResult(\n\t\t\tlocalData[fieldName],\n\t\t\tfieldName,\n\t\t\tlocalOp,\n\t\t\tremoteOp,\n\t\t\tlocalData[fieldName],\n\t\t\tbaseValue,\n\t\t\tbaseValue,\n\t\t\t'no-conflict-local',\n\t\t\t1,\n\t\t\tstartTime,\n\t\t)\n\t}\n\n\tif (!localChanged && remoteChanged) {\n\t\treturn createResult(\n\t\t\tremoteData[fieldName],\n\t\t\tfieldName,\n\t\t\tlocalOp,\n\t\t\tremoteOp,\n\t\t\tbaseValue,\n\t\t\tremoteData[fieldName],\n\t\t\tbaseValue,\n\t\t\t'no-conflict-remote',\n\t\t\t1,\n\t\t\tstartTime,\n\t\t)\n\t}\n\n\tif (!localChanged && !remoteChanged) {\n\t\treturn createResult(\n\t\t\tbaseValue,\n\t\t\tfieldName,\n\t\t\tlocalOp,\n\t\t\tremoteOp,\n\t\t\tbaseValue,\n\t\t\tbaseValue,\n\t\t\tbaseValue,\n\t\t\t'no-conflict-unchanged',\n\t\t\t1,\n\t\t\tstartTime,\n\t\t)\n\t}\n\n\t// Both sides changed this field — conflict resolution needed\n\n\tconst localValue = localData[fieldName]\n\tconst remoteValue = remoteData[fieldName]\n\n\t// Tier 3: Custom resolver takes precedence\n\tif (resolver !== undefined) {\n\t\tconst resolved = resolver(localValue, remoteValue, baseValue)\n\t\treturn createResult(\n\t\t\tresolved,\n\t\t\tfieldName,\n\t\t\tlocalOp,\n\t\t\tremoteOp,\n\t\t\tlocalValue,\n\t\t\tremoteValue,\n\t\t\tbaseValue,\n\t\t\t'custom',\n\t\t\t3,\n\t\t\tstartTime,\n\t\t)\n\t}\n\n\t// Tier 1: Auto-merge based on field kind\n\treturn autoMerge(\n\t\tfieldName,\n\t\tlocalOp,\n\t\tremoteOp,\n\t\tlocalValue,\n\t\tremoteValue,\n\t\tbaseValue,\n\t\tfieldDescriptor,\n\t\tstartTime,\n\t)\n}\n\nfunction autoMerge(\n\tfieldName: string,\n\tlocalOp: Operation,\n\tremoteOp: Operation,\n\tlocalValue: unknown,\n\tremoteValue: unknown,\n\tbaseValue: unknown,\n\tfieldDescriptor: FieldDescriptor,\n\tstartTime: number,\n): FieldMergeResult {\n\tswitch (fieldDescriptor.kind) {\n\t\tcase 'string':\n\t\tcase 'number':\n\t\tcase 'boolean':\n\t\tcase 'enum':\n\t\tcase 'timestamp': {\n\t\t\tconst lwwResult = lastWriteWins(\n\t\t\t\tlocalValue,\n\t\t\t\tremoteValue,\n\t\t\t\tlocalOp.timestamp,\n\t\t\t\tremoteOp.timestamp,\n\t\t\t)\n\t\t\treturn createResult(\n\t\t\t\tlwwResult.value,\n\t\t\t\tfieldName,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tlocalValue,\n\t\t\t\tremoteValue,\n\t\t\t\tbaseValue,\n\t\t\t\t'lww',\n\t\t\t\t1,\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'array': {\n\t\t\tconst baseArr = Array.isArray(baseValue) ? baseValue : []\n\t\t\tconst localArr = Array.isArray(localValue) ? localValue : []\n\t\t\tconst remoteArr = Array.isArray(remoteValue) ? remoteValue : []\n\n\t\t\tconst merged = addWinsSet(localArr, remoteArr, baseArr)\n\t\t\treturn createResult(\n\t\t\t\tmerged,\n\t\t\t\tfieldName,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tlocalValue,\n\t\t\t\tremoteValue,\n\t\t\t\tbaseValue,\n\t\t\t\t'add-wins-set',\n\t\t\t\t1,\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'richtext': {\n\t\t\tconst merged = mergeRichtext(\n\t\t\t\tlocalValue as RichtextValue,\n\t\t\t\tremoteValue as RichtextValue,\n\t\t\t\tbaseValue as RichtextValue,\n\t\t\t)\n\t\t\treturn createResult(\n\t\t\t\tmerged,\n\t\t\t\tfieldName,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tlocalValue,\n\t\t\t\tremoteValue,\n\t\t\t\tbaseValue,\n\t\t\t\t'crdt-text',\n\t\t\t\t1,\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunction createResult(\n\tvalue: unknown,\n\tfield: string,\n\toperationA: Operation,\n\toperationB: Operation,\n\tinputA: unknown,\n\tinputB: unknown,\n\tbase: unknown | null,\n\tstrategy: string,\n\ttier: 1 | 2 | 3,\n\tstartTime: number,\n): FieldMergeResult {\n\tconst trace: MergeTrace = {\n\t\toperationA,\n\t\toperationB,\n\t\tfield,\n\t\tstrategy,\n\t\tinputA,\n\t\tinputB,\n\t\tbase,\n\t\toutput: value,\n\t\ttier,\n\t\tconstraintViolated: null,\n\t\tduration: Date.now() - startTime,\n\t}\n\treturn { value, trace }\n}\n","import type { CollectionDefinition, Constraint } from '@korajs/core'\nimport type { ConstraintContext, ConstraintViolation } from '../types'\n\n/**\n * Checks all constraints on a collection against a candidate merged record.\n *\n * Called after Tier 1+3 merge produces a candidate state. Each violated\n * constraint is returned as a ConstraintViolation for Tier 2 resolution.\n *\n * @param mergedRecord - The candidate record state after field-level merge\n * @param recordId - ID of the record being merged\n * @param collection - Name of the collection\n * @param collectionDef - Schema definition for the collection\n * @param constraintContext - Pluggable DB lookup interface\n * @returns Array of violated constraints (empty if all pass)\n */\nexport async function checkConstraints(\n\tmergedRecord: Record<string, unknown>,\n\trecordId: string,\n\tcollection: string,\n\tcollectionDef: CollectionDefinition,\n\tconstraintContext: ConstraintContext,\n): Promise<ConstraintViolation[]> {\n\tconst violations: ConstraintViolation[] = []\n\n\tfor (const constraint of collectionDef.constraints) {\n\t\t// For unique and capacity constraints, where clause filters which records\n\t\t// the constraint applies to. For referential constraints, where clause\n\t\t// stores metadata (e.g., the referenced collection), not a record filter.\n\t\tif (\n\t\t\tconstraint.type !== 'referential' &&\n\t\t\tconstraint.where !== undefined &&\n\t\t\t!matchesWhere(mergedRecord, constraint.where)\n\t\t) {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst violation = await checkSingleConstraint(\n\t\t\tconstraint,\n\t\t\tmergedRecord,\n\t\t\trecordId,\n\t\t\tcollection,\n\t\t\tconstraintContext,\n\t\t)\n\t\tif (violation !== null) {\n\t\t\tviolations.push(violation)\n\t\t}\n\t}\n\n\treturn violations\n}\n\nasync function checkSingleConstraint(\n\tconstraint: Constraint,\n\tmergedRecord: Record<string, unknown>,\n\trecordId: string,\n\tcollection: string,\n\tctx: ConstraintContext,\n): Promise<ConstraintViolation | null> {\n\tswitch (constraint.type) {\n\t\tcase 'unique':\n\t\t\treturn checkUniqueConstraint(constraint, mergedRecord, recordId, collection, ctx)\n\t\tcase 'capacity':\n\t\t\treturn checkCapacityConstraint(constraint, mergedRecord, collection, ctx)\n\t\tcase 'referential':\n\t\t\treturn checkReferentialConstraint(constraint, mergedRecord, collection, ctx)\n\t}\n}\n\nasync function checkUniqueConstraint(\n\tconstraint: Constraint,\n\tmergedRecord: Record<string, unknown>,\n\trecordId: string,\n\tcollection: string,\n\tctx: ConstraintContext,\n): Promise<ConstraintViolation | null> {\n\t// Build a where clause from the constraint fields and the merged record values\n\tconst where: Record<string, unknown> = {}\n\tfor (const field of constraint.fields) {\n\t\twhere[field] = mergedRecord[field]\n\t}\n\n\tconst existing = await ctx.queryRecords(collection, where)\n\t// Filter out the current record itself\n\tconst duplicates = existing.filter((r) => r.id !== recordId)\n\n\tif (duplicates.length > 0) {\n\t\treturn {\n\t\t\tconstraint,\n\t\t\tfields: constraint.fields,\n\t\t\tmessage:\n\t\t\t\t`Unique constraint violated on fields [${constraint.fields.join(', ')}] ` +\n\t\t\t\t`in collection \"${collection}\": duplicate value(s) found`,\n\t\t}\n\t}\n\n\treturn null\n}\n\nasync function checkCapacityConstraint(\n\tconstraint: Constraint,\n\tmergedRecord: Record<string, unknown>,\n\tcollection: string,\n\tctx: ConstraintContext,\n): Promise<ConstraintViolation | null> {\n\t// Capacity constraint: the where clause defines the group, fields[0] is the capacity limit field name\n\t// We count records matching the where clause\n\tconst where = constraint.where ?? {}\n\tconst count = await ctx.countRecords(collection, where)\n\n\t// The capacity limit is stored in the first field name as a numeric value\n\t// For capacity constraints, fields represent the fields that define the capacity group\n\t// The capacity limit itself needs to come from somewhere — we use the constraint's where clause\n\t// to scope the group, and check if adding this record exceeds the existing count\n\t// For simplicity: if there's a capacity constraint, the presence of this violation\n\t// means the group is at or over capacity\n\tif (count > 0 && constraint.fields.length > 0) {\n\t\t// Build the group match from constraint fields\n\t\tconst groupWhere: Record<string, unknown> = { ...where }\n\t\tfor (const field of constraint.fields) {\n\t\t\tgroupWhere[field] = mergedRecord[field]\n\t\t}\n\n\t\tconst groupCount = await ctx.countRecords(collection, groupWhere)\n\t\t// Capacity constraints use the where clause to define the limit\n\t\t// If we can't determine a numeric limit, we flag the violation\n\t\t// The resolver will handle the actual resolution logic\n\t\tif (groupCount > 1) {\n\t\t\treturn {\n\t\t\t\tconstraint,\n\t\t\t\tfields: constraint.fields,\n\t\t\t\tmessage:\n\t\t\t\t\t`Capacity constraint violated on fields [${constraint.fields.join(', ')}] ` +\n\t\t\t\t\t`in collection \"${collection}\": group count ${groupCount} exceeds limit`,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null\n}\n\nasync function checkReferentialConstraint(\n\tconstraint: Constraint,\n\tmergedRecord: Record<string, unknown>,\n\tcollection: string,\n\tctx: ConstraintContext,\n): Promise<ConstraintViolation | null> {\n\t// Referential constraint: the first field is the foreign key field,\n\t// and where.collection (or similar) would specify the referenced collection.\n\t// For a simple check: the field value should reference an existing record.\n\tif (constraint.fields.length === 0) {\n\t\treturn null\n\t}\n\n\tconst fkField = constraint.fields[0]\n\tif (fkField === undefined) {\n\t\treturn null\n\t}\n\n\tconst fkValue = mergedRecord[fkField]\n\tif (fkValue === null || fkValue === undefined) {\n\t\t// Null FK is allowed (the relation is optional)\n\t\treturn null\n\t}\n\n\t// Look up the referenced record. The referenced collection is specified\n\t// in constraint.where.collection (convention for referential constraints).\n\tconst referencedCollection =\n\t\tconstraint.where !== undefined ? (constraint.where.collection as string | undefined) : undefined\n\tif (referencedCollection === undefined) {\n\t\treturn null\n\t}\n\n\tconst referenced = await ctx.queryRecords(referencedCollection, { id: fkValue })\n\tif (referenced.length === 0) {\n\t\treturn {\n\t\t\tconstraint,\n\t\t\tfields: constraint.fields,\n\t\t\tmessage:\n\t\t\t\t`Referential constraint violated on field \"${fkField}\" ` +\n\t\t\t\t`in collection \"${collection}\": referenced record not found ` +\n\t\t\t\t`in \"${referencedCollection}\" with id \"${String(fkValue)}\"`,\n\t\t}\n\t}\n\n\treturn null\n}\n\n/**\n * Check if a record matches a where clause (simple equality check).\n */\nfunction matchesWhere(record: Record<string, unknown>, where: Record<string, unknown>): boolean {\n\tfor (const [key, value] of Object.entries(where)) {\n\t\tif (record[key] !== value) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n","import { HybridLogicalClock } from '@korajs/core'\nimport type { CollectionDefinition, Operation } from '@korajs/core'\nimport type { MergeTrace } from '@korajs/core'\nimport type { ConstraintViolation } from '../types'\n\n/**\n * Result of resolving a constraint violation.\n */\nexport interface ConstraintResolution {\n\t/** The updated record after constraint resolution */\n\tresolvedRecord: Record<string, unknown>\n\t/** Trace of the resolution decision for DevTools */\n\ttrace: MergeTrace\n}\n\n/**\n * Resolves a constraint violation by applying the constraint's onConflict strategy.\n *\n * Strategies:\n * - `last-write-wins`: The operation with the later HLC timestamp wins entirely\n * - `first-write-wins`: The operation with the earlier HLC timestamp wins entirely\n * - `priority-field`: Compares a designated priority field to determine the winner\n * - `server-decides`: Returns a marker indicating deferred server resolution\n * - `custom`: Calls the constraint's resolve function\n *\n * @param violation - The constraint violation to resolve\n * @param mergedRecord - The current candidate record state\n * @param localOp - The local operation\n * @param remoteOp - The remote operation\n * @param baseState - The record state before either operation\n * @returns The resolved record and a trace\n */\nexport function resolveConstraintViolation(\n\tviolation: ConstraintViolation,\n\tmergedRecord: Record<string, unknown>,\n\tlocalOp: Operation,\n\tremoteOp: Operation,\n\tbaseState: Record<string, unknown>,\n): ConstraintResolution {\n\tconst startTime = Date.now()\n\tconst { constraint } = violation\n\n\tswitch (constraint.onConflict) {\n\t\tcase 'last-write-wins': {\n\t\t\tconst comparison = HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp)\n\t\t\tconst winner = comparison >= 0 ? localOp : remoteOp\n\t\t\tconst resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields)\n\t\t\treturn createResolution(\n\t\t\t\tresolvedRecord,\n\t\t\t\tviolation,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tbaseState,\n\t\t\t\t'constraint-lww',\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'first-write-wins': {\n\t\t\tconst comparison = HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp)\n\t\t\tconst winner = comparison <= 0 ? localOp : remoteOp\n\t\t\tconst resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields)\n\t\t\treturn createResolution(\n\t\t\t\tresolvedRecord,\n\t\t\t\tviolation,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tbaseState,\n\t\t\t\t'constraint-fww',\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'priority-field': {\n\t\t\tconst priorityField = constraint.priorityField\n\t\t\tif (priorityField === undefined) {\n\t\t\t\t// Fallback to LWW if no priority field specified\n\t\t\t\tconst comparison = HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp)\n\t\t\t\tconst winner = comparison >= 0 ? localOp : remoteOp\n\t\t\t\tconst resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields)\n\t\t\t\treturn createResolution(\n\t\t\t\t\tresolvedRecord,\n\t\t\t\t\tviolation,\n\t\t\t\t\tlocalOp,\n\t\t\t\t\tremoteOp,\n\t\t\t\t\tbaseState,\n\t\t\t\t\t'constraint-priority-fallback-lww',\n\t\t\t\t\tstartTime,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst localPriority = getFieldValue(localOp, priorityField, mergedRecord)\n\t\t\tconst remotePriority = getFieldValue(remoteOp, priorityField, mergedRecord)\n\n\t\t\t// Higher priority value wins (numeric or string comparison)\n\t\t\tconst winner = comparePriority(localPriority, remotePriority) >= 0 ? localOp : remoteOp\n\t\t\tconst resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields)\n\t\t\treturn createResolution(\n\t\t\t\tresolvedRecord,\n\t\t\t\tviolation,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tbaseState,\n\t\t\t\t'constraint-priority',\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'server-decides': {\n\t\t\t// Mark the record for server-side resolution. The sync layer handles this.\n\t\t\t// We keep the current merged record but flag it.\n\t\t\tconst resolvedRecord = {\n\t\t\t\t...mergedRecord,\n\t\t\t\t_pendingServerResolution: true,\n\t\t\t}\n\t\t\treturn createResolution(\n\t\t\t\tresolvedRecord,\n\t\t\t\tviolation,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tbaseState,\n\t\t\t\t'constraint-server-decides',\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\n\t\tcase 'custom': {\n\t\t\tif (constraint.resolve === undefined) {\n\t\t\t\t// No custom resolver provided — fallback to LWW\n\t\t\t\tconst comparison = HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp)\n\t\t\t\tconst winner = comparison >= 0 ? localOp : remoteOp\n\t\t\t\tconst resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields)\n\t\t\t\treturn createResolution(\n\t\t\t\t\tresolvedRecord,\n\t\t\t\t\tviolation,\n\t\t\t\t\tlocalOp,\n\t\t\t\t\tremoteOp,\n\t\t\t\t\tbaseState,\n\t\t\t\t\t'constraint-custom-fallback-lww',\n\t\t\t\t\tstartTime,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// For each violated field, call the custom resolver with the local, remote, and base values\n\t\t\tconst resolvedRecord = { ...mergedRecord }\n\t\t\tfor (const field of violation.fields) {\n\t\t\t\tconst localVal = getFieldValue(localOp, field, mergedRecord)\n\t\t\t\tconst remoteVal = getFieldValue(remoteOp, field, mergedRecord)\n\t\t\t\tconst baseVal = baseState[field]\n\t\t\t\tresolvedRecord[field] = constraint.resolve(localVal, remoteVal, baseVal)\n\t\t\t}\n\t\t\treturn createResolution(\n\t\t\t\tresolvedRecord,\n\t\t\t\tviolation,\n\t\t\t\tlocalOp,\n\t\t\t\tremoteOp,\n\t\t\t\tbaseState,\n\t\t\t\t'constraint-custom',\n\t\t\t\tstartTime,\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Apply the winning operation's field values to the merged record\n * for the specific fields involved in the constraint violation.\n */\nfunction applyWinnerFields(\n\tmergedRecord: Record<string, unknown>,\n\twinner: Operation,\n\tfields: string[],\n): Record<string, unknown> {\n\tconst result = { ...mergedRecord }\n\tconst winnerData = winner.data ?? {}\n\tfor (const field of fields) {\n\t\tif (field in winnerData) {\n\t\t\tresult[field] = winnerData[field]\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Get a field value from an operation's data, falling back to the merged record.\n */\nfunction getFieldValue(\n\top: Operation,\n\tfield: string,\n\tmergedRecord: Record<string, unknown>,\n): unknown {\n\tconst data = op.data ?? {}\n\tif (field in data) {\n\t\treturn data[field]\n\t}\n\treturn mergedRecord[field]\n}\n\n/**\n * Compare two priority values. Supports numbers and strings.\n * Returns positive if a > b, negative if a < b, zero if equal.\n */\nfunction comparePriority(a: unknown, b: unknown): number {\n\tif (typeof a === 'number' && typeof b === 'number') {\n\t\treturn a - b\n\t}\n\tif (typeof a === 'string' && typeof b === 'string') {\n\t\treturn a < b ? -1 : a > b ? 1 : 0\n\t}\n\t// Mixed types: convert to string for comparison\n\treturn String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0\n}\n\nfunction createResolution(\n\tresolvedRecord: Record<string, unknown>,\n\tviolation: ConstraintViolation,\n\tlocalOp: Operation,\n\tremoteOp: Operation,\n\tbaseState: Record<string, unknown>,\n\tstrategy: string,\n\tstartTime: number,\n): ConstraintResolution {\n\tconst field = violation.fields.join(', ')\n\tconst trace: MergeTrace = {\n\t\toperationA: localOp,\n\t\toperationB: remoteOp,\n\t\tfield,\n\t\tstrategy,\n\t\tinputA: extractFieldValues(localOp, violation.fields),\n\t\tinputB: extractFieldValues(remoteOp, violation.fields),\n\t\tbase: extractFields(baseState, violation.fields),\n\t\toutput: extractFields(resolvedRecord, violation.fields),\n\t\ttier: 2,\n\t\tconstraintViolated: violation.message,\n\t\tduration: Date.now() - startTime,\n\t}\n\treturn { resolvedRecord, trace }\n}\n\nfunction extractFieldValues(op: Operation, fields: string[]): Record<string, unknown> {\n\tconst data = op.data ?? {}\n\tconst result: Record<string, unknown> = {}\n\tfor (const field of fields) {\n\t\tresult[field] = data[field]\n\t}\n\treturn result\n}\n\nfunction extractFields(record: Record<string, unknown>, fields: string[]): Record<string, unknown> {\n\tconst result: Record<string, unknown> = {}\n\tfor (const field of fields) {\n\t\tresult[field] = record[field]\n\t}\n\treturn result\n}\n","import { HybridLogicalClock } from '@korajs/core'\nimport type { Operation } from '@korajs/core'\nimport type { MergeTrace } from '@korajs/core'\nimport { checkConstraints } from '../constraints/constraint-checker'\nimport { resolveConstraintViolation } from '../constraints/resolvers'\nimport type { ConstraintContext, MergeInput, MergeResult } from '../types'\nimport { mergeField } from './field-merger'\n\n/**\n * Three-tier merge engine for resolving concurrent operations.\n *\n * Tier 1: Auto-merge per field kind (LWW, add-wins set, CRDT)\n * Tier 3: Custom resolvers override Tier 1 for specific fields\n * Tier 2: Constraint validation against the candidate merged state\n *\n * Tier 3 runs BEFORE Tier 2 so that constraints validate the final merged state\n * including any custom resolver outputs.\n *\n * @example\n * ```typescript\n * const engine = new MergeEngine()\n * const result = await engine.merge({\n * local: localOp,\n * remote: remoteOp,\n * baseState: { title: 'old', completed: false },\n * collectionDef: schema.collections.todos,\n * })\n * ```\n */\nexport class MergeEngine {\n\t/**\n\t * Merge two concurrent operations with all three tiers.\n\t *\n\t * Flow:\n\t * 1. Determine which fields conflict (both ops modified the same field)\n\t * 2. For non-conflicting fields: take the changed value from whichever op modified it\n\t * 3. For conflicting fields: Tier 3 custom resolver if exists, else Tier 1 auto-merge\n\t * 4. Assemble candidate merged record\n\t * 5. If constraintContext provided: run Tier 2 constraint checks and resolve violations\n\t * 6. Return MergeResult with all traces\n\t *\n\t * @param input - The two operations, base state, and collection definition\n\t * @param constraintContext - Optional DB lookup interface for Tier 2 constraints\n\t * @returns The merged data and traces for DevTools\n\t */\n\tasync merge(input: MergeInput, constraintContext?: ConstraintContext): Promise<MergeResult> {\n\t\t// Handle delete vs delete: both agree, no merge needed\n\t\tif (input.local.type === 'delete' && input.remote.type === 'delete') {\n\t\t\treturn {\n\t\t\t\tmergedData: {},\n\t\t\t\ttraces: [],\n\t\t\t\tappliedOperation: 'merged',\n\t\t\t}\n\t\t}\n\n\t\t// Handle update vs delete (or delete vs update)\n\t\tif (input.local.type === 'delete' || input.remote.type === 'delete') {\n\t\t\treturn this.mergeWithDelete(input)\n\t\t}\n\n\t\t// Insert vs insert or update vs update: field-level merge\n\t\tconst fieldResult = this.mergeFields(input)\n\n\t\t// Tier 2: Constraint checking (requires async DB lookups)\n\t\tif (constraintContext !== undefined && input.collectionDef.constraints.length > 0) {\n\t\t\tconst recordWithId = { id: input.local.recordId, ...fieldResult.mergedData }\n\t\t\tconst violations = await checkConstraints(\n\t\t\t\trecordWithId,\n\t\t\t\tinput.local.recordId,\n\t\t\t\tinput.local.collection,\n\t\t\t\tinput.collectionDef,\n\t\t\t\tconstraintContext,\n\t\t\t)\n\n\t\t\tlet mergedData = fieldResult.mergedData\n\t\t\tconst allTraces = [...fieldResult.traces]\n\n\t\t\tfor (const violation of violations) {\n\t\t\t\tconst resolution = resolveConstraintViolation(\n\t\t\t\t\tviolation,\n\t\t\t\t\tmergedData,\n\t\t\t\t\tinput.local,\n\t\t\t\t\tinput.remote,\n\t\t\t\t\tinput.baseState,\n\t\t\t\t)\n\t\t\t\tmergedData = resolution.resolvedRecord\n\t\t\t\tallTraces.push(resolution.trace)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tmergedData,\n\t\t\t\ttraces: allTraces,\n\t\t\t\tappliedOperation: determineAppliedOperation(allTraces),\n\t\t\t}\n\t\t}\n\n\t\treturn fieldResult\n\t}\n\n\t/**\n\t * Synchronous field-level merge (Tier 1 + Tier 3 only).\n\t *\n\t * Useful when constraint context is unavailable or not needed.\n\t * Skips Tier 2 constraint checking entirely.\n\t *\n\t * @param input - The two operations, base state, and collection definition\n\t * @returns The merged data and traces for DevTools\n\t */\n\tmergeFields(input: MergeInput): MergeResult {\n\t\tconst { local, remote, baseState, collectionDef } = input\n\n\t\t// Collect all field names that either operation touches\n\t\tconst allFields = collectAffectedFields(local, remote, baseState, collectionDef)\n\n\t\tconst mergedData: Record<string, unknown> = {}\n\t\tconst traces: MergeTrace[] = []\n\n\t\tfor (const fieldName of allFields) {\n\t\t\tconst fieldDef = collectionDef.fields[fieldName]\n\t\t\tif (fieldDef === undefined) {\n\t\t\t\t// Field not in schema — skip (could be a removed field from migration)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst resolver = collectionDef.resolvers[fieldName]\n\t\t\tconst result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver)\n\n\t\t\tmergedData[fieldName] = result.value\n\n\t\t\t// Only include traces for actual conflicts (not no-conflict cases)\n\t\t\tif (\n\t\t\t\tresult.trace.strategy !== 'no-conflict-local' &&\n\t\t\t\tresult.trace.strategy !== 'no-conflict-remote' &&\n\t\t\t\tresult.trace.strategy !== 'no-conflict-unchanged'\n\t\t\t) {\n\t\t\t\ttraces.push(result.trace)\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tmergedData,\n\t\t\ttraces,\n\t\t\tappliedOperation: determineAppliedOperation(traces),\n\t\t}\n\t}\n\n\t/**\n\t * Handle merge when one operation is a delete.\n\t * Default: delete wins (LWW on the record level).\n\t */\n\tprivate mergeWithDelete(input: MergeInput): MergeResult {\n\t\tconst { local, remote } = input\n\n\t\t// LWW at the record level: later operation wins\n\t\tconst comparison = HybridLogicalClock.compare(local.timestamp, remote.timestamp)\n\n\t\tif (comparison >= 0) {\n\t\t\t// Local is later\n\t\t\tif (local.type === 'delete') {\n\t\t\t\treturn { mergedData: {}, traces: [], appliedOperation: 'local' }\n\t\t\t}\n\t\t\t// Local is an update that's later than remote delete → local wins\n\t\t\treturn {\n\t\t\t\tmergedData: { ...input.baseState, ...(local.data ?? {}) },\n\t\t\t\ttraces: [],\n\t\t\t\tappliedOperation: 'local',\n\t\t\t}\n\t\t}\n\n\t\t// Remote is later\n\t\tif (remote.type === 'delete') {\n\t\t\treturn { mergedData: {}, traces: [], appliedOperation: 'remote' }\n\t\t}\n\t\t// Remote is an update that's later than local delete → remote wins\n\t\treturn {\n\t\t\tmergedData: { ...input.baseState, ...(remote.data ?? {}) },\n\t\t\ttraces: [],\n\t\t\tappliedOperation: 'remote',\n\t\t}\n\t}\n}\n\n/**\n * Collect all field names affected by either operation or present in the base state.\n */\nfunction collectAffectedFields(\n\tlocal: Operation,\n\tremote: Operation,\n\tbaseState: Record<string, unknown>,\n\tcollectionDef: { fields: Record<string, unknown> },\n): Set<string> {\n\tconst fields = new Set<string>()\n\n\t// Fields from the schema definition\n\tfor (const fieldName of Object.keys(collectionDef.fields)) {\n\t\tfields.add(fieldName)\n\t}\n\n\t// Fields from local operation\n\tif (local.data !== null) {\n\t\tfor (const fieldName of Object.keys(local.data)) {\n\t\t\tfields.add(fieldName)\n\t\t}\n\t}\n\n\t// Fields from remote operation\n\tif (remote.data !== null) {\n\t\tfor (const fieldName of Object.keys(remote.data)) {\n\t\t\tfields.add(fieldName)\n\t\t}\n\t}\n\n\t// Fields from base state\n\tfor (const fieldName of Object.keys(baseState)) {\n\t\tfields.add(fieldName)\n\t}\n\n\treturn fields\n}\n\n/**\n * Determine which operation's values dominate overall.\n * If all conflict traces went the same way, report that side.\n * Otherwise, report 'merged'.\n */\nfunction determineAppliedOperation(traces: MergeTrace[]): 'local' | 'remote' | 'merged' {\n\tif (traces.length === 0) {\n\t\treturn 'merged'\n\t}\n\n\tlet allLocal = true\n\tlet allRemote = true\n\n\tfor (const trace of traces) {\n\t\tif (trace.strategy === 'lww' || trace.strategy === 'constraint-lww') {\n\t\t\t// Check if local or remote value was the output\n\t\t\tif (trace.output === trace.inputA) {\n\t\t\t\tallRemote = false\n\t\t\t} else if (trace.output === trace.inputB) {\n\t\t\t\tallLocal = false\n\t\t\t} else {\n\t\t\t\tallLocal = false\n\t\t\t\tallRemote = false\n\t\t\t}\n\t\t} else {\n\t\t\t// For non-LWW strategies (add-wins-set, custom, etc.), it's a merge\n\t\t\tallLocal = false\n\t\t\tallRemote = false\n\t\t}\n\t}\n\n\tif (allLocal) return 'local'\n\tif (allRemote) return 'remote'\n\treturn 'merged'\n}\n"],"mappings":"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","names":["import_core","winner","resolvedRecord","import_core"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Constraint, MergeTrace, Operation, CollectionDefinition, HLCTimestamp, FieldDescriptor, CustomResolver } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input to the merge engine when two concurrent operations conflict.
|
|
5
|
+
*/
|
|
6
|
+
interface MergeInput {
|
|
7
|
+
/** The locally-originated operation */
|
|
8
|
+
local: Operation;
|
|
9
|
+
/** The remotely-originated operation */
|
|
10
|
+
remote: Operation;
|
|
11
|
+
/** Full record state before either operation was applied */
|
|
12
|
+
baseState: Record<string, unknown>;
|
|
13
|
+
/** Schema definition for the collection being merged */
|
|
14
|
+
collectionDef: CollectionDefinition;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Output of the merge engine after resolving all field conflicts.
|
|
18
|
+
*/
|
|
19
|
+
interface MergeResult {
|
|
20
|
+
/** The resolved field values after merging */
|
|
21
|
+
mergedData: Record<string, unknown>;
|
|
22
|
+
/** One trace per conflicting field (for DevTools) */
|
|
23
|
+
traces: MergeTrace[];
|
|
24
|
+
/** Which operation's values dominate overall, or 'merged' if mixed */
|
|
25
|
+
appliedOperation: 'local' | 'remote' | 'merged';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Output of a single field-level merge decision.
|
|
29
|
+
*/
|
|
30
|
+
interface FieldMergeResult {
|
|
31
|
+
/** The resolved value for this field */
|
|
32
|
+
value: unknown;
|
|
33
|
+
/** Trace of the merge decision (for DevTools) */
|
|
34
|
+
trace: MergeTrace;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pluggable database lookup interface for Tier 2 constraint checking.
|
|
38
|
+
* @korajs/store provides the implementation at runtime; the merge package
|
|
39
|
+
* only depends on this interface, keeping it storage-agnostic.
|
|
40
|
+
*/
|
|
41
|
+
interface ConstraintContext {
|
|
42
|
+
/** Query records matching the given filter in a collection */
|
|
43
|
+
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
44
|
+
/** Count records matching the given filter in a collection */
|
|
45
|
+
countRecords(collection: string, where: Record<string, unknown>): Promise<number>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Describes a constraint that was violated after auto-merge.
|
|
49
|
+
*/
|
|
50
|
+
interface ConstraintViolation {
|
|
51
|
+
/** The constraint definition that was violated */
|
|
52
|
+
constraint: Constraint;
|
|
53
|
+
/** The field(s) involved in the violation */
|
|
54
|
+
fields: string[];
|
|
55
|
+
/** Human-readable description of the violation */
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of a Last-Write-Wins comparison.
|
|
61
|
+
*/
|
|
62
|
+
interface LWWResult {
|
|
63
|
+
/** The winning value */
|
|
64
|
+
value: unknown;
|
|
65
|
+
/** Which side won */
|
|
66
|
+
winner: 'local' | 'remote';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Last-Write-Wins merge strategy using HLC timestamps.
|
|
70
|
+
*
|
|
71
|
+
* Compares two values by their HLC timestamps and returns the value with the
|
|
72
|
+
* later timestamp. The HLC total order guarantees a deterministic winner even
|
|
73
|
+
* when wall-clock times and logical counters are identical (nodeId tiebreaker).
|
|
74
|
+
*
|
|
75
|
+
* @param localValue - The local field value
|
|
76
|
+
* @param remoteValue - The remote field value
|
|
77
|
+
* @param localTimestamp - HLC timestamp of the local operation
|
|
78
|
+
* @param remoteTimestamp - HLC timestamp of the remote operation
|
|
79
|
+
* @returns The winning value and which side won
|
|
80
|
+
*/
|
|
81
|
+
declare function lastWriteWins(localValue: unknown, remoteValue: unknown, localTimestamp: HLCTimestamp, remoteTimestamp: HLCTimestamp): LWWResult;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add-wins set merge strategy for array fields.
|
|
85
|
+
*
|
|
86
|
+
* When two sides concurrently modify an array, this strategy preserves all
|
|
87
|
+
* additions from both sides. An element is only removed from the result if
|
|
88
|
+
* BOTH sides independently removed it. This prevents data loss: if one side
|
|
89
|
+
* adds an element while another removes a different element, both changes
|
|
90
|
+
* are preserved.
|
|
91
|
+
*
|
|
92
|
+
* Algorithm:
|
|
93
|
+
* added_local = local - base
|
|
94
|
+
* added_remote = remote - base
|
|
95
|
+
* removed_local = base - local
|
|
96
|
+
* removed_remote = base - remote
|
|
97
|
+
* result = (base ∪ added_local ∪ added_remote) - (removed_local ∩ removed_remote)
|
|
98
|
+
*
|
|
99
|
+
* Uses JSON.stringify for element comparison to handle primitives and objects.
|
|
100
|
+
*
|
|
101
|
+
* @param localArray - The local array after local modifications
|
|
102
|
+
* @param remoteArray - The remote array after remote modifications
|
|
103
|
+
* @param baseArray - The array state before either modification
|
|
104
|
+
* @returns The merged array
|
|
105
|
+
*/
|
|
106
|
+
declare function addWinsSet(localArray: unknown[], remoteArray: unknown[], baseArray: unknown[]): unknown[];
|
|
107
|
+
|
|
108
|
+
type RichtextValue = string | Uint8Array | ArrayBuffer | null | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Merges richtext values using Yjs CRDT updates.
|
|
111
|
+
*/
|
|
112
|
+
declare function mergeRichtext(localValue: RichtextValue, remoteValue: RichtextValue, baseValue: RichtextValue): Uint8Array;
|
|
113
|
+
/**
|
|
114
|
+
* Converts a richtext state update to plain text.
|
|
115
|
+
*/
|
|
116
|
+
declare function richtextToString(value: RichtextValue): string;
|
|
117
|
+
/**
|
|
118
|
+
* Converts a plain string to a Yjs state update.
|
|
119
|
+
*/
|
|
120
|
+
declare function stringToRichtextUpdate(value: string): Uint8Array;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Merges a single field from two concurrent operations.
|
|
124
|
+
*
|
|
125
|
+
* Dispatches to the appropriate strategy based on field kind:
|
|
126
|
+
* - string, number, boolean, enum, timestamp → LWW (Last-Write-Wins via HLC)
|
|
127
|
+
* - array → add-wins set (union of additions, only mutual removals)
|
|
128
|
+
* - richtext → Yjs CRDT merge
|
|
129
|
+
*
|
|
130
|
+
* If a custom resolver (Tier 3) is provided, it overrides the default strategy.
|
|
131
|
+
*
|
|
132
|
+
* Handles non-conflict cases where only one side modified the field:
|
|
133
|
+
* - Only local changed → take local value
|
|
134
|
+
* - Only remote changed → take remote value
|
|
135
|
+
* - Neither changed → keep base value
|
|
136
|
+
*
|
|
137
|
+
* @param fieldName - Name of the field being merged
|
|
138
|
+
* @param localOp - The local operation
|
|
139
|
+
* @param remoteOp - The remote operation
|
|
140
|
+
* @param baseState - Full record state before either operation
|
|
141
|
+
* @param fieldDescriptor - Schema descriptor for this field
|
|
142
|
+
* @param resolver - Optional Tier 3 custom resolver for this field
|
|
143
|
+
* @returns The merged field value and a trace for DevTools
|
|
144
|
+
*/
|
|
145
|
+
declare function mergeField(fieldName: string, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>, fieldDescriptor: FieldDescriptor, resolver?: CustomResolver): FieldMergeResult;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Checks all constraints on a collection against a candidate merged record.
|
|
149
|
+
*
|
|
150
|
+
* Called after Tier 1+3 merge produces a candidate state. Each violated
|
|
151
|
+
* constraint is returned as a ConstraintViolation for Tier 2 resolution.
|
|
152
|
+
*
|
|
153
|
+
* @param mergedRecord - The candidate record state after field-level merge
|
|
154
|
+
* @param recordId - ID of the record being merged
|
|
155
|
+
* @param collection - Name of the collection
|
|
156
|
+
* @param collectionDef - Schema definition for the collection
|
|
157
|
+
* @param constraintContext - Pluggable DB lookup interface
|
|
158
|
+
* @returns Array of violated constraints (empty if all pass)
|
|
159
|
+
*/
|
|
160
|
+
declare function checkConstraints(mergedRecord: Record<string, unknown>, recordId: string, collection: string, collectionDef: CollectionDefinition, constraintContext: ConstraintContext): Promise<ConstraintViolation[]>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Result of resolving a constraint violation.
|
|
164
|
+
*/
|
|
165
|
+
interface ConstraintResolution {
|
|
166
|
+
/** The updated record after constraint resolution */
|
|
167
|
+
resolvedRecord: Record<string, unknown>;
|
|
168
|
+
/** Trace of the resolution decision for DevTools */
|
|
169
|
+
trace: MergeTrace;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolves a constraint violation by applying the constraint's onConflict strategy.
|
|
173
|
+
*
|
|
174
|
+
* Strategies:
|
|
175
|
+
* - `last-write-wins`: The operation with the later HLC timestamp wins entirely
|
|
176
|
+
* - `first-write-wins`: The operation with the earlier HLC timestamp wins entirely
|
|
177
|
+
* - `priority-field`: Compares a designated priority field to determine the winner
|
|
178
|
+
* - `server-decides`: Returns a marker indicating deferred server resolution
|
|
179
|
+
* - `custom`: Calls the constraint's resolve function
|
|
180
|
+
*
|
|
181
|
+
* @param violation - The constraint violation to resolve
|
|
182
|
+
* @param mergedRecord - The current candidate record state
|
|
183
|
+
* @param localOp - The local operation
|
|
184
|
+
* @param remoteOp - The remote operation
|
|
185
|
+
* @param baseState - The record state before either operation
|
|
186
|
+
* @returns The resolved record and a trace
|
|
187
|
+
*/
|
|
188
|
+
declare function resolveConstraintViolation(violation: ConstraintViolation, mergedRecord: Record<string, unknown>, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>): ConstraintResolution;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Three-tier merge engine for resolving concurrent operations.
|
|
192
|
+
*
|
|
193
|
+
* Tier 1: Auto-merge per field kind (LWW, add-wins set, CRDT)
|
|
194
|
+
* Tier 3: Custom resolvers override Tier 1 for specific fields
|
|
195
|
+
* Tier 2: Constraint validation against the candidate merged state
|
|
196
|
+
*
|
|
197
|
+
* Tier 3 runs BEFORE Tier 2 so that constraints validate the final merged state
|
|
198
|
+
* including any custom resolver outputs.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const engine = new MergeEngine()
|
|
203
|
+
* const result = await engine.merge({
|
|
204
|
+
* local: localOp,
|
|
205
|
+
* remote: remoteOp,
|
|
206
|
+
* baseState: { title: 'old', completed: false },
|
|
207
|
+
* collectionDef: schema.collections.todos,
|
|
208
|
+
* })
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
declare class MergeEngine {
|
|
212
|
+
/**
|
|
213
|
+
* Merge two concurrent operations with all three tiers.
|
|
214
|
+
*
|
|
215
|
+
* Flow:
|
|
216
|
+
* 1. Determine which fields conflict (both ops modified the same field)
|
|
217
|
+
* 2. For non-conflicting fields: take the changed value from whichever op modified it
|
|
218
|
+
* 3. For conflicting fields: Tier 3 custom resolver if exists, else Tier 1 auto-merge
|
|
219
|
+
* 4. Assemble candidate merged record
|
|
220
|
+
* 5. If constraintContext provided: run Tier 2 constraint checks and resolve violations
|
|
221
|
+
* 6. Return MergeResult with all traces
|
|
222
|
+
*
|
|
223
|
+
* @param input - The two operations, base state, and collection definition
|
|
224
|
+
* @param constraintContext - Optional DB lookup interface for Tier 2 constraints
|
|
225
|
+
* @returns The merged data and traces for DevTools
|
|
226
|
+
*/
|
|
227
|
+
merge(input: MergeInput, constraintContext?: ConstraintContext): Promise<MergeResult>;
|
|
228
|
+
/**
|
|
229
|
+
* Synchronous field-level merge (Tier 1 + Tier 3 only).
|
|
230
|
+
*
|
|
231
|
+
* Useful when constraint context is unavailable or not needed.
|
|
232
|
+
* Skips Tier 2 constraint checking entirely.
|
|
233
|
+
*
|
|
234
|
+
* @param input - The two operations, base state, and collection definition
|
|
235
|
+
* @returns The merged data and traces for DevTools
|
|
236
|
+
*/
|
|
237
|
+
mergeFields(input: MergeInput): MergeResult;
|
|
238
|
+
/**
|
|
239
|
+
* Handle merge when one operation is a delete.
|
|
240
|
+
* Default: delete wins (LWW on the record level).
|
|
241
|
+
*/
|
|
242
|
+
private mergeWithDelete;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { type ConstraintContext, type ConstraintResolution, type ConstraintViolation, type FieldMergeResult, type LWWResult, MergeEngine, type MergeInput, type MergeResult, addWinsSet, checkConstraints, lastWriteWins, mergeField, mergeRichtext, resolveConstraintViolation, richtextToString, stringToRichtextUpdate };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Constraint, MergeTrace, Operation, CollectionDefinition, HLCTimestamp, FieldDescriptor, CustomResolver } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input to the merge engine when two concurrent operations conflict.
|
|
5
|
+
*/
|
|
6
|
+
interface MergeInput {
|
|
7
|
+
/** The locally-originated operation */
|
|
8
|
+
local: Operation;
|
|
9
|
+
/** The remotely-originated operation */
|
|
10
|
+
remote: Operation;
|
|
11
|
+
/** Full record state before either operation was applied */
|
|
12
|
+
baseState: Record<string, unknown>;
|
|
13
|
+
/** Schema definition for the collection being merged */
|
|
14
|
+
collectionDef: CollectionDefinition;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Output of the merge engine after resolving all field conflicts.
|
|
18
|
+
*/
|
|
19
|
+
interface MergeResult {
|
|
20
|
+
/** The resolved field values after merging */
|
|
21
|
+
mergedData: Record<string, unknown>;
|
|
22
|
+
/** One trace per conflicting field (for DevTools) */
|
|
23
|
+
traces: MergeTrace[];
|
|
24
|
+
/** Which operation's values dominate overall, or 'merged' if mixed */
|
|
25
|
+
appliedOperation: 'local' | 'remote' | 'merged';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Output of a single field-level merge decision.
|
|
29
|
+
*/
|
|
30
|
+
interface FieldMergeResult {
|
|
31
|
+
/** The resolved value for this field */
|
|
32
|
+
value: unknown;
|
|
33
|
+
/** Trace of the merge decision (for DevTools) */
|
|
34
|
+
trace: MergeTrace;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pluggable database lookup interface for Tier 2 constraint checking.
|
|
38
|
+
* @korajs/store provides the implementation at runtime; the merge package
|
|
39
|
+
* only depends on this interface, keeping it storage-agnostic.
|
|
40
|
+
*/
|
|
41
|
+
interface ConstraintContext {
|
|
42
|
+
/** Query records matching the given filter in a collection */
|
|
43
|
+
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
44
|
+
/** Count records matching the given filter in a collection */
|
|
45
|
+
countRecords(collection: string, where: Record<string, unknown>): Promise<number>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Describes a constraint that was violated after auto-merge.
|
|
49
|
+
*/
|
|
50
|
+
interface ConstraintViolation {
|
|
51
|
+
/** The constraint definition that was violated */
|
|
52
|
+
constraint: Constraint;
|
|
53
|
+
/** The field(s) involved in the violation */
|
|
54
|
+
fields: string[];
|
|
55
|
+
/** Human-readable description of the violation */
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of a Last-Write-Wins comparison.
|
|
61
|
+
*/
|
|
62
|
+
interface LWWResult {
|
|
63
|
+
/** The winning value */
|
|
64
|
+
value: unknown;
|
|
65
|
+
/** Which side won */
|
|
66
|
+
winner: 'local' | 'remote';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Last-Write-Wins merge strategy using HLC timestamps.
|
|
70
|
+
*
|
|
71
|
+
* Compares two values by their HLC timestamps and returns the value with the
|
|
72
|
+
* later timestamp. The HLC total order guarantees a deterministic winner even
|
|
73
|
+
* when wall-clock times and logical counters are identical (nodeId tiebreaker).
|
|
74
|
+
*
|
|
75
|
+
* @param localValue - The local field value
|
|
76
|
+
* @param remoteValue - The remote field value
|
|
77
|
+
* @param localTimestamp - HLC timestamp of the local operation
|
|
78
|
+
* @param remoteTimestamp - HLC timestamp of the remote operation
|
|
79
|
+
* @returns The winning value and which side won
|
|
80
|
+
*/
|
|
81
|
+
declare function lastWriteWins(localValue: unknown, remoteValue: unknown, localTimestamp: HLCTimestamp, remoteTimestamp: HLCTimestamp): LWWResult;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add-wins set merge strategy for array fields.
|
|
85
|
+
*
|
|
86
|
+
* When two sides concurrently modify an array, this strategy preserves all
|
|
87
|
+
* additions from both sides. An element is only removed from the result if
|
|
88
|
+
* BOTH sides independently removed it. This prevents data loss: if one side
|
|
89
|
+
* adds an element while another removes a different element, both changes
|
|
90
|
+
* are preserved.
|
|
91
|
+
*
|
|
92
|
+
* Algorithm:
|
|
93
|
+
* added_local = local - base
|
|
94
|
+
* added_remote = remote - base
|
|
95
|
+
* removed_local = base - local
|
|
96
|
+
* removed_remote = base - remote
|
|
97
|
+
* result = (base ∪ added_local ∪ added_remote) - (removed_local ∩ removed_remote)
|
|
98
|
+
*
|
|
99
|
+
* Uses JSON.stringify for element comparison to handle primitives and objects.
|
|
100
|
+
*
|
|
101
|
+
* @param localArray - The local array after local modifications
|
|
102
|
+
* @param remoteArray - The remote array after remote modifications
|
|
103
|
+
* @param baseArray - The array state before either modification
|
|
104
|
+
* @returns The merged array
|
|
105
|
+
*/
|
|
106
|
+
declare function addWinsSet(localArray: unknown[], remoteArray: unknown[], baseArray: unknown[]): unknown[];
|
|
107
|
+
|
|
108
|
+
type RichtextValue = string | Uint8Array | ArrayBuffer | null | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Merges richtext values using Yjs CRDT updates.
|
|
111
|
+
*/
|
|
112
|
+
declare function mergeRichtext(localValue: RichtextValue, remoteValue: RichtextValue, baseValue: RichtextValue): Uint8Array;
|
|
113
|
+
/**
|
|
114
|
+
* Converts a richtext state update to plain text.
|
|
115
|
+
*/
|
|
116
|
+
declare function richtextToString(value: RichtextValue): string;
|
|
117
|
+
/**
|
|
118
|
+
* Converts a plain string to a Yjs state update.
|
|
119
|
+
*/
|
|
120
|
+
declare function stringToRichtextUpdate(value: string): Uint8Array;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Merges a single field from two concurrent operations.
|
|
124
|
+
*
|
|
125
|
+
* Dispatches to the appropriate strategy based on field kind:
|
|
126
|
+
* - string, number, boolean, enum, timestamp → LWW (Last-Write-Wins via HLC)
|
|
127
|
+
* - array → add-wins set (union of additions, only mutual removals)
|
|
128
|
+
* - richtext → Yjs CRDT merge
|
|
129
|
+
*
|
|
130
|
+
* If a custom resolver (Tier 3) is provided, it overrides the default strategy.
|
|
131
|
+
*
|
|
132
|
+
* Handles non-conflict cases where only one side modified the field:
|
|
133
|
+
* - Only local changed → take local value
|
|
134
|
+
* - Only remote changed → take remote value
|
|
135
|
+
* - Neither changed → keep base value
|
|
136
|
+
*
|
|
137
|
+
* @param fieldName - Name of the field being merged
|
|
138
|
+
* @param localOp - The local operation
|
|
139
|
+
* @param remoteOp - The remote operation
|
|
140
|
+
* @param baseState - Full record state before either operation
|
|
141
|
+
* @param fieldDescriptor - Schema descriptor for this field
|
|
142
|
+
* @param resolver - Optional Tier 3 custom resolver for this field
|
|
143
|
+
* @returns The merged field value and a trace for DevTools
|
|
144
|
+
*/
|
|
145
|
+
declare function mergeField(fieldName: string, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>, fieldDescriptor: FieldDescriptor, resolver?: CustomResolver): FieldMergeResult;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Checks all constraints on a collection against a candidate merged record.
|
|
149
|
+
*
|
|
150
|
+
* Called after Tier 1+3 merge produces a candidate state. Each violated
|
|
151
|
+
* constraint is returned as a ConstraintViolation for Tier 2 resolution.
|
|
152
|
+
*
|
|
153
|
+
* @param mergedRecord - The candidate record state after field-level merge
|
|
154
|
+
* @param recordId - ID of the record being merged
|
|
155
|
+
* @param collection - Name of the collection
|
|
156
|
+
* @param collectionDef - Schema definition for the collection
|
|
157
|
+
* @param constraintContext - Pluggable DB lookup interface
|
|
158
|
+
* @returns Array of violated constraints (empty if all pass)
|
|
159
|
+
*/
|
|
160
|
+
declare function checkConstraints(mergedRecord: Record<string, unknown>, recordId: string, collection: string, collectionDef: CollectionDefinition, constraintContext: ConstraintContext): Promise<ConstraintViolation[]>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Result of resolving a constraint violation.
|
|
164
|
+
*/
|
|
165
|
+
interface ConstraintResolution {
|
|
166
|
+
/** The updated record after constraint resolution */
|
|
167
|
+
resolvedRecord: Record<string, unknown>;
|
|
168
|
+
/** Trace of the resolution decision for DevTools */
|
|
169
|
+
trace: MergeTrace;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Resolves a constraint violation by applying the constraint's onConflict strategy.
|
|
173
|
+
*
|
|
174
|
+
* Strategies:
|
|
175
|
+
* - `last-write-wins`: The operation with the later HLC timestamp wins entirely
|
|
176
|
+
* - `first-write-wins`: The operation with the earlier HLC timestamp wins entirely
|
|
177
|
+
* - `priority-field`: Compares a designated priority field to determine the winner
|
|
178
|
+
* - `server-decides`: Returns a marker indicating deferred server resolution
|
|
179
|
+
* - `custom`: Calls the constraint's resolve function
|
|
180
|
+
*
|
|
181
|
+
* @param violation - The constraint violation to resolve
|
|
182
|
+
* @param mergedRecord - The current candidate record state
|
|
183
|
+
* @param localOp - The local operation
|
|
184
|
+
* @param remoteOp - The remote operation
|
|
185
|
+
* @param baseState - The record state before either operation
|
|
186
|
+
* @returns The resolved record and a trace
|
|
187
|
+
*/
|
|
188
|
+
declare function resolveConstraintViolation(violation: ConstraintViolation, mergedRecord: Record<string, unknown>, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>): ConstraintResolution;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Three-tier merge engine for resolving concurrent operations.
|
|
192
|
+
*
|
|
193
|
+
* Tier 1: Auto-merge per field kind (LWW, add-wins set, CRDT)
|
|
194
|
+
* Tier 3: Custom resolvers override Tier 1 for specific fields
|
|
195
|
+
* Tier 2: Constraint validation against the candidate merged state
|
|
196
|
+
*
|
|
197
|
+
* Tier 3 runs BEFORE Tier 2 so that constraints validate the final merged state
|
|
198
|
+
* including any custom resolver outputs.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const engine = new MergeEngine()
|
|
203
|
+
* const result = await engine.merge({
|
|
204
|
+
* local: localOp,
|
|
205
|
+
* remote: remoteOp,
|
|
206
|
+
* baseState: { title: 'old', completed: false },
|
|
207
|
+
* collectionDef: schema.collections.todos,
|
|
208
|
+
* })
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
declare class MergeEngine {
|
|
212
|
+
/**
|
|
213
|
+
* Merge two concurrent operations with all three tiers.
|
|
214
|
+
*
|
|
215
|
+
* Flow:
|
|
216
|
+
* 1. Determine which fields conflict (both ops modified the same field)
|
|
217
|
+
* 2. For non-conflicting fields: take the changed value from whichever op modified it
|
|
218
|
+
* 3. For conflicting fields: Tier 3 custom resolver if exists, else Tier 1 auto-merge
|
|
219
|
+
* 4. Assemble candidate merged record
|
|
220
|
+
* 5. If constraintContext provided: run Tier 2 constraint checks and resolve violations
|
|
221
|
+
* 6. Return MergeResult with all traces
|
|
222
|
+
*
|
|
223
|
+
* @param input - The two operations, base state, and collection definition
|
|
224
|
+
* @param constraintContext - Optional DB lookup interface for Tier 2 constraints
|
|
225
|
+
* @returns The merged data and traces for DevTools
|
|
226
|
+
*/
|
|
227
|
+
merge(input: MergeInput, constraintContext?: ConstraintContext): Promise<MergeResult>;
|
|
228
|
+
/**
|
|
229
|
+
* Synchronous field-level merge (Tier 1 + Tier 3 only).
|
|
230
|
+
*
|
|
231
|
+
* Useful when constraint context is unavailable or not needed.
|
|
232
|
+
* Skips Tier 2 constraint checking entirely.
|
|
233
|
+
*
|
|
234
|
+
* @param input - The two operations, base state, and collection definition
|
|
235
|
+
* @returns The merged data and traces for DevTools
|
|
236
|
+
*/
|
|
237
|
+
mergeFields(input: MergeInput): MergeResult;
|
|
238
|
+
/**
|
|
239
|
+
* Handle merge when one operation is a delete.
|
|
240
|
+
* Default: delete wins (LWW on the record level).
|
|
241
|
+
*/
|
|
242
|
+
private mergeWithDelete;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { type ConstraintContext, type ConstraintResolution, type ConstraintViolation, type FieldMergeResult, type LWWResult, MergeEngine, type MergeInput, type MergeResult, addWinsSet, checkConstraints, lastWriteWins, mergeField, mergeRichtext, resolveConstraintViolation, richtextToString, stringToRichtextUpdate };
|