Merge API Reference
@korajs/merge implements the three-tier conflict resolution engine for Kora.js. It determines what happens when concurrent operations from different devices modify the same data.
- Tier 1 -- Auto-merge per field kind (LWW, add-wins set, CRDT)
- Tier 3 -- Custom resolvers override Tier 1 for specific fields
- Tier 2 -- Constraint validation against the candidate merged state
Tier 3 runs before Tier 2 so that constraints validate the final merged state including any custom resolver outputs.
Imports
import {
MergeEngine,
lastWriteWins,
addWinsSet,
mergeRichtext,
richtextToString,
stringToRichtextUpdate,
mergeField,
checkConstraints,
resolveConstraintViolation,
checkReferentialIntegrityOnDelete,
resolveDeleteVsInsertConflict,
buildMergeRelationLookup,
resolveStateMachineMerge,
isStateMachineField,
} from '@korajs/merge'
import type {
MergeInput,
MergeResult,
FieldMergeResult,
ConstraintContext,
ConstraintViolation,
ConstraintResolution,
LWWResult,
ReferentialMergeContext,
ReferentialCheckResult,
SideEffectOp,
DeleteVsInsertResolution,
MergeIncomingRelation,
StateMachineMergeResult,
} from '@korajs/merge'MergeEngine
The main entry point for resolving concurrent operations. Orchestrates all three merge tiers.
Constructor
new MergeEngine()Takes no parameters.
Methods
.merge(input, constraintContext?)
Merges two concurrent operations using all three tiers. This is the primary method for full conflict resolution.
async merge(input: MergeInput, constraintContext?: ConstraintContext): Promise<MergeResult>| Parameter | Type | Required | Description |
|---|---|---|---|
input | MergeInput | Yes | The two operations, base state, and collection definition. |
constraintContext | ConstraintContext | No | Pluggable DB lookup interface for Tier 2 constraint checking. If omitted, Tier 2 is skipped. |
Returns: Promise<MergeResult> -- The merged data, traces for DevTools, and which operation dominated.
Flow:
- If both operations are deletes, returns empty merged data (both agree).
- If one operation is a delete, applies record-level LWW (later timestamp wins).
- Otherwise, runs field-level merge (Tier 1 + Tier 3) via
mergeFields(). - If
constraintContextis provided and the collection has constraints, runs Tier 2 constraint checking and resolves any violations.
const engine = new MergeEngine()
const result = await engine.merge({
local: localOp,
remote: remoteOp,
baseState: { title: 'Buy groceries', completed: false },
collectionDef: schema.collections.todos,
})
console.log(result.mergedData) // { title: 'Buy groceries', completed: true }
console.log(result.appliedOperation) // 'local' | 'remote' | 'merged'
console.log(result.traces) // MergeTrace[] for DevTools.mergeFields(input)
Synchronous field-level merge using Tier 1 and Tier 3 only. Skips Tier 2 constraint checking entirely. Useful when constraint context is unavailable or not needed.
mergeFields(input: MergeInput): MergeResult| Parameter | Type | Required | Description |
|---|---|---|---|
input | MergeInput | Yes | The two operations, base state, and collection definition. |
Returns: MergeResult -- The merged data and traces. Note this is synchronous (no Promise).
const engine = new MergeEngine()
const result = engine.mergeFields({
local: localOp,
remote: remoteOp,
baseState: { title: 'Old title', tags: ['work'] },
collectionDef: schema.collections.todos,
})Merge Strategies
Low-level merge functions used by the engine. These are also exported for direct use in custom resolvers or testing.
lastWriteWins(localValue, remoteValue, localTimestamp, remoteTimestamp)
Last-Write-Wins merge strategy using HLC timestamps. The value with the later timestamp wins. The HLC total order guarantees a deterministic winner even when wall-clock times and logical counters are identical (nodeId tiebreaker).
Used by default for string, number, boolean, enum, and timestamp fields.
function lastWriteWins(
localValue: unknown,
remoteValue: unknown,
localTimestamp: HLCTimestamp,
remoteTimestamp: HLCTimestamp,
): LWWResult| Parameter | Type | Description |
|---|---|---|
localValue | unknown | The local field value. |
remoteValue | unknown | The remote field value. |
localTimestamp | HLCTimestamp | HLC timestamp of the local operation. |
remoteTimestamp | HLCTimestamp | HLC timestamp of the remote operation. |
Returns: LWWResult
import { lastWriteWins } from '@korajs/merge'
const result = lastWriteWins('local title', 'remote title', localTs, remoteTs)
console.log(result.value) // The winning value
console.log(result.winner) // 'local' or 'remote'addWinsSet(localArray, remoteArray, baseArray)
Add-wins set merge strategy for array fields. Preserves all additions from both sides. An element is only removed from the result if both sides independently removed it.
Algorithm:
added_local = local - base
added_remote = remote - base
removed_local = base - local
removed_remote = base - remote
result = (base U added_local U added_remote) - (removed_local intersection removed_remote)Element ordering: base elements first (preserving original order), then local additions, then remote additions. Uses JSON.stringify for element comparison.
function addWinsSet(
localArray: unknown[],
remoteArray: unknown[],
baseArray: unknown[],
): unknown[]| Parameter | Type | Description |
|---|---|---|
localArray | unknown[] | The local array after local modifications. |
remoteArray | unknown[] | The remote array after remote modifications. |
baseArray | unknown[] | The array state before either modification. |
Returns: unknown[] -- The merged array.
import { addWinsSet } from '@korajs/merge'
const base = ['a', 'b', 'c']
const local = ['a', 'b', 'c', 'd'] // added 'd'
const remote = ['a', 'c', 'e'] // removed 'b', added 'e'
const merged = addWinsSet(local, remote, base)
// ['a', 'b', 'c', 'd', 'e']
// 'b' is kept because only one side removed it (add-wins)
// 'd' and 'e' are both preservedmergeRichtext(localValue, remoteValue, baseValue)
Merges rich text values using Yjs CRDT document updates. Supports character-level collaborative editing with automatic merge. Used by default for richtext fields.
function mergeRichtext(
localValue: RichtextValue,
remoteValue: RichtextValue,
baseValue: RichtextValue,
): Uint8Array| Parameter | Type | Description |
|---|---|---|
localValue | RichtextValue | The local rich text state. |
remoteValue | RichtextValue | The remote rich text state. |
baseValue | RichtextValue | The base rich text state before either modification. |
RichtextValue is string | Uint8Array | ArrayBuffer | null | undefined.
Returns: Uint8Array -- The merged Yjs state update.
import { mergeRichtext, richtextToString } from '@korajs/merge'
const merged = mergeRichtext(localUpdate, remoteUpdate, baseUpdate)
const text = richtextToString(merged) // plain text representationrichtextToString(value)
Converts a Yjs rich text state update to a plain text string.
function richtextToString(value: RichtextValue): string| Parameter | Type | Description |
|---|---|---|
value | RichtextValue | A Yjs state update, plain string, or null/undefined. |
Returns: string -- The plain text content.
stringToRichtextUpdate(value)
Converts a plain string to a Yjs state update. Useful for initializing rich text fields from plain text.
function stringToRichtextUpdate(value: string): Uint8Array| Parameter | Type | Description |
|---|---|---|
value | string | Plain text to convert. |
Returns: Uint8Array -- A Yjs state update containing the text.
Field Merger
mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver?)
Merges a single field from two concurrent operations. Dispatches to the appropriate strategy based on field kind, or uses a custom resolver (Tier 3) if provided. Handles non-conflict cases where only one side modified the field.
function mergeField(
fieldName: string,
localOp: Operation,
remoteOp: Operation,
baseState: Record<string, unknown>,
fieldDescriptor: FieldDescriptor,
resolver?: CustomResolver,
): FieldMergeResult| Parameter | Type | Required | Description |
|---|---|---|---|
fieldName | string | Yes | Name of the field being merged. |
localOp | Operation | Yes | The local operation. |
remoteOp | Operation | Yes | The remote operation. |
baseState | Record<string, unknown> | Yes | Full record state before either operation. |
fieldDescriptor | FieldDescriptor | Yes | Schema descriptor for this field (from @korajs/core). |
resolver | CustomResolver | No | Tier 3 custom resolver function. Signature: (local, remote, base) => resolved. |
Returns: FieldMergeResult -- The resolved value and a trace for DevTools.
Strategy dispatch by field kind:
| Field Kind | Strategy | Description |
|---|---|---|
string | LWW | Last-Write-Wins via HLC timestamp |
number | LWW | Last-Write-Wins via HLC timestamp |
boolean | LWW | Last-Write-Wins via HLC timestamp |
enum | LWW | Last-Write-Wins via HLC timestamp |
timestamp | LWW | Last-Write-Wins via HLC timestamp |
array | add-wins set | Union of additions, only mutual removals |
richtext | Yjs CRDT | Character-level merge via Yjs Y.Text |
Constraint Checking
checkConstraints(mergedRecord, recordId, collection, collectionDef, constraintContext)
Checks all constraints on a collection against a candidate merged record. Called after Tier 1 + Tier 3 merge produces a candidate state. Returns an array of violated constraints for Tier 2 resolution.
async function checkConstraints(
mergedRecord: Record<string, unknown>,
recordId: string,
collection: string,
collectionDef: CollectionDefinition,
constraintContext: ConstraintContext,
): Promise<ConstraintViolation[]>| Parameter | Type | Description |
|---|---|---|
mergedRecord | Record<string, unknown> | The candidate record state after field-level merge. |
recordId | string | ID of the record being merged. |
collection | string | Name of the collection. |
collectionDef | CollectionDefinition | Schema definition for the collection (from @korajs/core). |
constraintContext | ConstraintContext | Pluggable DB lookup interface. |
Returns: Promise<ConstraintViolation[]> -- Array of violated constraints. Empty if all constraints pass.
Supported constraint types:
| Type | Description |
|---|---|
unique | No two records may have the same value(s) for the constrained fields. |
capacity | Limits the number of records in a group defined by the constrained fields. |
referential | Foreign key must reference an existing record in another collection. |
import { checkConstraints } from '@korajs/merge'
const violations = await checkConstraints(
candidateRecord,
recordId,
'todos',
schema.collections.todos,
{
queryRecords: async (coll, where) => store.query(coll, where),
countRecords: async (coll, where) => store.count(coll, where),
},
)
if (violations.length > 0) {
// Handle violations with resolveConstraintViolation
}resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState)
Resolves a constraint violation by applying the constraint's onConflict strategy. Returns the updated record and a merge trace for DevTools.
function resolveConstraintViolation(
violation: ConstraintViolation,
mergedRecord: Record<string, unknown>,
localOp: Operation,
remoteOp: Operation,
baseState: Record<string, unknown>,
): ConstraintResolution| Parameter | Type | Description |
|---|---|---|
violation | ConstraintViolation | The constraint violation to resolve. |
mergedRecord | Record<string, unknown> | The current candidate record state. |
localOp | Operation | The local operation. |
remoteOp | Operation | The remote operation. |
baseState | Record<string, unknown> | The record state before either operation. |
Returns: ConstraintResolution -- The resolved record and a trace.
Resolution strategies (set via constraint.onConflict):
| Strategy | Description |
|---|---|
last-write-wins | The operation with the later HLC timestamp wins for the violated fields. |
first-write-wins | The operation with the earlier HLC timestamp wins for the violated fields. |
priority-field | Compares a designated priority field to determine the winner. Falls back to LWW if priorityField is not set. |
server-decides | Marks the record with _pendingServerResolution: true for deferred server-side resolution. |
custom | Calls the constraint's resolve(local, remote, base) function for each violated field. Falls back to LWW if no resolver is provided. |
import { resolveConstraintViolation } from '@korajs/merge'
for (const violation of violations) {
const resolution = resolveConstraintViolation(
violation,
mergedData,
localOp,
remoteOp,
baseState,
)
mergedData = resolution.resolvedRecord
traces.push(resolution.trace)
}Types
MergeInput
Input to the merge engine when two concurrent operations conflict.
interface MergeInput {
/** The locally-originated operation */
local: Operation
/** The remotely-originated operation */
remote: Operation
/** Full record state before either operation was applied */
baseState: Record<string, unknown>
/** Schema definition for the collection being merged */
collectionDef: CollectionDefinition
}MergeResult
Output of the merge engine after resolving all field conflicts.
interface MergeResult {
/** The resolved field values after merging */
mergedData: Record<string, unknown>
/** One trace per conflicting field (for DevTools) */
traces: MergeTrace[]
/** Which operation's values dominate overall, or 'merged' if mixed */
appliedOperation: 'local' | 'remote' | 'merged'
}FieldMergeResult
Output of a single field-level merge decision.
interface FieldMergeResult {
/** The resolved value for this field */
value: unknown
/** Trace of the merge decision (for DevTools) */
trace: MergeTrace
}LWWResult
Result of a Last-Write-Wins comparison.
interface LWWResult {
/** The winning value */
value: unknown
/** Which side won */
winner: 'local' | 'remote'
}ConstraintContext
Pluggable database lookup interface for Tier 2 constraint checking. The @korajs/store package provides the runtime implementation; the merge package depends only on this interface, keeping it storage-agnostic.
interface ConstraintContext {
/** Query records matching the given filter in a collection */
queryRecords(
collection: string,
where: Record<string, unknown>,
): Promise<Record<string, unknown>[]>
/** Count records matching the given filter in a collection */
countRecords(
collection: string,
where: Record<string, unknown>,
): Promise<number>
}ConstraintViolation
Describes a constraint that was violated after auto-merge.
interface ConstraintViolation {
/** The constraint definition that was violated */
constraint: Constraint
/** The field(s) involved in the violation */
fields: string[]
/** Human-readable description of the violation */
message: string
}ConstraintResolution
Result of resolving a constraint violation.
interface ConstraintResolution {
/** The updated record after constraint resolution */
resolvedRecord: Record<string, unknown>
/** Trace of the resolution decision for DevTools */
trace: MergeTrace
}MergeTrace
Records the full context of a merge decision. Defined in @korajs/core and re-used throughout the merge package. See the Core API Reference for the full definition.
Referential Integrity
Functions for enforcing referential integrity during merge, specifically when delete operations affect records referenced by foreign keys in other collections.
buildMergeRelationLookup(schema)
Pre-computes a lookup map from target collection name to all incoming relations. This allows the delete-side referential integrity checker to efficiently find all foreign key relationships that reference a given collection.
The map is deterministic: relations are sorted by relation name within each target collection.
function buildMergeRelationLookup(
schema: SchemaDefinition,
): Map<string, MergeIncomingRelation[]>| Parameter | Type | Description |
|---|---|---|
schema | SchemaDefinition | The full schema definition including relations. |
Returns: Map<string, MergeIncomingRelation[]> -- Map from target collection name to array of incoming relations.
import { buildMergeRelationLookup } from '@korajs/merge'
const lookup = buildMergeRelationLookup(schema)
// lookup.get('projects') => [
// { relationName: 'todoBelongsToProject', sourceCollection: 'todos',
// foreignKeyField: 'projectId', onDelete: 'set-null' }
// ]checkReferentialIntegrityOnDelete(deleteOp, schema, ctx, relationLookup?)
Checks referential integrity when a delete operation is being applied. For each relation that targets the deleted record's collection, queries for referencing records and applies the relation's onDelete policy.
Processing is deterministic: relations are processed in sorted order by name, and referencing records within each relation are processed in sorted order by ID.
async function checkReferentialIntegrityOnDelete(
deleteOp: Operation,
schema: SchemaDefinition,
ctx: ReferentialMergeContext,
relationLookup?: Map<string, MergeIncomingRelation[]>,
): Promise<ReferentialCheckResult>| Parameter | Type | Required | Description |
|---|---|---|---|
deleteOp | Operation | Yes | The delete operation being evaluated. |
schema | SchemaDefinition | Yes | The full schema definition with relations. |
ctx | ReferentialMergeContext | Yes | Database lookup context for querying referencing records. |
relationLookup | Map<string, MergeIncomingRelation[]> | No | Pre-built relation lookup. Built automatically if not provided. |
Returns: Promise<ReferentialCheckResult> -- Whether the delete is allowed, side-effect operations, and traces for DevTools.
onDelete policies:
| Policy | Behavior |
|---|---|
restrict | Blocks the delete if any references exist. Returns immediately without processing further relations. |
cascade | Allows the delete and generates cascaded delete SideEffectOp entries for each referencing record. |
set-null | Allows the delete and generates update SideEffectOp entries to null out foreign key fields. |
no-action | Allows the delete with no side effects. Dangling references are permitted. |
import { checkReferentialIntegrityOnDelete } from '@korajs/merge'
const result = await checkReferentialIntegrityOnDelete(
deleteOp,
schema,
{
queryRecords: async (collection, where) => store.query(collection, where),
recordExists: async (collection, id) => (await store.findById(collection, id)) !== null,
},
)
if (!result.allowed) {
// Delete is blocked by a 'restrict' policy
console.log('Cannot delete: referenced by other records')
} else {
// Apply the delete, then apply any side-effect operations
for (const sideEffect of result.sideEffectOps) {
if (sideEffect.type === 'delete') {
await store.delete(sideEffect.collection, sideEffect.recordId)
} else {
await store.update(sideEffect.collection, sideEffect.recordId, sideEffect.data)
}
}
}resolveDeleteVsInsertConflict(deleteOp, insertOp, relation)
Resolves a concurrent delete-vs-insert conflict: one node deletes a record while another concurrently inserts a record that references the deleted record via a foreign key.
The resolution strategy depends on the relation's onDelete policy.
function resolveDeleteVsInsertConflict(
deleteOp: Operation,
insertOp: Operation,
relation: MergeIncomingRelation,
): DeleteVsInsertResolution| Parameter | Type | Description |
|---|---|---|
deleteOp | Operation | The delete operation on the referenced (parent) record. |
insertOp | Operation | The concurrent insert operation that references the deleted record. |
relation | MergeIncomingRelation | The relation definition connecting the two collections. |
Returns: DeleteVsInsertResolution -- The resolution action, side effects, and trace.
Resolution by onDelete policy:
| Policy | Resolution |
|---|---|
restrict | Blocks the delete (the insert wins). Data integrity is preserved. |
cascade | Allows the delete and cascade-deletes the newly inserted record. |
set-null | Allows the delete and nulls out the FK on the newly inserted record. |
no-action | Allows the delete. The dangling reference is acceptable. |
import { resolveDeleteVsInsertConflict } from '@korajs/merge'
const resolution = resolveDeleteVsInsertConflict(deleteOp, insertOp, {
relationName: 'todoBelongsToProject',
sourceCollection: 'todos',
foreignKeyField: 'projectId',
onDelete: 'set-null',
})
if (resolution.action === 'block-delete') {
// The insert wins -- do not apply the delete
} else {
// Apply the delete and any side-effect operations
for (const sideEffect of resolution.sideEffects) {
// Apply update or delete to the inserted record
}
}State Machine Merge
Functions for resolving concurrent modifications to state-machine-controlled fields during merge.
isStateMachineField(collectionDef, fieldName)
Checks whether a collection has a state machine defined and whether the given field is the state machine field. Used by the merge engine to intercept field-level merges for state machine fields.
function isStateMachineField(
collectionDef: CollectionDefinition,
fieldName: string,
): boolean| Parameter | Type | Description |
|---|---|---|
collectionDef | CollectionDefinition | The schema definition for the collection. |
fieldName | string | The field name to check. |
Returns: boolean -- true if the field is the state machine field for this collection.
import { isStateMachineField } from '@korajs/merge'
if (isStateMachineField(schema.collections.orders, 'status')) {
// Use state machine merge resolution instead of standard LWW
}resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine)
Resolves concurrent state machine transitions during merge. When two operations concurrently modify a state-machine-controlled field, this function determines the correct resolved value based on transition validity.
function resolveStateMachineMerge(
fieldName: string,
localOp: Operation,
remoteOp: Operation,
baseState: Record<string, unknown>,
stateMachine: StateMachineDefinition,
): StateMachineMergeResult| Parameter | Type | Description |
|---|---|---|
fieldName | string | The state machine field name. |
localOp | Operation | The local operation. |
remoteOp | Operation | The remote operation. |
baseState | Record<string, unknown> | The record state before either operation (must contain the base state value). |
stateMachine | StateMachineDefinition | The state machine definition with transitions. |
Returns: StateMachineMergeResult -- The resolved state value and a trace for DevTools.
Resolution rules:
| Scenario | Resolution |
|---|---|
| Both transitions valid from base state | LWW via HLC timestamp picks the winner. |
| One transition valid, one invalid | The valid transition wins regardless of timestamp. |
| Both transitions invalid | Keeps the base state. Reports the constraint violation. |
| Only one side changed | Validates that single transition. If invalid, keeps base state. |
import { resolveStateMachineMerge } from '@korajs/merge'
const result = resolveStateMachineMerge(
'status',
localOp, // data: { status: 'shipped' }
remoteOp, // data: { status: 'cancelled' }
{ status: 'pending' },
{
field: 'status',
transitions: {
pending: ['shipped', 'cancelled'],
shipped: ['delivered'],
cancelled: [],
delivered: [],
},
},
)
console.log(result.value) // Winner decided by LWW (both are valid from 'pending')
console.log(result.trace.strategy) // 'state-machine-lww'Referential Integrity Types
ReferentialMergeContext
Pluggable database lookup interface for referential integrity checks during merge. The store layer provides the runtime implementation; the merge package depends only on this interface.
interface ReferentialMergeContext {
/** Query records in a collection matching a where clause */
queryRecords(
collection: string,
where: Record<string, unknown>,
): Promise<Record<string, unknown>[]>
/** Check if a record exists in a collection */
recordExists(collection: string, recordId: string): Promise<boolean>
}ReferentialCheckResult
Result of a referential integrity check on a delete operation.
interface ReferentialCheckResult {
/** Whether the delete operation is allowed to proceed */
allowed: boolean
/** Side-effect operations generated by cascade/set-null policies */
sideEffectOps: SideEffectOp[]
/** MergeTraces for every decision made (feeds DevTools) */
traces: MergeTrace[]
}SideEffectOp
A side-effect operation generated during referential integrity enforcement. These describe mutations the caller must apply after the primary delete.
interface SideEffectOp {
/** The type of mutation to apply */
type: 'delete' | 'update'
/** The collection containing the affected record */
collection: string
/** The ID of the affected record */
recordId: string
/** For updates: the new field values. null for deletes. */
data: Record<string, unknown> | null
/** For updates: the previous field values. null for deletes. */
previousData: Record<string, unknown> | null
/** The onDelete policy that produced this side effect */
policy: OnDeleteAction
/** The relation that triggered this side effect */
relationName: string
}DeleteVsInsertResolution
Result of resolving a concurrent delete-vs-insert conflict on a referenced record.
interface DeleteVsInsertResolution {
/** Whether to block the delete or allow it */
action: 'block-delete' | 'allow-delete'
/** Any side-effect operations (for cascade/set-null after allowing delete) */
sideEffects: SideEffectOp[]
/** Trace of the resolution decision, or null if no trace needed */
trace: MergeTrace | null
}MergeIncomingRelation
Describes an incoming relation to a target collection. Used by the delete-side referential integrity checker to know which source collections reference the collection being deleted from.
interface MergeIncomingRelation {
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
relationName: string
/** The collection that holds the foreign key (source of the relation) */
sourceCollection: string
/** The foreign key field in the source collection */
foreignKeyField: string
/** What to do when the referenced record is deleted */
onDelete: OnDeleteAction
}StateMachineMergeResult
Result of state machine merge validation.
interface StateMachineMergeResult {
/** The resolved value for the state field */
value: string
/** Trace for DevTools */
trace: MergeTrace
}Examples
Basic merge with MergeEngine
import { MergeEngine } from '@korajs/merge'
const engine = new MergeEngine()
// Two users concurrently edit the same todo
const result = await engine.merge({
local: {
// ...operation fields
type: 'update',
collection: 'todos',
recordId: 'todo-1',
data: { title: 'Updated locally' },
previousData: { title: 'Original' },
timestamp: localClock.now(),
},
remote: {
// ...operation fields
type: 'update',
collection: 'todos',
recordId: 'todo-1',
data: { completed: true },
previousData: { completed: false },
timestamp: remoteClock.now(),
},
baseState: { title: 'Original', completed: false },
collectionDef: schema.collections.todos,
})
// No conflict: different fields changed
// result.mergedData = { title: 'Updated locally', completed: true }
// result.appliedOperation = 'merged'Merge with Tier 2 constraint checking
import { MergeEngine } from '@korajs/merge'
const engine = new MergeEngine()
const constraintContext = {
queryRecords: async (collection, where) => {
return db.query(`SELECT * FROM ${collection} WHERE ...`, where)
},
countRecords: async (collection, where) => {
return db.count(collection, where)
},
}
const result = await engine.merge(
{
local: localOp,
remote: remoteOp,
baseState,
collectionDef: schema.collections.todos,
},
constraintContext,
)
// If a unique constraint was violated, it will be resolved
// according to the constraint's onConflict strategy.
// All resolution traces appear in result.traces.Using strategies directly
import { lastWriteWins, addWinsSet, mergeRichtext, richtextToString } from '@korajs/merge'
// LWW for scalar values
const lww = lastWriteWins('value-a', 'value-b', timestampA, timestampB)
console.log(lww.value, lww.winner)
// Add-wins set for arrays
const merged = addWinsSet(
['a', 'b', 'new-local'],
['a', 'b', 'new-remote'],
['a', 'b'],
)
// ['a', 'b', 'new-local', 'new-remote']
// Yjs CRDT for rich text
const mergedDoc = mergeRichtext(localUpdate, remoteUpdate, baseUpdate)
console.log(richtextToString(mergedDoc))Custom resolver (Tier 3) for additive quantity merge
import { defineSchema, t } from '@korajs/core'
const schema = defineSchema({
version: 1,
collections: {
inventory: {
fields: {
productId: t.string(),
quantity: t.number(),
},
resolve: {
quantity: (local, remote, base) => {
// Additive merge: apply both deltas to the base value
const localDelta = (local as number) - (base as number)
const remoteDelta = (remote as number) - (base as number)
return Math.max(0, (base as number) + localDelta + remoteDelta)
},
},
},
},
})
// base quantity = 10
// local sets quantity to 8 (delta = -2)
// remote sets quantity to 13 (delta = +3)
// resolved = max(0, 10 + (-2) + 3) = 11