@rohal12/spindle 0.59.14 → 0.59.15

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.
package/src/structural.ts CHANGED
@@ -12,7 +12,17 @@
12
12
  import { registeredClassName } from './class-registry';
13
13
  import { hasOwn, setOwn } from './utils/namespace';
14
14
  import { deleteByPath, getByPath, setByPath } from './utils/object-path';
15
- import { extraKeys, isAtomic, isBoxed, isTemporal } from './utils/value-kinds';
15
+ import {
16
+ collectionSize,
17
+ extraKeys,
18
+ isAtomic,
19
+ isBoxed,
20
+ isTemporal,
21
+ mapEntries,
22
+ mapGet,
23
+ mapSet,
24
+ setMembers,
25
+ } from './utils/value-kinds';
16
26
 
17
27
  export { isAtomic };
18
28
 
@@ -25,13 +35,13 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
25
35
  }
26
36
 
27
37
  /**
28
- * `empty`, a new collection (Array, Map or Set) standing for `obj`, made
29
- * like it: an instance of a registered subclass keeps its class and its
30
- * extra keys (#391), with the values `value` gives for them; other
31
- * collections are plain. `made` gets it before its keys are filled, so
38
+ * `empty`, a new built-in (an Array, Map, Set, Date or RegExp) standing for
39
+ * `obj`, made like it: an instance of a registered subclass keeps its class
40
+ * and its extra keys (#391, #393), with the values `value` gives for them;
41
+ * other built-ins are plain. `made` gets it before its keys are filled, so
32
42
  * that cycles through it find it.
33
43
  */
34
- function collectionLike<C extends object>(
44
+ function builtinLike<C extends object>(
35
45
  obj: object,
36
46
  empty: C,
37
47
  made: (copy: C) => void,
@@ -103,11 +113,17 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
103
113
  }
104
114
 
105
115
  function cloneBuiltin(val: object): object | undefined {
106
- if (val instanceof Date) return keep(val, new Date(val.getTime()));
116
+ // Read with the built-in getters: a subclass may override them
117
+ if (val instanceof Date) {
118
+ return builtin(val, new Date(Date.prototype.getTime.call(val)));
119
+ }
107
120
  if (val instanceof RegExp) {
108
- const copy = new RegExp(val.source, val.flags);
121
+ const copy = new RegExp(
122
+ Reflect.get(RegExp.prototype, 'source', val) as string,
123
+ Reflect.get(RegExp.prototype, 'flags', val) as string,
124
+ );
109
125
  copy.lastIndex = val.lastIndex; // the scanning cursor of g/y patterns
110
- return keep(val, copy);
126
+ return builtin(val, copy);
111
127
  }
112
128
  if (val instanceof ArrayBuffer) return keep(val, val.slice(0));
113
129
  if (ArrayBuffer.isView(val)) {
@@ -151,9 +167,9 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
151
167
  : copy;
152
168
  }
153
169
 
154
- /** `empty`, made the copy of the collection `obj` (see collectionLike). */
155
- const collection = <C extends object>(obj: object, empty: C): C =>
156
- collectionLike(
170
+ /** `empty`, made the copy of the built-in `obj` (see builtinLike). */
171
+ const builtin = <C extends object>(obj: object, empty: C): C =>
172
+ builtinLike(
157
173
  obj,
158
174
  empty,
159
175
  (copy) => keep(obj, copy),
@@ -168,30 +184,33 @@ export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
168
184
 
169
185
  if (Array.isArray(val)) {
170
186
  // Holes stay holes
171
- const arr = collection(obj, new Array(val.length) as unknown[]);
187
+ const arr = builtin(obj, new Array(val.length) as unknown[]);
172
188
  for (let i = 0; i < val.length; i++) {
173
189
  if (i in val) arr[i] = clone(val[i]);
174
190
  }
175
191
  return arr;
176
192
  }
177
193
 
178
- // Filled with the built-in methods: a subclass may override them
194
+ // Read and filled with the built-in methods: a subclass may override
195
+ // them (see mapEntries)
179
196
  if (val instanceof Map) {
180
- const copy = collection(obj, new Map());
181
- for (const [k, v] of val) {
197
+ const copy = builtin(obj, new Map());
198
+ for (const [k, v] of mapEntries(val)) {
182
199
  Map.prototype.set.call(copy, settled(clone(k), k), clone(v));
183
200
  }
184
201
  return copy;
185
202
  }
186
203
 
187
204
  if (val instanceof Set) {
188
- const copy = collection(obj, new Set());
189
- for (const v of val) Set.prototype.add.call(copy, settled(clone(v), v));
205
+ const copy = builtin(obj, new Set());
206
+ for (const v of setMembers(val)) {
207
+ Set.prototype.add.call(copy, settled(clone(v), v));
208
+ }
190
209
  return copy;
191
210
  }
192
211
 
193
- const builtin = cloneBuiltin(obj);
194
- if (builtin !== undefined) return builtin;
212
+ const copied = cloneBuiltin(obj);
213
+ if (copied !== undefined) return copied;
195
214
 
196
215
  // A registered class instance keeps its class, and a plain object its
197
216
  // prototype (which may be null). An instance of an unregistered class
@@ -332,13 +351,13 @@ export function shareEqual<T>(prev: unknown, curr: T): T {
332
351
  /** What the result holds at `key` of `c`. */
333
352
  const shareKey = (key: string) =>
334
353
  share(hasOwn(pr, key) ? pr[key] : undefined, cr[key]);
335
- /** `empty`, made the result for the collection `c` (see collectionLike). */
354
+ /** `empty`, made the result for the collection `c` (see builtinLike). */
336
355
  const collection = <C extends object>(empty: C): C =>
337
- collectionLike(c, empty, (out) => done.set(c, out), shareKey);
356
+ builtinLike(c, empty, (out) => done.set(c, out), shareKey);
338
357
  if (c instanceof Map && p instanceof Map) {
339
358
  const out = collection(new Map());
340
- for (const [k, v] of c) {
341
- Map.prototype.set.call(out, fresh(k), share(p.get(k), v));
359
+ for (const [k, v] of mapEntries(c)) {
360
+ Map.prototype.set.call(out, fresh(k), share(mapGet(p, k), v));
342
361
  }
343
362
  return out;
344
363
  }
@@ -381,9 +400,20 @@ function equalBuiltin(
381
400
  b: object,
382
401
  assumed: Pairs,
383
402
  ): boolean | undefined {
384
- if (a instanceof Date) return Object.is(a.getTime(), (b as Date).getTime());
403
+ if (a instanceof Date) {
404
+ const time = Date.prototype.getTime;
405
+ return (
406
+ Object.is(time.call(a), time.call(b as Date)) &&
407
+ equalExtraKeys(a, b, assumed)
408
+ );
409
+ }
385
410
  if (a instanceof RegExp) {
386
- return String(a) === String(b) && a.lastIndex === (b as RegExp).lastIndex;
411
+ const text = RegExp.prototype.toString;
412
+ return (
413
+ text.call(a) === text.call(b as RegExp) &&
414
+ a.lastIndex === (b as RegExp).lastIndex &&
415
+ equalExtraKeys(a, b, assumed)
416
+ );
387
417
  }
388
418
  if (ArrayBuffer.isView(a)) {
389
419
  // A view is its place in its backing buffer, and the whole buffer (which
@@ -435,20 +465,22 @@ function equalKeys(
435
465
  return true;
436
466
  }
437
467
 
438
- /** Built-in collection prototypes, whose instances hold no extra keys. */
439
- const COLLECTION_PROTOS: ReadonlySet<object | null> = new Set([
468
+ /** Built-in prototypes (see builtinLike), whose instances hold no extra keys. */
469
+ const BUILTIN_PROTOS: ReadonlySet<object | null> = new Set([
440
470
  Array.prototype,
441
471
  Map.prototype,
442
472
  Set.prototype,
473
+ Date.prototype,
474
+ RegExp.prototype,
443
475
  ]);
444
476
 
445
477
  /**
446
- * Equality of the extra keys (see extraKeys) of two collections of one
478
+ * Equality of the extra keys (see extraKeys) of two built-ins of one
447
479
  * class: those of a subclass instance are part of its value.
448
480
  */
449
481
  function equalExtraKeys(a: object, b: object, assumed: Pairs): boolean {
450
482
  return (
451
- COLLECTION_PROTOS.has(Object.getPrototypeOf(a) as object | null) ||
483
+ BUILTIN_PROTOS.has(Object.getPrototypeOf(a) as object | null) ||
452
484
  equalKeys(a, b, assumed, extraKeys)
453
485
  );
454
486
  }
@@ -470,9 +502,9 @@ function equal(a: unknown, b: unknown, assumed: Pairs): boolean {
470
502
 
471
503
  if (a instanceof Map || a instanceof Set) {
472
504
  const bc = b as Map<unknown, unknown> | Set<unknown>;
473
- if (a.size !== bc.size) return false;
474
- const ai = a.entries();
475
- const bi = bc.entries();
505
+ if (collectionSize(a) !== collectionSize(bc)) return false;
506
+ const ai = a instanceof Map ? mapEntries(a) : setMembers(a);
507
+ const bi = bc instanceof Map ? mapEntries(bc) : setMembers(bc);
476
508
  for (
477
509
  let x = ai.next(), y = bi.next();
478
510
  !x.done;
@@ -762,13 +794,15 @@ function entryChildren(value: object): [string, unknown][] | undefined {
762
794
  const children: [string, unknown][] = [];
763
795
  if (value instanceof Map) {
764
796
  let index = 0;
765
- for (const [k, v] of value) {
797
+ for (const [k, v] of mapEntries(value)) {
766
798
  children.push([entryKey('k', index), k], [entryKey('v', index), v]);
767
799
  index++;
768
800
  }
769
801
  } else if (value instanceof Set) {
770
802
  let index = 0;
771
- for (const member of value) children.push([entryKey('s', index++), member]);
803
+ for (const member of setMembers(value)) {
804
+ children.push([entryKey('s', index++), member]);
805
+ }
772
806
  } else if (value instanceof Error) {
773
807
  for (const key of ['cause', 'errors'] as const) {
774
808
  if (hasOwn(value, key)) {
@@ -819,8 +853,9 @@ function setEntryValue(
819
853
  const holder = getByEntryPath(root, path.slice(0, -1));
820
854
  const match = /^\0v(\d+)$/.exec(path[path.length - 1]!);
821
855
  if (!(holder instanceof Map) || !match) return false;
822
- const key = [...holder.keys()][Number(match[1])];
823
- holder.set(key, value);
856
+ const entry = [...mapEntries(holder)][Number(match[1])];
857
+ if (!entry) return false;
858
+ mapSet(holder, entry[0], value);
824
859
  return true;
825
860
  }
826
861
 
@@ -921,7 +956,8 @@ function aliasMoved(
921
956
  if (xs && ys) {
922
957
  if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
923
958
  if (x instanceof Map || x instanceof Set) {
924
- if (x.size !== (y as Map<unknown, unknown>).size) return false;
959
+ const ySize = collectionSize(y as Map<unknown, unknown>);
960
+ if (collectionSize(x) !== ySize) return false;
925
961
  }
926
962
  const held = new Map(ys);
927
963
  for (const [segment, a] of xs) {
package/src/triggers.ts CHANGED
@@ -294,10 +294,14 @@ export function reinitTriggerState(): void {
294
294
  }
295
295
  }
296
296
 
297
+ /**
298
+ * Forget every watcher and close every dialog on display, whoever opened it
299
+ * (restart): a `{dialog}` or menubar dialog of the story interface, which
300
+ * stays mounted, would otherwise stay open over the new game (#397).
301
+ */
297
302
  export function resetTriggers(): void {
298
303
  triggers = [];
299
- dialogQueue = [];
300
- dialogHostCallbacks?.closeAll();
304
+ closeAllOpenDialogs();
301
305
  }
302
306
 
303
307
  export function subscribeTriggerDialogs(cb: () => void): () => void {
@@ -1,4 +1,5 @@
1
1
  import { registeredClassName } from '../class-registry';
2
+ import { mapEntries, setMembers } from './value-kinds';
2
3
 
3
4
  /**
4
5
  * Content-derived string key for a value, used to remount components when
@@ -48,8 +49,11 @@ function keyOf(val: unknown, ancestors: object[]): string {
48
49
  const depth = ancestors.indexOf(obj);
49
50
  if (depth !== -1) return `<cycle ${ancestors.length - depth}>`;
50
51
 
51
- if (val instanceof Date) return `Date(${val.getTime()})`;
52
- if (val instanceof RegExp) return `RegExp(${String(val)}@${val.lastIndex})`;
52
+ // Read with the built-in methods: a subclass may override them
53
+ if (val instanceof Date) return `Date(${Date.prototype.getTime.call(val)})`;
54
+ if (val instanceof RegExp) {
55
+ return `RegExp(${RegExp.prototype.toString.call(val)}@${val.lastIndex})`;
56
+ }
53
57
 
54
58
  ancestors.push(obj);
55
59
  try {
@@ -60,13 +64,13 @@ function keyOf(val: unknown, ancestors: object[]): string {
60
64
  return `[${Array.from(val, (v) => keyOf(v, ancestors)).join(',')}]`;
61
65
  }
62
66
  if (val instanceof Map) {
63
- const entries = [...val].map(
67
+ const entries = [...mapEntries(val)].map(
64
68
  ([k, v]) => `[${keyOf(k, ancestors)},${keyOf(v, ancestors)}]`,
65
69
  );
66
70
  return `Map[${entries.join(',')}]`;
67
71
  }
68
72
  if (val instanceof Set) {
69
- return `Set[${[...val].map((v) => keyOf(v, ancestors)).join(',')}]`;
73
+ return `Set[${[...setMembers(val)].map((v) => keyOf(v, ancestors)).join(',')}]`;
70
74
  }
71
75
 
72
76
  const record = obj as Record<string, unknown>;
@@ -1,6 +1,8 @@
1
1
  // Kinds of story values that structural operations (clone, equality, merge)
2
2
  // and property paths treat as a whole, by their content.
3
3
 
4
+ import { isDraft } from 'immer';
5
+
4
6
  export const toStringTag = (value: object): string =>
5
7
  Object.prototype.toString.call(value).slice(8, -1);
6
8
 
@@ -57,3 +59,43 @@ export function extraKeys(collection: object): string[] {
57
59
  const keys = Object.keys(collection);
58
60
  return Array.isArray(collection) ? keys.filter((k) => !isIndexKey(k)) : keys;
59
61
  }
62
+
63
+ // A subclass of Map or Set may override how it is iterated, read or written
64
+ // (an inventory that iterates only the items on hand). What it holds is in
65
+ // its built-in slots, so structural operations read and write those with the
66
+ // built-in methods (#391, #394). An Immer draft keeps its entries in its own
67
+ // state instead, so it is read through its methods.
68
+
69
+ /** The entries of a Map, as the built-in iterator gives them. */
70
+ export function mapEntries<K, V>(map: Map<K, V>): MapIterator<[K, V]> {
71
+ return isDraft(map)
72
+ ? map.entries()
73
+ : (Map.prototype.entries.call(map) as MapIterator<[K, V]>);
74
+ }
75
+
76
+ /** The members of a Set, as the built-in iterator gives them. */
77
+ export function setMembers<T>(set: Set<T>): SetIterator<T> {
78
+ return isDraft(set)
79
+ ? set.values()
80
+ : (Set.prototype.values.call(set) as SetIterator<T>);
81
+ }
82
+
83
+ /** The number of entries of a Map or members of a Set. */
84
+ export function collectionSize(
85
+ collection: Map<unknown, unknown> | Set<unknown>,
86
+ ) {
87
+ if (isDraft(collection)) return collection.size;
88
+ const proto = collection instanceof Map ? Map.prototype : Set.prototype;
89
+ return Reflect.get(proto, 'size', collection) as number;
90
+ }
91
+
92
+ /** The value a Map holds for `key`. */
93
+ export function mapGet<K, V>(map: Map<K, V>, key: K): V | undefined {
94
+ return isDraft(map) ? map.get(key) : Map.prototype.get.call(map, key);
95
+ }
96
+
97
+ /** Make a Map hold `value` for `key`. */
98
+ export function mapSet<K, V>(map: Map<K, V>, key: K, value: V): void {
99
+ if (isDraft(map)) map.set(key, value);
100
+ else Map.prototype.set.call(map, key, value);
101
+ }
package/types/index.d.ts CHANGED
@@ -794,7 +794,9 @@ export interface StoryAPI {
794
794
  /**
795
795
  * Register a class so its instances keep their class through clones,
796
796
  * history, saves and loads. A save refuses instances of classes that are
797
- * not registered.
797
+ * not registered. A class may extend Array, Map, Set, Date, RegExp or
798
+ * Error; one extending another built-in whose value a save cannot hold (a
799
+ * typed array, URL, Promise...) throws.
798
800
  */
799
801
  registerClass(name: string, ctor: new (...args: any[]) => any): void;
800
802