jq79 0.4.12 → 0.4.14
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/README.md +1 -1
- package/dev/vite.ts +47 -0
- package/dist/jq79.cjs +12 -12
- package/dist/jq79.cjs.map +1 -1
- package/dist/jq79.d.ts +10 -1
- package/dist/jq79.global.js +12 -12
- package/dist/jq79.global.js.map +1 -1
- package/dist/jq79.js +12 -12
- package/dist/jq79.js.map +1 -1
- package/dist/reactive.d.ts +2 -1
- package/dist/transform.d.ts +1 -0
- package/dist/vite.cjs +39 -0
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +39 -0
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/jq79.ts +543 -27
- package/src/reactive.ts +55 -4
- package/src/transform.ts +12 -2
package/src/reactive.ts
CHANGED
|
@@ -11,8 +11,10 @@ export type ReactiveDeepData<T> = T & {
|
|
|
11
11
|
$on: (dotKey: string, listener: ChangeListener, options?: ListenerOptions) => Unsubscribe
|
|
12
12
|
$onAny: (listener: AnyChangeListener, options?: ListenerOptions) => Unsubscribe
|
|
13
13
|
// runs `run` immediately, recording every dotKey it reads off this store, then
|
|
14
|
-
// re-runs it whenever a changed dotKey overlaps one of those - see pathsOverlap
|
|
15
|
-
|
|
14
|
+
// re-runs it whenever a changed dotKey overlaps one of those - see pathsOverlap.
|
|
15
|
+
// `alsoWakenBy` registers the same effect with other stores as well, so a
|
|
16
|
+
// change in any of them wakes it too (see ATTACH)
|
|
17
|
+
$effect: (run: () => void, alsoWakenBy?: Record<string, any>[]) => Unsubscribe
|
|
16
18
|
// drops this store's subscriptions to the stores nested inside it (see
|
|
17
19
|
// bridge). A store that outlives the one holding it - the shared-state case -
|
|
18
20
|
// would otherwise keep the dead holder's listeners on its own list forever
|
|
@@ -85,6 +87,29 @@ export const untracked = <T>(fn: () => T): T => {
|
|
|
85
87
|
|
|
86
88
|
type Effect = { deps: Set<string>; run: () => void }
|
|
87
89
|
|
|
90
|
+
// an effect lives in exactly one store's `effects` set - the one whose
|
|
91
|
+
// $effect created it - and only that store's notify walks it. Content that
|
|
92
|
+
// reads two stores at once (a component's slot content: the parent's names
|
|
93
|
+
// plus the slot props the child passes it) needs one record in both sets, so
|
|
94
|
+
// a store serves this attach handle beside $on/$effect. Tracking already
|
|
95
|
+
// spans stores - trackerStack is module-level, so one run's deps are whatever
|
|
96
|
+
// it read, wherever it read it - only the waking didn't.
|
|
97
|
+
//
|
|
98
|
+
// Named like the compiled scripts' internals ($__effect, $__import) because it
|
|
99
|
+
// is one: `key in store` never answers true for a storeApi name, so `with`
|
|
100
|
+
// can't see it and no template expression can reach it.
|
|
101
|
+
//
|
|
102
|
+
// The cost, accepted: deps are dot-paths with no store namespace, so a name
|
|
103
|
+
// that exists in both stores wakes the effect from either. A spurious re-run,
|
|
104
|
+
// never a stale render
|
|
105
|
+
const ATTACH = "$__attach"
|
|
106
|
+
|
|
107
|
+
// the extra stores every effect created off a scope must be attached to. Read
|
|
108
|
+
// by createEffectScope off the scope it is given, so a scope can hand the
|
|
109
|
+
// arrangement down to whatever renders inside it (nested :each item scopes,
|
|
110
|
+
// a nested component's prop-sync effects) without every call site knowing
|
|
111
|
+
export const ALSO_WAKEN_BY = Symbol("jq79.alsoWakenBy")
|
|
112
|
+
|
|
88
113
|
export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepData<T> => {
|
|
89
114
|
const exactListeners = new Map<string, Set<ChangeListener>>()
|
|
90
115
|
const anyListeners = new Set<AnyChangeListener>()
|
|
@@ -269,7 +294,7 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
269
294
|
return () => anyListeners.delete(listener)
|
|
270
295
|
}
|
|
271
296
|
|
|
272
|
-
const $effect = (run: () => void): Unsubscribe => {
|
|
297
|
+
const $effect = (run: () => void, alsoWakenBy?: Record<string, any>[]): Unsubscribe => {
|
|
273
298
|
// a notify landing while this effect runs (an item's render writing to
|
|
274
299
|
// the store, waking the very effect that is rendering it) must not
|
|
275
300
|
// re-enter mid-run - the half-done run would race its own repeat over
|
|
@@ -309,7 +334,28 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
309
334
|
},
|
|
310
335
|
}
|
|
311
336
|
effects.add(effect)
|
|
337
|
+
// the shared case is rare (only slot content asks for it) and this
|
|
338
|
+
// function is on the stack for as long as whatever it renders - a
|
|
339
|
+
// component that renders itself stacks 200 of these - so it keeps the
|
|
340
|
+
// shape it had, and the extra bookkeeping lives in its own frame
|
|
341
|
+
if (alsoWakenBy?.length) return attachAndRun(effect, alsoWakenBy)
|
|
342
|
+
effect.run()
|
|
343
|
+
return () => { effects.delete(effect) }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// attached before the first run, so a store that notifies during it (a setup
|
|
347
|
+
// script's write, a prop sync) reaches this effect like any other
|
|
348
|
+
const attachAndRun = (effect: Effect, alsoWakenBy: Record<string, any>[]): Unsubscribe => {
|
|
349
|
+
const detach = alsoWakenBy.map(store => store?.[ATTACH]?.(effect)).filter(Boolean) as Unsubscribe[]
|
|
312
350
|
effect.run()
|
|
351
|
+
return () => {
|
|
352
|
+
effects.delete(effect)
|
|
353
|
+
detach.forEach(drop => drop())
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const $__attach = (effect: Effect): Unsubscribe => {
|
|
358
|
+
effects.add(effect)
|
|
313
359
|
return () => { effects.delete(effect) }
|
|
314
360
|
}
|
|
315
361
|
|
|
@@ -322,6 +368,7 @@ export const $reactive = <T extends Record<string, any>>(data: T): ReactiveDeepD
|
|
|
322
368
|
storeApi.$onAny = $onAny
|
|
323
369
|
storeApi.$effect = $effect
|
|
324
370
|
storeApi.$dispose = $dispose
|
|
371
|
+
storeApi[ATTACH] = $__attach
|
|
325
372
|
|
|
326
373
|
return reactive
|
|
327
374
|
}
|
|
@@ -346,9 +393,13 @@ export type EffectScope = {
|
|
|
346
393
|
export const createEffectScope = (scope: Record<string, any>): EffectScope => {
|
|
347
394
|
const disposers: Unsubscribe[] = []
|
|
348
395
|
const runs: (() => void)[] = []
|
|
396
|
+
// whatever the scope was handed (slot content is the only thing that sets
|
|
397
|
+
// it today): the stores this scope's effects belong to besides their own.
|
|
398
|
+
// Left undefined when there are none, which is $effect's fast path
|
|
399
|
+
const alsoWakenBy: Record<string, any>[] | undefined = (scope as any)[ALSO_WAKEN_BY]
|
|
349
400
|
return {
|
|
350
401
|
effect: run => {
|
|
351
|
-
disposers.push(scope.$effect(run))
|
|
402
|
+
disposers.push(scope.$effect(run, alsoWakenBy))
|
|
352
403
|
runs.push(run)
|
|
353
404
|
},
|
|
354
405
|
onDispose: fn => { disposers.push(fn) },
|
package/src/transform.ts
CHANGED
|
@@ -475,7 +475,11 @@ const staticImportToAwait = (clause: string | undefined, spec: string, n: number
|
|
|
475
475
|
// identifier, no attribute at all) declares nothing and stays permissive.
|
|
476
476
|
// ---------------------------------------------------------------------------
|
|
477
477
|
|
|
478
|
-
|
|
478
|
+
// `as` is the local name the pattern binds the key to, when it isn't the key
|
|
479
|
+
// itself (`{ item: row }`). A prop signature has no use for it - what the
|
|
480
|
+
// store holds is the key - but the slot binder (`:slot="{ item: row }"`) is
|
|
481
|
+
// the same pattern read for the other half: which names the content uses
|
|
482
|
+
export type PropDecl = { name: string; default?: string; as?: string }
|
|
479
483
|
|
|
480
484
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/
|
|
481
485
|
|
|
@@ -576,7 +580,13 @@ export const parsePropsPattern = (pattern: string | undefined): PropDecl[] | nul
|
|
|
576
580
|
const colon = indexOfTopLevel(named, ":")
|
|
577
581
|
const name = (colon === -1 ? named : named.slice(0, colon)).trim()
|
|
578
582
|
if (!IDENTIFIER_RE.test(name)) continue
|
|
579
|
-
|
|
583
|
+
const decl: PropDecl = { name }
|
|
584
|
+
if (fallback !== undefined) decl.default = fallback
|
|
585
|
+
// only a plain rename is kept: `{ user: { id } }` binds no single name, so
|
|
586
|
+
// there is nothing to record - the prop is still declared under its key
|
|
587
|
+
const local = colon === -1 ? "" : named.slice(colon + 1).trim()
|
|
588
|
+
if (IDENTIFIER_RE.test(local)) decl.as = local
|
|
589
|
+
props.push(decl)
|
|
580
590
|
}
|
|
581
591
|
return props
|
|
582
592
|
}
|