@rohal12/spindle 0.59.6 → 0.59.7

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.
@@ -12575,10 +12575,18 @@ var defineData = (target, key, value) => {
12575
12575
  });
12576
12576
  };
12577
12577
  /**
12578
+ * A reducer extracts string keys, so devalue never sees the symbol keys of
12579
+ * the original object: refuse them here, as it does for a plain object.
12580
+ */
12581
+ function refuseSymbolKeys(value) {
12582
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError("spindle: Cannot save an object with symbol keys");
12583
+ }
12584
+ /**
12578
12585
  * Own enumerable keys of `value`, as a plain object; for an error also its
12579
12586
  * message, cause and (AggregateError) errors, which are not enumerable.
12580
12587
  */
12581
12588
  function ownData(value) {
12589
+ refuseSymbolKeys(value);
12582
12590
  const data = {};
12583
12591
  if (value instanceof Error) {
12584
12592
  for (const key of ERROR_HIDDEN_KEYS$1) if (hasOwn(value, key)) defineData(data, key, value[key]);
@@ -12605,6 +12613,7 @@ function reducers() {
12605
12613
  if (!isObject(v) || Object.getPrototypeOf(v) !== Object.prototype) return false;
12606
12614
  const keys = Object.keys(v);
12607
12615
  if (!keys.some(isEscapedOneCharKey)) return false;
12616
+ refuseSymbolKeys(v);
12608
12617
  if (keys.includes("__proto__")) throw new TypeError("spindle: Cannot save a property named \"__proto__\"");
12609
12618
  return keys.flatMap((k) => [k, v[k]]);
12610
12619
  };
@@ -13496,6 +13505,13 @@ function strictPairs(ab = /* @__PURE__ */ new Map(), ba = /* @__PURE__ */ new Ma
13496
13505
  return pairs;
13497
13506
  }
13498
13507
  /**
13508
+ * Whether `a` and `b` are equal as deepEqual() has it and also share
13509
+ * references at the same places (a save of one loads as the other).
13510
+ */
13511
+ function deepEqualStrict(a, b) {
13512
+ return equal(a, b, strictPairs());
13513
+ }
13514
+ /**
13499
13515
  * `curr` with each part that equals (see deepEqualStrict) the part of
13500
13516
  * `prev` at the same place replaced by `prev`'s: the history moments of a
13501
13517
  * save then share what did not change between them, which a save stores
@@ -13765,13 +13781,14 @@ function underChange(path, keys) {
13765
13781
  }
13766
13782
  /**
13767
13783
  * The segment standing for the entry `index` of a Map or Set in a path, or
13768
- * for the `cause` or `errors` of an Error ('c', 'e').
13784
+ * for the `cause` or `errors` of an Error ('c', 'e'), or for the buffer a
13785
+ * typed array or DataView views ('b').
13769
13786
  */
13770
13787
  var entryKey = (kind, index) => `\0${kind}${index}`;
13771
13788
  var isEntryKey = (segment) => segment.startsWith("\0");
13772
13789
  /**
13773
- * What a Map, Set or Error holds in place of properties (keys and values,
13774
- * members, `cause` and `errors`), each with the segment standing for it in
13790
+ * What a Map, Set, Error or view holds in place of properties (keys and
13791
+ * values, members, `cause` and `errors`, the buffer), each with the segment standing for it in
13775
13792
  * a path (see entryKey); undefined for any other value.
13776
13793
  */
13777
13794
  function entryChildren(value) {
@@ -13787,7 +13804,8 @@ function entryChildren(value) {
13787
13804
  for (const member of value) children.push([entryKey("s", index++), member]);
13788
13805
  } else if (value instanceof Error) {
13789
13806
  for (const key of ["cause", "errors"]) if (hasOwn(value, key)) children.push([entryKey(key === "cause" ? "c" : "e", 0), value[key]]);
13790
- } else return;
13807
+ } else if (ArrayBuffer.isView(value)) children.push([entryKey("b", 0), value.buffer]);
13808
+ else return;
13791
13809
  return children;
13792
13810
  }
13793
13811
  /**
@@ -14245,7 +14263,9 @@ function storedRecord(meta, payload) {
14245
14263
  };
14246
14264
  }
14247
14265
  var createSave = queued(createSaveNow);
14248
- async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14266
+ async function createSaveNow(ifid, playthroughPromise, payload, custom = {}) {
14267
+ const playthroughId = await playthroughPromise;
14268
+ if (!playthroughId) throw new Error("No playthrough");
14249
14269
  const now = (/* @__PURE__ */ new Date()).toISOString();
14250
14270
  const record = storedRecord({
14251
14271
  id: randomUUID(),
@@ -14261,6 +14281,10 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14261
14281
  return record;
14262
14282
  }
14263
14283
  /**
14284
+ * A playthrough passed as a promise (see resolvePlaythroughId) is awaited
14285
+ * inside the operation, so the call takes its place in the order of
14286
+ * operations when it is made.
14287
+ *
14264
14288
  * Replace the payload of an existing save, keeping its ID and `createdAt`.
14265
14289
  * Pass the current `playthroughId` when the new payload comes from the
14266
14290
  * running game: the save then holds that playthrough's state, so it is
@@ -14276,7 +14300,8 @@ async function createSaveNow(ifid, playthroughId, payload, custom = {}) {
14276
14300
  * before, so slot keys and metadata not passed again are kept.
14277
14301
  */
14278
14302
  var overwriteSave = queued(overwriteSaveNow);
14279
- async function overwriteSaveNow(saveId, payload, custom, playthroughId) {
14303
+ async function overwriteSaveNow(saveId, payload, custom, playthroughPromise) {
14304
+ const playthroughId = await playthroughPromise;
14280
14305
  const backend = await getBackend();
14281
14306
  const existing = await backend.getSave(saveId);
14282
14307
  if (!existing) return void 0;
@@ -14983,9 +15008,18 @@ function relink(draft, work, changes) {
14983
15008
  function cloneNamespaces(from) {
14984
15009
  const seen = /* @__PURE__ */ new Map();
14985
15010
  return {
14986
- variables: deepClone(from.variables, { seen }),
14987
- temporary: deepClone(from.temporary, { seen }),
14988
- transient: deepClone(from.transient, { seen })
15011
+ variables: deepClone(from.variables, {
15012
+ keepUnregistered: true,
15013
+ seen
15014
+ }),
15015
+ temporary: deepClone(from.temporary, {
15016
+ keepUnregistered: true,
15017
+ seen
15018
+ }),
15019
+ transient: deepClone(from.transient, {
15020
+ keepUnregistered: true,
15021
+ seen
15022
+ })
14989
15023
  };
14990
15024
  }
14991
15025
  /** The three variable namespaces of `state`, as an object to walk. */
@@ -16734,10 +16768,10 @@ function App() {
16734
16768
  /* @__PURE__ */ jsx(TriggerDialogHost, {}),
16735
16769
  /* @__PURE__ */ jsx(RuntimeErrors, {})
16736
16770
  ] });
16737
- return nobr ? /* @__PURE__ */ jsx(NobrContext.Provider, {
16738
- value: true,
16771
+ return /* @__PURE__ */ jsx(NobrContext.Provider, {
16772
+ value: nobr,
16739
16773
  children: content
16740
- }) : content;
16774
+ });
16741
16775
  }
16742
16776
  //#endregion
16743
16777
  //#region src/parser.ts
@@ -18779,7 +18813,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
18779
18813
  logMacroError(`computed ${rawArgs}`, err, location);
18780
18814
  return;
18781
18815
  }
18782
- if (!deepEqual(prevRef.current, newValue)) {
18816
+ if (!deepEqualStrict(prevRef.current, newValue)) {
18783
18817
  prevRef.current = newValue;
18784
18818
  if (isLocal) try {
18785
18819
  localsUpdate(name, newValue);
@@ -19715,9 +19749,7 @@ function SaveManagerContent() {
19715
19749
  const playthrough = resolvePlaythroughId();
19716
19750
  try {
19717
19751
  await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
19718
- const ptId = await playthrough;
19719
- if (!ptId) throw new Error("No playthrough");
19720
- return createSave(ifid, ptId, payload);
19752
+ return createSave(ifid, playthrough, payload);
19721
19753
  });
19722
19754
  showStatus("Save created");
19723
19755
  await refresh();
@@ -19729,7 +19761,7 @@ function SaveManagerContent() {
19729
19761
  const playthrough = resolvePlaythroughId();
19730
19762
  try {
19731
19763
  await saveWithHooks(void 0, void 0, beginSave, async (payload) => {
19732
- if (!await overwriteSave(saveId, payload, void 0, await playthrough || void 0)) throw new Error("Save not found");
19764
+ if (!await overwriteSave(saveId, payload, void 0, playthrough)) throw new Error("Save not found");
19733
19765
  });
19734
19766
  showStatus("Save overwritten");
19735
19767
  await refresh();
@@ -20566,13 +20598,13 @@ defineMacro({
20566
20598
  useEffect(() => {
20567
20599
  const store = useStoryStore.getState();
20568
20600
  const { history, historyIndex, transitionConfig } = store;
20569
- const next = store.consumeNextTransition();
20570
20601
  const prevLen = prevHistoryLenRef.current;
20571
20602
  prevHistoryLenRef.current = history.length;
20572
20603
  const isFirstLoad = !displayedPassage;
20573
20604
  const isRestart = history.length === 1 && historyIndex === 0 && prevLen > 1;
20574
20605
  const isLoad = history.length > 1 && historyIndex === history.length - 1 && prevLen > 1 && Math.abs(history.length - prevLen) > 1;
20575
20606
  if (navigationId === displayed.id) return;
20607
+ const next = store.consumeNextTransition();
20576
20608
  cancelTransition();
20577
20609
  const targetPassage = storyData?.passages.get(currentPassage);
20578
20610
  if (!targetPassage) {
@@ -20753,6 +20785,15 @@ function stopWithErrors(errors) {
20753
20785
  throw new Error(`spindle: ${errors.length} validation error(s):\n${errors.join("\n")}`);
20754
20786
  }
20755
20787
  /**
20788
+ * The undeclared variables the passages refer to, with the macros known now:
20789
+ * the input macros that bind a variable (registered by author JS or
20790
+ * StoryInit) and the roles of their parameters decide what is a reference.
20791
+ */
20792
+ function variableErrors(storyData, schema) {
20793
+ const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
20794
+ return validatePassages(storyData.passages, schema, storeVarMacros);
20795
+ }
20796
+ /**
20756
20797
  * The markup errors (malformed markup, unknown macros, syntax errors in
20757
20798
  * code) of the passages for which `only` holds, with the macros and widgets
20758
20799
  * known now.
@@ -20801,9 +20842,8 @@ function boot() {
20801
20842
  throw new Error(`spindle: ${msg}`);
20802
20843
  }
20803
20844
  const schema = parseStoryVariables(storyVarsPassage.content);
20804
- const storeVarMacros = getMacroRegistry().filter((m) => m.storeVar).map((m) => m.name);
20805
20845
  for (const name of blockWidgetNames(storyData.passages.values())) registerBlockMacro(name);
20806
- const errors = validatePassages(storyData.passages, schema, storeVarMacros);
20846
+ const errors = variableErrors(storyData, schema);
20807
20847
  errors.push(...markupErrors(storyData, (p) => p.name === "StoryInit"));
20808
20848
  let transientDefaults = {};
20809
20849
  const storyTransientsPassage = storyData.passages.get("StoryTransients");
@@ -20818,7 +20858,7 @@ function boot() {
20818
20858
  useStoryStore.getState().init(storyData, defaults, transientDefaults);
20819
20859
  enterRuntimePhase();
20820
20860
  initializeStory(loadSession(storyData.ifid));
20821
- const markup = markupErrors(storyData, (p) => p.name !== "StoryInit");
20861
+ const markup = [...variableErrors(storyData, schema), ...markupErrors(storyData, (p) => p.name !== "StoryInit")];
20822
20862
  if (markup.length > 0) stopWithErrors(markup);
20823
20863
  for (const [, passage] of storyData.passages) if (passage.tags.includes("widget")) {
20824
20864
  const widgetAST = parseMarkup(passage.content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.59.6",
3
+ "version": "0.59.7",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
@@ -110,11 +110,22 @@ const defineData = (target: object, key: string, value: unknown): void => {
110
110
  });
111
111
  };
112
112
 
113
+ /**
114
+ * A reducer extracts string keys, so devalue never sees the symbol keys of
115
+ * the original object: refuse them here, as it does for a plain object.
116
+ */
117
+ function refuseSymbolKeys(value: object): void {
118
+ if (Object.getOwnPropertySymbols(value).length > 0) {
119
+ throw new TypeError('spindle: Cannot save an object with symbol keys');
120
+ }
121
+ }
122
+
113
123
  /**
114
124
  * Own enumerable keys of `value`, as a plain object; for an error also its
115
125
  * message, cause and (AggregateError) errors, which are not enumerable.
116
126
  */
117
127
  function ownData(value: object): Record<string, unknown> {
128
+ refuseSymbolKeys(value);
118
129
  const data: Record<string, unknown> = {};
119
130
  if (value instanceof Error) {
120
131
  for (const key of ERROR_HIDDEN_KEYS) {
@@ -163,6 +174,7 @@ function reducers(): Record<string, (value: unknown) => unknown> {
163
174
  }
164
175
  const keys = Object.keys(v);
165
176
  if (!keys.some(isEscapedOneCharKey)) return false;
177
+ refuseSymbolKeys(v);
166
178
  if (keys.includes('__proto__')) {
167
179
  throw new TypeError(
168
180
  'spindle: Cannot save a property named "__proto__"',
@@ -41,9 +41,7 @@ export function App() {
41
41
  </>
42
42
  );
43
43
 
44
- return nobr ? (
45
- <NobrContext.Provider value={true}>{content}</NobrContext.Provider>
46
- ) : (
47
- content
48
- );
44
+ // One provider whatever `nobr` is: a changing tree shape would remount the
45
+ // passage and run its mount-only macros again
46
+ return <NobrContext.Provider value={nobr}>{content}</NobrContext.Provider>;
49
47
  }
@@ -1,7 +1,7 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { evaluate } from '../../expression';
3
3
  import { readState } from '../../execute-mutation';
4
- import { deepEqual } from '../../structural';
4
+ import { deepEqualStrict } from '../../structural';
5
5
  import { defineMacro } from '../../define-macro';
6
6
  import { MacroError, logMacroError } from './MacroError';
7
7
  import { currentSourceLocation } from '../../utils/source-location';
@@ -67,7 +67,8 @@ function computeAndApply(
67
67
  return;
68
68
  }
69
69
 
70
- if (!deepEqual(prevRef.current, newValue)) {
70
+ // Strict: a change of only the aliases inside the result is a change
71
+ if (!deepEqualStrict(prevRef.current, newValue)) {
71
72
  prevRef.current = newValue;
72
73
  if (isLocal) {
73
74
  try {
@@ -131,9 +131,6 @@ defineMacro({
131
131
  const store = useStoryStore.getState();
132
132
  const { history, historyIndex, transitionConfig } = store;
133
133
 
134
- // Always consume the next transition (one-shot), regardless of what we do
135
- const next = store.consumeNextTransition();
136
-
137
134
  // Determine if this is a first load, restart, or save-load
138
135
  const prevLen = prevHistoryLenRef.current;
139
136
  prevHistoryLenRef.current = history.length;
@@ -152,6 +149,10 @@ defineMacro({
152
149
  return;
153
150
  }
154
151
 
152
+ // One-shot, used up by the first navigation, whatever we do with it
153
+ // (not by the initial render, which no navigation caused)
154
+ const next = store.consumeNextTransition();
155
+
155
156
  // Cancel any in-progress transition
156
157
  cancelTransition();
157
158
 
@@ -127,9 +127,8 @@ export function SaveManagerContent() {
127
127
  const playthrough = resolvePlaythroughId();
128
128
  try {
129
129
  await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
130
- const ptId = await playthrough;
131
- if (!ptId) throw new Error('No playthrough');
132
- return createSave(ifid, ptId, payload);
130
+ // Queued now, in call order with other storage operations
131
+ return createSave(ifid, playthrough, payload);
133
132
  });
134
133
  showStatus('Save created');
135
134
  await refresh();
@@ -143,8 +142,7 @@ export function SaveManagerContent() {
143
142
  try {
144
143
  await saveWithHooks(undefined, undefined, beginSave, async (payload) => {
145
144
  // Only the current playthrough's saves offer "Save Here"
146
- const ptId = (await playthrough) || undefined;
147
- if (!(await overwriteSave(saveId, payload, undefined, ptId))) {
145
+ if (!(await overwriteSave(saveId, payload, undefined, playthrough))) {
148
146
  throw new Error('Save not found');
149
147
  }
150
148
  });
@@ -381,9 +381,9 @@ function relink(
381
381
  function cloneNamespaces(from: VariableNamespaces): VariableNamespaces {
382
382
  const seen = new Map<object, object>();
383
383
  return {
384
- variables: deepClone(from.variables, { seen }),
385
- temporary: deepClone(from.temporary, { seen }),
386
- transient: deepClone(from.transient, { seen }),
384
+ variables: deepClone(from.variables, { keepUnregistered: true, seen }),
385
+ temporary: deepClone(from.temporary, { keepUnregistered: true, seen }),
386
+ transient: deepClone(from.transient, { keepUnregistered: true, seen }),
387
387
  };
388
388
  }
389
389
 
package/src/index.tsx CHANGED
@@ -62,6 +62,21 @@ function stopWithErrors(errors: string[]): never {
62
62
  );
63
63
  }
64
64
 
65
+ /**
66
+ * The undeclared variables the passages refer to, with the macros known now:
67
+ * the input macros that bind a variable (registered by author JS or
68
+ * StoryInit) and the roles of their parameters decide what is a reference.
69
+ */
70
+ function variableErrors(
71
+ storyData: StoryData,
72
+ schema: Parameters<typeof validatePassages>[1],
73
+ ): string[] {
74
+ const storeVarMacros = getMacroRegistry()
75
+ .filter((m) => m.storeVar)
76
+ .map((m) => m.name);
77
+ return validatePassages(storyData.passages, schema, storeVarMacros);
78
+ }
79
+
65
80
  /**
66
81
  * The markup errors (malformed markup, unknown macros, syntax errors in
67
82
  * code) of the passages for which `only` holds, with the macros and widgets
@@ -134,10 +149,6 @@ export function boot() {
134
149
  }
135
150
 
136
151
  const schema = parseStoryVariables(storyVarsPassage.content);
137
- // Include input macros registered by author JS, which ran above.
138
- const storeVarMacros = getMacroRegistry()
139
- .filter((m) => m.storeVar)
140
- .map((m) => m.name);
141
152
  // Pass 1: Register the block widgets as block macros BEFORE any passage
142
153
  // is parsed (validation and StoryInit included), so that passages
143
154
  // invoking block widgets and widget bodies using other block widgets
@@ -146,7 +157,7 @@ export function boot() {
146
157
  registerBlockMacro(name);
147
158
  }
148
159
 
149
- const errors = validatePassages(storyData.passages, schema, storeVarMacros);
160
+ const errors = variableErrors(storyData, schema);
150
161
  // StoryInit's markup must be valid before it runs; the other passages are
151
162
  // validated once it has run, as it may define macros (see below).
152
163
  errors.push(...markupErrors(storyData, (p) => p.name === 'StoryInit'));
@@ -186,8 +197,12 @@ export function boot() {
186
197
  // storyinit after all state is settled (defaults + StoryInit + session)
187
198
  initializeStory(loadSession(storyData.ifid));
188
199
 
189
- // Every other passage's markup, now that StoryInit may have defined macros
190
- const markup = markupErrors(storyData, (p) => p.name !== 'StoryInit');
200
+ // Every other passage's markup, now that StoryInit may have defined macros;
201
+ // so are the variable references, which macros' parameters decide
202
+ const markup = [
203
+ ...variableErrors(storyData, schema),
204
+ ...markupErrors(storyData, (p) => p.name !== 'StoryInit'),
205
+ ];
191
206
  if (markup.length > 0) stopWithErrors(markup);
192
207
 
193
208
  // Pass 2: Full parse and register widgets from passages tagged "widget"
@@ -291,10 +291,12 @@ export const createSave = queued(createSaveNow);
291
291
 
292
292
  async function createSaveNow(
293
293
  ifid: string,
294
- playthroughId: string,
294
+ playthroughPromise: string | PromiseLike<string>,
295
295
  payload: SavePayload,
296
296
  custom: Record<string, unknown> = {},
297
297
  ): Promise<SaveRecord> {
298
+ const playthroughId = await playthroughPromise;
299
+ if (!playthroughId) throw new Error('No playthrough');
298
300
  const now = new Date().toISOString();
299
301
  const meta: SaveMeta = {
300
302
  id: randomUUID(),
@@ -313,6 +315,10 @@ async function createSaveNow(
313
315
  }
314
316
 
315
317
  /**
318
+ * A playthrough passed as a promise (see resolvePlaythroughId) is awaited
319
+ * inside the operation, so the call takes its place in the order of
320
+ * operations when it is made.
321
+ *
316
322
  * Replace the payload of an existing save, keeping its ID and `createdAt`.
317
323
  * Pass the current `playthroughId` when the new payload comes from the
318
324
  * running game: the save then holds that playthrough's state, so it is
@@ -333,8 +339,9 @@ async function overwriteSaveNow(
333
339
  saveId: string,
334
340
  payload: SavePayload,
335
341
  custom?: Record<string, unknown>,
336
- playthroughId?: string,
342
+ playthroughPromise?: string | PromiseLike<string>,
337
343
  ): Promise<SaveRecord | undefined> {
344
+ const playthroughId = await playthroughPromise;
338
345
  const backend = await getBackend();
339
346
  const existing = await backend.getSave(saveId);
340
347
  if (!existing) return undefined;
package/src/structural.ts CHANGED
@@ -674,17 +674,20 @@ export function underChange(
674
674
 
675
675
  /**
676
676
  * The segment standing for the entry `index` of a Map or Set in a path, or
677
- * for the `cause` or `errors` of an Error ('c', 'e').
677
+ * for the `cause` or `errors` of an Error ('c', 'e'), or for the buffer a
678
+ * typed array or DataView views ('b').
678
679
  */
679
- const entryKey = (kind: 'k' | 'v' | 's' | 'c' | 'e', index: number): string =>
680
- `\0${kind}${index}`;
680
+ const entryKey = (
681
+ kind: 'k' | 'v' | 's' | 'c' | 'e' | 'b',
682
+ index: number,
683
+ ): string => `\0${kind}${index}`;
681
684
 
682
685
  export const isEntryKey = (segment: string): boolean =>
683
686
  segment.startsWith('\0');
684
687
 
685
688
  /**
686
- * What a Map, Set or Error holds in place of properties (keys and values,
687
- * members, `cause` and `errors`), each with the segment standing for it in
689
+ * What a Map, Set, Error or view holds in place of properties (keys and
690
+ * values, members, `cause` and `errors`, the buffer), each with the segment standing for it in
688
691
  * a path (see entryKey); undefined for any other value.
689
692
  */
690
693
  function entryChildren(value: object): [string, unknown][] | undefined {
@@ -707,6 +710,9 @@ function entryChildren(value: object): [string, unknown][] | undefined {
707
710
  ]);
708
711
  }
709
712
  }
713
+ } else if (ArrayBuffer.isView(value)) {
714
+ // Views of one buffer share it, which an alias of the buffer changes
715
+ children.push([entryKey('b', 0), value.buffer]);
710
716
  } else {
711
717
  return undefined;
712
718
  }