@mmstack/primitives 21.8.0 → 21.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.
@@ -4348,12 +4348,9 @@ function invertBatch(batch) {
4348
4348
  if (op.kind === 'clear')
4349
4349
  continue;
4350
4350
  if (op.kind === 'delete') {
4351
- inverted.push({
4352
- kind: 'set',
4353
- path: op.path,
4354
- next: op.prev,
4355
- prev: undefined,
4356
- });
4351
+ // no `prev`: the key is ABSENT once the delete applied, so this inverse is an add —
4352
+ // inverting it again yields the delete back (redo removes the key, not sets undefined)
4353
+ inverted.push({ kind: 'set', path: op.path, next: op.prev });
4357
4354
  continue;
4358
4355
  }
4359
4356
  if (!Object.hasOwn(op, 'prev')) {
@@ -6192,6 +6189,8 @@ function evenPositions(n) {
6192
6189
  return out;
6193
6190
  }
6194
6191
 
6192
+ const PATH_SEP = '';
6193
+ const OP_SEP = '';
6195
6194
  /**
6196
6195
  * Undo/redo for a copy-on-write store, built on the op-log: each tracked change is stored as
6197
6196
  * its inverse batch, so `undo()` is one `apply` and history costs only the diffs, not full
@@ -6200,20 +6199,53 @@ function evenPositions(n) {
6200
6199
  *
6201
6200
  * Composes with sync for collaborative undo: pass `track: syncClient` so only YOUR writes are
6202
6201
  * undoable, while `undo()` emits a normal op that propagates to peers (it writes through the
6203
- * store, which the sync client picks up).
6202
+ * store, which the sync client picks up). Coalescing groups only this stack's entries — what
6203
+ * goes over the wire is untouched.
6204
6204
  */
6205
6205
  function storeHistory(source, opt) {
6206
6206
  const limit = opt?.limit ?? 100;
6207
+ const coalesce = opt?.coalesce;
6208
+ const now = opt?.now ?? Date.now;
6207
6209
  const logOpt = { origin: opt?.origin };
6208
6210
  if (opt?.driver)
6209
6211
  logOpt.driver = opt.driver;
6210
6212
  else
6211
- logOpt.injector = opt?.injector ?? inject(Injector);
6213
+ logOpt.injector =
6214
+ opt?.injector ?? inject(Injector);
6212
6215
  const log = opLog(source, logOpt);
6213
6216
  const undoStack = [];
6214
6217
  const redoStack = [];
6215
6218
  const version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ [])); // monotonic: bumps on every mutation so the computeds recompute
6216
6219
  let applying = false;
6220
+ let runOpen = false;
6221
+ let lastAt = 0;
6222
+ let lastSig = '';
6223
+ const sigOf = (ops) => ops.map((o) => `${o.kind}:${o.path.join(PATH_SEP)}`).join(OP_SEP);
6224
+ const mergeInto = (entry, incoming) => {
6225
+ const merged = [...entry];
6226
+ const rest = [];
6227
+ for (const inc of incoming) {
6228
+ const key = inc.path.join(PATH_SEP);
6229
+ const at = merged.findIndex((o) => o.path.join(PATH_SEP) === key);
6230
+ const cur = at >= 0 ? merged[at] : undefined;
6231
+ if (cur && cur.kind === 'set' && inc.kind === 'set') {
6232
+ const composed = {
6233
+ kind: 'set',
6234
+ path: cur.path,
6235
+ next: cur.next,
6236
+ };
6237
+ // absent `prev` means the composed inverse is an add (inverts to a delete): the newest
6238
+ // forward op removed the key, so redo must remove it again
6239
+ if (Object.hasOwn(inc, 'prev'))
6240
+ composed.prev = inc.prev;
6241
+ merged[at] = composed;
6242
+ }
6243
+ else {
6244
+ rest.push(inc);
6245
+ }
6246
+ }
6247
+ return rest.length ? [...rest, ...merged] : merged;
6248
+ };
6217
6249
  const push = (stack, inverse) => {
6218
6250
  stack.push(inverse);
6219
6251
  if (stack.length > limit)
@@ -6224,20 +6256,39 @@ function storeHistory(source, opt) {
6224
6256
  return; // an undo/redo's own emission must not re-enter history
6225
6257
  if (!batch.ops.length)
6226
6258
  return;
6227
- push(undoStack, invertBatch(batch));
6259
+ const inverse = invertBatch(batch);
6260
+ const at = now();
6261
+ const sig = coalesce ? sigOf(batch.ops) : '';
6262
+ if (coalesce &&
6263
+ runOpen &&
6264
+ undoStack.length > 0 &&
6265
+ at - lastAt <= coalesce.ms &&
6266
+ (coalesce.samePath === false || sig === lastSig)) {
6267
+ undoStack[undoStack.length - 1] = mergeInto(undoStack[undoStack.length - 1], inverse);
6268
+ }
6269
+ else {
6270
+ push(undoStack, inverse);
6271
+ }
6272
+ runOpen = true;
6273
+ lastAt = at;
6274
+ lastSig = sig;
6228
6275
  redoStack.length = 0; // a fresh edit forks the timeline
6229
6276
  version.update((v) => v + 1);
6230
6277
  };
6231
6278
  // track the sync client's local stream when given, else self-diff every store change
6232
6279
  const unsub = (opt?.track ?? log).subscribe(record);
6280
+ const flushTrack = () => opt?.track?.flush?.();
6233
6281
  const run = (from, to) => {
6282
+ flushTrack();
6283
+ log.flush();
6234
6284
  const inverse = from.pop();
6235
6285
  if (!inverse)
6236
6286
  return;
6237
- log.flush(); // settle pending local writes before applying
6287
+ runOpen = false; // stepping through history is a boundary: the next edit starts fresh
6238
6288
  applying = true;
6239
6289
  try {
6240
6290
  log.apply(inverse);
6291
+ flushTrack();
6241
6292
  }
6242
6293
  finally {
6243
6294
  applying = false;
@@ -6250,7 +6301,11 @@ function storeHistory(source, opt) {
6250
6301
  canRedo: computed(() => (version(), redoStack.length > 0)),
6251
6302
  undo: () => run(undoStack, redoStack),
6252
6303
  redo: () => run(redoStack, undoStack),
6304
+ checkpoint: () => {
6305
+ runOpen = false;
6306
+ },
6253
6307
  clear: () => {
6308
+ runOpen = false;
6254
6309
  undoStack.length = 0;
6255
6310
  redoStack.length = 0;
6256
6311
  version.update((v) => v + 1);