@vidact/runtime 0.2.0-beta.0 → 0.2.0-beta.5
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/CHANGELOG.md +52 -0
- package/README.md +1 -1
- package/dist/actions.d.ts +32 -30
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +251 -266
- package/dist/actions.js.map +1 -1
- package/dist/async.d.ts +20 -17
- package/dist/async.d.ts.map +1 -1
- package/dist/async.js +18 -17
- package/dist/async.js.map +1 -1
- package/dist/compiled/async-resource.d.ts +9 -6
- package/dist/compiled/async-resource.d.ts.map +1 -1
- package/dist/compiled/async-resource.js +69 -77
- package/dist/compiled/async-resource.js.map +1 -1
- package/dist/compiled/core.d.ts +136 -147
- package/dist/compiled/core.d.ts.map +1 -1
- package/dist/compiled/core.js +2672 -2985
- package/dist/compiled/core.js.map +1 -1
- package/dist/compiled/types.d.ts +44 -56
- package/dist/compiled/types.d.ts.map +1 -1
- package/dist/compiled/types.js +10 -6
- package/dist/compiled/types.js.map +1 -1
- package/dist/compiled/validation.d.ts +4 -1
- package/dist/compiled/validation.d.ts.map +1 -1
- package/dist/compiled/validation.js +17 -20
- package/dist/compiled/validation.js.map +1 -1
- package/dist/component-spread.d.ts +5 -3
- package/dist/component-spread.d.ts.map +1 -1
- package/dist/component-spread.js +49 -67
- package/dist/component-spread.js.map +1 -1
- package/dist/concurrent.d.ts +20 -18
- package/dist/concurrent.d.ts.map +1 -1
- package/dist/concurrent.js +191 -210
- package/dist/concurrent.js.map +1 -1
- package/dist/direct-dom.d.ts +17 -12
- package/dist/direct-dom.d.ts.map +1 -1
- package/dist/direct-dom.js +187 -221
- package/dist/direct-dom.js.map +1 -1
- package/dist/dom/attributes.d.ts +9 -6
- package/dist/dom/attributes.d.ts.map +1 -1
- package/dist/dom/attributes.js +65 -47
- package/dist/dom/attributes.js.map +1 -1
- package/dist/dom/events.d.ts +5 -2
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +53 -65
- package/dist/dom/events.js.map +1 -1
- package/dist/dom/forms.d.ts +9 -6
- package/dist/dom/forms.d.ts.map +1 -1
- package/dist/dom/forms.js +152 -198
- package/dist/dom/forms.js.map +1 -1
- package/dist/dom/intrinsic.d.ts +17 -15
- package/dist/dom/intrinsic.d.ts.map +1 -1
- package/dist/dom/intrinsic.js +48 -50
- package/dist/dom/intrinsic.js.map +1 -1
- package/dist/dom/namespace.d.ts +4 -1
- package/dist/dom/namespace.d.ts.map +1 -1
- package/dist/dom/namespace.js +25 -28
- package/dist/dom/namespace.js.map +1 -1
- package/dist/dom/namespaces.d.ts +11 -8
- package/dist/dom/namespaces.d.ts.map +1 -1
- package/dist/dom/namespaces.js +14 -12
- package/dist/dom/namespaces.js.map +1 -1
- package/dist/dom/properties.d.ts +18 -16
- package/dist/dom/properties.d.ts.map +1 -1
- package/dist/dom/properties.js +273 -296
- package/dist/dom/properties.js.map +1 -1
- package/dist/dom/style-properties.d.ts +4 -1
- package/dist/dom/style-properties.d.ts.map +1 -1
- package/dist/dom/style-properties.js +49 -45
- package/dist/dom/style-properties.js.map +1 -1
- package/dist/dom/styles.d.ts +5 -2
- package/dist/dom/styles.d.ts.map +1 -1
- package/dist/dom/styles.js +47 -67
- package/dist/dom/styles.js.map +1 -1
- package/dist/dom-forms.d.ts +4 -1
- package/dist/dom-forms.d.ts.map +1 -1
- package/dist/dom-forms.js +16 -13
- package/dist/dom-forms.js.map +1 -1
- package/dist/dom-styles.d.ts +4 -1
- package/dist/dom-styles.d.ts.map +1 -1
- package/dist/dom-styles.js +10 -7
- package/dist/dom-styles.js.map +1 -1
- package/dist/framework-hydrate.d.ts +20 -17
- package/dist/framework-hydrate.d.ts.map +1 -1
- package/dist/framework-hydrate.js +128 -146
- package/dist/framework-hydrate.js.map +1 -1
- package/dist/framework-protocol.d.ts +27 -25
- package/dist/framework-protocol.d.ts.map +1 -1
- package/dist/framework-protocol.js +173 -211
- package/dist/framework-protocol.js.map +1 -1
- package/dist/framework.d.ts +34 -32
- package/dist/framework.d.ts.map +1 -1
- package/dist/framework.js +74 -93
- package/dist/framework.js.map +1 -1
- package/dist/hydrate.d.ts +11 -8
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +13 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/hydration-bridge.d.ts +70 -58
- package/dist/hydration-bridge.d.ts.map +1 -1
- package/dist/hydration-bridge.js +77 -66
- package/dist/hydration-bridge.js.map +1 -1
- package/dist/hydration.d.ts +54 -28
- package/dist/hydration.d.ts.map +1 -1
- package/dist/hydration.js +450 -463
- package/dist/hydration.js.map +1 -1
- package/dist/index.d.ts +13 -13
- package/dist/index.js +11 -13
- package/dist/indexed-list.d.ts +8 -5
- package/dist/indexed-list.d.ts.map +1 -1
- package/dist/indexed-list.js +14 -7
- package/dist/indexed-list.js.map +1 -1
- package/dist/jsx-runtime.d.ts +10 -8
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +28 -23
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/keyed-list.d.ts +13 -14
- package/dist/keyed-list.d.ts.map +1 -1
- package/dist/keyed-list.js +132 -160
- package/dist/keyed-list.js.map +1 -1
- package/dist/metadata.d.ts +6 -3
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +131 -164
- package/dist/metadata.js.map +1 -1
- package/dist/nested-prop.d.ts +4 -2
- package/dist/nested-prop.d.ts.map +1 -1
- package/dist/nested-prop.js +17 -16
- package/dist/nested-prop.js.map +1 -1
- package/dist/profiling.d.ts +11 -8
- package/dist/profiling.d.ts.map +1 -1
- package/dist/profiling.js +17 -17
- package/dist/profiling.js.map +1 -1
- package/dist/protocol.d.ts +5 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +8 -5
- package/dist/protocol.js.map +1 -1
- package/dist/raw-html.d.ts +5 -2
- package/dist/raw-html.d.ts.map +1 -1
- package/dist/raw-html.js +86 -146
- package/dist/raw-html.js.map +1 -1
- package/dist/ref.d.ts +8 -3
- package/dist/ref.d.ts.map +1 -1
- package/dist/ref.js +10 -2
- package/dist/ref.js.map +1 -1
- package/dist/renderable-protocol.d.ts +26 -8
- package/dist/renderable-protocol.d.ts.map +1 -1
- package/dist/renderable-protocol.js +30 -9
- package/dist/renderable-protocol.js.map +1 -1
- package/dist/renderable.d.ts +27 -22
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +141 -133
- package/dist/renderable.js.map +1 -1
- package/dist/retained-ui.d.ts +9 -6
- package/dist/retained-ui.d.ts.map +1 -1
- package/dist/retained-ui.js +12 -10
- package/dist/retained-ui.js.map +1 -1
- package/dist/root.d.ts +18 -15
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +73 -85
- package/dist/root.js.map +1 -1
- package/dist/scheduler.d.ts +10 -8
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +97 -105
- package/dist/scheduler.js.map +1 -1
- package/dist/server-framework.d.ts +45 -42
- package/dist/server-framework.d.ts.map +1 -1
- package/dist/server-framework.js +209 -217
- package/dist/server-framework.js.map +1 -1
- package/dist/server.d.ts +130 -128
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +603 -710
- package/dist/server.js.map +1 -1
- package/dist/shared/promise.d.ts +6 -3
- package/dist/shared/promise.d.ts.map +1 -1
- package/dist/shared/promise.js +21 -19
- package/dist/shared/promise.js.map +1 -1
- package/dist/source-mask.d.ts +9 -6
- package/dist/source-mask.d.ts.map +1 -1
- package/dist/source-mask.js +45 -63
- package/dist/source-mask.js.map +1 -1
- package/dist/spread.d.ts +5 -2
- package/dist/spread.d.ts.map +1 -1
- package/dist/spread.js +79 -99
- package/dist/spread.js.map +1 -1
- package/dist/state-slot.d.ts +18 -15
- package/dist/state-slot.d.ts.map +1 -1
- package/dist/state-slot.js +43 -42
- package/dist/state-slot.js.map +1 -1
- package/dist/testing.d.ts +9 -6
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +16 -12
- package/dist/testing.js.map +1 -1
- package/package.json +8 -7
- package/src/actions.ts +415 -0
- package/src/async.ts +78 -0
- package/src/compiled/async-resource.ts +113 -0
- package/src/compiled/core.ts +4041 -0
- package/src/compiled/types.ts +96 -0
- package/src/compiled/validation.ts +23 -0
- package/src/component-spread.ts +86 -0
- package/src/concurrent.ts +310 -0
- package/src/direct-dom.ts +365 -0
- package/src/dom/attributes.ts +55 -0
- package/src/dom/events.ts +93 -0
- package/src/dom/forms.ts +220 -0
- package/src/dom/intrinsic.ts +94 -0
- package/src/dom/namespace.ts +30 -0
- package/src/dom/namespaces.ts +14 -0
- package/src/dom/properties.ts +355 -0
- package/src/dom/style-properties.ts +46 -0
- package/src/dom/styles.ts +86 -0
- package/src/dom-forms.ts +23 -0
- package/src/dom-styles.ts +11 -0
- package/src/env.d.ts +6 -0
- package/src/framework-hydrate.ts +224 -0
- package/src/framework-protocol.ts +335 -0
- package/src/framework.ts +161 -0
- package/src/hydrate.ts +38 -0
- package/src/hydration-bridge.ts +196 -0
- package/src/hydration.ts +642 -0
- package/src/index.ts +92 -0
- package/src/indexed-list.ts +20 -0
- package/src/jsx-runtime.ts +55 -0
- package/src/keyed-list.ts +233 -0
- package/src/metadata.ts +212 -0
- package/src/nested-prop.ts +27 -0
- package/src/profiling.ts +43 -0
- package/src/protocol.ts +8 -0
- package/src/raw-html.ts +206 -0
- package/src/ref.ts +13 -0
- package/src/renderable-protocol.ts +63 -0
- package/src/renderable.ts +283 -0
- package/src/retained-ui.ts +19 -0
- package/src/root.ts +122 -0
- package/src/scheduler.ts +136 -0
- package/src/server-framework.ts +397 -0
- package/src/server.ts +1160 -0
- package/src/shared/promise.ts +28 -0
- package/src/source-mask.ts +83 -0
- package/src/spread.ts +131 -0
- package/src/state-slot.ts +76 -0
- package/src/testing.ts +27 -0
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/src/root.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { hydrateCompiled, mountCompiled, type MountCompiledOptions } from './compiled/core.ts'
|
|
2
|
+
import type { CompiledComponentResult } from './compiled/types.ts'
|
|
3
|
+
|
|
4
|
+
const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
|
|
5
|
+
|
|
6
|
+
export interface CompiledRoot {
|
|
7
|
+
readonly mount: (application: () => CompiledComponentResult) => void
|
|
8
|
+
readonly replace: (application: () => CompiledComponentResult) => void
|
|
9
|
+
readonly unmount: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HotContext {
|
|
13
|
+
readonly data: Record<string, unknown>
|
|
14
|
+
readonly accept: () => void
|
|
15
|
+
readonly dispose: (callback: (data: Record<string, unknown>) => void) => void
|
|
16
|
+
readonly prune: (callback: () => void) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const HOT_ROOT = '__vidactHotRoot'
|
|
20
|
+
|
|
21
|
+
export function createRoot(host: ParentNode, options?: MountCompiledOptions): CompiledRoot {
|
|
22
|
+
let dispose: (() => void) | undefined
|
|
23
|
+
let terminal = false
|
|
24
|
+
return {
|
|
25
|
+
mount(application) {
|
|
26
|
+
if (terminal) throw new Error(DEV ? 'cannot mount an unmounted root' : 'V024')
|
|
27
|
+
if (dispose !== undefined) {
|
|
28
|
+
throw new Error(DEV ? 'compiled root already has a mounted application' : 'V025')
|
|
29
|
+
}
|
|
30
|
+
dispose = mountCompiled(application, host, options).dispose
|
|
31
|
+
},
|
|
32
|
+
replace(nextApplication) {
|
|
33
|
+
if (terminal) throw new Error(DEV ? 'cannot replace an unmounted root' : 'V030')
|
|
34
|
+
const previous = dispose
|
|
35
|
+
const next = mountCompiled(nextApplication, host, options).dispose
|
|
36
|
+
dispose = next
|
|
37
|
+
previous?.()
|
|
38
|
+
},
|
|
39
|
+
unmount() {
|
|
40
|
+
if (terminal) return
|
|
41
|
+
terminal = true
|
|
42
|
+
dispose?.()
|
|
43
|
+
dispose = undefined
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hydrateRoot(
|
|
49
|
+
host: ParentNode,
|
|
50
|
+
application: () => CompiledComponentResult,
|
|
51
|
+
options?: MountCompiledOptions,
|
|
52
|
+
): CompiledRoot {
|
|
53
|
+
let dispose: (() => void) | undefined = hydrateCompiled(application, host, options).dispose
|
|
54
|
+
let terminal = false
|
|
55
|
+
return {
|
|
56
|
+
mount() {
|
|
57
|
+
throw new Error(DEV ? 'hydration roots are mounted during creation' : 'V029')
|
|
58
|
+
},
|
|
59
|
+
replace(nextApplication) {
|
|
60
|
+
if (terminal) throw new Error(DEV ? 'cannot replace an unmounted root' : 'V030')
|
|
61
|
+
const previous = dispose
|
|
62
|
+
const next = mountCompiled(nextApplication, host, options).dispose
|
|
63
|
+
dispose = next
|
|
64
|
+
previous?.()
|
|
65
|
+
},
|
|
66
|
+
unmount() {
|
|
67
|
+
if (terminal) return
|
|
68
|
+
terminal = true
|
|
69
|
+
dispose?.()
|
|
70
|
+
dispose = undefined
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mountHotRoot(
|
|
76
|
+
hot: HotContext,
|
|
77
|
+
host: ParentNode,
|
|
78
|
+
application: () => CompiledComponentResult,
|
|
79
|
+
options?: MountCompiledOptions,
|
|
80
|
+
): CompiledRoot {
|
|
81
|
+
const previous = hot.data[HOT_ROOT]
|
|
82
|
+
const root = isCompiledRoot(previous) ? previous : createRoot(host, options)
|
|
83
|
+
if (isCompiledRoot(previous)) root.replace(application)
|
|
84
|
+
else root.mount(application)
|
|
85
|
+
registerHotRoot(hot, root)
|
|
86
|
+
return root
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function hydrateHotRoot(
|
|
90
|
+
hot: HotContext,
|
|
91
|
+
host: ParentNode,
|
|
92
|
+
application: () => CompiledComponentResult,
|
|
93
|
+
options?: MountCompiledOptions,
|
|
94
|
+
): CompiledRoot {
|
|
95
|
+
const previous = hot.data[HOT_ROOT]
|
|
96
|
+
const root = isCompiledRoot(previous) ? previous : hydrateRoot(host, application, options)
|
|
97
|
+
if (isCompiledRoot(previous)) root.replace(application)
|
|
98
|
+
registerHotRoot(hot, root)
|
|
99
|
+
return root
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function registerHotRoot(hot: HotContext, root: CompiledRoot): void {
|
|
103
|
+
hot.data[HOT_ROOT] = root
|
|
104
|
+
hot.accept()
|
|
105
|
+
hot.dispose((data) => {
|
|
106
|
+
data[HOT_ROOT] = root
|
|
107
|
+
})
|
|
108
|
+
hot.prune(() => {
|
|
109
|
+
if (hot.data[HOT_ROOT] !== root) return
|
|
110
|
+
delete hot.data[HOT_ROOT]
|
|
111
|
+
root.unmount()
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCompiledRoot(value: unknown): value is CompiledRoot {
|
|
116
|
+
return (
|
|
117
|
+
typeof value === 'object' &&
|
|
118
|
+
value !== null &&
|
|
119
|
+
typeof Reflect.get(value, 'replace') === 'function' &&
|
|
120
|
+
typeof Reflect.get(value, 'unmount') === 'function'
|
|
121
|
+
)
|
|
122
|
+
}
|
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
type ScheduledTask = () => void
|
|
2
|
+
export type CancelScheduledTask = () => void
|
|
3
|
+
|
|
4
|
+
type DeferredTask = {
|
|
5
|
+
readonly run: ScheduledTask
|
|
6
|
+
canceled: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let tasks: ScheduledTask[] = []
|
|
10
|
+
let deferredTasks: DeferredTask[] = []
|
|
11
|
+
let microtaskScheduled = false
|
|
12
|
+
let deferredTurnScheduled = false
|
|
13
|
+
let flushing = false
|
|
14
|
+
let actDepth = 0
|
|
15
|
+
|
|
16
|
+
export function scheduleTask(task: ScheduledTask): void {
|
|
17
|
+
tasks.push(task)
|
|
18
|
+
requestFlush()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function scheduleDeferredTask(task: ScheduledTask): CancelScheduledTask {
|
|
22
|
+
const entry: DeferredTask = { run: task, canceled: false }
|
|
23
|
+
deferredTasks.push(entry)
|
|
24
|
+
requestDeferredTurn()
|
|
25
|
+
return () => {
|
|
26
|
+
entry.canceled = true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function flushScheduledTasks(): number {
|
|
31
|
+
if (flushing) return 0
|
|
32
|
+
flushing = true
|
|
33
|
+
microtaskScheduled = false
|
|
34
|
+
let firstError: unknown
|
|
35
|
+
let failed = false
|
|
36
|
+
let count = 0
|
|
37
|
+
try {
|
|
38
|
+
const queued = tasks
|
|
39
|
+
const deferred = deferredTasks
|
|
40
|
+
tasks = []
|
|
41
|
+
deferredTasks = []
|
|
42
|
+
for (const task of queued) {
|
|
43
|
+
count += 1
|
|
44
|
+
try {
|
|
45
|
+
task()
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!failed) {
|
|
48
|
+
firstError = error
|
|
49
|
+
failed = true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const task of deferred) {
|
|
54
|
+
if (task.canceled) continue
|
|
55
|
+
count += 1
|
|
56
|
+
try {
|
|
57
|
+
task.run()
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!failed) {
|
|
60
|
+
firstError = error
|
|
61
|
+
failed = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
flushing = false
|
|
67
|
+
requestFlush()
|
|
68
|
+
requestDeferredTurn()
|
|
69
|
+
}
|
|
70
|
+
if (failed) throw firstError
|
|
71
|
+
return count
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function hasScheduledTasks(): boolean {
|
|
75
|
+
return tasks.length > 0 || deferredTasks.some((task) => !task.canceled)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function beginActScope(): void {
|
|
79
|
+
actDepth += 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function endActScope(): void {
|
|
83
|
+
actDepth -= 1
|
|
84
|
+
requestFlush()
|
|
85
|
+
requestDeferredTurn()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requestFlush(): void {
|
|
89
|
+
if (tasks.length === 0 || microtaskScheduled || flushing || actDepth > 0) return
|
|
90
|
+
microtaskScheduled = true
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
microtaskScheduled = false
|
|
93
|
+
if (actDepth === 0) flushScheduledTasks()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function requestDeferredTurn(): void {
|
|
98
|
+
if (
|
|
99
|
+
deferredTasks.every((task) => task.canceled) ||
|
|
100
|
+
deferredTurnScheduled ||
|
|
101
|
+
flushing ||
|
|
102
|
+
actDepth > 0
|
|
103
|
+
) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
deferredTurnScheduled = true
|
|
107
|
+
const channel = new MessageChannel()
|
|
108
|
+
channel.port1.addEventListener(
|
|
109
|
+
'message',
|
|
110
|
+
() => {
|
|
111
|
+
channel.port1.close()
|
|
112
|
+
channel.port2.close()
|
|
113
|
+
deferredTurnScheduled = false
|
|
114
|
+
if (actDepth > 0 || flushing) {
|
|
115
|
+
requestDeferredTurn()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
flushOneDeferredTask()
|
|
119
|
+
},
|
|
120
|
+
{ once: true },
|
|
121
|
+
)
|
|
122
|
+
channel.port1.start()
|
|
123
|
+
channel.port2.postMessage(undefined)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function flushOneDeferredTask(): void {
|
|
127
|
+
let task: DeferredTask | undefined
|
|
128
|
+
while ((task = deferredTasks.shift()) !== undefined && task.canceled) {}
|
|
129
|
+
if (task === undefined) return
|
|
130
|
+
try {
|
|
131
|
+
task.run()
|
|
132
|
+
} finally {
|
|
133
|
+
requestFlush()
|
|
134
|
+
requestDeferredTurn()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import type { CompiledComponentResult } from './compiled/types.ts'
|
|
2
|
+
import {
|
|
3
|
+
decodeFrameworkValue,
|
|
4
|
+
encodeFrameworkValue,
|
|
5
|
+
type ClientModuleManifest,
|
|
6
|
+
type ClientReference,
|
|
7
|
+
type FrameworkValue,
|
|
8
|
+
} from './framework-protocol.ts'
|
|
9
|
+
import {
|
|
10
|
+
createServerHydrationBoundary,
|
|
11
|
+
createServerFrameworkRenderContext,
|
|
12
|
+
renderToString,
|
|
13
|
+
withServerFrameworkRenderContext,
|
|
14
|
+
type ServerChild,
|
|
15
|
+
type ServerFrameworkRenderContext,
|
|
16
|
+
type ServerNode,
|
|
17
|
+
type ServerRenderOptions,
|
|
18
|
+
} from './server.ts'
|
|
19
|
+
import { abortReason, withAbort } from './shared/promise.ts'
|
|
20
|
+
|
|
21
|
+
export interface FrameworkRenderOptions extends ServerRenderOptions {
|
|
22
|
+
readonly signal?: AbortSignal
|
|
23
|
+
readonly progressiveChunkSize?: number
|
|
24
|
+
readonly onError?: (error: unknown) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PipeableRenderOptions extends FrameworkRenderOptions {
|
|
28
|
+
readonly onAllReady?: () => void
|
|
29
|
+
readonly onShellError?: (error: unknown) => void
|
|
30
|
+
readonly onShellReady?: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PipeDestination {
|
|
34
|
+
readonly write: (chunk: Uint8Array) => boolean
|
|
35
|
+
readonly end: () => void
|
|
36
|
+
readonly destroy?: (error: unknown) => void
|
|
37
|
+
readonly once?: (event: 'drain', listener: () => void) => void
|
|
38
|
+
readonly off?: (event: 'drain', listener: () => void) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PipeableStream {
|
|
42
|
+
readonly abort: (reason?: unknown) => void
|
|
43
|
+
readonly pipe: (destination: PipeDestination) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type VidactReadableStream = ReadableStream<Uint8Array> & {
|
|
47
|
+
readonly allReady: Promise<void>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PrerenderResult<Prelude> {
|
|
51
|
+
readonly prelude: Prelude
|
|
52
|
+
readonly postponed: string | null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ServerComponentPayload {
|
|
56
|
+
readonly kind: 'server-component'
|
|
57
|
+
readonly html: string
|
|
58
|
+
readonly model: FrameworkValue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ClientBoundaryOptions {
|
|
62
|
+
readonly clientManifest: ClientModuleManifest
|
|
63
|
+
readonly hostProps?: Readonly<Record<string, unknown>>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createClientBoundary(
|
|
67
|
+
reference: ClientReference,
|
|
68
|
+
props: FrameworkValue,
|
|
69
|
+
serverContent: ServerChild | CompiledComponentResult,
|
|
70
|
+
options: ClientBoundaryOptions,
|
|
71
|
+
): ServerNode & CompiledComponentResult {
|
|
72
|
+
const payload = encodeFrameworkValue({ reference, props }, options.clientManifest)
|
|
73
|
+
return createServerHydrationBoundary(serverContent as ServerChild, {
|
|
74
|
+
...options.hostProps,
|
|
75
|
+
'data-vidact-client-boundary': true,
|
|
76
|
+
'data-vidact-client-payload': payload,
|
|
77
|
+
}) as ServerNode & CompiledComponentResult
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderToReadableStream(
|
|
81
|
+
value: ServerChild | (() => ServerChild),
|
|
82
|
+
options: FrameworkRenderOptions = {},
|
|
83
|
+
): Promise<VidactReadableStream> {
|
|
84
|
+
const controller = linkedAbortController(options.signal)
|
|
85
|
+
const html = renderFrameworkHtml(value, options, controller.signal)
|
|
86
|
+
return Promise.resolve(readableFromText(html, options.progressiveChunkSize, controller))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderToPipeableStream(
|
|
90
|
+
value: ServerChild | (() => ServerChild),
|
|
91
|
+
options: PipeableRenderOptions = {},
|
|
92
|
+
): PipeableStream {
|
|
93
|
+
const controller = linkedAbortController(options.signal)
|
|
94
|
+
const html = renderFrameworkHtml(value, options, controller.signal, options)
|
|
95
|
+
// A caller may create a stream for lifecycle callbacks and never pipe it. Mark the
|
|
96
|
+
// render rejection as observed while retaining the original promise for pipe().
|
|
97
|
+
void html.catch(() => undefined)
|
|
98
|
+
let piped = false
|
|
99
|
+
return {
|
|
100
|
+
abort(reason) {
|
|
101
|
+
controller.abort(reason)
|
|
102
|
+
},
|
|
103
|
+
pipe(destination) {
|
|
104
|
+
if (piped) throw new Error('a Vidact pipeable stream can only be piped once')
|
|
105
|
+
piped = true
|
|
106
|
+
void pipeText(html, destination, options.progressiveChunkSize, controller.signal).catch(
|
|
107
|
+
(error: unknown) => destination.destroy?.(error),
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function prerender(
|
|
114
|
+
value: ServerChild | (() => ServerChild),
|
|
115
|
+
options: FrameworkRenderOptions = {},
|
|
116
|
+
): Promise<PrerenderResult<ReadableStream<Uint8Array>>> {
|
|
117
|
+
const controller = linkedAbortController(options.signal)
|
|
118
|
+
const result = await prerenderText(value, options, controller.signal)
|
|
119
|
+
return {
|
|
120
|
+
prelude: readableFromText(
|
|
121
|
+
Promise.resolve(result.prelude),
|
|
122
|
+
options.progressiveChunkSize,
|
|
123
|
+
controller,
|
|
124
|
+
),
|
|
125
|
+
postponed: result.postponed,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function prerenderToNodeStream(
|
|
130
|
+
value: ServerChild | (() => ServerChild),
|
|
131
|
+
options: FrameworkRenderOptions = {},
|
|
132
|
+
): Promise<PrerenderResult<AsyncIterable<Uint8Array>>> {
|
|
133
|
+
const controller = linkedAbortController(options.signal)
|
|
134
|
+
const result = await prerenderText(value, options, controller.signal)
|
|
135
|
+
return {
|
|
136
|
+
prelude: textIterable(result.prelude, options.progressiveChunkSize, controller.signal),
|
|
137
|
+
postponed: result.postponed,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resume(
|
|
142
|
+
postponed: string,
|
|
143
|
+
options: FrameworkRenderOptions = {},
|
|
144
|
+
): Promise<VidactReadableStream> {
|
|
145
|
+
const controller = linkedAbortController(options.signal)
|
|
146
|
+
const html = Promise.resolve(readContinuation(postponed))
|
|
147
|
+
return Promise.resolve(readableFromText(html, options.progressiveChunkSize, controller))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resumeToPipeableStream(
|
|
151
|
+
postponed: string,
|
|
152
|
+
options: PipeableRenderOptions = {},
|
|
153
|
+
): PipeableStream {
|
|
154
|
+
const controller = linkedAbortController(options.signal)
|
|
155
|
+
const html = Promise.resolve(readContinuation(postponed))
|
|
156
|
+
let piped = false
|
|
157
|
+
queueMicrotask(() => options.onShellReady?.())
|
|
158
|
+
void html.then(
|
|
159
|
+
() => options.onAllReady?.(),
|
|
160
|
+
(error: unknown) => options.onShellError?.(error),
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
abort(reason) {
|
|
164
|
+
controller.abort(reason)
|
|
165
|
+
},
|
|
166
|
+
pipe(destination) {
|
|
167
|
+
if (piped) throw new Error('a Vidact pipeable stream can only be piped once')
|
|
168
|
+
piped = true
|
|
169
|
+
void pipeText(html, destination, options.progressiveChunkSize, controller.signal).catch(
|
|
170
|
+
(error: unknown) => destination.destroy?.(error),
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function renderServerComponentPayload(
|
|
177
|
+
value: ServerChild | (() => ServerChild),
|
|
178
|
+
model: FrameworkValue,
|
|
179
|
+
options: FrameworkRenderOptions & { readonly clientManifest?: ClientModuleManifest } = {},
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
const controller = linkedAbortController(options.signal)
|
|
182
|
+
const html = await renderFrameworkHtml(value, options, controller.signal)
|
|
183
|
+
return encodeFrameworkValue({ kind: 'server-component', html, model }, options.clientManifest)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function decodeServerComponentPayload(
|
|
187
|
+
payload: string,
|
|
188
|
+
clientManifest?: ClientModuleManifest,
|
|
189
|
+
): ServerComponentPayload {
|
|
190
|
+
const value = decodeFrameworkValue(payload, clientManifest)
|
|
191
|
+
if (
|
|
192
|
+
!isRecord(value) ||
|
|
193
|
+
value.kind !== 'server-component' ||
|
|
194
|
+
typeof value.html !== 'string' ||
|
|
195
|
+
!Object.hasOwn(value, 'model')
|
|
196
|
+
) {
|
|
197
|
+
throw new TypeError('invalid Vidact Server Component payload')
|
|
198
|
+
}
|
|
199
|
+
return { kind: 'server-component', html: value.html, model: value.model as FrameworkValue }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function renderFrameworkHtml(
|
|
203
|
+
value: ServerChild | (() => ServerChild),
|
|
204
|
+
options: FrameworkRenderOptions,
|
|
205
|
+
signal: AbortSignal,
|
|
206
|
+
callbacks: PipeableRenderOptions = {},
|
|
207
|
+
): Promise<string> {
|
|
208
|
+
const context = createServerFrameworkRenderContext(signal)
|
|
209
|
+
let shellReported = false
|
|
210
|
+
for (let pass = 0; pass < 100; pass += 1) {
|
|
211
|
+
throwIfAborted(signal)
|
|
212
|
+
let html: string
|
|
213
|
+
try {
|
|
214
|
+
html = renderFrameworkPass(value, options, context)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
options.onError?.(error)
|
|
217
|
+
if (!shellReported) callbacks.onShellError?.(error)
|
|
218
|
+
throw error
|
|
219
|
+
}
|
|
220
|
+
if (!shellReported) {
|
|
221
|
+
shellReported = true
|
|
222
|
+
callbacks.onShellReady?.()
|
|
223
|
+
}
|
|
224
|
+
if (context.pending.size === 0) {
|
|
225
|
+
callbacks.onAllReady?.()
|
|
226
|
+
return html
|
|
227
|
+
}
|
|
228
|
+
// oxlint-disable-next-line eslint/no-await-in-loop -- Each pass discovers the next resource frontier.
|
|
229
|
+
await waitForResources(context.pending, signal)
|
|
230
|
+
}
|
|
231
|
+
throw new Error('Vidact framework render did not stabilize after 100 resource passes')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function prerenderText(
|
|
235
|
+
value: ServerChild | (() => ServerChild),
|
|
236
|
+
options: FrameworkRenderOptions,
|
|
237
|
+
signal: AbortSignal,
|
|
238
|
+
): Promise<{ readonly prelude: string; readonly postponed: string | null }> {
|
|
239
|
+
const context = createServerFrameworkRenderContext(signal)
|
|
240
|
+
const shell = renderFrameworkPass(value, options, context)
|
|
241
|
+
if (context.pending.size === 0) return { prelude: shell, postponed: null }
|
|
242
|
+
await waitForResources(context.pending, signal)
|
|
243
|
+
const final = await settleFrameworkContext(value, options, context)
|
|
244
|
+
return {
|
|
245
|
+
prelude: shell,
|
|
246
|
+
postponed: encodeFrameworkValue({ kind: 'continuation', html: final }),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function settleFrameworkContext(
|
|
251
|
+
value: ServerChild | (() => ServerChild),
|
|
252
|
+
options: FrameworkRenderOptions,
|
|
253
|
+
context: ServerFrameworkRenderContext,
|
|
254
|
+
): Promise<string> {
|
|
255
|
+
for (let pass = 0; pass < 100; pass += 1) {
|
|
256
|
+
throwIfAborted(context.signal)
|
|
257
|
+
const html = renderFrameworkPass(value, options, context)
|
|
258
|
+
if (context.pending.size === 0) return html
|
|
259
|
+
// oxlint-disable-next-line eslint/no-await-in-loop -- Each pass discovers the next resource frontier.
|
|
260
|
+
await waitForResources(context.pending, context.signal)
|
|
261
|
+
}
|
|
262
|
+
throw new Error('Vidact prerender did not stabilize after 100 resource passes')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderFrameworkPass(
|
|
266
|
+
value: ServerChild | (() => ServerChild),
|
|
267
|
+
options: ServerRenderOptions,
|
|
268
|
+
context: ServerFrameworkRenderContext,
|
|
269
|
+
): string {
|
|
270
|
+
return withServerFrameworkRenderContext(context, () => renderToString(value, options))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readContinuation(postponed: string): string {
|
|
274
|
+
const value = decodeFrameworkValue(postponed)
|
|
275
|
+
if (!isRecord(value) || value.kind !== 'continuation' || typeof value.html !== 'string') {
|
|
276
|
+
throw new TypeError('invalid Vidact continuation payload')
|
|
277
|
+
}
|
|
278
|
+
return value.html
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readableFromText(
|
|
282
|
+
text: Promise<string>,
|
|
283
|
+
requestedChunkSize: number | undefined,
|
|
284
|
+
abortController: AbortController,
|
|
285
|
+
): VidactReadableStream {
|
|
286
|
+
const chunkSize = normalizeChunkSize(requestedChunkSize)
|
|
287
|
+
const encoder = new TextEncoder()
|
|
288
|
+
let content: Uint8Array | undefined
|
|
289
|
+
let offset = 0
|
|
290
|
+
const allReady = text.then(() => undefined)
|
|
291
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
292
|
+
async pull(controller) {
|
|
293
|
+
try {
|
|
294
|
+
throwIfAborted(abortController.signal)
|
|
295
|
+
content ??= encoder.encode(await text)
|
|
296
|
+
if (offset >= content.length) {
|
|
297
|
+
controller.close()
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
const next = content.slice(offset, offset + chunkSize)
|
|
301
|
+
offset += next.length
|
|
302
|
+
controller.enqueue(next)
|
|
303
|
+
} catch (error) {
|
|
304
|
+
controller.error(error)
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
cancel(reason) {
|
|
308
|
+
abortController.abort(reason)
|
|
309
|
+
},
|
|
310
|
+
}) as VidactReadableStream
|
|
311
|
+
Object.defineProperty(stream, 'allReady', { value: allReady })
|
|
312
|
+
return stream
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function pipeText(
|
|
316
|
+
text: Promise<string>,
|
|
317
|
+
destination: PipeDestination,
|
|
318
|
+
requestedChunkSize: number | undefined,
|
|
319
|
+
signal: AbortSignal,
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
const content = new TextEncoder().encode(await text)
|
|
322
|
+
const chunkSize = normalizeChunkSize(requestedChunkSize)
|
|
323
|
+
for (let offset = 0; offset < content.length; offset += chunkSize) {
|
|
324
|
+
throwIfAborted(signal)
|
|
325
|
+
if (!destination.write(content.slice(offset, offset + chunkSize))) {
|
|
326
|
+
// oxlint-disable-next-line eslint/no-await-in-loop -- Stream chunks must honor destination backpressure in order.
|
|
327
|
+
await waitForDrain(destination, signal)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
destination.end()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function* textIterable(
|
|
334
|
+
text: string,
|
|
335
|
+
requestedChunkSize: number | undefined,
|
|
336
|
+
signal: AbortSignal,
|
|
337
|
+
): AsyncIterable<Uint8Array> {
|
|
338
|
+
const content = new TextEncoder().encode(text)
|
|
339
|
+
const chunkSize = normalizeChunkSize(requestedChunkSize)
|
|
340
|
+
for (let offset = 0; offset < content.length; offset += chunkSize) {
|
|
341
|
+
throwIfAborted(signal)
|
|
342
|
+
yield content.slice(offset, offset + chunkSize)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function waitForResources(
|
|
347
|
+
resources: Set<PromiseLike<unknown>>,
|
|
348
|
+
signal: AbortSignal,
|
|
349
|
+
): Promise<void> {
|
|
350
|
+
return withAbort(
|
|
351
|
+
Promise.allSettled(resources).then(() => undefined),
|
|
352
|
+
signal,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function waitForDrain(destination: PipeDestination, signal: AbortSignal): Promise<void> {
|
|
357
|
+
if (destination.once === undefined) return Promise.resolve()
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
if (signal.aborted) {
|
|
360
|
+
reject(abortReason(signal))
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
const onDrain = (): void => {
|
|
364
|
+
signal.removeEventListener('abort', onAbort)
|
|
365
|
+
resolve()
|
|
366
|
+
}
|
|
367
|
+
const onAbort = (): void => {
|
|
368
|
+
destination.off?.('drain', onDrain)
|
|
369
|
+
reject(abortReason(signal))
|
|
370
|
+
}
|
|
371
|
+
destination.once?.('drain', onDrain)
|
|
372
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function linkedAbortController(signal?: AbortSignal): AbortController {
|
|
377
|
+
const controller = new AbortController()
|
|
378
|
+
if (signal?.aborted) controller.abort(signal.reason)
|
|
379
|
+
else signal?.addEventListener('abort', () => controller.abort(signal.reason), { once: true })
|
|
380
|
+
return controller
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeChunkSize(value: number | undefined): number {
|
|
384
|
+
if (value === undefined) return 16_384
|
|
385
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
386
|
+
throw new RangeError('progressiveChunkSize must be a positive safe integer')
|
|
387
|
+
}
|
|
388
|
+
return value
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function throwIfAborted(signal: AbortSignal): void {
|
|
392
|
+
if (signal.aborted) throw abortReason(signal)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isRecord(value: unknown): value is Record<string, FrameworkValue> {
|
|
396
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
397
|
+
}
|