@rohal12/spindle 0.59.3 → 0.59.5

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.
@@ -38,6 +38,13 @@ defineMacro({
38
38
  [passage],
39
39
  );
40
40
 
41
+ // Count the inclusion when its content mounts or the passage changes,
42
+ // not on every render (an unrelated variable update re-renders it)
43
+ const included = !failure && !!passage;
44
+ ctx.hooks.useEffect(() => {
45
+ if (included) useStoryStore.getState().trackRender(passageName);
46
+ }, [included, passageName]);
47
+
41
48
  if (!storyData) return null;
42
49
  if (failure) {
43
50
  return (
@@ -49,8 +56,6 @@ defineMacro({
49
56
  }
50
57
  if (!passage || !ast) return null;
51
58
 
52
- useStoryStore.getState().trackRender(passageName);
53
-
54
59
  const nobr = passage.tags.includes('nobr');
55
60
  const content = inline
56
61
  ? ctx.renderInlineNodes(ast)
@@ -18,7 +18,7 @@ import {
18
18
  renderInlineNodes,
19
19
  } from './markup/render';
20
20
  import type { ASTNode } from './markup/ast';
21
- import { executeMutation, readState } from './execute-mutation';
21
+ import { executeMutation, mutateState, readState } from './execute-mutation';
22
22
  import { evaluate } from './expression';
23
23
  import { useStoryStore } from './store';
24
24
  import { getByPath, setByPath } from './utils/object-path';
@@ -228,8 +228,11 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
228
228
  // In program order, also when mutation code performs the input
229
229
  ctx.getValue = () => getByPath(readState().variables, segments);
230
230
  ctx.setValue = (value: unknown) => {
231
- useStoryStore.setState((state) => {
232
- setByPath(state.variables, segments, value, {
231
+ // The commit mutation code uses, which keeps the references among
232
+ // variables: a write to a draft copies only the path written, and
233
+ // other variables sharing the object would keep the old one.
234
+ mutateState((work, adopt) => {
235
+ setByPath(work.variables, segments, adopt(value), {
233
236
  createMissing: true,
234
237
  });
235
238
  });
@@ -10,21 +10,20 @@ import { useStoryStore } from './store';
10
10
  import type { StoryState, VariableNamespaces } from './store';
11
11
  import { execute } from './expression';
12
12
  import {
13
- aliasChanges,
13
+ changesBetween,
14
14
  applyChange,
15
15
  deepClone,
16
16
  deepEqual,
17
- diffPaths,
18
17
  existingObjects,
19
18
  isApplied,
20
19
  isMergeable,
21
20
  locateObjects,
22
21
  mergeKeys,
23
22
  pathKey,
24
- underChange,
23
+ sharedPaths,
25
24
  type PathChange,
26
25
  } from './structural';
27
- import { getByPath } from './utils/object-path';
26
+ import { getByPath, setByPath } from './utils/object-path';
28
27
  import { asNamespace, hasOwn } from './utils/namespace';
29
28
 
30
29
  type NamespaceName = keyof VariableNamespaces;
@@ -76,7 +75,11 @@ export function readState(): StoryState {
76
75
  const cloneValue = <T>(value: T): T =>
77
76
  deepClone(value, { keepUnregistered: true });
78
77
 
79
- /** Copies values for one target, so what they share they share there too. */
78
+ /**
79
+ * Copies values for one target (the three variable namespaces, so an object
80
+ * shared between them is shared there too), and what the values share they
81
+ * share there too.
82
+ */
80
83
  type Cloner = <T>(value: T) => T;
81
84
 
82
85
  /**
@@ -87,19 +90,30 @@ type Cloner = <T>(value: T) => T;
87
90
  * one, not a copy: `$a.self = $a` refers to the root of `a` in the store.
88
91
  */
89
92
  function clonerFor(
90
- target: Record<string, unknown>,
93
+ target: object,
91
94
  paths?: ReadonlyMap<object, string[]>,
92
95
  ): Cloner {
93
96
  const seen = paths ? existingObjects(paths, target) : new Map();
94
- return (value) => deepClone(value, { keepUnregistered: true, seen });
97
+ // Immer does not finalize a draft held as a Map key, so an existing object
98
+ // met there is put in the draft as the object itself: its base while the
99
+ // code did not change it, which other references to it then agree with.
100
+ const mapKey = (copy: object, original: object): object => {
101
+ const path = paths?.get(original);
102
+ if (!path || !isDraft(copy)) return copy;
103
+ const plain = currentDraft(copy) as object;
104
+ setByPath(target as Record<string, unknown>, path, plain);
105
+ seen.set(original, plain);
106
+ return plain;
107
+ };
108
+ return (value) => deepClone(value, { keepUnregistered: true, seen, mapKey });
95
109
  }
96
110
 
97
111
  /** One cloner per target copy, made when first used. */
98
112
  const clonersFor = (
99
- paths?: (target: Record<string, unknown>) => ReadonlyMap<object, string[]>,
113
+ paths?: (target: object) => ReadonlyMap<object, string[]>,
100
114
  ) => {
101
115
  const made = new Map<object, Cloner>();
102
- return (target: Record<string, unknown>): Cloner => {
116
+ return (target: object): Cloner => {
103
117
  let cloner = made.get(target);
104
118
  if (!cloner) {
105
119
  cloner = clonerFor(target, paths?.(target));
@@ -129,14 +143,13 @@ export function frozenCopy<T>(value: T): T {
129
143
  */
130
144
  function mirror(
131
145
  draft: VariableNamespaces,
132
- ns: NamespaceName,
133
146
  change: PathChange,
134
147
  scopes: readonly MutationScope[] = activeScopes,
135
- cloner: (target: Record<string, unknown>) => Cloner = clonerOf,
148
+ cloner: (target: object) => Cloner = clonerOf,
136
149
  ): void {
137
- const root = change.path[0]!;
150
+ const [ns, root] = change.path as [NamespaceName, string];
138
151
  for (const scope of scopes) {
139
- for (const copy of [scope.work[ns], scope.base[ns]]) {
152
+ for (const copy of [scope.work, scope.base]) {
140
153
  try {
141
154
  applyChange(
142
155
  copy,
@@ -147,11 +160,11 @@ function mirror(
147
160
  } catch {
148
161
  const stored = draft[ns][root];
149
162
  if (hasOwn(draft[ns], root)) {
150
- copy[root] = cloneValue(
163
+ copy[ns][root] = cloneValue(
151
164
  isDraft(stored) ? currentDraft(stored) : stored,
152
165
  );
153
166
  } else {
154
- delete copy[root];
167
+ delete copy[ns][root];
155
168
  }
156
169
  }
157
170
  }
@@ -159,7 +172,7 @@ function mirror(
159
172
  }
160
173
 
161
174
  /** A cloner of a copy of its own (no references shared with others). */
162
- const clonerOf = (target: Record<string, unknown>): Cloner => clonerFor(target);
175
+ const clonerOf = (target: object): Cloner => clonerFor(target);
163
176
 
164
177
  const NAMESPACE_KEYS: ReadonlySet<string> = new Set(NAMESPACES);
165
178
 
@@ -279,27 +292,25 @@ export function routeStoreUpdate<S extends VariableNamespaces>(
279
292
  view,
280
293
  recipe as (draft: Draft<S>) => void,
281
294
  );
282
- const changes = NAMESPACES.map((ns) => {
295
+ // Paths below the namespaces, so one cloner and one application serve all
296
+ const changes = NAMESPACES.flatMap((ns) => {
283
297
  const own = patches.filter((p) => p.path[0] === ns);
284
- return [
285
- ns,
286
- own.length ? writtenPaths(view[ns], next[ns], own) : [],
287
- ] as const;
298
+ return own.length
299
+ ? writtenPaths(view[ns], next[ns], own).map(
300
+ (change): PathChange => ({ ...change, path: [ns, ...change.path] }),
301
+ )
302
+ : [];
288
303
  });
289
- // Each target takes its own copies, which share what the written values do
290
- const cloners = clonersFor();
291
- const owned = (
292
- change: PathChange,
293
- target: Record<string, unknown>,
294
- ): PathChange =>
304
+ // Each target takes its own copies, which share what the written values do,
305
+ // and an object the code holds elsewhere stays that one (`Story.set('b', $a)`)
306
+ const held = locateObjects(namespacesOf(inner.work), changes);
307
+ const cloners = clonersFor(() => held);
308
+ const owned = (change: PathChange, target: object): PathChange =>
295
309
  change.deleted
296
310
  ? change
297
311
  : { ...change, value: cloners(target)(change.value) };
298
- for (const [ns, list] of changes) {
299
- for (const change of list) {
300
- applyChange(inner.work[ns], owned(change, inner.work[ns]));
301
- }
302
- }
312
+ for (const change of changes)
313
+ applyChange(inner.work, owned(change, inner.work));
303
314
 
304
315
  return (draft) => {
305
316
  const target = draft as unknown as Record<string, unknown>;
@@ -313,28 +324,68 @@ export function routeStoreUpdate<S extends VariableNamespaces>(
313
324
  }
314
325
  }
315
326
  const namespaces = draft as unknown as VariableNamespaces;
316
- for (const [ns, list] of changes) {
317
- for (const change of list) {
318
- const root = change.path[0]!;
319
- try {
320
- applyChange(namespaces[ns], owned(change, namespaces[ns]));
321
- } catch {
322
- if (hasOwn(inner.work[ns], root)) {
323
- namespaces[ns][root] = cloneValue(inner.work[ns][root]);
324
- } else {
325
- delete namespaces[ns][root];
326
- }
327
+ for (const change of changes) {
328
+ const [ns, root] = change.path as [NamespaceName, string];
329
+ try {
330
+ applyChange(namespaces, owned(change, namespaces));
331
+ } catch {
332
+ if (hasOwn(inner.work[ns], root)) {
333
+ namespaces[ns][root] = cloneValue(inner.work[ns][root]);
334
+ } else {
335
+ delete namespaces[ns][root];
327
336
  }
328
- mirror(namespaces, ns, change, activeScopes, cloners);
329
337
  }
338
+ mirror(namespaces, change, activeScopes, cloners);
330
339
  }
331
340
  };
332
341
  }
333
342
 
334
- const cloneNamespaces = (from: VariableNamespaces): VariableNamespaces => ({
335
- variables: deepClone(from.variables),
336
- temporary: deepClone(from.temporary),
337
- transient: deepClone(from.transient),
343
+ /**
344
+ * Make the objects `work` holds at several paths one object in `draft`
345
+ * again where `changes` wrote below one of them: a write through one path
346
+ * of a draft gives that path a new object, and the other paths would go on
347
+ * referring to the old one (`$a.n = 2` with `$b` the same object as `$a`).
348
+ */
349
+ function relink(
350
+ draft: VariableNamespaces,
351
+ work: VariableNamespaces,
352
+ changes: readonly PathChange[],
353
+ ): void {
354
+ const written = changes.map((c) => pathKey(c.path).slice(0, -1));
355
+ for (const [first, ...others] of sharedPaths(work)) {
356
+ // Written at or below one of the paths: the written key follows its
357
+ // JSON, which opens with the path's own
358
+ const prefixes = [first!, ...others].map((p) => pathKey(p).slice(0, -1));
359
+ if (!written.some((w) => prefixes.some((p) => w.startsWith(p)))) continue;
360
+ try {
361
+ const object = getByPath(draft, first!);
362
+ if (object === null || typeof object !== 'object') continue;
363
+ for (const path of others)
364
+ setByPath(draft as unknown as Record<string, unknown>, path, object);
365
+ } catch {
366
+ // A path the store does not hold: the code's view is not the store's
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Copies of the namespaces that keep their references to each other: an
373
+ * object held in two of them (`%copy = $a`) is one object in the copies too.
374
+ */
375
+ function cloneNamespaces(from: VariableNamespaces): VariableNamespaces {
376
+ const seen = new Map<object, object>();
377
+ return {
378
+ variables: deepClone(from.variables, { seen }),
379
+ temporary: deepClone(from.temporary, { seen }),
380
+ transient: deepClone(from.transient, { seen }),
381
+ };
382
+ }
383
+
384
+ /** The three variable namespaces of `state`, as an object to walk. */
385
+ const namespacesOf = (state: VariableNamespaces): VariableNamespaces => ({
386
+ variables: state.variables,
387
+ temporary: state.temporary,
388
+ transient: state.transient,
338
389
  });
339
390
 
340
391
  /** A mutation to commit, and whether its code goes on running after. */
@@ -362,13 +413,9 @@ function commitScopes(commits: readonly Commit[]): void {
362
413
  const all = commits.map(({ scope, keepRunning }) => ({
363
414
  scope,
364
415
  keepRunning,
365
- changes: NAMESPACES.map(
366
- (ns) => [ns, changesOf(scope.base[ns], scope.work[ns])] as const,
367
- ),
416
+ changes: changesOf(scope.base, scope.work),
368
417
  }));
369
- const changed = all.filter(({ changes }) =>
370
- changes.some(([, list]) => list.length > 0),
371
- );
418
+ const changed = all.filter(({ changes }) => changes.length > 0);
372
419
  if (changed.length === 0) return;
373
420
  // Before the store update: watchers it fires may commit again (a goto)
374
421
  for (const { scope, keepRunning } of changed) {
@@ -390,80 +437,67 @@ function commitScopes(commits: readonly Commit[]): void {
390
437
  all.forEach(({ scope, changes }, i) => {
391
438
  // The copies the store and the enclosing mutations take are their own,
392
439
  // and keep the references of the values written (one object written
393
- // at two paths is one object there) and to the objects they hold
394
- // already (see clonerFor). The code's working copy stays its own.
395
- const paths = new Map(
396
- changes.map(([ns, list]) => [ns, locateObjects(scope.work[ns], list)]),
397
- );
398
- const own = new Map(
399
- changes.map(([ns]) => [ns, clonerFor(draft[ns], paths.get(ns))]),
400
- );
440
+ // at two paths, in one namespace or two, is one object there) and to
441
+ // the objects they hold already (see clonerFor). The code's working
442
+ // copy stays its own.
443
+ const paths = locateObjects(scope.work, changes);
444
+ const own = clonerFor(draft, paths);
401
445
  const enclosing = all.slice(0, i).map((c) => c.scope);
402
- const enclosingCloners = new Map(
403
- changes.map(([ns]) => [ns, clonersFor(() => paths.get(ns)!)]),
404
- );
405
- for (const [ns, list] of changes) {
406
- const replaced = new Set<string>();
407
- for (const change of list) {
408
- const root = change.path[0]!;
409
- if (replaced.has(root)) continue;
410
- try {
411
- if (change.deleted) {
412
- if (!isApplied(draft[ns], change)) applyChange(draft[ns], change);
413
- continue;
414
- }
415
- // A changed path already holding the value keeps its reference;
416
- // an alias change is one of reference only: it holds when the
417
- // very object is there.
418
- const value = change.alias ? own.get(ns)!(change.value) : undefined;
419
- if (
420
- change.alias
421
- ? Object.is(getByPath(draft[ns], change.path), value)
422
- : isApplied(draft[ns], change)
423
- ) {
424
- continue;
425
- }
426
- applyChange(draft[ns], {
427
- ...change,
428
- value: change.alias ? value : own.get(ns)!(change.value),
429
- });
430
- } catch {
431
- // An intermediate object the code wrote into is gone from the
432
- // store: the code's view of the whole root wins.
433
- draft[ns][root] = own.get(ns)!(scope.work[ns][root]);
434
- replaced.add(root);
446
+ const enclosingCloners = clonersFor(() => paths);
447
+ const replaced = new Set<string>();
448
+ for (const change of changes) {
449
+ const [ns, root] = change.path as [NamespaceName, string];
450
+ const rootKey = pathKey([ns, root]);
451
+ if (replaced.has(rootKey)) continue;
452
+ try {
453
+ if (change.deleted) {
454
+ if (!isApplied(draft, change)) applyChange(draft, change);
455
+ continue;
435
456
  }
436
- }
437
- // Hand the changes to the mutations this one runs inside, before
438
- // watchers fired by this update run.
439
- for (const change of list) {
440
- mirror(draft, ns, change, enclosing, enclosingCloners.get(ns)!);
457
+ // A changed path already holding the value keeps its reference;
458
+ // an alias change is one of reference only: it holds when the
459
+ // very object is there.
460
+ const value = change.alias ? own(change.value) : undefined;
461
+ if (
462
+ change.alias
463
+ ? Object.is(getByPath(draft, change.path), value)
464
+ : isApplied(draft, change)
465
+ ) {
466
+ continue;
467
+ }
468
+ applyChange(draft, {
469
+ ...change,
470
+ value: change.alias ? value : own(change.value),
471
+ });
472
+ } catch {
473
+ // An intermediate object the code wrote into is gone from the
474
+ // store: the code's view of the whole root wins.
475
+ draft[ns][root] = own(scope.work[ns][root]);
476
+ replaced.add(rootKey);
441
477
  }
442
478
  }
479
+ relink(draft, scope.work, changes);
480
+ // Hand the changes to the mutations this one runs inside, before
481
+ // watchers fired by this update run.
482
+ for (const change of changes) {
483
+ mirror(draft, change, enclosing, enclosingCloners);
484
+ }
443
485
  });
444
486
  }
445
487
  }
446
488
 
447
489
  /**
448
- * The changes of the code to a namespace: its property paths that differ
449
- * (see diffPaths), and those that hold equal content but another object
450
- * than they did (see aliasChanges), those first.
490
+ * The changes of the code to the namespaces, as paths below them (`['variables',
491
+ * 'a']`; see changesBetween).
451
492
  */
452
- function changesOf(
453
- base: Record<string, unknown>,
454
- work: Record<string, unknown>,
455
- ): PathChange[] {
456
- const written = diffPaths(base, work);
457
- const keys = new Set(written.map((c) => pathKey(c.path)));
458
- const aliases: PathChange[] = [];
459
- for (const change of aliasChanges(base, work)) {
460
- // Written whole by a change at it or above it
461
- if (underChange(change.path, keys)) continue;
462
- keys.add(pathKey(change.path));
463
- aliases.push(change);
464
- }
465
- return [...aliases, ...written];
466
- }
493
+ const changesOf = (
494
+ base: VariableNamespaces,
495
+ work: VariableNamespaces,
496
+ ): PathChange[] =>
497
+ changesBetween(
498
+ base as unknown as Record<string, unknown>,
499
+ work as unknown as Record<string, unknown>,
500
+ );
467
501
 
468
502
  /** Every running mutation, to commit with its code going on. */
469
503
  const running = (): Commit[] =>
@@ -503,11 +537,37 @@ export function runWithCommittedMutations<T>(action: () => T): T {
503
537
  }
504
538
  }
505
539
 
506
- export function executeMutation(
507
- code: string,
508
- mergedLocals: Record<string, unknown>,
509
- scopeUpdate: (key: string, value: unknown) => void,
540
+ /**
541
+ * Run `run` on working copies of the story state and commit what it changed
542
+ * to the store, as mutation code does (see commitScopes): the references
543
+ * between values are kept, which a store update made on a draft does not do.
544
+ * Nothing is committed when `run` throws.
545
+ */
546
+ export function mutateState(
547
+ run: (work: VariableNamespaces, adopt: Cloner) => void,
510
548
  ): void {
549
+ // The writes of the code this runs inside come first, and stay even if
550
+ // `run` throws
551
+ commitScopes(running());
552
+ // Where the objects `run` may be handed are: in the state it continues from
553
+ const held = locateObjects(
554
+ namespacesOf(getActiveMutationScope() ?? useStoryStore.getState()),
555
+ [],
556
+ );
557
+ const scope = startScope();
558
+ // Copies values into the working copy, an object it holds already staying
559
+ // that one, and what the values share staying shared
560
+ const adopt = clonerFor(scope.work, held);
561
+ try {
562
+ run(scope.work, adopt);
563
+ } finally {
564
+ activeScopes.pop();
565
+ }
566
+ commitScopes([...running(), { scope, keepRunning: false }]);
567
+ }
568
+
569
+ /** Start a mutation, from the pending state of the one it runs inside. */
570
+ function startScope(): MutationScope {
511
571
  // A mutation started while another executes (a watcher run action fired
512
572
  // by a Story.set in its code) continues from the enclosing code's pending
513
573
  // state rather than the store, as a direct call at that point would.
@@ -516,6 +576,15 @@ export function executeMutation(
516
576
  work: cloneNamespaces(start),
517
577
  base: cloneNamespaces(start),
518
578
  };
579
+ activeScopes.push(scope);
580
+ return scope;
581
+ }
582
+
583
+ export function executeMutation(
584
+ code: string,
585
+ mergedLocals: Record<string, unknown>,
586
+ scopeUpdate: (key: string, value: unknown) => void,
587
+ ): void {
519
588
  // Locals are deep-cloned like the store namespaces: a loop item or widget
520
589
  // argument taken from story state is Immer-frozen, and an unfrozen object
521
590
  // mutated in place would keep its reference, so a nested assignment
@@ -525,7 +594,7 @@ export function executeMutation(
525
594
  deepClone(mergedLocals, { keepUnregistered: true }),
526
595
  );
527
596
 
528
- activeScopes.push(scope);
597
+ const scope = startScope();
529
598
  try {
530
599
  execute(
531
600
  code,
@@ -15,7 +15,7 @@ import {
15
15
  IncompatibleSaveError,
16
16
  SAVE_FORMAT_VERSION,
17
17
  } from './format';
18
- import { getBackend, resetBackend } from './storage';
18
+ import { getBackend, resetBackend, META_PREFIXES } from './storage';
19
19
  import { deepClone } from '../structural';
20
20
  import { emit } from '../event-emitter';
21
21
  import { withoutDraws } from '../prng';
@@ -152,7 +152,7 @@ async function startNewPlaythroughNow(
152
152
  const PLAYTHROUGH_LABEL = /^Playthrough (\d+)$/;
153
153
 
154
154
  function playthroughCountKey(ifid: string): string {
155
- return `playthroughCount.${ifid}`;
155
+ return `${META_PREFIXES.playthroughCount}${ifid}`;
156
156
  }
157
157
 
158
158
  /**
@@ -173,7 +173,7 @@ async function nextPlaythroughNumber(ifid: string): Promise<number> {
173
173
  }
174
174
 
175
175
  function currentPlaythroughKey(ifid: string): string {
176
- return `currentPlaythroughId.${ifid}`;
176
+ return `${META_PREFIXES.currentPlaythrough}${ifid}`;
177
177
  }
178
178
 
179
179
  export function getCurrentPlaythroughId(
@@ -476,9 +476,9 @@ export const getSavesGrouped = queued(async function getSavesGroupedNow(
476
476
 
477
477
  // --- Quick Save / Slot Save ---
478
478
 
479
- const AUTOSAVE_KEY_PREFIX = 'autosave.';
480
- const SLOT_KEY_PREFIX = 'slot.';
481
- const SLOT_INDEX_KEY_PREFIX = 'slotIndex.';
479
+ const AUTOSAVE_KEY_PREFIX = META_PREFIXES.autosave;
480
+ const SLOT_KEY_PREFIX = META_PREFIXES.slot;
481
+ const SLOT_INDEX_KEY_PREFIX = META_PREFIXES.slotIndex;
482
482
 
483
483
  /**
484
484
  * Whether `slot` names a slot. The default (autosave) slot is addressed by
@@ -46,6 +46,28 @@ function makeTables(table: MakeTable): Tables {
46
46
  };
47
47
  }
48
48
 
49
+ /**
50
+ * The meta keys a story owns, in one place: save-manager builds them,
51
+ * deleteMetaByIfid() matches them. A story's key is its prefix followed by
52
+ * its IFID, except a named slot's pointer, `slot.<name>.<IFID>`.
53
+ */
54
+ export const META_PREFIXES = {
55
+ autosave: 'autosave.',
56
+ slot: 'slot.',
57
+ slotIndex: 'slotIndex.',
58
+ playthroughCount: 'playthroughCount.',
59
+ currentPlaythrough: 'currentPlaythroughId.',
60
+ } as const;
61
+
62
+ /** Whether the meta `key` belongs to the story `ifid` (and no other). */
63
+ function isMetaKeyOf(key: string, ifid: string): boolean {
64
+ const { slot, ...others } = META_PREFIXES;
65
+ return (
66
+ Object.values(others).some((prefix) => key === prefix + ifid) ||
67
+ (key.startsWith(slot) && key.endsWith(`.${ifid}`))
68
+ );
69
+ }
70
+
49
71
  function createBackend(
50
72
  type: StorageBackend['type'],
51
73
  { saves, playthroughs, meta }: Tables,
@@ -96,7 +118,8 @@ function createBackend(
96
118
  deleteMeta: (key) => meta.delete(key),
97
119
  deleteMetaByPrefix: (prefix) =>
98
120
  deleteMetaWhere((key) => key.startsWith(prefix)),
99
- deleteMetaByIfid: (ifid) => deleteMetaWhere((key) => key.includes(ifid)),
121
+ deleteMetaByIfid: (ifid) =>
122
+ deleteMetaWhere((key) => isMetaKeyOf(key, ifid)),
100
123
  getAllMetaKeys: () => meta.keys(),
101
124
 
102
125
  destroy,