@mmstack/primitives 22.8.0 → 22.8.2

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