@korajs/merge 0.3.1 → 0.4.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 +650 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -2
- package/dist/index.d.ts +206 -2
- package/dist/index.js +640 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constraint, MergeTrace, Operation, CollectionDefinition, HLCTimestamp, FieldDescriptor, CustomResolver } from '@korajs/core';
|
|
1
|
+
import { Constraint, MergeTrace, Operation, CollectionDefinition, HLCTimestamp, AtomicOp, FieldMergeStrategy, FieldDescriptor, CustomResolver, OnDeleteAction, SchemaDefinition } from '@korajs/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Input to the merge engine when two concurrent operations conflict.
|
|
@@ -105,6 +105,43 @@ declare function lastWriteWins(localValue: unknown, remoteValue: unknown, localT
|
|
|
105
105
|
*/
|
|
106
106
|
declare function addWinsSet(localArray: unknown[], remoteArray: unknown[], baseArray: unknown[]): unknown[];
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Result of merging two atomic operations on the same field.
|
|
110
|
+
* If `merged` is false, the caller should fall back to LWW on the resolved values.
|
|
111
|
+
*/
|
|
112
|
+
interface AtomicMergeResult {
|
|
113
|
+
merged: true;
|
|
114
|
+
value: unknown;
|
|
115
|
+
strategy: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Sentinel returned when atomic ops cannot be composed (e.g., mismatched types).
|
|
119
|
+
*/
|
|
120
|
+
interface AtomicMergeFallback {
|
|
121
|
+
merged: false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Compose two concurrent atomic operations on the same field.
|
|
125
|
+
*
|
|
126
|
+
* When both operations expressed an atomic intent (e.g., both used op.increment()),
|
|
127
|
+
* their intents can be composed to produce a correct merged result without LWW:
|
|
128
|
+
*
|
|
129
|
+
* - increment + increment → sum of both deltas applied to base
|
|
130
|
+
* - max + max → max of all values (base, local, remote)
|
|
131
|
+
* - min + min → min of all values (base, local, remote)
|
|
132
|
+
* - append + append → base array with both appended items
|
|
133
|
+
* - remove + remove → base array with both items removed
|
|
134
|
+
*
|
|
135
|
+
* Falls back (returns { merged: false }) when atomic op types differ,
|
|
136
|
+
* since there is no meaningful composition for mixed intents.
|
|
137
|
+
*
|
|
138
|
+
* @param localAtomicOp - The atomic op from the local operation
|
|
139
|
+
* @param remoteAtomicOp - The atomic op from the remote operation
|
|
140
|
+
* @param baseValue - The field value before either operation was applied
|
|
141
|
+
* @returns The composed result, or a fallback signal to use LWW
|
|
142
|
+
*/
|
|
143
|
+
declare function mergeAtomicOps(localAtomicOp: AtomicOp, remoteAtomicOp: AtomicOp, baseValue: unknown): AtomicMergeResult | AtomicMergeFallback;
|
|
144
|
+
|
|
108
145
|
type RichtextValue = string | Uint8Array | ArrayBuffer | null | undefined;
|
|
109
146
|
/**
|
|
110
147
|
* Merges richtext values using Yjs CRDT updates.
|
|
@@ -119,6 +156,51 @@ declare function richtextToString(value: RichtextValue): string;
|
|
|
119
156
|
*/
|
|
120
157
|
declare function stringToRichtextUpdate(value: string): Uint8Array;
|
|
121
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Schema-declared merge strategies.
|
|
161
|
+
*
|
|
162
|
+
* These are invoked when a field has an explicit `mergeStrategy` declared in the schema
|
|
163
|
+
* via `t.number().merge('counter')` etc. They override the default kind-based auto-merge.
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* Counter merge: sum of deltas from base.
|
|
167
|
+
* Both sides' changes are additive relative to the base value.
|
|
168
|
+
*
|
|
169
|
+
* Example: base=10, local=15, remote=12 → result = 10 + 5 + 2 = 17
|
|
170
|
+
*/
|
|
171
|
+
declare function counterMerge(localValue: unknown, remoteValue: unknown, baseValue: unknown): unknown;
|
|
172
|
+
/**
|
|
173
|
+
* Max merge: keep the maximum of all concurrent values.
|
|
174
|
+
* Works for numbers and timestamps.
|
|
175
|
+
*/
|
|
176
|
+
declare function maxMerge(localValue: unknown, remoteValue: unknown, baseValue: unknown): unknown;
|
|
177
|
+
/**
|
|
178
|
+
* Min merge: keep the minimum of all concurrent values.
|
|
179
|
+
* Works for numbers and timestamps.
|
|
180
|
+
*/
|
|
181
|
+
declare function minMerge(localValue: unknown, remoteValue: unknown, baseValue: unknown): unknown;
|
|
182
|
+
/**
|
|
183
|
+
* Append-only merge for arrays: concatenate additions from both sides.
|
|
184
|
+
* Unlike add-wins-set, removals are ignored — items can only be added, never removed.
|
|
185
|
+
*
|
|
186
|
+
* Result = base + local additions + remote additions (preserving order)
|
|
187
|
+
*/
|
|
188
|
+
declare function appendOnlyMerge(localValue: unknown, remoteValue: unknown, baseValue: unknown): unknown[];
|
|
189
|
+
/**
|
|
190
|
+
* Server-authoritative merge: always prefer the remote (server) value.
|
|
191
|
+
*/
|
|
192
|
+
declare function serverAuthoritativeMerge(_localValue: unknown, remoteValue: unknown, _baseValue: unknown): unknown;
|
|
193
|
+
/**
|
|
194
|
+
* Dispatch to the appropriate schema-declared strategy.
|
|
195
|
+
*
|
|
196
|
+
* @returns The merged value, or null if the strategy is not handled here
|
|
197
|
+
* (e.g., 'lww' and 'union' which are handled by the default autoMerge).
|
|
198
|
+
*/
|
|
199
|
+
declare function applySchemaStrategy(strategy: FieldMergeStrategy, localValue: unknown, remoteValue: unknown, baseValue: unknown, localTimestamp: HLCTimestamp, remoteTimestamp: HLCTimestamp): {
|
|
200
|
+
value: unknown;
|
|
201
|
+
strategyName: string;
|
|
202
|
+
} | null;
|
|
203
|
+
|
|
122
204
|
/**
|
|
123
205
|
* Merges a single field from two concurrent operations.
|
|
124
206
|
*
|
|
@@ -187,6 +269,128 @@ interface ConstraintResolution {
|
|
|
187
269
|
*/
|
|
188
270
|
declare function resolveConstraintViolation(violation: ConstraintViolation, mergedRecord: Record<string, unknown>, localOp: Operation, remoteOp: Operation, baseState: Record<string, unknown>): ConstraintResolution;
|
|
189
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Describes an incoming relation to a target collection.
|
|
274
|
+
* Used by the delete-side referential integrity checker to know which
|
|
275
|
+
* source collections reference the collection being deleted from.
|
|
276
|
+
*/
|
|
277
|
+
interface MergeIncomingRelation {
|
|
278
|
+
/** Name of the relation in the schema (e.g., 'todoBelongsToProject') */
|
|
279
|
+
relationName: string;
|
|
280
|
+
/** The collection that holds the foreign key (source of the relation) */
|
|
281
|
+
sourceCollection: string;
|
|
282
|
+
/** The foreign key field in the source collection */
|
|
283
|
+
foreignKeyField: string;
|
|
284
|
+
/** What to do when the referenced record is deleted */
|
|
285
|
+
onDelete: OnDeleteAction;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Pluggable context for querying the database during referential integrity checks.
|
|
289
|
+
* The store layer provides the implementation; the merge package only depends on this interface.
|
|
290
|
+
*/
|
|
291
|
+
interface ReferentialMergeContext {
|
|
292
|
+
/** Query records in a collection matching a where clause */
|
|
293
|
+
queryRecords(collection: string, where: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
294
|
+
/** Check if a record exists in a collection */
|
|
295
|
+
recordExists(collection: string, recordId: string): Promise<boolean>;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Result of a referential integrity check on a delete operation.
|
|
299
|
+
*/
|
|
300
|
+
interface ReferentialCheckResult {
|
|
301
|
+
/** Whether the delete operation is allowed to proceed */
|
|
302
|
+
allowed: boolean;
|
|
303
|
+
/** Side-effect operations generated by cascade/set-null policies */
|
|
304
|
+
sideEffectOps: SideEffectOp[];
|
|
305
|
+
/** MergeTraces for every decision made (feeds DevTools) */
|
|
306
|
+
traces: MergeTrace[];
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* A side-effect operation generated during referential integrity enforcement.
|
|
310
|
+
* These are not full Operations; they describe mutations the caller must apply.
|
|
311
|
+
*/
|
|
312
|
+
interface SideEffectOp {
|
|
313
|
+
/** The type of mutation to apply */
|
|
314
|
+
type: 'delete' | 'update';
|
|
315
|
+
/** The collection containing the affected record */
|
|
316
|
+
collection: string;
|
|
317
|
+
/** The ID of the affected record */
|
|
318
|
+
recordId: string;
|
|
319
|
+
/** For updates: the new field values. null for deletes. */
|
|
320
|
+
data: Record<string, unknown> | null;
|
|
321
|
+
/** For updates: the previous field values. null for deletes. */
|
|
322
|
+
previousData: Record<string, unknown> | null;
|
|
323
|
+
/** The onDelete policy that produced this side effect */
|
|
324
|
+
policy: OnDeleteAction;
|
|
325
|
+
/** The relation that triggered this side effect */
|
|
326
|
+
relationName: string;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Result of resolving a concurrent delete-vs-insert conflict.
|
|
330
|
+
*/
|
|
331
|
+
interface DeleteVsInsertResolution {
|
|
332
|
+
/** Whether to block the delete or allow it */
|
|
333
|
+
action: 'block-delete' | 'allow-delete';
|
|
334
|
+
/** Any side-effect operations (for cascade/set-null after allowing delete) */
|
|
335
|
+
sideEffects: SideEffectOp[];
|
|
336
|
+
/** Trace of the resolution decision, or null if no trace needed */
|
|
337
|
+
trace: MergeTrace | null;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Builds a lookup map from target collection to all incoming relations.
|
|
341
|
+
*
|
|
342
|
+
* This pre-computes which collections reference a given collection via foreign keys,
|
|
343
|
+
* so that delete operations can efficiently find all relations that need enforcement.
|
|
344
|
+
*
|
|
345
|
+
* @param schema - The full schema definition with relations
|
|
346
|
+
* @returns Map from target collection name to array of incoming relations, sorted by relation name for determinism
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* const lookup = buildMergeRelationLookup(schema)
|
|
351
|
+
* // lookup.get('projects') => [{ relationName: 'todoBelongsToProject', sourceCollection: 'todos', ... }]
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
declare function buildMergeRelationLookup(schema: SchemaDefinition): Map<string, MergeIncomingRelation[]>;
|
|
355
|
+
/**
|
|
356
|
+
* Checks referential integrity when a delete operation is being applied.
|
|
357
|
+
*
|
|
358
|
+
* For each relation that targets the deleted record's collection, queries
|
|
359
|
+
* for referencing records and applies the relation's onDelete policy:
|
|
360
|
+
* - `restrict`: Blocks the delete if any references exist
|
|
361
|
+
* - `cascade`: Allows the delete and generates cascaded delete SideEffectOps
|
|
362
|
+
* - `set-null`: Allows the delete and generates update SideEffectOps to null out FKs
|
|
363
|
+
* - `no-action`: Allows the delete with no side effects (dangling references permitted)
|
|
364
|
+
*
|
|
365
|
+
* Processing is deterministic: relations are processed in sorted order by name,
|
|
366
|
+
* and referencing records within each relation are processed in sorted order by ID.
|
|
367
|
+
*
|
|
368
|
+
* @param deleteOp - The delete operation being evaluated
|
|
369
|
+
* @param schema - The full schema definition
|
|
370
|
+
* @param ctx - Database lookup context
|
|
371
|
+
* @param relationLookup - Pre-built relation lookup (optional; will be built if not provided)
|
|
372
|
+
* @returns Whether the delete is allowed, any side-effect ops, and traces for DevTools
|
|
373
|
+
*/
|
|
374
|
+
declare function checkReferentialIntegrityOnDelete(deleteOp: Operation, schema: SchemaDefinition, ctx: ReferentialMergeContext, relationLookup?: Map<string, MergeIncomingRelation[]>): Promise<ReferentialCheckResult>;
|
|
375
|
+
/**
|
|
376
|
+
* Resolves a concurrent delete-vs-insert conflict on a referenced record.
|
|
377
|
+
*
|
|
378
|
+
* When one node deletes a record while another concurrently inserts a record
|
|
379
|
+
* that references the deleted record, we need to decide what wins.
|
|
380
|
+
*
|
|
381
|
+
* Resolution strategy depends on the relation's onDelete policy:
|
|
382
|
+
* - `restrict`: Block the delete (the insert wins — data integrity preserved)
|
|
383
|
+
* - `cascade`: Allow the delete and cascade-delete the newly inserted record
|
|
384
|
+
* - `set-null`: Allow the delete and null out the FK on the newly inserted record
|
|
385
|
+
* - `no-action`: Allow the delete (dangling reference is acceptable)
|
|
386
|
+
*
|
|
387
|
+
* @param deleteOp - The delete operation on the referenced record
|
|
388
|
+
* @param insertOp - The concurrent insert operation that references the deleted record
|
|
389
|
+
* @param relation - The relation definition connecting the two collections
|
|
390
|
+
* @returns The resolution action, side effects, and trace
|
|
391
|
+
*/
|
|
392
|
+
declare function resolveDeleteVsInsertConflict(deleteOp: Operation, insertOp: Operation, relation: MergeIncomingRelation): DeleteVsInsertResolution;
|
|
393
|
+
|
|
190
394
|
/**
|
|
191
395
|
* Three-tier merge engine for resolving concurrent operations.
|
|
192
396
|
*
|
|
@@ -242,4 +446,4 @@ declare class MergeEngine {
|
|
|
242
446
|
private mergeWithDelete;
|
|
243
447
|
}
|
|
244
448
|
|
|
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 };
|
|
449
|
+
export { type AtomicMergeFallback, type AtomicMergeResult, type ConstraintContext, type ConstraintResolution, type ConstraintViolation, type DeleteVsInsertResolution, type FieldMergeResult, type LWWResult, MergeEngine, type MergeIncomingRelation, type MergeInput, type MergeResult, type ReferentialCheckResult, type ReferentialMergeContext, type SideEffectOp, addWinsSet, appendOnlyMerge, applySchemaStrategy, buildMergeRelationLookup, checkConstraints, checkReferentialIntegrityOnDelete, counterMerge, lastWriteWins, maxMerge, mergeAtomicOps, mergeField, mergeRichtext, minMerge, resolveConstraintViolation, resolveDeleteVsInsertConflict, richtextToString, serverAuthoritativeMerge, stringToRichtextUpdate };
|