@mmstack/primitives 20.13.0 → 20.13.1

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.
@@ -4311,12 +4311,9 @@ function invertBatch(batch) {
4311
4311
  if (op.kind === 'clear')
4312
4312
  continue;
4313
4313
  if (op.kind === 'delete') {
4314
- inverted.push({
4315
- kind: 'set',
4316
- path: op.path,
4317
- next: op.prev,
4318
- prev: undefined,
4319
- });
4314
+ // no `prev`: the key is ABSENT once the delete applied, so this inverse is an add —
4315
+ // inverting it again yields the delete back (redo removes the key, not sets undefined)
4316
+ inverted.push({ kind: 'set', path: op.path, next: op.prev });
4320
4317
  continue;
4321
4318
  }
4322
4319
  if (!Object.hasOwn(op, 'prev')) {
@@ -6222,6 +6219,8 @@ function syncedFork(sync, store, opt) {
6222
6219
  };
6223
6220
  }
6224
6221
 
6222
+ const PATH_SEP = '';
6223
+ const OP_SEP = '';
6225
6224
  /**
6226
6225
  * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
6227
6226
  * its inverse batch, so `undo()` is one `apply` and history costs only the diffs, not full
@@ -6230,20 +6229,53 @@ function syncedFork(sync, store, opt) {
6230
6229
  *
6231
6230
  * Composes with sync for collaborative undo: pass `track: syncClient` so only YOUR writes are
6232
6231
  * undoable, while `undo()` emits a normal op that propagates to peers (it writes through the
6233
- * store, which the sync client picks up).
6232
+ * store, which the sync client picks up). Coalescing groups only this stack's entries — what
6233
+ * goes over the wire is untouched.
6234
6234
  */
6235
6235
  function storeHistory(source, opt) {
6236
6236
  const limit = opt?.limit ?? 100;
6237
+ const coalesce = opt?.coalesce;
6238
+ const now = opt?.now ?? Date.now;
6237
6239
  const logOpt = { origin: opt?.origin };
6238
6240
  if (opt?.driver)
6239
6241
  logOpt.driver = opt.driver;
6240
6242
  else
6241
- logOpt.injector = opt?.injector ?? inject(Injector);
6243
+ logOpt.injector =
6244
+ opt?.injector ?? inject(Injector);
6242
6245
  const log = opLog(source, logOpt);
6243
6246
  const undoStack = [];
6244
6247
  const redoStack = [];
6245
6248
  const version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : [])); // monotonic: bumps on every mutation so the computeds recompute
6246
6249
  let applying = false;
6250
+ let runOpen = false;
6251
+ let lastAt = 0;
6252
+ let lastSig = '';
6253
+ const sigOf = (ops) => ops.map((o) => `${o.kind}:${o.path.join(PATH_SEP)}`).join(OP_SEP);
6254
+ const mergeInto = (entry, incoming) => {
6255
+ const merged = [...entry];
6256
+ const rest = [];
6257
+ for (const inc of incoming) {
6258
+ const key = inc.path.join(PATH_SEP);
6259
+ const at = merged.findIndex((o) => o.path.join(PATH_SEP) === key);
6260
+ const cur = at >= 0 ? merged[at] : undefined;
6261
+ if (cur && cur.kind === 'set' && inc.kind === 'set') {
6262
+ const composed = {
6263
+ kind: 'set',
6264
+ path: cur.path,
6265
+ next: cur.next,
6266
+ };
6267
+ // absent `prev` means the composed inverse is an add (inverts to a delete): the newest
6268
+ // forward op removed the key, so redo must remove it again
6269
+ if (Object.hasOwn(inc, 'prev'))
6270
+ composed.prev = inc.prev;
6271
+ merged[at] = composed;
6272
+ }
6273
+ else {
6274
+ rest.push(inc);
6275
+ }
6276
+ }
6277
+ return rest.length ? [...rest, ...merged] : merged;
6278
+ };
6247
6279
  const push = (stack, inverse) => {
6248
6280
  stack.push(inverse);
6249
6281
  if (stack.length > limit)
@@ -6254,20 +6286,39 @@ function storeHistory(source, opt) {
6254
6286
  return; // an undo/redo's own emission must not re-enter history
6255
6287
  if (!batch.ops.length)
6256
6288
  return;
6257
- push(undoStack, invertBatch(batch));
6289
+ const inverse = invertBatch(batch);
6290
+ const at = now();
6291
+ const sig = coalesce ? sigOf(batch.ops) : '';
6292
+ if (coalesce &&
6293
+ runOpen &&
6294
+ undoStack.length > 0 &&
6295
+ at - lastAt <= coalesce.ms &&
6296
+ (coalesce.samePath === false || sig === lastSig)) {
6297
+ undoStack[undoStack.length - 1] = mergeInto(undoStack[undoStack.length - 1], inverse);
6298
+ }
6299
+ else {
6300
+ push(undoStack, inverse);
6301
+ }
6302
+ runOpen = true;
6303
+ lastAt = at;
6304
+ lastSig = sig;
6258
6305
  redoStack.length = 0; // a fresh edit forks the timeline
6259
6306
  version.update((v) => v + 1);
6260
6307
  };
6261
6308
  // track the sync client's local stream when given, else self-diff every store change
6262
6309
  const unsub = (opt?.track ?? log).subscribe(record);
6310
+ const flushTrack = () => opt?.track?.flush?.();
6263
6311
  const run = (from, to) => {
6312
+ flushTrack();
6313
+ log.flush();
6264
6314
  const inverse = from.pop();
6265
6315
  if (!inverse)
6266
6316
  return;
6267
- log.flush(); // settle pending local writes before applying
6317
+ runOpen = false; // stepping through history is a boundary: the next edit starts fresh
6268
6318
  applying = true;
6269
6319
  try {
6270
6320
  log.apply(inverse);
6321
+ flushTrack();
6271
6322
  }
6272
6323
  finally {
6273
6324
  applying = false;
@@ -6280,7 +6331,11 @@ function storeHistory(source, opt) {
6280
6331
  canRedo: computed(() => (version(), redoStack.length > 0)),
6281
6332
  undo: () => run(undoStack, redoStack),
6282
6333
  redo: () => run(redoStack, undoStack),
6334
+ checkpoint: () => {
6335
+ runOpen = false;
6336
+ },
6283
6337
  clear: () => {
6338
+ runOpen = false;
6284
6339
  undoStack.length = 0;
6285
6340
  redoStack.length = 0;
6286
6341
  version.update((v) => v + 1);