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