brustjs 0.1.21-alpha → 0.1.22-alpha
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 +12 -1
- package/example/pokedex/components/AddToTeamButton.tsx +55 -64
- package/example/pokedex/lib/loaders.ts +13 -3
- package/example/pokedex/lib/types.ts +6 -3
- package/example/pokedex/pages/DetailPage.tsx +1 -2
- package/package.json +9 -8
- package/runtime/cli/build.ts +41 -0
- package/runtime/cli/native-routes-emit.ts +28 -4
- package/runtime/index.js +52 -52
- package/runtime/index.ts +24 -1
- package/runtime/islands/importmap.ts +6 -0
- package/runtime/native/build.ts +147 -0
- package/runtime/native/index.ts +3 -0
- package/runtime/native/runtime.ts +324 -0
- package/runtime/store/signal.ts +36 -18
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Directive runtime — react-free, dom-only. Scans the DOM for x-* directives and
|
|
2
|
+
// binds them to per-element component instances via brustjs/store's `effect`.
|
|
3
|
+
import { effect, isComputed, isSignal } from '../store/index.ts'
|
|
4
|
+
|
|
5
|
+
export type Instance = Record<string, unknown>
|
|
6
|
+
export type Behavior = (ctx: { el: HTMLElement; props: unknown }) => Instance
|
|
7
|
+
|
|
8
|
+
interface Mounted {
|
|
9
|
+
disposers: Array<() => void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const registry = new Map<string, Behavior>()
|
|
13
|
+
const mounted = new WeakMap<HTMLElement, Mounted>()
|
|
14
|
+
const loading = new Map<string, Promise<unknown>>()
|
|
15
|
+
let started = false
|
|
16
|
+
|
|
17
|
+
/** Per-component behavior chunk URL. Each native interactive component is built to
|
|
18
|
+
* its OWN `<name>.directive.js` chunk (served from the islands static route) and
|
|
19
|
+
* loaded ON DEMAND — only when an `x-data="<name>"` for it actually appears (initial
|
|
20
|
+
* render OR after an SPA-nav swap). The chunk self-registers via the global below. */
|
|
21
|
+
const CHUNK_BASE = '/_brust/islands/'
|
|
22
|
+
|
|
23
|
+
/** Register a component behavior under `name`. Called by `<name>.directive.js` chunks
|
|
24
|
+
* via the global handle below (they do NOT import this module — keeps each chunk to
|
|
25
|
+
* just its behavior, with the runtime shared as the single `_directives.js` copy). */
|
|
26
|
+
export function register(name: string, behavior: Behavior): void {
|
|
27
|
+
registry.set(name, behavior)
|
|
28
|
+
}
|
|
29
|
+
// Expose `register` on a global so dynamically-imported behavior chunks self-register
|
|
30
|
+
// into THIS runtime's registry without importing/duplicating the runtime. Symbol.for
|
|
31
|
+
// → shared across chunks (same rationale as the store's brands/reactive ctx).
|
|
32
|
+
;(globalThis as { [k: symbol]: unknown })[Symbol.for('brust.directive.register')] = register
|
|
33
|
+
|
|
34
|
+
/** Scan `root` (default: document) for [x-data], mount each, and (once) attach a
|
|
35
|
+
* MutationObserver for dynamic mount/dispose. Idempotent. NOTE: `root` scopes the
|
|
36
|
+
* INITIAL scan only; the observer always watches the global `document.body` (one
|
|
37
|
+
* observer per document handles every later mount/dispose, incl. SPA-nav swaps). */
|
|
38
|
+
export function start(root?: ParentNode): void {
|
|
39
|
+
const scope: ParentNode | undefined =
|
|
40
|
+
root ?? (typeof document !== 'undefined' ? document : undefined)
|
|
41
|
+
if (!scope) return
|
|
42
|
+
const run = () => {
|
|
43
|
+
scanAndMount(scope)
|
|
44
|
+
if (!started) {
|
|
45
|
+
started = true
|
|
46
|
+
observe()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof document !== 'undefined' && document.readyState === 'loading') {
|
|
50
|
+
document.addEventListener('DOMContentLoaded', run, { once: true })
|
|
51
|
+
} else {
|
|
52
|
+
run()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scanAndMount(scope: ParentNode): void {
|
|
57
|
+
if (scope instanceof HTMLElement && scope.hasAttribute('x-data')) mountElement(scope)
|
|
58
|
+
for (const el of Array.from(scope.querySelectorAll<HTMLElement>('[x-data]'))) {
|
|
59
|
+
mountElement(el)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mountElement(el: HTMLElement): void {
|
|
64
|
+
if (mounted.has(el)) return
|
|
65
|
+
const name = el.getAttribute('x-data') ?? ''
|
|
66
|
+
const behavior = registry.get(name)
|
|
67
|
+
if (!behavior) {
|
|
68
|
+
// Behavior chunk not loaded yet → fetch it on demand, then mount this name.
|
|
69
|
+
loadBehavior(name)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
let props: unknown = {}
|
|
73
|
+
const rawProps = el.getAttribute('x-props')
|
|
74
|
+
if (rawProps) {
|
|
75
|
+
try {
|
|
76
|
+
props = JSON.parse(rawProps)
|
|
77
|
+
} catch {
|
|
78
|
+
console.warn(`[brust] x-props on "${name}" is not valid JSON`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const instance = behavior({ el, props })
|
|
82
|
+
const m: Mounted = { disposers: [] }
|
|
83
|
+
mounted.set(el, m)
|
|
84
|
+
bindTree(el, instance, m.disposers)
|
|
85
|
+
if (typeof instance.init === 'function') {
|
|
86
|
+
try {
|
|
87
|
+
Promise.resolve((instance.init as () => unknown)()).catch((e) =>
|
|
88
|
+
console.error('[brust] x-data init() failed:', e),
|
|
89
|
+
)
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error('[brust] x-data init() threw:', e)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Dynamically import a component's behavior chunk (once), then mount every pending
|
|
97
|
+
// element for that name. The chunk self-registers via the global register handle.
|
|
98
|
+
function loadBehavior(name: string): void {
|
|
99
|
+
if (registry.has(name) || loading.has(name)) return
|
|
100
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
101
|
+
console.warn(`[brust] unsafe x-data component name "${name}" — not loaded`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
// Promise.resolve().then(...) so a synchronous import() throw (e.g. happy-dom in
|
|
105
|
+
// unit tests, or a bad specifier) becomes a rejection the .catch() handles.
|
|
106
|
+
const p = Promise.resolve()
|
|
107
|
+
.then(() => import(/* @vite-ignore */ `${CHUNK_BASE}${name}.directive.js`))
|
|
108
|
+
.then(() => {
|
|
109
|
+
if (!registry.has(name)) {
|
|
110
|
+
console.warn(`[brust] "${name}.directive.js" loaded but did not register "${name}"`)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
// Mount every element waiting on this name (initial + swapped-in).
|
|
114
|
+
if (typeof document !== 'undefined') {
|
|
115
|
+
for (const el of Array.from(document.querySelectorAll<HTMLElement>(`[x-data="${name}"]`))) {
|
|
116
|
+
mountElement(el)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch((e) => console.error(`[brust] failed to load directive component "${name}":`, e))
|
|
121
|
+
loading.set(name, p)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Bind this element's directives, then recurse — but never descend into a nested
|
|
125
|
+
// [x-data] (it owns its own subtree and is mounted independently).
|
|
126
|
+
function bindTree(el: HTMLElement, instance: Instance, disposers: Array<() => void>): void {
|
|
127
|
+
if (el.hasAttribute('x-for')) {
|
|
128
|
+
bindFor(el, instance, disposers)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
bindAttrs(el, instance, disposers)
|
|
132
|
+
for (const child of Array.from(el.children)) {
|
|
133
|
+
if (!(child instanceof HTMLElement)) continue
|
|
134
|
+
if (child.hasAttribute('x-data')) continue
|
|
135
|
+
bindTree(child, instance, disposers)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const FOR_RE = /^\s*(\w+)\s+in\s+([\w.]+)\s*$/
|
|
140
|
+
|
|
141
|
+
// `x-for="item in member"` — the element is the template. Replace it with a comment
|
|
142
|
+
// anchor; on each change of `member`, clear previous clones and render one per item,
|
|
143
|
+
// binding each clone with a child scope { [item]: value } prototype-linked to the
|
|
144
|
+
// instance (so instance members + methods stay visible). v1 = full re-render.
|
|
145
|
+
function bindFor(tplEl: HTMLElement, instance: Instance, disposers: Array<() => void>): void {
|
|
146
|
+
const raw = tplEl.getAttribute('x-for') ?? ''
|
|
147
|
+
const m = FOR_RE.exec(raw)
|
|
148
|
+
if (!m) {
|
|
149
|
+
console.warn(`[brust] malformed x-for expression: "${raw}"`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const itemName = m[1] as string
|
|
153
|
+
const listPath = m[2] as string
|
|
154
|
+
const parent = tplEl.parentNode
|
|
155
|
+
if (!parent) return
|
|
156
|
+
const anchor = tplEl.ownerDocument.createComment(`x-for:${itemName}`)
|
|
157
|
+
parent.insertBefore(anchor, tplEl)
|
|
158
|
+
tplEl.removeAttribute('x-for')
|
|
159
|
+
const template = tplEl.cloneNode(true) as HTMLElement
|
|
160
|
+
tplEl.remove()
|
|
161
|
+
|
|
162
|
+
const rendered: HTMLElement[] = []
|
|
163
|
+
const childDisposers: Array<() => void> = []
|
|
164
|
+
|
|
165
|
+
const clear = () => {
|
|
166
|
+
for (const d of childDisposers.splice(0)) {
|
|
167
|
+
try {
|
|
168
|
+
d()
|
|
169
|
+
} catch {
|
|
170
|
+
/* keep clearing */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const node of rendered.splice(0)) node.remove()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
disposers.push(
|
|
177
|
+
effect(() => {
|
|
178
|
+
clear()
|
|
179
|
+
const list = read(instance, listPath)
|
|
180
|
+
if (!Array.isArray(list)) return
|
|
181
|
+
for (const item of list) {
|
|
182
|
+
const clone = template.cloneNode(true) as HTMLElement
|
|
183
|
+
const childScope: Instance = Object.create(instance)
|
|
184
|
+
childScope[itemName] = item
|
|
185
|
+
bindTree(clone, childScope, childDisposers)
|
|
186
|
+
parent.insertBefore(clone, anchor) // before anchor → preserves order
|
|
187
|
+
rendered.push(clone)
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
disposers.push(clear)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function bindAttrs(el: HTMLElement, scope: Instance, disposers: Array<() => void>): void {
|
|
195
|
+
for (const attr of Array.from(el.attributes)) {
|
|
196
|
+
const name = attr.name
|
|
197
|
+
const value = attr.value
|
|
198
|
+
if (name === 'x-data' || name === 'x-props') continue
|
|
199
|
+
if (name === 'x-text') {
|
|
200
|
+
disposers.push(
|
|
201
|
+
effect(() => {
|
|
202
|
+
const v = read(scope, value)
|
|
203
|
+
el.textContent = v == null ? '' : String(v)
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (name === 'x-show') {
|
|
209
|
+
disposers.push(
|
|
210
|
+
effect(() => {
|
|
211
|
+
el.style.display = read(scope, value) ? '' : 'none'
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
if (name.startsWith('x-bind-')) {
|
|
217
|
+
const target = name.slice('x-bind-'.length)
|
|
218
|
+
disposers.push(
|
|
219
|
+
effect(() => {
|
|
220
|
+
setBound(el, target, read(scope, value))
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (name.startsWith('x-on-')) {
|
|
226
|
+
const eventName = name.slice('x-on-'.length)
|
|
227
|
+
const handler = (e: Event) => callMethod(scope, value, e)
|
|
228
|
+
el.addEventListener(eventName, handler)
|
|
229
|
+
disposers.push(() => el.removeEventListener(eventName, handler))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function observe(): void {
|
|
235
|
+
if (typeof MutationObserver === 'undefined' || typeof document === 'undefined') return
|
|
236
|
+
if (!document.body) return // nothing to observe yet (called pre-<body>); start() re-runs on DOMContentLoaded
|
|
237
|
+
const obs = new MutationObserver((records) => {
|
|
238
|
+
for (const rec of records) {
|
|
239
|
+
for (const node of Array.from(rec.removedNodes)) {
|
|
240
|
+
if (node instanceof HTMLElement) disposeTree(node)
|
|
241
|
+
}
|
|
242
|
+
for (const node of Array.from(rec.addedNodes)) {
|
|
243
|
+
if (node instanceof HTMLElement) scanAndMount(node)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
obs.observe(document.body, { childList: true, subtree: true })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function disposeTree(node: HTMLElement): void {
|
|
251
|
+
if (mounted.has(node)) disposeElement(node)
|
|
252
|
+
for (const el of Array.from(node.querySelectorAll<HTMLElement>('[x-data]'))) {
|
|
253
|
+
disposeElement(el)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function disposeElement(el: HTMLElement): void {
|
|
258
|
+
const m = mounted.get(el)
|
|
259
|
+
if (!m) return
|
|
260
|
+
for (const d of m.disposers.splice(0)) {
|
|
261
|
+
try {
|
|
262
|
+
d()
|
|
263
|
+
} catch {
|
|
264
|
+
// disposer must not break sibling disposal
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
mounted.delete(el)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const BOOL_PROPS = new Set(['disabled', 'checked', 'hidden', 'readonly', 'required', 'selected'])
|
|
271
|
+
|
|
272
|
+
/** Apply a bound value to a DOM attr/property. class → className; boolean props →
|
|
273
|
+
* property (when present) + attribute presence; value → property; else attribute. */
|
|
274
|
+
export function setBound(el: HTMLElement, attr: string, value: unknown): void {
|
|
275
|
+
if (attr === 'class') {
|
|
276
|
+
el.className = value == null ? '' : String(value)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (attr === 'value') {
|
|
280
|
+
;(el as unknown as { value: unknown }).value = value == null ? '' : value
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
if (BOOL_PROPS.has(attr)) {
|
|
284
|
+
const on = Boolean(value)
|
|
285
|
+
if (attr in el) (el as unknown as Record<string, unknown>)[attr] = on
|
|
286
|
+
if (on) el.setAttribute(attr, '')
|
|
287
|
+
else el.removeAttribute(attr)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (value == null || value === false) el.removeAttribute(attr)
|
|
291
|
+
else el.setAttribute(attr, String(value))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- reactive read helpers (used by later tasks) -------------------------------
|
|
295
|
+
|
|
296
|
+
/** Walk a dotted member path against `scope`; at the LEAF, call signals/computeds/
|
|
297
|
+
* functions to obtain the reactive value (this read is what `effect` tracks). */
|
|
298
|
+
export function read(scope: Instance, path: string): unknown {
|
|
299
|
+
let cur: unknown = scope
|
|
300
|
+
for (const part of path.split('.')) {
|
|
301
|
+
if (cur == null) return undefined
|
|
302
|
+
cur = (cur as Record<string, unknown>)[part]
|
|
303
|
+
}
|
|
304
|
+
if (isSignal(cur) || isComputed(cur)) return (cur as () => unknown)()
|
|
305
|
+
if (typeof cur === 'function') return (cur as () => unknown)()
|
|
306
|
+
return cur
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Resolve a dotted path WITHOUT calling the leaf (for x-on handlers). */
|
|
310
|
+
export function resolveRaw(scope: Instance, path: string): unknown {
|
|
311
|
+
let cur: unknown = scope
|
|
312
|
+
for (const part of path.split('.')) {
|
|
313
|
+
if (cur == null) return undefined
|
|
314
|
+
cur = (cur as Record<string, unknown>)[part]
|
|
315
|
+
}
|
|
316
|
+
return cur
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Resolve `path` on `scope` and, if a function, call it with the event. */
|
|
320
|
+
export function callMethod(scope: Instance, path: string, event: Event): void {
|
|
321
|
+
const fn = resolveRaw(scope, path)
|
|
322
|
+
if (typeof fn === 'function') (fn as (e: Event) => unknown)(event)
|
|
323
|
+
else console.warn(`[brust] x-on target "${path}" is not a function`)
|
|
324
|
+
}
|
package/runtime/store/signal.ts
CHANGED
|
@@ -35,38 +35,56 @@ interface Consumer {
|
|
|
35
35
|
running: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// The dependency-tracking state (the "currently running consumer", the batch
|
|
39
|
+
// depth, the pending-notify queue) MUST be shared across chunks, for the SAME
|
|
40
|
+
// reason the brands use Symbol.for: every island / the directive runtime is a
|
|
41
|
+
// SEPARATE Bun.build that inlines its own copy of THIS module. If `activeConsumer`
|
|
42
|
+
// were a module-local `let`, each chunk would have its own — so an `effect` in
|
|
43
|
+
// chunk B reading a `signal` created in chunk A would register against chunk A's
|
|
44
|
+
// (always-null) activeConsumer and never subscribe. Concretely: a native directive
|
|
45
|
+
// button's effect (its own chunk) would never re-run when a React island (another
|
|
46
|
+
// chunk) mutated the shared store, even though the store value changed. Holding the
|
|
47
|
+
// context on `globalThis` under a Symbol.for key makes all chunks share ONE tracker.
|
|
48
|
+
interface ReactiveCtx {
|
|
49
|
+
activeConsumer: Consumer | null
|
|
50
|
+
batchDepth: number
|
|
51
|
+
pendingNotify: Set<Consumer>
|
|
52
|
+
}
|
|
53
|
+
const CTX_KEY = Symbol.for('brust.reactive.ctx')
|
|
54
|
+
const ctxHolder = globalThis as { [CTX_KEY]?: ReactiveCtx }
|
|
55
|
+
if (!ctxHolder[CTX_KEY]) {
|
|
56
|
+
ctxHolder[CTX_KEY] = { activeConsumer: null, batchDepth: 0, pendingNotify: new Set<Consumer>() }
|
|
57
|
+
}
|
|
58
|
+
const ctx: ReactiveCtx = ctxHolder[CTX_KEY]
|
|
41
59
|
|
|
42
60
|
function track(subscribers: Set<Consumer>): void {
|
|
43
|
-
if (activeConsumer) {
|
|
44
|
-
subscribers.add(activeConsumer)
|
|
45
|
-
activeConsumer.deps.add(subscribers)
|
|
61
|
+
if (ctx.activeConsumer) {
|
|
62
|
+
subscribers.add(ctx.activeConsumer)
|
|
63
|
+
ctx.activeConsumer.deps.add(subscribers)
|
|
46
64
|
}
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
function notify(subscribers: Set<Consumer>): void {
|
|
50
68
|
// Snapshot — a consumer re-running mutates the set.
|
|
51
69
|
for (const c of [...subscribers]) {
|
|
52
|
-
if (batchDepth > 0) pendingNotify.add(c)
|
|
70
|
+
if (ctx.batchDepth > 0) ctx.pendingNotify.add(c)
|
|
53
71
|
else c.run()
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
function flush(): void {
|
|
58
|
-
const queued = [...pendingNotify]
|
|
59
|
-
pendingNotify.clear()
|
|
76
|
+
const queued = [...ctx.pendingNotify]
|
|
77
|
+
ctx.pendingNotify.clear()
|
|
60
78
|
for (const c of queued) c.run()
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
export function batch(fn: () => void): void {
|
|
64
|
-
batchDepth++
|
|
82
|
+
ctx.batchDepth++
|
|
65
83
|
try {
|
|
66
84
|
fn()
|
|
67
85
|
} finally {
|
|
68
|
-
batchDepth--
|
|
69
|
-
if (batchDepth === 0) flush()
|
|
86
|
+
ctx.batchDepth--
|
|
87
|
+
if (ctx.batchDepth === 0) flush()
|
|
70
88
|
}
|
|
71
89
|
}
|
|
72
90
|
|
|
@@ -114,13 +132,13 @@ export function computed<T>(fn: () => T): Computed<T> {
|
|
|
114
132
|
track(subscribers)
|
|
115
133
|
if (dirty) {
|
|
116
134
|
clearDeps(self)
|
|
117
|
-
const prev = activeConsumer
|
|
118
|
-
activeConsumer = self
|
|
135
|
+
const prev = ctx.activeConsumer
|
|
136
|
+
ctx.activeConsumer = self
|
|
119
137
|
try {
|
|
120
138
|
cached = fn()
|
|
121
139
|
dirty = false
|
|
122
140
|
} finally {
|
|
123
|
-
activeConsumer = prev
|
|
141
|
+
ctx.activeConsumer = prev
|
|
124
142
|
}
|
|
125
143
|
}
|
|
126
144
|
return cached
|
|
@@ -137,12 +155,12 @@ export function effect(fn: () => void): () => void {
|
|
|
137
155
|
if (self.running) return
|
|
138
156
|
self.running = true
|
|
139
157
|
clearDeps(self)
|
|
140
|
-
const prev = activeConsumer
|
|
141
|
-
activeConsumer = self
|
|
158
|
+
const prev = ctx.activeConsumer
|
|
159
|
+
ctx.activeConsumer = self
|
|
142
160
|
try {
|
|
143
161
|
fn()
|
|
144
162
|
} finally {
|
|
145
|
-
activeConsumer = prev
|
|
163
|
+
ctx.activeConsumer = prev
|
|
146
164
|
self.running = false
|
|
147
165
|
}
|
|
148
166
|
},
|