@poveste/shared 0.5.2 → 0.6.0

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/dist/state.d.ts CHANGED
@@ -18,10 +18,67 @@ export declare function isEquivalent(a: any, b: any, seen?: WeakMap<object, Weak
18
18
  /**
19
19
  * Copies `state` onto `target`, and reports whether it wrote anything.
20
20
  *
21
- * The return value is what the state syncs in `plugin-vue`, `plugin-svelte` and
22
- * the sandbox bridge use to decide whether to expect an echo. Each of them holds
23
- * a flag meaning "the next firing is mine, ignore it", and that flag is only
24
- * safe to set when a firing is actually coming. See #95.
21
+ * The return value is what the syncs in `plugin-svelte` and the sandbox bridge
22
+ * use to decide whether to expect an echo. Each holds a flag meaning "the next
23
+ * firing is mine, ignore it", and that flag is only safe to set when a firing is
24
+ * actually coming. See #95.
25
+ *
26
+ * `plugin-vue` no longer needs it: `createStateBaseline` recognises an echo by
27
+ * it not being a change, so there is no flag left to keep honest. The two are
28
+ * the same idea at different strengths, and the others can move across (#96).
25
29
  */
26
30
  export declare function applyState(target: any, state: any, override?: boolean): boolean;
31
+ /**
32
+ * The subset of `next` that differs from `baseline`, shaped so `applyState` can
33
+ * copy it faithfully: a key it will merge is narrowed to the nested keys that
34
+ * moved, and everything else is carried whole. `null` when nothing changed.
35
+ *
36
+ * The narrowing is what lets two sides edit one object at once without either
37
+ * clobbering the other — whatever is not sent cannot be overwritten — so it has
38
+ * to line up with how `applyState` writes, exactly. Both ask `mergesNested`.
39
+ * A key it refuses, `_h`-prefixed ones included, crosses whole, so concurrent
40
+ * edits *inside* one of those still race; ordinary story state does not live
41
+ * there.
42
+ *
43
+ * Only keys `next` has are considered. A key `baseline` has and `next` does not
44
+ * is a removal, which `applyState` cannot express, so reporting it would produce
45
+ * a write that changes nothing — and, worse, one the baseline would go on
46
+ * reporting forever. Removals stay unmirrored, exactly as they were.
47
+ */
48
+ export declare function diffState(baseline: any, next: any): Record<string, any> | null;
49
+ /**
50
+ * Tracks the last state both sides of a sync agreed on.
51
+ *
52
+ * The syncs used to mirror whole state objects and coordinate with a boolean
53
+ * meaning "the next firing is my own echo, ignore it". That has two costs. An
54
+ * echo is only distinguishable from a genuine edit by counting firings, which
55
+ * is what made the flag load-bearing and fragile (#95). And a side that mirrors
56
+ * everything it holds also mirrors the keys it did *not* change, stale by one
57
+ * edit if the far side changed them in the same tick — so the second firing
58
+ * reverted the first side's edit and it was lost from both (#96).
59
+ *
60
+ * A baseline answers both. Ask it for what a side changed and it reports that
61
+ * side's own edits, never the far side's; an echo diffs to nothing and needs no
62
+ * flag to recognise, because it is not a change.
63
+ */
64
+ export declare function createStateBaseline(): {
65
+ /**
66
+ * What `next` changed since both sides last agreed, or `null` for nothing.
67
+ * The changes count as agreed from here, so ask once per firing and mirror
68
+ * what you get.
69
+ */
70
+ take(next: any): Record<string, any> | null;
71
+ /**
72
+ * Take `changes` as agreed without reporting them.
73
+ *
74
+ * What a peer just sent is by definition something both sides now hold, and
75
+ * it must not come back the other way. Two sides sharing one baseline —
76
+ * `plugin-vue`, where both watchers run in one context — never need this,
77
+ * because `take` on either side records for both. Two sides holding a
78
+ * baseline each — the sandbox bridge, split across a `postMessage` — do:
79
+ * the receiver has to record what arrived, or its own watcher will read the
80
+ * applied write as a local edit and echo it straight back.
81
+ */
82
+ record(changes: any): void;
83
+ };
27
84
  //# sourceMappingURL=state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,IAAI,KAAA,OAczB;AAED,wBAAgB,IAAI,CAAC,IAAI,KAAA,EAAE,IAAI,EAAE,MAAM,EAAE,MAQxC;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAsC7F;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,UAAQ,WAiEnE"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,IAAI,KAAA,OAczB;AAED,wBAAgB,IAAI,CAAC,IAAI,KAAA,EAAE,IAAI,EAAE,MAAM,EAAE,MAQxC;AAWD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAsC7F;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,UAAQ,WAiEnE;AA2BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CA4B9E;AA4DD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB;IAI/B;;;;OAIG;eACQ,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAU3C;;;;;;;;;;OAUG;oBACa,GAAG;EAItB"}
package/dist/state.js CHANGED
@@ -72,13 +72,42 @@ export function isEquivalent(a, b, seen) {
72
72
  }
73
73
  return keys.every(key => Object.hasOwn(b, key) && isEquivalent(a[key], b[key], visited));
74
74
  }
75
+ /**
76
+ * Whether a key's value is merged one level rather than replaced whole.
77
+ *
78
+ * One rule, stated once, because it has to hold in three places at once:
79
+ * `applyState` writes by it, `diffState` narrows by it — sending less than the
80
+ * whole object is only safe as far as the write will merge — and the baseline in
81
+ * `createStateBaseline` records by it. They drifted apart when it was three
82
+ * copies, and the `_h` clause was the one that went missing.
83
+ *
84
+ * `_h`-prefixed keys are never merged: the sandbox needs those replaced outright
85
+ * so a nested key the story dropped actually disappears.
86
+ *
87
+ * Both sides have to be objects worth merging. `existing` is loose about how it
88
+ * got its prototype, because a target may hold anything a story put there; the
89
+ * incoming value must be a plain object, because merging a primitive or an array
90
+ * into an object writes nothing useful — `for (const nested in 5)` iterates
91
+ * nothing at all, and the assignment is then skipped entirely.
92
+ */
93
+ function mergesNested(key, existing, incoming) {
94
+ return !key.startsWith('_h')
95
+ && isPlainObject(incoming)
96
+ && existing != null
97
+ && typeof existing === 'object'
98
+ && !Array.isArray(existing);
99
+ }
75
100
  /**
76
101
  * Copies `state` onto `target`, and reports whether it wrote anything.
77
102
  *
78
- * The return value is what the state syncs in `plugin-vue`, `plugin-svelte` and
79
- * the sandbox bridge use to decide whether to expect an echo. Each of them holds
80
- * a flag meaning "the next firing is mine, ignore it", and that flag is only
81
- * safe to set when a firing is actually coming. See #95.
103
+ * The return value is what the syncs in `plugin-svelte` and the sandbox bridge
104
+ * use to decide whether to expect an echo. Each holds a flag meaning "the next
105
+ * firing is mine, ignore it", and that flag is only safe to set when a firing is
106
+ * actually coming. See #95.
107
+ *
108
+ * `plugin-vue` no longer needs it: `createStateBaseline` recognises an echo by
109
+ * it not being a change, so there is no flag left to keep honest. The two are
110
+ * the same idea at different strengths, and the others can move across (#96).
82
111
  */
83
112
  export function applyState(target, state, override = false) {
84
113
  let wrote = false;
@@ -102,7 +131,7 @@ export function applyState(target, state, override = false) {
102
131
  continue;
103
132
  }
104
133
  // iframe sync needs to update properties without overriding them
105
- if (!override && current && !key.startsWith('_h') && typeof current === 'object' && !Array.isArray(current)) {
134
+ if (!override && mergesNested(key, current, state[key])) {
106
135
  // Not `Object.assign`, because the reason the two are not equivalent may
107
136
  // be a key that `current` has and `state[key]` does not — a removal, which
108
137
  // this merge cannot express. Assigning the rest would then change nothing
@@ -138,3 +167,160 @@ export function applyState(target, state, override = false) {
138
167
  }
139
168
  return wrote;
140
169
  }
170
+ /**
171
+ * The nested keys of `after` that differ from `before`, or `null` for none.
172
+ *
173
+ * One level, and no deeper, because that is exactly how far `applyState` merges:
174
+ * it walks the keys of a nested object and assigns each, so a value handed to it
175
+ * at depth two is written whole. Narrow past that and the write lands the narrow
176
+ * subset *as* the object and takes its siblings with it.
177
+ *
178
+ * Whether a key gets here at all is `mergesNested`'s call, not this function's.
179
+ */
180
+ function narrowState(before, after) {
181
+ let changes = null;
182
+ for (const key in after) {
183
+ if (Object.hasOwn(before, key) && isEquivalent(before[key], after[key])) {
184
+ continue;
185
+ }
186
+ changes ??= {};
187
+ changes[key] = after[key];
188
+ }
189
+ return changes;
190
+ }
191
+ /**
192
+ * The subset of `next` that differs from `baseline`, shaped so `applyState` can
193
+ * copy it faithfully: a key it will merge is narrowed to the nested keys that
194
+ * moved, and everything else is carried whole. `null` when nothing changed.
195
+ *
196
+ * The narrowing is what lets two sides edit one object at once without either
197
+ * clobbering the other — whatever is not sent cannot be overwritten — so it has
198
+ * to line up with how `applyState` writes, exactly. Both ask `mergesNested`.
199
+ * A key it refuses, `_h`-prefixed ones included, crosses whole, so concurrent
200
+ * edits *inside* one of those still race; ordinary story state does not live
201
+ * there.
202
+ *
203
+ * Only keys `next` has are considered. A key `baseline` has and `next` does not
204
+ * is a removal, which `applyState` cannot express, so reporting it would produce
205
+ * a write that changes nothing — and, worse, one the baseline would go on
206
+ * reporting forever. Removals stay unmirrored, exactly as they were.
207
+ */
208
+ export function diffState(baseline, next) {
209
+ let changes = null;
210
+ for (const key in next) {
211
+ const before = baseline[key];
212
+ const after = next[key];
213
+ const known = Object.hasOwn(baseline, key);
214
+ if (known && isEquivalent(before, after)) {
215
+ continue;
216
+ }
217
+ let value = after;
218
+ if (known && mergesNested(key, before, after)) {
219
+ value = narrowState(before, after);
220
+ // Only a removal, then. Nothing to send.
221
+ if (value === null) {
222
+ continue;
223
+ }
224
+ }
225
+ changes ??= {};
226
+ changes[key] = value;
227
+ }
228
+ return changes;
229
+ }
230
+ /**
231
+ * A structural copy, narrow in exactly the way `isEquivalent` is: plain objects
232
+ * and arrays get fresh containers, everything else is carried by reference —
233
+ * which is right, because those are the values `isEquivalent` compares by
234
+ * `Object.is`, so a reference is the identity the comparison is about.
235
+ */
236
+ function copyState(value, seen = new WeakMap()) {
237
+ if (!Array.isArray(value) && !isPlainObject(value)) {
238
+ return value;
239
+ }
240
+ if (seen.has(value)) {
241
+ return seen.get(value);
242
+ }
243
+ const copy = Array.isArray(value) ? [] : {};
244
+ seen.set(value, copy);
245
+ for (const key in value) {
246
+ copy[key] = copyState(value[key], seen);
247
+ }
248
+ return copy;
249
+ }
250
+ /**
251
+ * Records `changes` — the shape `diffState` returns — into `baseline`, on the
252
+ * same terms `applyState` writes it: `mergesNested` decides, the one narrowed
253
+ * level is merged, everything under it taken whole. Keys the diff did not
254
+ * mention survive, which is what keeps a removal the far side could not mirror
255
+ * from being replayed.
256
+ *
257
+ * Getting that predicate wrong here is quiet and permanent. Merge an `_h` key
258
+ * that the write replaced and the baseline keeps a nested key the real state has
259
+ * dropped — it can never match again, so every later pass reports the same
260
+ * phantom change, forever.
261
+ *
262
+ * Copies on the way in. The same `changes` object goes to `applyState`, which
263
+ * assigns its values straight into a reactive state; sharing them would let a
264
+ * later write through that state's proxy mutate the baseline as well, and a
265
+ * baseline that tracks a live side reports every one of that side's edits as
266
+ * already agreed — which is to say, drops them.
267
+ */
268
+ function recordState(baseline, changes) {
269
+ for (const key in changes) {
270
+ const value = changes[key];
271
+ if (mergesNested(key, baseline[key], value)) {
272
+ for (const nested in value) {
273
+ baseline[key][nested] = copyState(value[nested]);
274
+ }
275
+ }
276
+ else {
277
+ baseline[key] = copyState(value);
278
+ }
279
+ }
280
+ }
281
+ /**
282
+ * Tracks the last state both sides of a sync agreed on.
283
+ *
284
+ * The syncs used to mirror whole state objects and coordinate with a boolean
285
+ * meaning "the next firing is my own echo, ignore it". That has two costs. An
286
+ * echo is only distinguishable from a genuine edit by counting firings, which
287
+ * is what made the flag load-bearing and fragile (#95). And a side that mirrors
288
+ * everything it holds also mirrors the keys it did *not* change, stale by one
289
+ * edit if the far side changed them in the same tick — so the second firing
290
+ * reverted the first side's edit and it was lost from both (#96).
291
+ *
292
+ * A baseline answers both. Ask it for what a side changed and it reports that
293
+ * side's own edits, never the far side's; an echo diffs to nothing and needs no
294
+ * flag to recognise, because it is not a change.
295
+ */
296
+ export function createStateBaseline() {
297
+ const baseline = {};
298
+ return {
299
+ /**
300
+ * What `next` changed since both sides last agreed, or `null` for nothing.
301
+ * The changes count as agreed from here, so ask once per firing and mirror
302
+ * what you get.
303
+ */
304
+ take(next) {
305
+ const changes = diffState(baseline, next);
306
+ if (changes) {
307
+ recordState(baseline, changes);
308
+ }
309
+ return changes;
310
+ },
311
+ /**
312
+ * Take `changes` as agreed without reporting them.
313
+ *
314
+ * What a peer just sent is by definition something both sides now hold, and
315
+ * it must not come back the other way. Two sides sharing one baseline —
316
+ * `plugin-vue`, where both watchers run in one context — never need this,
317
+ * because `take` on either side records for both. Two sides holding a
318
+ * baseline each — the sandbox bridge, split across a `postMessage` — do:
319
+ * the receiver has to record what arrived, or its own watcher will read the
320
+ * applied write as a local edit and echo it straight back.
321
+ */
322
+ record(changes) {
323
+ recordState(baseline, changes);
324
+ },
325
+ };
326
+ }
@@ -42,6 +42,7 @@ export interface PovesteConfig {
42
42
  storyMatch: string[];
43
43
  /**
44
44
  * Glob patterns to ignore files while searching for story files.
45
+ * Added to the defaults (`node_modules`, `dist`), not a replacement for them.
45
46
  */
46
47
  storyIgnored: string[];
47
48
  /**
@@ -204,6 +205,10 @@ export interface PovesteConfig {
204
205
  head?: UseHeadInput;
205
206
  /**
206
207
  * Class added to the html root of the story preview when dark mode is enabled.
208
+ *
209
+ * Applied only when you set it. It has no default, so a book that configures
210
+ * `theme.darkClass` alone gets that one class on every render path rather
211
+ * than a second, different one (#126).
207
212
  * @deprecated use `theme.darkClass` instead
208
213
  */
209
214
  sandboxDarkClass?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"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"}
@@ -13,10 +13,19 @@ export interface StoryFile {
13
13
  export type StoryLayout = {
14
14
  type: 'single';
15
15
  iframe?: boolean;
16
+ /**
17
+ * Give every render of this story a fresh sandbox document instead of
18
+ * reusing a warm one. Style isolation is the same either way; this is for
19
+ * stories that leave JS state behind — patched globals, leaked timers —
20
+ * that the next occupant of the realm must not see.
21
+ */
22
+ isolate?: boolean;
16
23
  } | {
17
24
  type: 'grid';
18
25
  width?: number | string;
19
26
  iframeGrid?: boolean;
27
+ /** See the single layout's `isolate`. */
28
+ isolate?: boolean;
20
29
  };
21
30
  export interface CommonProps {
22
31
  id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/types/story.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,GAAG,CAAA;IACd,KAAK,EAAE,KAAK,CAAA;IACZ,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC3C;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,GAAG;IACF,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAA;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW,EAAE,cAAc;CAEhE;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW,EAAE,cAAc;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;CAAG;AAE9B,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,CAAA;IACjB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,MAAM,WAAW,WAAY,SAAQ,UAAU;CAAG;AAElD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAA;IACpC,KAAK,CAAC,EAAE,MAAM;QAAE,OAAO,EAAE,GAAG,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,CAAA;IAC1D,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,GAAG,CAAA;CACd;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,EAAE,CAAA;CACxB;AAID,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ;;OAEG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAA;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;OAEG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAA;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,GAAG,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,gBAAgB,GAAG,cAAc,CAAC,EAAE,CAAA;CAChD;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,IAAI,CAAA;IACX,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,gBAAgB,GAAG,cAAc,CAAC,EAAE,CAAA;CAChD;AAED,MAAM,MAAM,UAAU,GAAG,CAAC,eAAe,GAAG,gBAAgB,GAAG,cAAc,CAAC,EAAE,CAAA;AAEhF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,eAAe,CAAA;IACrB,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,EAAE,EAAE,WAAW,CAAA;CAChB"}
1
+ {"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/types/story.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@poveste/shared",
3
3
  "type": "module",
4
- "version": "0.5.2",
4
+ "version": "0.6.0",
5
5
  "description": "Shared utilities for Poveste",
6
6
  "author": {
7
7
  "name": "Sorin Gitlan"
@@ -34,7 +34,7 @@
34
34
  "pathe": "^1.1.2",
35
35
  "picocolors": "^1.1.1",
36
36
  "unhead": "^3.1.0",
37
- "@poveste/vendors": "^0.5.2"
37
+ "@poveste/vendors": "^0.6.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "5.6.3",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { applyState, isEquivalent } from '../state.js'
2
+ import { applyState, createStateBaseline, diffState, isEquivalent } from '../state.js'
3
3
 
4
4
  describe('isEquivalent', () => {
5
5
  it('compares primitives the way Vue decides whether to trigger', () => {
@@ -124,6 +124,21 @@ describe('applyState', () => {
124
124
  expect(target.nested).toEqual({ a: 1, b: 3 })
125
125
  })
126
126
 
127
+ it('replaces an object with a value it cannot merge into it', () => {
128
+ // The merge branch used to be chosen from the *target*'s type alone, so an
129
+ // incoming primitive met `for (const nested in 5)` — no iterations, nothing
130
+ // written, and the write silently dropped. A story swapping an object for a
131
+ // scalar is ordinary; a sync that records the write as done while the far
132
+ // side never got it then reverts that edit on the next pass.
133
+ const target: any = { config: { a: 1 }, list: { a: 1 } }
134
+
135
+ expect(applyState(target, { config: 5 })).toBe(true)
136
+ expect(target.config).toBe(5)
137
+
138
+ expect(applyState(target, { list: [1, 2] })).toBe(true)
139
+ expect(target.list).toEqual([1, 2])
140
+ })
141
+
127
142
  it('swallows writes to read-only properties', () => {
128
143
  const target: any = {}
129
144
  Object.defineProperty(target, 'ro', { get: () => 1, enumerable: true })
@@ -141,11 +156,12 @@ describe('applyState', () => {
141
156
  })
142
157
  })
143
158
 
144
- // Every state sync in the repo holds a flag meaning "ignore the next firing, it
145
- // is the echo of my own write", and clears it when that firing arrives. This
146
- // return value is what tells them whether a firing is coming at all. Report
147
- // `true` when nothing moved and the flag stays set forever, swallowing the next
148
- // real edit — which is #95.
159
+ // `plugin-svelte` and the sandbox bridge hold a flag meaning "ignore the next
160
+ // firing, it is the echo of my own write", and clear it when that firing
161
+ // arrives. This return value is what tells them whether a firing is coming at
162
+ // all. Report `true` when nothing moved and the flag stays set forever,
163
+ // swallowing the next real edit — which is #95. (`plugin-vue` has since dropped
164
+ // the flag for a baseline; see `createStateBaseline` below.)
149
165
  describe('applyState reporting whether it wrote', () => {
150
166
  it('reports a write', () => {
151
167
  expect(applyState({ a: 1 }, { a: 2 })).toBe(true)
@@ -187,3 +203,151 @@ describe('applyState reporting whether it wrote', () => {
187
203
  expect(applyState(target, { ro: 2 })).toBe(true)
188
204
  })
189
205
  })
206
+
207
+ describe('diffState', () => {
208
+ it('reports nothing when the two match', () => {
209
+ expect(diffState({ a: 1 }, { a: 1 })).toBeNull()
210
+ expect(diffState({ nested: { a: 1 } }, { nested: { a: 1 } })).toBeNull()
211
+ expect(diffState({ list: [1, 2] }, { list: [1, 2] })).toBeNull()
212
+ })
213
+
214
+ it('reports only the keys that moved', () => {
215
+ expect(diffState({ a: 1, b: 2 }, { a: 1, b: 3 })).toEqual({ b: 3 })
216
+ })
217
+
218
+ it('reports a key the baseline has never seen', () => {
219
+ expect(diffState({}, { a: 1 })).toEqual({ a: 1 })
220
+ expect(diffState({}, { a: undefined })).toEqual({ a: undefined })
221
+ })
222
+
223
+ it('narrows a nested object to its own changed keys', () => {
224
+ // The point of the whole exercise: what is not sent cannot be clobbered, so
225
+ // the far side keeps its concurrent edit to `b`.
226
+ expect(diffState({ items: { a: 1, b: 2 } }, { items: { a: 9, b: 2 } })).toEqual({ items: { a: 9 } })
227
+ })
228
+
229
+ it('narrows one level and no further, which is as far as applyState merges', () => {
230
+ // `applyState` assigns what it finds at depth two, so a narrowed object there
231
+ // would land as the whole value and drop `y`.
232
+ expect(diffState({ deep: { inner: { x: 1, y: 2 } } }, { deep: { inner: { x: 9, y: 2 } } }))
233
+ .toEqual({ deep: { inner: { x: 9, y: 2 } } })
234
+ })
235
+
236
+ it('carries an `_h` key whole, because applyState replaces rather than merges it', () => {
237
+ // The sandbox bridge needs those replaced outright, so `applyState` sends
238
+ // them down the assignment branch. Narrowing one would empty it.
239
+ expect(diffState({ _hPropState: { a: 1, b: 2 } }, { _hPropState: { a: 9, b: 2 } }))
240
+ .toEqual({ _hPropState: { a: 9, b: 2 } })
241
+ })
242
+
243
+ it('carries an array whole', () => {
244
+ // `applyState` assigns arrays rather than merging them, so a partial one
245
+ // would be read as the entire new value.
246
+ expect(diffState({ list: [1, 2] }, { list: [1, 2, 3] })).toEqual({ list: [1, 2, 3] })
247
+ })
248
+
249
+ it('carries a value whole when the two sides are not both plain objects', () => {
250
+ expect(diffState({ a: 1 }, { a: { b: 2 } })).toEqual({ a: { b: 2 } })
251
+ expect(diffState({ a: { b: 2 } }, { a: 1 })).toEqual({ a: 1 })
252
+ })
253
+
254
+ it('ignores a removal, at any depth', () => {
255
+ // Deliberate, and the reason the baseline stays usable. `applyState` cannot
256
+ // express a removal, so reporting one produces a write that changes nothing
257
+ // — and one the baseline would go on reporting on every later pass.
258
+ expect(diffState({ a: 1, gone: 2 }, { a: 1 })).toBeNull()
259
+ expect(diffState({ items: { a: 1, gone: 2 } }, { items: { a: 1 } })).toBeNull()
260
+ })
261
+ })
262
+
263
+ describe('createStateBaseline', () => {
264
+ it('reports everything the first time, since it has agreed to nothing yet', () => {
265
+ const baseline = createStateBaseline()
266
+ expect(baseline.take({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })
267
+ })
268
+
269
+ it('reports nothing for a repeat, which is what makes an echo recognisable', () => {
270
+ const baseline = createStateBaseline()
271
+ baseline.take({ a: 1 })
272
+ expect(baseline.take({ a: 1 })).toBeNull()
273
+ })
274
+
275
+ it('reports each side only its own change', () => {
276
+ // #96 in miniature. Both sides changed a different key in the same tick, so
277
+ // each holds one fresh value and one stale one — and is asked about the
278
+ // fresh one alone.
279
+ const baseline = createStateBaseline()
280
+ baseline.take({ a: 0, b: 0 })
281
+
282
+ expect(baseline.take({ a: 1, b: 0 })).toEqual({ a: 1 })
283
+ expect(baseline.take({ a: 1, b: 1 })).toEqual({ b: 1 })
284
+ expect(baseline.take({ a: 1, b: 1 })).toBeNull()
285
+ })
286
+
287
+ it('stops reporting an `_h` key once the far side holds the shrunk value', () => {
288
+ // `_h` keys are replaced whole rather than merged, so the baseline has to
289
+ // record them the same way. Merging instead leaves behind a nested key the
290
+ // real state has dropped, and the baseline can never match again — every
291
+ // later pass reports the same phantom change, forever. Every Vue story
292
+ // carries `_hPropState`, so "forever" means every story whose auto-props
293
+ // ever shrink.
294
+ const baseline = createStateBaseline()
295
+ baseline.take({ _hPropState: { a: 1, b: 2 } })
296
+
297
+ expect(baseline.take({ _hPropState: { a: 9 } })).toEqual({ _hPropState: { a: 9 } })
298
+ expect(baseline.take({ _hPropState: { a: 9 } })).toBeNull()
299
+ })
300
+
301
+ it('keeps a key the far side removed rather than replaying it', () => {
302
+ const baseline = createStateBaseline()
303
+ baseline.take({ a: 1, gone: 2 })
304
+
305
+ expect(baseline.take({ a: 1 })).toBeNull()
306
+ // Still agreed as far as the side that kept it is concerned, so it does not
307
+ // come back the next time that side is asked.
308
+ expect(baseline.take({ a: 1, gone: 2 })).toBeNull()
309
+ })
310
+
311
+ it('does not keep a reference to what it was handed', () => {
312
+ // The caller passes the same object on to `applyState`, which assigns it
313
+ // into a reactive state. Sharing it would let a later write land in the
314
+ // baseline too, and an edit the baseline already knows is an edit dropped.
315
+ const baseline = createStateBaseline()
316
+ const next: any = { list: [1, 2], nested: { a: 1 } }
317
+ baseline.take(next)
318
+
319
+ next.list.push(3)
320
+ next.nested.a = 9
321
+
322
+ expect(baseline.take({ list: [1, 2, 3], nested: { a: 9 } }))
323
+ .toEqual({ list: [1, 2, 3], nested: { a: 9 } })
324
+ })
325
+
326
+ it('survives a cyclic state', () => {
327
+ const baseline = createStateBaseline()
328
+ const next: any = { name: 'a' }
329
+ next.self = next
330
+
331
+ expect(() => baseline.take(next)).not.toThrow()
332
+ expect(baseline.take(next)).toBeNull()
333
+ })
334
+
335
+ it('survives a cyclic state that changes', () => {
336
+ // The harder half. `isEquivalent` short-circuits an unchanged cycle before
337
+ // the diff ever recurses; a changed one gets past it, and then meets the
338
+ // same pair on every lap.
339
+ const baseline = createStateBaseline()
340
+ const first: any = { name: 'a' }
341
+ first.self = first
342
+ baseline.take(first)
343
+
344
+ const second: any = { name: 'b' }
345
+ second.self = second
346
+
347
+ // `self.name` did genuinely move, so it is reported at that path too. The
348
+ // walk terminates because it stops after one level, not because a cycle is
349
+ // detected — below that the value is carried whole, cycle and all.
350
+ expect(baseline.take(second)).toEqual({ name: 'b', self: { name: 'b', self: second } })
351
+ expect(baseline.take(second)).toBeNull()
352
+ })
353
+ })
package/src/state.ts CHANGED
@@ -87,13 +87,43 @@ export function isEquivalent(a: any, b: any, seen?: WeakMap<object, WeakSet<obje
87
87
  return keys.every(key => Object.hasOwn(b, key) && isEquivalent(a[key], b[key], visited))
88
88
  }
89
89
 
90
+ /**
91
+ * Whether a key's value is merged one level rather than replaced whole.
92
+ *
93
+ * One rule, stated once, because it has to hold in three places at once:
94
+ * `applyState` writes by it, `diffState` narrows by it — sending less than the
95
+ * whole object is only safe as far as the write will merge — and the baseline in
96
+ * `createStateBaseline` records by it. They drifted apart when it was three
97
+ * copies, and the `_h` clause was the one that went missing.
98
+ *
99
+ * `_h`-prefixed keys are never merged: the sandbox needs those replaced outright
100
+ * so a nested key the story dropped actually disappears.
101
+ *
102
+ * Both sides have to be objects worth merging. `existing` is loose about how it
103
+ * got its prototype, because a target may hold anything a story put there; the
104
+ * incoming value must be a plain object, because merging a primitive or an array
105
+ * into an object writes nothing useful — `for (const nested in 5)` iterates
106
+ * nothing at all, and the assignment is then skipped entirely.
107
+ */
108
+ function mergesNested(key: string, existing: any, incoming: any) {
109
+ return !key.startsWith('_h')
110
+ && isPlainObject(incoming)
111
+ && existing != null
112
+ && typeof existing === 'object'
113
+ && !Array.isArray(existing)
114
+ }
115
+
90
116
  /**
91
117
  * Copies `state` onto `target`, and reports whether it wrote anything.
92
118
  *
93
- * The return value is what the state syncs in `plugin-vue`, `plugin-svelte` and
94
- * the sandbox bridge use to decide whether to expect an echo. Each of them holds
95
- * a flag meaning "the next firing is mine, ignore it", and that flag is only
96
- * safe to set when a firing is actually coming. See #95.
119
+ * The return value is what the syncs in `plugin-svelte` and the sandbox bridge
120
+ * use to decide whether to expect an echo. Each holds a flag meaning "the next
121
+ * firing is mine, ignore it", and that flag is only safe to set when a firing is
122
+ * actually coming. See #95.
123
+ *
124
+ * `plugin-vue` no longer needs it: `createStateBaseline` recognises an echo by
125
+ * it not being a change, so there is no flag left to keep honest. The two are
126
+ * the same idea at different strengths, and the others can move across (#96).
97
127
  */
98
128
  export function applyState(target: any, state: any, override = false) {
99
129
  let wrote = false
@@ -120,7 +150,7 @@ export function applyState(target: any, state: any, override = false) {
120
150
  }
121
151
 
122
152
  // iframe sync needs to update properties without overriding them
123
- if (!override && current && !key.startsWith('_h') && typeof current === 'object' && !Array.isArray(current)) {
153
+ if (!override && mergesNested(key, current, state[key])) {
124
154
  // Not `Object.assign`, because the reason the two are not equivalent may
125
155
  // be a key that `current` has and `state[key]` does not — a removal, which
126
156
  // this merge cannot express. Assigning the rest would then change nothing
@@ -161,3 +191,184 @@ export function applyState(target: any, state: any, override = false) {
161
191
 
162
192
  return wrote
163
193
  }
194
+
195
+ /**
196
+ * The nested keys of `after` that differ from `before`, or `null` for none.
197
+ *
198
+ * One level, and no deeper, because that is exactly how far `applyState` merges:
199
+ * it walks the keys of a nested object and assigns each, so a value handed to it
200
+ * at depth two is written whole. Narrow past that and the write lands the narrow
201
+ * subset *as* the object and takes its siblings with it.
202
+ *
203
+ * Whether a key gets here at all is `mergesNested`'s call, not this function's.
204
+ */
205
+ function narrowState(before: any, after: any): Record<string, any> | null {
206
+ let changes: Record<string, any> | null = null
207
+
208
+ for (const key in after) {
209
+ if (Object.hasOwn(before, key) && isEquivalent(before[key], after[key])) {
210
+ continue
211
+ }
212
+
213
+ changes ??= {}
214
+ changes[key] = after[key]
215
+ }
216
+
217
+ return changes
218
+ }
219
+
220
+ /**
221
+ * The subset of `next` that differs from `baseline`, shaped so `applyState` can
222
+ * copy it faithfully: a key it will merge is narrowed to the nested keys that
223
+ * moved, and everything else is carried whole. `null` when nothing changed.
224
+ *
225
+ * The narrowing is what lets two sides edit one object at once without either
226
+ * clobbering the other — whatever is not sent cannot be overwritten — so it has
227
+ * to line up with how `applyState` writes, exactly. Both ask `mergesNested`.
228
+ * A key it refuses, `_h`-prefixed ones included, crosses whole, so concurrent
229
+ * edits *inside* one of those still race; ordinary story state does not live
230
+ * there.
231
+ *
232
+ * Only keys `next` has are considered. A key `baseline` has and `next` does not
233
+ * is a removal, which `applyState` cannot express, so reporting it would produce
234
+ * a write that changes nothing — and, worse, one the baseline would go on
235
+ * reporting forever. Removals stay unmirrored, exactly as they were.
236
+ */
237
+ export function diffState(baseline: any, next: any): Record<string, any> | null {
238
+ let changes: Record<string, any> | null = null
239
+
240
+ for (const key in next) {
241
+ const before = baseline[key]
242
+ const after = next[key]
243
+ const known = Object.hasOwn(baseline, key)
244
+
245
+ if (known && isEquivalent(before, after)) {
246
+ continue
247
+ }
248
+
249
+ let value = after
250
+
251
+ if (known && mergesNested(key, before, after)) {
252
+ value = narrowState(before, after)
253
+
254
+ // Only a removal, then. Nothing to send.
255
+ if (value === null) {
256
+ continue
257
+ }
258
+ }
259
+
260
+ changes ??= {}
261
+ changes[key] = value
262
+ }
263
+
264
+ return changes
265
+ }
266
+
267
+ /**
268
+ * A structural copy, narrow in exactly the way `isEquivalent` is: plain objects
269
+ * and arrays get fresh containers, everything else is carried by reference —
270
+ * which is right, because those are the values `isEquivalent` compares by
271
+ * `Object.is`, so a reference is the identity the comparison is about.
272
+ */
273
+ function copyState(value: any, seen = new WeakMap<object, any>()) {
274
+ if (!Array.isArray(value) && !isPlainObject(value)) {
275
+ return value
276
+ }
277
+
278
+ if (seen.has(value)) {
279
+ return seen.get(value)
280
+ }
281
+
282
+ const copy: any = Array.isArray(value) ? [] : {}
283
+ seen.set(value, copy)
284
+
285
+ for (const key in value) {
286
+ copy[key] = copyState(value[key], seen)
287
+ }
288
+
289
+ return copy
290
+ }
291
+
292
+ /**
293
+ * Records `changes` — the shape `diffState` returns — into `baseline`, on the
294
+ * same terms `applyState` writes it: `mergesNested` decides, the one narrowed
295
+ * level is merged, everything under it taken whole. Keys the diff did not
296
+ * mention survive, which is what keeps a removal the far side could not mirror
297
+ * from being replayed.
298
+ *
299
+ * Getting that predicate wrong here is quiet and permanent. Merge an `_h` key
300
+ * that the write replaced and the baseline keeps a nested key the real state has
301
+ * dropped — it can never match again, so every later pass reports the same
302
+ * phantom change, forever.
303
+ *
304
+ * Copies on the way in. The same `changes` object goes to `applyState`, which
305
+ * assigns its values straight into a reactive state; sharing them would let a
306
+ * later write through that state's proxy mutate the baseline as well, and a
307
+ * baseline that tracks a live side reports every one of that side's edits as
308
+ * already agreed — which is to say, drops them.
309
+ */
310
+ function recordState(baseline: any, changes: any) {
311
+ for (const key in changes) {
312
+ const value = changes[key]
313
+
314
+ if (mergesNested(key, baseline[key], value)) {
315
+ for (const nested in value) {
316
+ baseline[key][nested] = copyState(value[nested])
317
+ }
318
+ }
319
+ else {
320
+ baseline[key] = copyState(value)
321
+ }
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Tracks the last state both sides of a sync agreed on.
327
+ *
328
+ * The syncs used to mirror whole state objects and coordinate with a boolean
329
+ * meaning "the next firing is my own echo, ignore it". That has two costs. An
330
+ * echo is only distinguishable from a genuine edit by counting firings, which
331
+ * is what made the flag load-bearing and fragile (#95). And a side that mirrors
332
+ * everything it holds also mirrors the keys it did *not* change, stale by one
333
+ * edit if the far side changed them in the same tick — so the second firing
334
+ * reverted the first side's edit and it was lost from both (#96).
335
+ *
336
+ * A baseline answers both. Ask it for what a side changed and it reports that
337
+ * side's own edits, never the far side's; an echo diffs to nothing and needs no
338
+ * flag to recognise, because it is not a change.
339
+ */
340
+ export function createStateBaseline() {
341
+ const baseline: Record<string, any> = {}
342
+
343
+ return {
344
+ /**
345
+ * What `next` changed since both sides last agreed, or `null` for nothing.
346
+ * The changes count as agreed from here, so ask once per firing and mirror
347
+ * what you get.
348
+ */
349
+ take(next: any): Record<string, any> | null {
350
+ const changes = diffState(baseline, next)
351
+
352
+ if (changes) {
353
+ recordState(baseline, changes)
354
+ }
355
+
356
+ return changes
357
+ },
358
+
359
+ /**
360
+ * Take `changes` as agreed without reporting them.
361
+ *
362
+ * What a peer just sent is by definition something both sides now hold, and
363
+ * it must not come back the other way. Two sides sharing one baseline —
364
+ * `plugin-vue`, where both watchers run in one context — never need this,
365
+ * because `take` on either side records for both. Two sides holding a
366
+ * baseline each — the sandbox bridge, split across a `postMessage` — do:
367
+ * the receiver has to record what arrived, or its own watcher will read the
368
+ * applied write as a local edit and echo it straight back.
369
+ */
370
+ record(changes: any) {
371
+ recordState(baseline, changes)
372
+ },
373
+ }
374
+ }
@@ -52,6 +52,7 @@ export interface PovesteConfig {
52
52
  storyMatch: string[]
53
53
  /**
54
54
  * Glob patterns to ignore files while searching for story files.
55
+ * Added to the defaults (`node_modules`, `dist`), not a replacement for them.
55
56
  */
56
57
  storyIgnored: string[]
57
58
  /**
@@ -214,6 +215,10 @@ export interface PovesteConfig {
214
215
  head?: UseHeadInput
215
216
  /**
216
217
  * Class added to the html root of the story preview when dark mode is enabled.
218
+ *
219
+ * Applied only when you set it. It has no default, so a book that configures
220
+ * `theme.darkClass` alone gets that one class on every render path rather
221
+ * than a second, different one (#126).
217
222
  * @deprecated use `theme.darkClass` instead
218
223
  */
219
224
  sandboxDarkClass?: string
@@ -12,10 +12,19 @@ export interface StoryFile {
12
12
  export type StoryLayout = {
13
13
  type: 'single'
14
14
  iframe?: boolean
15
+ /**
16
+ * Give every render of this story a fresh sandbox document instead of
17
+ * reusing a warm one. Style isolation is the same either way; this is for
18
+ * stories that leave JS state behind — patched globals, leaked timers —
19
+ * that the next occupant of the realm must not see.
20
+ */
21
+ isolate?: boolean
15
22
  } | {
16
23
  type: 'grid'
17
24
  width?: number | string
18
25
  iframeGrid?: boolean
26
+ /** See the single layout's `isolate`. */
27
+ isolate?: boolean
19
28
  }
20
29
 
21
30
  export interface CommonProps {