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