@poveste/shared 0.6.1 → 0.8.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/codegen/const.d.ts +0 -1
- package/dist/codegen/index.d.ts +0 -1
- package/dist/codegen/serialize-js.d.ts +0 -1
- package/dist/codegen/util.d.ts +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/setup.d.ts +0 -1
- package/dist/state.d.ts +0 -1
- package/dist/story-error.d.ts +13 -0
- package/dist/story-error.js +30 -0
- package/dist/story.d.ts +0 -1
- package/dist/type-utils.d.ts +0 -1
- package/dist/types/command.d.ts +0 -1
- package/dist/types/config.d.ts +0 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/plugin.d.ts +0 -1
- package/dist/types/prompt.d.ts +0 -1
- package/dist/types/story.d.ts +0 -1
- package/package.json +14 -4
- package/dist/codegen/const.d.ts.map +0 -1
- package/dist/codegen/index.d.ts.map +0 -1
- package/dist/codegen/serialize-js.d.ts.map +0 -1
- package/dist/codegen/util.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/state.d.ts.map +0 -1
- package/dist/story.d.ts.map +0 -1
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/types/command.d.ts.map +0 -1
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/plugin.d.ts.map +0 -1
- package/dist/types/prompt.d.ts.map +0 -1
- package/dist/types/story.d.ts.map +0 -1
- package/src/__tests__/setup.spec.ts +0 -60
- package/src/__tests__/state.spec.ts +0 -353
- package/src/codegen/const.ts +0 -17
- package/src/codegen/index.ts +0 -3
- package/src/codegen/serialize-js.ts +0 -143
- package/src/codegen/util.ts +0 -77
- package/src/index.ts +0 -6
- package/src/setup.ts +0 -29
- package/src/state.ts +0 -374
- package/src/story.ts +0 -10
- package/src/type-utils.ts +0 -1
- package/src/types/command.ts +0 -38
- package/src/types/config.ts +0 -288
- package/src/types/index.ts +0 -5
- package/src/types/plugin.ts +0 -135
- package/src/types/prompt.ts +0 -25
- package/src/types/story.ts +0 -221
- package/tsconfig.json +0 -51
package/src/state.ts
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
export function clone(data) {
|
|
2
|
-
try {
|
|
3
|
-
return structuredClone(data)
|
|
4
|
-
}
|
|
5
|
-
catch (e) {
|
|
6
|
-
console.warn(e, `Fallback to JSON cloning`)
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(JSON.stringify(data))
|
|
9
|
-
}
|
|
10
|
-
catch (e) {
|
|
11
|
-
console.error(e)
|
|
12
|
-
}
|
|
13
|
-
return data
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function omit(data, keys: string[]) {
|
|
18
|
-
const copy = {}
|
|
19
|
-
for (const key in data) {
|
|
20
|
-
if (!keys.includes(key)) {
|
|
21
|
-
copy[key] = data[key]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return copy
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isPlainObject(value: any) {
|
|
28
|
-
if (value === null || typeof value !== 'object') {
|
|
29
|
-
return false
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const proto = Object.getPrototypeOf(value)
|
|
33
|
-
return proto === Object.prototype || proto === null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Structural comparison, deliberately narrow: it recurses into plain objects and
|
|
38
|
-
* arrays and compares everything else by `Object.is`.
|
|
39
|
-
*
|
|
40
|
-
* That narrowness is the point. Anything with a prototype of its own — a `Date`,
|
|
41
|
-
* a `Map`, a class instance, a DOM node — carries state that own enumerable keys
|
|
42
|
-
* do not describe, and two distinct `Date`s would otherwise compare equal on an
|
|
43
|
-
* empty key list. Reporting "not equivalent" for those is the safe answer: the
|
|
44
|
-
* caller writes, which is what it did before this existed.
|
|
45
|
-
*
|
|
46
|
-
* `Object.is` is not an arbitrary choice either — it is exactly the predicate
|
|
47
|
-
* Vue's `hasChanged` uses, so a value this reports as equivalent is a value Vue
|
|
48
|
-
* would have refused to trigger on anyway.
|
|
49
|
-
*/
|
|
50
|
-
export function isEquivalent(a: any, b: any, seen?: WeakMap<object, WeakSet<object>>): boolean {
|
|
51
|
-
if (Object.is(a, b)) {
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const bothArrays = Array.isArray(a) && Array.isArray(b)
|
|
56
|
-
|
|
57
|
-
if (!bothArrays && !(isPlainObject(a) && isPlainObject(b))) {
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (bothArrays && a.length !== b.length) {
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// State is user data and can be cyclic. A pair already on the stack is
|
|
66
|
-
// assumed equivalent — the usual co-inductive treatment, and it terminates.
|
|
67
|
-
const visited = seen ?? new WeakMap<object, WeakSet<object>>()
|
|
68
|
-
let peers = visited.get(a)
|
|
69
|
-
|
|
70
|
-
if (peers?.has(b)) {
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!peers) {
|
|
75
|
-
peers = new WeakSet<object>()
|
|
76
|
-
visited.set(a, peers)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
peers.add(b)
|
|
80
|
-
|
|
81
|
-
const keys = Object.keys(a)
|
|
82
|
-
|
|
83
|
-
if (keys.length !== Object.keys(b).length) {
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return keys.every(key => Object.hasOwn(b, key) && isEquivalent(a[key], b[key], visited))
|
|
88
|
-
}
|
|
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
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Copies `state` onto `target`, and reports whether it wrote anything.
|
|
118
|
-
*
|
|
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).
|
|
127
|
-
*/
|
|
128
|
-
export function applyState(target: any, state: any, override = false) {
|
|
129
|
-
let wrote = false
|
|
130
|
-
|
|
131
|
-
for (const key in state) {
|
|
132
|
-
const current = target[key]
|
|
133
|
-
|
|
134
|
-
// Skip writes that cannot change anything. Vue drops a re-assigned primitive
|
|
135
|
-
// on its own, but never an object: `state` here has always been rebuilt by a
|
|
136
|
-
// `toRawDeep`/`clone` on the way in, so assigning one lands a fresh identity
|
|
137
|
-
// over an equal-but-distinct old one, and triggers every watcher on it.
|
|
138
|
-
//
|
|
139
|
-
// Those spurious triggers were load-bearing, which is the whole of #95 — the
|
|
140
|
-
// syncs counted on them to keep their flags alternating. Skipping them is
|
|
141
|
-
// both the efficiency win and the thing that makes `wrote` mean something.
|
|
142
|
-
//
|
|
143
|
-
// The key has to exist first. `undefined` is equivalent to `undefined`, so a
|
|
144
|
-
// key the target does not have yet and whose incoming value is `undefined`
|
|
145
|
-
// would otherwise never be created — and a story that declares its shape
|
|
146
|
-
// upfront (`initState: () => ({ pending: undefined })`) would get no control
|
|
147
|
-
// for it, because the panel lists the keys the state actually has.
|
|
148
|
-
if (Object.hasOwn(target, key) && isEquivalent(current, state[key])) {
|
|
149
|
-
continue
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// iframe sync needs to update properties without overriding them
|
|
153
|
-
if (!override && mergesNested(key, current, state[key])) {
|
|
154
|
-
// Not `Object.assign`, because the reason the two are not equivalent may
|
|
155
|
-
// be a key that `current` has and `state[key]` does not — a removal, which
|
|
156
|
-
// this merge cannot express. Assigning the rest would then change nothing
|
|
157
|
-
// while still looking like a write, and a caller that took that for a
|
|
158
|
-
// write would wait for an echo that never comes. Compare per key and
|
|
159
|
-
// report only what actually moved.
|
|
160
|
-
let merged = false
|
|
161
|
-
|
|
162
|
-
for (const nested in state[key]) {
|
|
163
|
-
if (isEquivalent(current[nested], state[key][nested])) {
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
current[nested] = state[key][nested]
|
|
169
|
-
merged = true
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// noop
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (!merged) {
|
|
177
|
-
continue
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
try {
|
|
182
|
-
target[key] = state[key]
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// noop
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
wrote = true
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return wrote
|
|
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
|
-
}
|
package/src/story.ts
DELETED
package/src/type-utils.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type Awaitable<T> = Promise<T> | T
|
package/src/types/command.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
2
|
-
import type { Prompt } from './prompt.js'
|
|
3
|
-
import type { Story, Variant } from './story.js'
|
|
4
|
-
|
|
5
|
-
export interface CommonCommandOptions {
|
|
6
|
-
icon?: string
|
|
7
|
-
searchText?: string
|
|
8
|
-
prompts?: Prompt[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface Command extends CommonCommandOptions {
|
|
12
|
-
id: string
|
|
13
|
-
label: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ClientCommandOptions extends CommonCommandOptions {
|
|
17
|
-
showIf?: (ctx: ClientCommandContext) => boolean
|
|
18
|
-
getParams?: (ctx: ClientCommandContext & { answers?: Record<string, any> }) => Record<string, any>
|
|
19
|
-
clientAction?: (params: Record<string, any>, ctx: ClientCommandContext) => unknown
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* A command that can be executed from the search bar.
|
|
24
|
-
*/
|
|
25
|
-
export type ClientCommand = Command & ClientCommandOptions
|
|
26
|
-
|
|
27
|
-
export interface ClientCommandContext {
|
|
28
|
-
route: RouteLocationNormalizedLoaded
|
|
29
|
-
currentStory: Story
|
|
30
|
-
currentVariant: Variant
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface PluginCommand<
|
|
34
|
-
TParams = Record<string, any>,
|
|
35
|
-
> extends Command {
|
|
36
|
-
serverAction?: (params: TParams) => unknown // @TODO ctx
|
|
37
|
-
clientSetupFile?: string | { file: string, importName: string }
|
|
38
|
-
}
|