@kudzujs/core 0.8.14 → 0.8.15

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.
@@ -0,0 +1,884 @@
1
+ import { join } from "node:path"
2
+
3
+ export function createEffectCodegen({ assetPath, inlineJson, relativeModulePath }) {
4
+ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
5
+ const hasCleanup = effects.some(effect => effect.cleanup)
6
+ const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
7
+ const hasOwners = effects.some(effect => effect.owner)
8
+ const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
9
+ const moduleUrls = [...new Set(effects.map(effect => effect.module))]
10
+ const modules = moduleUrls.map(url => {
11
+ const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
12
+ if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
13
+ return module
14
+ })
15
+ const imports = [
16
+ hasCleanup || hasDependencies || hasOwners
17
+ ? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
18
+ : `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}`,
19
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
20
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
21
+ ...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
22
+ ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
23
+ ]
24
+ const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
25
+ if (hasOwners) return printOwnedEffectEntry(imports, effects, entries)
26
+ if (effects.length === 1 && effects[0].dependencies?.length === 1 && !hasDependencyExpressions) return printSingleDependencyEffect(imports, effects[0], hasCleanup)
27
+ const disposal = hasCleanup ? `
28
+ let disposed = false
29
+ const dispose = root => {
30
+ if (root !== document || disposed) return
31
+ disposed = true
32
+ active = false
33
+ pending.clear()
34
+ for (const record of records) invokeCleanup(record)
35
+ }
36
+
37
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
38
+ addEventListener("pagehide", event => {
39
+ if (event.persisted) return
40
+ if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
41
+ else dispose(document)
42
+ })` : ""
43
+ if (hasDependencies) return `${imports.join("\n")}
44
+ const effects = ${inlineJson(effects)}
45
+ const modules = new Map([${entries}])
46
+ const records = effects.map((effect, index) => ({ effect, index, values: undefined, cleanup: undefined, token: undefined }))
47
+ const dependencies = new Map()
48
+ const pending = new Set()
49
+ let scheduled = false
50
+ let flushing = false
51
+ let active = true
52
+ for (const record of records) {
53
+ for (const id of record.effect.dependencies ?? []) {
54
+ const subscribers = dependencies.get(id) ?? new Set()
55
+ subscribers.add(record)
56
+ dependencies.set(id, subscribers)
57
+ }
58
+ }
59
+ __kRuntime.registerCommitter(id => {
60
+ if (!active) return
61
+ for (const record of dependencies.get(id) ?? []) pending.add(record)
62
+ schedule()
63
+ })
64
+ for (const record of records) {
65
+ try {
66
+ record.values = readDependencies(record)
67
+ invoke(record)
68
+ } catch (error) {
69
+ console.error(error)
70
+ }
71
+ }
72
+ function schedule() {
73
+ if (!pending.size || scheduled || flushing) return
74
+ scheduled = true
75
+ queueMicrotask(flush)
76
+ }
77
+ async function flush() {
78
+ scheduled = false
79
+ if (!active) return pending.clear()
80
+ flushing = true
81
+ try {
82
+ const selected = [...pending].sort((left, right) => left.index - right.index)
83
+ pending.clear()
84
+ const changed = []
85
+ for (const record of selected) {
86
+ try {
87
+ const values = readDependencies(record)
88
+ if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) {
89
+ record.values = values
90
+ changed.push(record)
91
+ }
92
+ } catch (error) {
93
+ console.error(error)
94
+ }
95
+ }
96
+ for (const record of changed) await invokeCleanup(record)
97
+ if (active) for (const record of changed) invoke(record)
98
+ } finally {
99
+ flushing = false
100
+ if (active) schedule()
101
+ }
102
+ }
103
+ function readDependencies(record) {
104
+ ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
105
+ return (record.effect.dependencies ?? []).map(id => {
106
+ const value = browserState.get(id)
107
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
108
+ return value
109
+ })
110
+ }
111
+ function invoke(record) {
112
+ try {
113
+ const effect = record.effect
114
+ const token = { active: true }
115
+ record.token = token
116
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope, () => active && token.active && record.token === token))
117
+ if (effect.cleanup && typeof result === "function") record.cleanup = result
118
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
119
+ } catch (error) {
120
+ console.error(error)
121
+ }
122
+ }
123
+ async function invokeCleanup(record) {
124
+ if (record.token) record.token.active = false
125
+ record.token = undefined
126
+ const cleanup = record.cleanup
127
+ record.cleanup = undefined
128
+ if (!cleanup) return
129
+ try {
130
+ await cleanup()
131
+ } catch (error) {
132
+ console.error(error)
133
+ }
134
+ }${disposal}`
135
+ if (!hasCleanup) return `${imports.join("\n")}
136
+ const effects = ${inlineJson(effects)}
137
+ const modules = new Map([${entries}])
138
+ for (const effect of effects) {
139
+ try {
140
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
141
+ if (result && typeof result.then === "function") result.catch(error => console.error(error))
142
+ } catch (error) {
143
+ console.error(error)
144
+ }
145
+ }`
146
+ return `${imports.join("\n")}
147
+ const effects = ${inlineJson(effects)}
148
+ const modules = new Map([${entries}])
149
+ const cleanups = []
150
+ for (const effect of effects) {
151
+ try {
152
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
153
+ if (effect.cleanup && typeof result === "function") cleanups.push(result)
154
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
155
+ } catch (error) {
156
+ console.error(error)
157
+ }
158
+ }
159
+ let cleaned = false
160
+ const dispose = root => {
161
+ if (root !== document || cleaned) return
162
+ cleaned = true
163
+ for (const cleanup of cleanups) {
164
+ try {
165
+ const result = cleanup()
166
+ if (result && typeof result.then === "function") result.catch(error => console.error(error))
167
+ } catch (error) {
168
+ console.error(error)
169
+ }
170
+ }
171
+ cleanups.length = 0
172
+ }
173
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
174
+ addEventListener("pagehide", event => {
175
+ if (event.persisted) return
176
+ if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
177
+ else dispose(document)
178
+ })`
179
+ }
180
+
181
+ function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
182
+ const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
183
+ const moduleUrls = [...new Set(effects.map(effect => effect.module))]
184
+ const modules = moduleUrls.map(url => {
185
+ const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
186
+ if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
187
+ return module
188
+ })
189
+ const imports = [
190
+ `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
191
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
192
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
193
+ ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
194
+ ]
195
+ const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
196
+ return `${imports.join("\n")}
197
+ const effects = ${inlineJson(effects)}
198
+ const modules = new Map([${entries}])
199
+ export const mountLayoutEffects = () => mount("layout")
200
+ export const mountRouteEffects = () => mount("route")
201
+ function mount(lifetime) {
202
+ let active = true
203
+ let flushing
204
+ const records = effects.filter(effect => effect.lifetime === lifetime).map((effect, index) => ({ effect, index, values: undefined, cleanup: undefined, token: undefined }))
205
+ const dependencies = new Map()
206
+ const pending = new Set()
207
+ let scheduled = false
208
+ for (const record of records) for (const id of record.effect.dependencies ?? []) {
209
+ const subscribers = dependencies.get(id) ?? new Set()
210
+ subscribers.add(record)
211
+ dependencies.set(id, subscribers)
212
+ }
213
+ const unsubscribe = dependencies.size ? __kRuntime.registerCommitter(id => {
214
+ if (!active) return
215
+ for (const record of dependencies.get(id) ?? []) pending.add(record)
216
+ if (pending.size && !scheduled && !flushing) {
217
+ scheduled = true
218
+ queueMicrotask(flush)
219
+ }
220
+ }) : undefined
221
+ for (const record of records) {
222
+ try {
223
+ record.values = readDependencies(record)
224
+ invoke(record)
225
+ } catch (error) {
226
+ console.error(error)
227
+ }
228
+ }
229
+ async function flush() {
230
+ scheduled = false
231
+ if (!active) return pending.clear()
232
+ const operation = (async () => {
233
+ const selected = [...pending].sort((left, right) => left.index - right.index)
234
+ pending.clear()
235
+ const changed = []
236
+ for (const record of selected) {
237
+ try {
238
+ const values = readDependencies(record)
239
+ if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) {
240
+ record.values = values
241
+ changed.push(record)
242
+ }
243
+ } catch (error) {
244
+ console.error(error)
245
+ }
246
+ }
247
+ for (const record of changed) await cleanup(record)
248
+ if (active) for (const record of changed) invoke(record)
249
+ })()
250
+ flushing = operation
251
+ try { await operation } finally {
252
+ if (flushing === operation) flushing = undefined
253
+ if (active && pending.size && !scheduled) {
254
+ scheduled = true
255
+ queueMicrotask(flush)
256
+ }
257
+ }
258
+ }
259
+ function readDependencies(record) {
260
+ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
261
+ return (record.effect.dependencies ?? []).map(id => {
262
+ const value = __kRuntime.browserState.get(id)
263
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
264
+ return value
265
+ })
266
+ }
267
+ function invoke(record) {
268
+ const token = { active: true }
269
+ record.token = token
270
+ try {
271
+ const effect = record.effect
272
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(__kRuntime.browserState, effect.states, __kRuntime.commitDom, effect.scope, () => active && token.active && record.token === token))
273
+ if (effect.cleanup && typeof result === "function") record.cleanup = result
274
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
275
+ } catch (error) {
276
+ console.error(error)
277
+ }
278
+ }
279
+ async function cleanup(record) {
280
+ if (record.token) record.token.active = false
281
+ record.token = undefined
282
+ const current = record.cleanup
283
+ record.cleanup = undefined
284
+ if (!current) return
285
+ try { await current() } catch (error) { console.error(error) }
286
+ }
287
+ let disposal
288
+ return async function dispose() {
289
+ if (disposal) return disposal
290
+ disposal = (async () => {
291
+ active = false
292
+ unsubscribe?.()
293
+ pending.clear()
294
+ for (const record of records) if (record.token) record.token.active = false
295
+ if (flushing) await flushing
296
+ for (const record of records) await cleanup(record)
297
+ })()
298
+ return disposal
299
+ }
300
+ }`
301
+ }
302
+
303
+ function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
304
+ const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
305
+ const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
306
+ const moduleUrls = [...new Set(effects.map(effect => effect.module))]
307
+ const modules = moduleUrls.map(url => {
308
+ const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
309
+ if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
310
+ return module
311
+ })
312
+ const imports = [
313
+ `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
314
+ `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
315
+ ...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
316
+ ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
317
+ ]
318
+ const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
319
+ return `${imports.join("\n")}
320
+ const effects = ${inlineJson(effects)}
321
+ const modules = new Map([${entries}])
322
+ export const mountLayoutEffects = () => mount("layout")
323
+ export const mountRouteEffects = () => mount("route")
324
+ function mount(lifetime) {
325
+ let active = true
326
+ let flushing
327
+ let order = 0
328
+ const selectedEffects = effects.map((effect, index) => ({ effect, index })).filter(entry => entry.effect.lifetime === lifetime)
329
+ const records = new Set()
330
+ const owners = new Map()
331
+ const listTemplates = new Map()
332
+ const registrations = new WeakMap()
333
+ const dependencies = new Map()
334
+ const pending = new Set()
335
+ const startedCleanups = new Set()
336
+ let scheduled = false
337
+ for (const template of selectedEffects) {
338
+ if (template.effect.list) listTemplates.set(template.effect.owner, template)
339
+ else {
340
+ const record = createRecord(template, !template.effect.owner)
341
+ if (template.effect.owner) owners.set(template.effect.owner, record)
342
+ }
343
+ }
344
+ const unsubscribeCommitter = selectedEffects.some(({ effect }) => effect.dependencies?.length) ? __kRuntime.registerCommitter(id => {
345
+ if (!active) return
346
+ for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
347
+ schedule()
348
+ }) : undefined
349
+ const unsubscribeMount = __kRuntime.registerMountHook(mountOwned)
350
+ const unsubscribeUnmount = __kRuntime.registerUnmountHook(unmountOwned)
351
+ ${hasItemDependencies ? `const unsubscribeItems = [...new Set(selectedEffects.filter(({ effect }) => effect.itemDependencies?.length).map(({ effect }) => effect.listState))].map(listState => __kRuntime.registerListItemHook(listState, root => {
352
+ if (!active) return
353
+ for (const record of registrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
354
+ schedule()
355
+ }))
356
+ ` : ""}for (const record of records) if (record.mounted) start(record)
357
+ mountOwned(document)
358
+ function createRecord(template, mounted = true) {
359
+ const record = { ...template, order: order++, mounted, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
360
+ records.add(record)
361
+ registerDependencies(record)
362
+ return record
363
+ }
364
+ function registerDependencies(record) {
365
+ for (const id of record.effect.dependencies ?? []) {
366
+ const subscribers = dependencies.get(id) ?? new Set()
367
+ subscribers.add(record)
368
+ dependencies.set(id, subscribers)
369
+ }
370
+ }
371
+ function unregisterDependencies(record) {
372
+ for (const id of record.effect.dependencies ?? []) {
373
+ const subscribers = dependencies.get(id)
374
+ subscribers?.delete(record)
375
+ if (!subscribers?.size) dependencies.delete(id)
376
+ }
377
+ }
378
+ function mountOwned(root) {
379
+ if (!active) return
380
+ for (const marker of matching(root)) {
381
+ if (!marker.isConnected) continue
382
+ if (marker.dataset.kEffects) {
383
+ if (registrations.has(marker)) continue
384
+ const rowRecords = JSON.parse(marker.dataset.kEffects).flatMap(owner => {
385
+ const template = listTemplates.get(owner)
386
+ if (!template) return []
387
+ const record = createRecord(template)
388
+ record.marker = marker
389
+ start(record)
390
+ return [record]
391
+ })
392
+ if (rowRecords.length) registrations.set(marker, rowRecords)
393
+ continue
394
+ }
395
+ const record = owners.get(marker.dataset.kEffect)
396
+ if (record && !record.mounted) mountRecord(record, marker)
397
+ }
398
+ }
399
+ function unmountOwned(root) {
400
+ if (!active) return
401
+ for (const marker of matching(root)) {
402
+ const rowRecords = registrations.get(marker)
403
+ if (rowRecords) {
404
+ for (const record of rowRecords) unmountRecord(record, true)
405
+ registrations.delete(marker)
406
+ continue
407
+ }
408
+ const record = owners.get(marker.dataset.kEffect)
409
+ if (record?.marker === marker) unmountRecord(record)
410
+ }
411
+ }
412
+ function matching(root) {
413
+ const selector = "template[data-k-effect],[data-k-effects]"
414
+ return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
415
+ }
416
+ function mountRecord(record, marker) {
417
+ record.mounted = true
418
+ record.marker = marker
419
+ const version = ++record.version
420
+ const begin = () => {
421
+ if (active && record.mounted && record.version === version && marker.isConnected) start(record)
422
+ }
423
+ if (record.disposal) record.disposal.then(begin)
424
+ else begin()
425
+ }
426
+ function unmountRecord(record, dynamic = false) {
427
+ if (!record.mounted) return
428
+ record.mounted = false
429
+ record.marker = undefined
430
+ record.version++
431
+ pending.delete(record)
432
+ if (dynamic) {
433
+ unregisterDependencies(record)
434
+ records.delete(record)
435
+ }
436
+ void cleanup(record)
437
+ }
438
+ function start(record) {
439
+ try {
440
+ record.values = readDependencies(record)
441
+ invoke(record)
442
+ } catch (error) {
443
+ console.error(error)
444
+ }
445
+ }
446
+ function schedule() {
447
+ if (!pending.size || scheduled || flushing) return
448
+ scheduled = true
449
+ queueMicrotask(flush)
450
+ }
451
+ async function flush() {
452
+ scheduled = false
453
+ if (!active) return pending.clear()
454
+ const operation = (async () => {
455
+ const changed = []
456
+ const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index || left.order - right.order)
457
+ pending.clear()
458
+ for (const record of selected) {
459
+ try {
460
+ const values = readDependencies(record)
461
+ if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
462
+ } catch (error) {
463
+ console.error(error)
464
+ }
465
+ }
466
+ for (const [record] of changed) await cleanup(record)
467
+ if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
468
+ try {
469
+ record.values = readDependencies(record)
470
+ invoke(record)
471
+ } catch (error) {
472
+ record.values = undefined
473
+ console.error(error)
474
+ }
475
+ }
476
+ })()
477
+ flushing = operation
478
+ try { await operation } finally {
479
+ if (flushing === operation) flushing = undefined
480
+ if (active) schedule()
481
+ }
482
+ }
483
+ function readDependencies(record) {
484
+ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
485
+ const values = (record.effect.dependencies ?? []).map(id => {
486
+ const value = __kRuntime.browserState.get(id)
487
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
488
+ return value
489
+ })
490
+ ${hasItemDependencies ? `if (record.effect.itemDependencies) {
491
+ const item = JSON.parse(record.marker.dataset.kEffectItem)
492
+ for (const field of record.effect.itemDependencies) {
493
+ const value = item[field]
494
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
495
+ values.push(value)
496
+ }
497
+ }` : ""}
498
+ return values
499
+ }
500
+ function invoke(record) {
501
+ const token = { active: true }
502
+ record.token = token
503
+ try {
504
+ const effect = record.effect
505
+ const scope = effect.list
506
+ ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
507
+ : effect.scope
508
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(__kRuntime.browserState, effect.states, __kRuntime.commitDom, scope, () => active && token.active && record.token === token))
509
+ if (effect.cleanup && typeof result === "function") record.cleanup = result
510
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
511
+ } catch (error) {
512
+ console.error(error)
513
+ }
514
+ }
515
+ function cleanup(record) {
516
+ if (record.token) record.token.active = false
517
+ record.token = undefined
518
+ if (record.disposal) return record.disposal
519
+ const current = record.cleanup
520
+ record.cleanup = undefined
521
+ if (!current) return Promise.resolve()
522
+ const disposal = (async () => {
523
+ try { await current() } catch (error) { console.error(error) }
524
+ })()
525
+ record.disposal = disposal
526
+ startedCleanups.add(disposal)
527
+ disposal.finally(() => {
528
+ startedCleanups.delete(disposal)
529
+ if (record.disposal === disposal) record.disposal = undefined
530
+ })
531
+ return disposal
532
+ }
533
+ let disposal
534
+ return async function dispose() {
535
+ if (disposal) return disposal
536
+ disposal = (async () => {
537
+ active = false
538
+ unsubscribeCommitter?.()
539
+ ${hasItemDependencies ? "for (const unsubscribe of unsubscribeItems) unsubscribe()\n " : ""}unsubscribeMount()
540
+ unsubscribeUnmount()
541
+ pending.clear()
542
+ for (const record of records) if (record.token) record.token.active = false
543
+ if (flushing) await flushing
544
+ const mounted = [...records].filter(record => record.mounted).sort((left, right) => left.index - right.index || left.order - right.order)
545
+ for (const record of mounted) {
546
+ record.mounted = false
547
+ await cleanup(record)
548
+ }
549
+ await Promise.all([...startedCleanups])
550
+ records.clear()
551
+ })()
552
+ return disposal
553
+ }
554
+ }`
555
+ }
556
+
557
+ function printDerivedDependencyRead(state) {
558
+ return ` if (record.effect.dependencyExpressions) return record.effect.dependencyExpressions.map(expression => {
559
+ const value = __kEvaluateDependency(expression, undefined, undefined, name => ${state}.get(record.effect.dependencyStates[name]))
560
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() derived dependency must remain a JSON-safe primitive")
561
+ return value
562
+ })`
563
+ }
564
+
565
+ function printOwnedEffectEntry(imports, effects, entries) {
566
+ const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
567
+ const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
568
+ const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
569
+ const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.dependencyStates, effect.states, effect.scope]).includes("$k"))
570
+ return `${imports.join("\n")}
571
+ const effects = ${inlineJson(effects)}
572
+ const modules = new Map([${entries}])
573
+ ${hasItemDependencies ? "let order = 0\n" : ""}const records = effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)
574
+ const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
575
+ const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
576
+ const listRegistrations = new WeakMap()
577
+ const mountedRecords = new Set(records.filter(record => record.mounted))
578
+ const dependencies = new Map()
579
+ const pending = new Set()
580
+ let scheduled = false
581
+ let flushing = false
582
+ let active = true
583
+ for (const record of records) registerDependencies(record)
584
+ function createRecord(effect, index${hasRowState ? ", marker" : ""}) {
585
+ return { effect: ${hasRowState ? "marker ? specializeRowEffect(effect, marker) : effect" : "effect"}, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
586
+ }
587
+ ${hasRowState ? `function specializeRowEffect(effect, marker) {
588
+ const path = marker.dataset.kRowPath
589
+ const id = value => typeof value === "string" ? value.replace("$k", path) : value
590
+ const capture = value => value?.type === "state" || value?.type === "setter" || value?.type === "ref" ? { ...value, id: id(value.id) } : value?.type === "array" ? { ...value, value: value.value.map(capture) } : value?.type === "object" ? { ...value, value: value.value.map(([key, entry]) => [key, capture(entry)]) } : value
591
+ return { ...effect, dependencies: effect.dependencies?.map(id), dependencyStates: effect.dependencyStates && Object.fromEntries(Object.entries(effect.dependencyStates).map(([name, value]) => [name, id(value)])), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
592
+ }
593
+ ` : ""}
594
+ function registerDependencies(record) {
595
+ for (const id of record.effect.dependencies ?? []) {
596
+ const subscribers = dependencies.get(id) ?? new Set()
597
+ subscribers.add(record)
598
+ dependencies.set(id, subscribers)
599
+ }
600
+ }
601
+ function unregisterDependencies(record) {
602
+ for (const id of record.effect.dependencies ?? []) {
603
+ const subscribers = dependencies.get(id)
604
+ subscribers?.delete(record)
605
+ if (!subscribers?.size) dependencies.delete(id)
606
+ }
607
+ }
608
+ ${hasItemDependencies ? `if (${hasOrdinaryDependencies}) ` : ""}__kRuntime.registerCommitter(id => {
609
+ if (!active) return
610
+ for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
611
+ schedule()
612
+ })
613
+ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect => effect.itemDependencies?.length).map(effect => effect.listState))) __kRuntime.registerListItemHook(listState, root => {
614
+ if (!active) return
615
+ for (const record of listRegistrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
616
+ schedule()
617
+ })
618
+ ` : ""}__kRuntime.registerMountHook(root => {
619
+ if (!active) return
620
+ for (const marker of matching(root)) {
621
+ if (marker.dataset.kEffects) {
622
+ if (listRegistrations.has(marker)) continue
623
+ const rowRecords = JSON.parse(marker.dataset.kEffects).map(owner => {
624
+ const template = listTemplates.get(owner)
625
+ if (!template) throw new Error("Keyed row effect template was not emitted")
626
+ const record = createRecord(template.effect, template.index${hasRowState ? ", marker" : ""})
627
+ registerDependencies(record)
628
+ mount(record, marker)
629
+ return record
630
+ })
631
+ listRegistrations.set(marker, rowRecords)
632
+ continue
633
+ }
634
+ const record = owners.get(marker.dataset.kEffect)
635
+ if (!record?.mounted) mount(record, marker)
636
+ }
637
+ })
638
+ __kRuntime.registerUnmountHook(root => {
639
+ if (root === document) {
640
+ if (!active) return
641
+ active = false
642
+ pending.clear()
643
+ for (const record of [...mountedRecords]) unmount(record, record.effect.list)
644
+ return
645
+ }
646
+ for (const marker of matching(root)) {
647
+ const rowRecords = listRegistrations.get(marker)
648
+ if (rowRecords) {
649
+ for (const record of rowRecords) unmount(record, true)
650
+ listRegistrations.delete(marker)
651
+ continue
652
+ }
653
+ const record = owners.get(marker.dataset.kEffect)
654
+ if (record?.marker === marker) unmount(record)
655
+ }
656
+ })
657
+ for (const record of records) if (record.mounted) start(record)
658
+ __kRuntime.mountDom(document)
659
+ addEventListener("pagehide", event => {
660
+ if (!event.persisted) __kRuntime.unmountDom(document)
661
+ })
662
+ function matching(root) {
663
+ const selector = "template[data-k-effect],[data-k-effects]"
664
+ return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
665
+ }
666
+ function mount(record, marker) {
667
+ record.mounted = true
668
+ record.marker = marker
669
+ mountedRecords.add(record)
670
+ const version = ++record.version
671
+ const begin = () => {
672
+ if (!active || !record.mounted || record.version !== version || !marker.isConnected) return
673
+ start(record)
674
+ }
675
+ if (record.disposal) record.disposal.then(begin)
676
+ else begin()
677
+ }
678
+ function unmount(record, dynamic = false) {
679
+ if (!record.mounted) return
680
+ record.mounted = false
681
+ record.marker = undefined
682
+ mountedRecords.delete(record)
683
+ if (dynamic) unregisterDependencies(record)
684
+ record.version++
685
+ pending.delete(record)
686
+ invokeCleanup(record)
687
+ }
688
+ function start(record) {
689
+ try {
690
+ record.values = readDependencies(record)
691
+ invoke(record)
692
+ } catch (error) {
693
+ console.error(error)
694
+ }
695
+ }
696
+ function schedule() {
697
+ if (!pending.size || scheduled || flushing) return
698
+ scheduled = true
699
+ queueMicrotask(flush)
700
+ }
701
+ async function flush() {
702
+ scheduled = false
703
+ if (!active) return pending.clear()
704
+ flushing = true
705
+ try {
706
+ const selected = [...pending].filter(record => record.mounted).sort((left, right) => left.index - right.index${hasItemDependencies ? " || left.order - right.order" : ""})
707
+ pending.clear()
708
+ const changed = []
709
+ for (const record of selected) {
710
+ try {
711
+ const values = readDependencies(record)
712
+ if (!record.values || values.some((value, index) => !Object.is(value, record.values[index]))) changed.push([record, record.version])
713
+ } catch (error) {
714
+ console.error(error)
715
+ }
716
+ }
717
+ for (const [record] of changed) await invokeCleanup(record)
718
+ if (active) for (const [record, version] of changed) if (record.mounted && record.version === version) {
719
+ try {
720
+ record.values = readDependencies(record)
721
+ invoke(record)
722
+ } catch (error) {
723
+ record.values = undefined
724
+ console.error(error)
725
+ }
726
+ }
727
+ } finally {
728
+ flushing = false
729
+ if (active) schedule()
730
+ }
731
+ }
732
+ function readDependencies(record) {
733
+ ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
734
+ const values = (record.effect.dependencies ?? []).map(id => {
735
+ const value = browserState.get(id)
736
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
737
+ return value
738
+ })
739
+ ${hasItemDependencies ? `if (record.effect.itemDependencies) {
740
+ const item = JSON.parse(record.marker.dataset.kEffectItem)
741
+ for (const field of record.effect.itemDependencies) {
742
+ const value = item[field]
743
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error(\`useEffect() keyed item dependency "\${field}" must remain a JSON-safe primitive\`)
744
+ values.push(value)
745
+ }
746
+ }` : ""}
747
+ return values
748
+ }
749
+ function invoke(record) {
750
+ const token = { active: true }
751
+ record.token = token
752
+ try {
753
+ const effect = record.effect
754
+ const scope = effect.list
755
+ ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
756
+ : effect.scope
757
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, scope, () => active && token.active && record.token === token))
758
+ if (effect.cleanup && typeof result === "function") record.cleanup = result
759
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
760
+ } catch (error) {
761
+ console.error(error)
762
+ }
763
+ }
764
+ function invokeCleanup(record) {
765
+ if (record.token) record.token.active = false
766
+ record.token = undefined
767
+ if (record.disposal) return record.disposal
768
+ const cleanup = record.cleanup
769
+ record.cleanup = undefined
770
+ if (!cleanup) return Promise.resolve()
771
+ const disposal = (async () => {
772
+ try {
773
+ await cleanup()
774
+ } catch (error) {
775
+ console.error(error)
776
+ }
777
+ })()
778
+ record.disposal = disposal
779
+ disposal.finally(() => {
780
+ if (record.disposal === disposal) record.disposal = undefined
781
+ })
782
+ return disposal
783
+ }`
784
+ }
785
+
786
+ function printSingleDependencyEffect(imports, effect, hasCleanup) {
787
+ const disposal = hasCleanup ? `
788
+ const dispose = root => {
789
+ if (root !== document || !active) return
790
+ active = false
791
+ pending = false
792
+ invokeCleanup()
793
+ }
794
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
795
+ addEventListener("pagehide", event => {
796
+ if (event.persisted) return
797
+ if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
798
+ else dispose(document)
799
+ })` : ""
800
+ return `${imports.join("\n")}
801
+ const effect = ${inlineJson(effect)}
802
+ const dependency = effect.dependencies[0]
803
+ let value
804
+ let cleanup
805
+ let token
806
+ let active = true
807
+ let pending = false
808
+ let scheduled = false
809
+ let running = false
810
+ __kRuntime.registerCommitter(id => {
811
+ if (active && id === dependency) {
812
+ pending = true
813
+ schedule()
814
+ }
815
+ })
816
+ try {
817
+ value = readDependency()
818
+ invoke()
819
+ } catch (error) {
820
+ console.error(error)
821
+ }
822
+ function schedule() {
823
+ if (!pending || scheduled || running) return
824
+ scheduled = true
825
+ queueMicrotask(flush)
826
+ }
827
+ async function flush() {
828
+ scheduled = false
829
+ if (!active) return
830
+ let next
831
+ try {
832
+ next = readDependency()
833
+ } catch (error) {
834
+ console.error(error)
835
+ return
836
+ }
837
+ pending = false
838
+ if (Object.is(next, value)) return
839
+ value = next
840
+ running = true
841
+ try {
842
+ await invokeCleanup()
843
+ if (active) invoke()
844
+ } finally {
845
+ running = false
846
+ if (active) schedule()
847
+ }
848
+ }
849
+ function readDependency() {
850
+ const next = browserState.get(dependency)
851
+ if (next !== null && typeof next !== "string" && typeof next !== "boolean" && !(typeof next === "number" && Number.isFinite(next) && !Object.is(next, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
852
+ return next
853
+ }
854
+ function invoke() {
855
+ try {
856
+ const current = { active: true }
857
+ token = current
858
+ const result = __kEffectModule0[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope, () => active && current.active && token === current))
859
+ if (effect.cleanup && typeof result === "function") cleanup = result
860
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
861
+ } catch (error) {
862
+ console.error(error)
863
+ }
864
+ }
865
+ async function invokeCleanup() {
866
+ if (token) token.active = false
867
+ token = undefined
868
+ const current = cleanup
869
+ cleanup = undefined
870
+ if (!current) return
871
+ try {
872
+ await current()
873
+ } catch (error) {
874
+ console.error(error)
875
+ }
876
+ }${disposal}`
877
+ }
878
+
879
+ return (effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName, navigable) => navigable
880
+ ? effects.some(effect => effect.owner)
881
+ ? printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
882
+ : printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
883
+ : printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName)
884
+ }