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