@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { MountCompiledOptions } from './compiled/core.ts'
|
|
2
|
+
import type { CompiledComponentResult } from './compiled/types.ts'
|
|
3
|
+
import {
|
|
4
|
+
decodeFrameworkValue,
|
|
5
|
+
isClientReference,
|
|
6
|
+
type ClientReference,
|
|
7
|
+
type FrameworkValue,
|
|
8
|
+
} from './framework-protocol.ts'
|
|
9
|
+
import { isClientBoundaryDefinition } from './framework.ts'
|
|
10
|
+
import { installHydration } from './hydration.ts'
|
|
11
|
+
import { hydrateRoot, type CompiledRoot } from './root.ts'
|
|
12
|
+
|
|
13
|
+
installHydration()
|
|
14
|
+
|
|
15
|
+
export interface EventReplayQueue {
|
|
16
|
+
readonly dispose: () => void
|
|
17
|
+
readonly replay: () => void
|
|
18
|
+
readonly size: () => number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ClientModuleLoader = (
|
|
22
|
+
reference: ClientReference,
|
|
23
|
+
) => PromiseLike<Readonly<Record<string, unknown>>>
|
|
24
|
+
|
|
25
|
+
export interface HydratedClientBoundaries {
|
|
26
|
+
readonly roots: readonly CompiledRoot[]
|
|
27
|
+
readonly dispose: () => void
|
|
28
|
+
readonly replace: (loadModule?: ClientModuleLoader) => Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ReplayRecord = {
|
|
32
|
+
readonly type: string
|
|
33
|
+
readonly path: readonly number[]
|
|
34
|
+
readonly bubbles: boolean
|
|
35
|
+
readonly cancelable: boolean
|
|
36
|
+
readonly value?: string
|
|
37
|
+
readonly checked?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_REPLAY_EVENTS = ['click', 'input', 'change', 'submit'] as const
|
|
41
|
+
|
|
42
|
+
export function createEventReplayQueue(
|
|
43
|
+
host: ParentNode,
|
|
44
|
+
events: readonly string[] = DEFAULT_REPLAY_EVENTS,
|
|
45
|
+
): EventReplayQueue {
|
|
46
|
+
const records: ReplayRecord[] = []
|
|
47
|
+
let disposed = false
|
|
48
|
+
const capture = (event: Event): void => {
|
|
49
|
+
const target = event.target
|
|
50
|
+
if (!(target instanceof Node)) return
|
|
51
|
+
const path = nodePath(host, target)
|
|
52
|
+
if (path === undefined) return
|
|
53
|
+
if (event.type === 'submit') event.preventDefault()
|
|
54
|
+
records.push({
|
|
55
|
+
type: event.type,
|
|
56
|
+
path,
|
|
57
|
+
bubbles: event.bubbles,
|
|
58
|
+
cancelable: event.cancelable,
|
|
59
|
+
...(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement
|
|
60
|
+
? { value: target.value }
|
|
61
|
+
: {}),
|
|
62
|
+
...(target instanceof HTMLInputElement ? { checked: target.checked } : {}),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
for (const event of events) host.addEventListener(event, capture, true)
|
|
66
|
+
const dispose = (): void => {
|
|
67
|
+
if (disposed) return
|
|
68
|
+
disposed = true
|
|
69
|
+
for (const event of events) host.removeEventListener(event, capture, true)
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
dispose,
|
|
73
|
+
replay() {
|
|
74
|
+
dispose()
|
|
75
|
+
for (const record of records.splice(0)) {
|
|
76
|
+
const target = resolveNodePath(host, record.path)
|
|
77
|
+
if (!(target instanceof Element)) continue
|
|
78
|
+
if (
|
|
79
|
+
record.value !== undefined &&
|
|
80
|
+
(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
|
|
81
|
+
) {
|
|
82
|
+
target.value = record.value
|
|
83
|
+
}
|
|
84
|
+
if (record.checked !== undefined && target instanceof HTMLInputElement) {
|
|
85
|
+
target.checked = record.checked
|
|
86
|
+
}
|
|
87
|
+
target.dispatchEvent(
|
|
88
|
+
new Event(record.type, { bubbles: record.bubbles, cancelable: record.cancelable }),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
size: () => records.length,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function hydrateFrameworkBoundary(
|
|
97
|
+
host: ParentNode,
|
|
98
|
+
application: () => CompiledComponentResult,
|
|
99
|
+
options?: MountCompiledOptions & { readonly replay?: EventReplayQueue },
|
|
100
|
+
): CompiledRoot {
|
|
101
|
+
try {
|
|
102
|
+
const root = hydrateRoot(host, application, options)
|
|
103
|
+
options?.replay?.replay()
|
|
104
|
+
return root
|
|
105
|
+
} catch (error) {
|
|
106
|
+
options?.replay?.dispose()
|
|
107
|
+
throw error
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function hydrateClientBoundaries(
|
|
112
|
+
root: ParentNode,
|
|
113
|
+
loadModule: ClientModuleLoader,
|
|
114
|
+
options?: MountCompiledOptions,
|
|
115
|
+
): Promise<HydratedClientBoundaries> {
|
|
116
|
+
const hosts = clientBoundaryHosts(root)
|
|
117
|
+
const payloads = hosts.map(readClientBoundaryPayload)
|
|
118
|
+
const replays = hosts.map((host) => createEventReplayQueue(host))
|
|
119
|
+
const roots: CompiledRoot[] = []
|
|
120
|
+
try {
|
|
121
|
+
const applications = await Promise.all(
|
|
122
|
+
payloads.map((payload) => prepareClientBoundary(loadModule, payload)),
|
|
123
|
+
)
|
|
124
|
+
for (const [index, host] of hosts.entries()) {
|
|
125
|
+
const identifierPrefix = host.getAttribute('data-vidact-identifier-prefix') ?? ''
|
|
126
|
+
roots[index] = hydrateFrameworkBoundary(host, applications[index]!, {
|
|
127
|
+
...options,
|
|
128
|
+
identifierPrefix,
|
|
129
|
+
replay: replays[index]!,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
for (const replay of replays) replay.dispose()
|
|
134
|
+
for (const compiledRoot of roots) compiledRoot?.unmount()
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
roots,
|
|
139
|
+
dispose() {
|
|
140
|
+
for (const compiledRoot of roots) compiledRoot.unmount()
|
|
141
|
+
},
|
|
142
|
+
async replace(nextLoader = loadModule) {
|
|
143
|
+
const applications = await Promise.all(
|
|
144
|
+
payloads.map((payload) => prepareClientBoundary(nextLoader, payload)),
|
|
145
|
+
)
|
|
146
|
+
for (const [index, compiledRoot] of roots.entries()) {
|
|
147
|
+
compiledRoot.replace(applications[index]!)
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function prepareClientBoundary(
|
|
154
|
+
loadModule: ClientModuleLoader,
|
|
155
|
+
{ reference, props }: ClientBoundaryPayload,
|
|
156
|
+
): Promise<() => CompiledComponentResult> {
|
|
157
|
+
const definition = await loadClientBoundary(loadModule, reference)
|
|
158
|
+
const prepared = await definition.prepare?.(props)
|
|
159
|
+
return () => definition.render(props, prepared)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadClientBoundary(loadModule: ClientModuleLoader, reference: ClientReference) {
|
|
163
|
+
const module = await loadModule(reference)
|
|
164
|
+
const definition = module[reference.exportName]
|
|
165
|
+
if (!isClientBoundaryDefinition(definition)) {
|
|
166
|
+
throw new TypeError(
|
|
167
|
+
`client module ${reference.id} does not export Vidact boundary ${reference.exportName}`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return definition
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function clientBoundaryHosts(root: ParentNode): HTMLElement[] {
|
|
174
|
+
const selector = '[data-vidact-client-boundary][data-vidact-client-payload]'
|
|
175
|
+
const descendants = [...root.querySelectorAll<HTMLElement>(selector)]
|
|
176
|
+
return root instanceof HTMLElement && root.matches(selector)
|
|
177
|
+
? [root, ...descendants]
|
|
178
|
+
: descendants
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface ClientBoundaryPayload {
|
|
182
|
+
readonly reference: ClientReference
|
|
183
|
+
readonly props: FrameworkValue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function readClientBoundaryPayload(host: HTMLElement): ClientBoundaryPayload {
|
|
187
|
+
const encoded = host.getAttribute('data-vidact-client-payload')
|
|
188
|
+
if (encoded === null) throw new TypeError('client boundary payload is missing')
|
|
189
|
+
const payload = decodeFrameworkValue(encoded)
|
|
190
|
+
if (
|
|
191
|
+
!isRecord(payload) ||
|
|
192
|
+
!Object.hasOwn(payload, 'props') ||
|
|
193
|
+
!isClientReference(payload.reference)
|
|
194
|
+
) {
|
|
195
|
+
throw new TypeError('invalid Vidact client boundary payload')
|
|
196
|
+
}
|
|
197
|
+
return { reference: payload.reference, props: payload.props as FrameworkValue }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
201
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function nodePath(root: ParentNode, node: Node): readonly number[] | undefined {
|
|
205
|
+
const path: number[] = []
|
|
206
|
+
let current: Node | null = node
|
|
207
|
+
while (current !== null && current !== root) {
|
|
208
|
+
const parentNode: ParentNode | null = current.parentNode
|
|
209
|
+
if (parentNode === null) return undefined
|
|
210
|
+
path.push(Array.prototype.indexOf.call(parentNode.childNodes, current) as number)
|
|
211
|
+
current = parentNode
|
|
212
|
+
}
|
|
213
|
+
return current === root ? path.toReversed() : undefined
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveNodePath(root: ParentNode, path: readonly number[]): Node | undefined {
|
|
217
|
+
let current: Node = root as Node
|
|
218
|
+
for (const index of path) {
|
|
219
|
+
const next = current.childNodes[index]
|
|
220
|
+
if (next === undefined) return undefined
|
|
221
|
+
current = next
|
|
222
|
+
}
|
|
223
|
+
return current
|
|
224
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { abortReason, withAbort } from './shared/promise.ts'
|
|
2
|
+
|
|
3
|
+
const CLIENT_REFERENCE = Symbol('Vidact.ClientReference')
|
|
4
|
+
const SERVER_REFERENCE = Symbol('Vidact.ServerReference')
|
|
5
|
+
|
|
6
|
+
export const VIDACT_FRAMEWORK_PROTOCOL = 'vidact-framework-v1'
|
|
7
|
+
|
|
8
|
+
export interface ClientReference {
|
|
9
|
+
readonly [CLIENT_REFERENCE]: true
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly exportName: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ServerFunctionReference {
|
|
15
|
+
readonly [SERVER_REFERENCE]: true
|
|
16
|
+
readonly id: string
|
|
17
|
+
readonly bound: readonly FrameworkValue[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type FrameworkValue =
|
|
21
|
+
| null
|
|
22
|
+
| undefined
|
|
23
|
+
| boolean
|
|
24
|
+
| string
|
|
25
|
+
| number
|
|
26
|
+
| bigint
|
|
27
|
+
| readonly FrameworkValue[]
|
|
28
|
+
| { readonly [key: string]: FrameworkValue }
|
|
29
|
+
| ClientReference
|
|
30
|
+
| ServerFunctionReference
|
|
31
|
+
|
|
32
|
+
export interface ClientModuleManifest {
|
|
33
|
+
readonly has: (id: string, exportName: string) => boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ServerFunctionRegistry {
|
|
37
|
+
readonly invoke: (
|
|
38
|
+
id: string,
|
|
39
|
+
arguments_: readonly FrameworkValue[],
|
|
40
|
+
signal?: AbortSignal,
|
|
41
|
+
) => Promise<FrameworkValue>
|
|
42
|
+
readonly register: (
|
|
43
|
+
id: string,
|
|
44
|
+
implementation: (
|
|
45
|
+
...arguments_: FrameworkValue[]
|
|
46
|
+
) => FrameworkValue | PromiseLike<FrameworkValue>,
|
|
47
|
+
) => () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type EncodedValue =
|
|
51
|
+
| null
|
|
52
|
+
| boolean
|
|
53
|
+
| string
|
|
54
|
+
| number
|
|
55
|
+
| readonly EncodedValue[]
|
|
56
|
+
| { readonly [key: string]: EncodedValue }
|
|
57
|
+
|
|
58
|
+
interface FrameworkEnvelope {
|
|
59
|
+
readonly protocol: typeof VIDACT_FRAMEWORK_PROTOCOL
|
|
60
|
+
readonly checksum: string
|
|
61
|
+
readonly body: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface TraversalBudget {
|
|
65
|
+
nodes: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const MAX_FRAMEWORK_PAYLOAD_CHARACTERS = 64 * 1024 * 1024
|
|
69
|
+
const MAX_FRAMEWORK_BODY_CHARACTERS = 32 * 1024 * 1024
|
|
70
|
+
const MAX_FRAMEWORK_VALUE_DEPTH = 100
|
|
71
|
+
const MAX_FRAMEWORK_VALUE_NODES = 100_000
|
|
72
|
+
|
|
73
|
+
export function createClientReference(id: string, exportName = 'default'): ClientReference {
|
|
74
|
+
assertReferencePart(id, 'client module id')
|
|
75
|
+
assertReferencePart(exportName, 'client export name')
|
|
76
|
+
return Object.freeze({ [CLIENT_REFERENCE]: true as const, id, exportName })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createServerFunctionReference(
|
|
80
|
+
id: string,
|
|
81
|
+
bound: readonly FrameworkValue[] = [],
|
|
82
|
+
): ServerFunctionReference {
|
|
83
|
+
assertReferencePart(id, 'server function id')
|
|
84
|
+
return Object.freeze({ [SERVER_REFERENCE]: true as const, id, bound: Object.freeze([...bound]) })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createClientModuleManifest(
|
|
88
|
+
references: Readonly<Record<string, readonly string[]>>,
|
|
89
|
+
): ClientModuleManifest {
|
|
90
|
+
const allowed = new Set<string>()
|
|
91
|
+
for (const [id, exports] of Object.entries(references)) {
|
|
92
|
+
assertReferencePart(id, 'client module id')
|
|
93
|
+
for (const exportName of exports) {
|
|
94
|
+
assertReferencePart(exportName, 'client export name')
|
|
95
|
+
allowed.add(`${id}\0${exportName}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const manifest: ClientModuleManifest = {
|
|
99
|
+
has: (id: string, exportName: string) => allowed.has(`${id}\0${exportName}`),
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze(manifest)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createServerFunctionRegistry(): ServerFunctionRegistry {
|
|
105
|
+
const functions = new Map<
|
|
106
|
+
string,
|
|
107
|
+
(...arguments_: FrameworkValue[]) => FrameworkValue | PromiseLike<FrameworkValue>
|
|
108
|
+
>()
|
|
109
|
+
return {
|
|
110
|
+
register(id, implementation) {
|
|
111
|
+
assertReferencePart(id, 'server function id')
|
|
112
|
+
if (functions.has(id)) throw new Error(`server function ${id} is already registered`)
|
|
113
|
+
functions.set(id, implementation)
|
|
114
|
+
return () => {
|
|
115
|
+
if (functions.get(id) === implementation) functions.delete(id)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async invoke(id, arguments_, signal) {
|
|
119
|
+
if (signal?.aborted) throw abortReason(signal)
|
|
120
|
+
const implementation = functions.get(id)
|
|
121
|
+
if (implementation === undefined) throw new Error(`unknown server function ${id}`)
|
|
122
|
+
const operation = Promise.resolve(implementation(...arguments_))
|
|
123
|
+
const result = signal === undefined ? await operation : await withAbort(operation, signal)
|
|
124
|
+
assertFrameworkValue(result)
|
|
125
|
+
return result
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function encodeFrameworkValue(
|
|
131
|
+
value: FrameworkValue,
|
|
132
|
+
manifest?: ClientModuleManifest,
|
|
133
|
+
): string {
|
|
134
|
+
const body = JSON.stringify(encodeValue(value, manifest, new Set(), { nodes: 0 }, 0))
|
|
135
|
+
assertFrameworkPayloadSize(body, MAX_FRAMEWORK_BODY_CHARACTERS, 'body')
|
|
136
|
+
const payload = JSON.stringify({
|
|
137
|
+
protocol: VIDACT_FRAMEWORK_PROTOCOL,
|
|
138
|
+
checksum: checksum(body),
|
|
139
|
+
body,
|
|
140
|
+
} satisfies FrameworkEnvelope)
|
|
141
|
+
assertFrameworkPayloadSize(payload, MAX_FRAMEWORK_PAYLOAD_CHARACTERS, 'envelope')
|
|
142
|
+
return payload
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function decodeFrameworkValue(
|
|
146
|
+
payload: string,
|
|
147
|
+
manifest?: ClientModuleManifest,
|
|
148
|
+
): FrameworkValue {
|
|
149
|
+
assertFrameworkPayloadSize(payload, MAX_FRAMEWORK_PAYLOAD_CHARACTERS, 'envelope')
|
|
150
|
+
const envelope = JSON.parse(payload) as Partial<FrameworkEnvelope>
|
|
151
|
+
if (
|
|
152
|
+
envelope.protocol !== VIDACT_FRAMEWORK_PROTOCOL ||
|
|
153
|
+
typeof envelope.body !== 'string' ||
|
|
154
|
+
typeof envelope.checksum !== 'string'
|
|
155
|
+
) {
|
|
156
|
+
throw new Error('unsupported Vidact framework payload')
|
|
157
|
+
}
|
|
158
|
+
if (checksum(envelope.body) !== envelope.checksum) {
|
|
159
|
+
throw new Error('Vidact framework payload integrity check failed')
|
|
160
|
+
}
|
|
161
|
+
assertFrameworkPayloadSize(envelope.body, MAX_FRAMEWORK_BODY_CHARACTERS, 'body')
|
|
162
|
+
return decodeValue(JSON.parse(envelope.body) as EncodedValue, manifest, { nodes: 0 }, 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function invokeServerFunctionPayload(
|
|
166
|
+
registry: ServerFunctionRegistry,
|
|
167
|
+
payload: string,
|
|
168
|
+
signal?: AbortSignal,
|
|
169
|
+
): Promise<string> {
|
|
170
|
+
const request = decodeFrameworkValue(payload)
|
|
171
|
+
if (!isPlainObject(request) || typeof request.id !== 'string' || !Array.isArray(request.args)) {
|
|
172
|
+
return Promise.reject(new TypeError('invalid server function request payload'))
|
|
173
|
+
}
|
|
174
|
+
return registry
|
|
175
|
+
.invoke(request.id, request.args, signal)
|
|
176
|
+
.then((result) => encodeFrameworkValue(result))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function encodeValue(
|
|
180
|
+
value: FrameworkValue,
|
|
181
|
+
manifest: ClientModuleManifest | undefined,
|
|
182
|
+
ancestors: Set<object>,
|
|
183
|
+
budget: TraversalBudget,
|
|
184
|
+
depth: number,
|
|
185
|
+
): EncodedValue {
|
|
186
|
+
consumeTraversalBudget(budget, depth)
|
|
187
|
+
if (value === undefined) return { $vidact: 'undefined' }
|
|
188
|
+
if (typeof value === 'bigint') return { $vidact: 'bigint', value: String(value) }
|
|
189
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return value
|
|
190
|
+
if (typeof value === 'number') {
|
|
191
|
+
if (!Number.isFinite(value)) throw new TypeError('framework values require finite numbers')
|
|
192
|
+
return value
|
|
193
|
+
}
|
|
194
|
+
if (isClientReference(value)) {
|
|
195
|
+
if (manifest !== undefined && !manifest.has(value.id, value.exportName)) {
|
|
196
|
+
throw new Error(`client reference ${value.id}#${value.exportName} is not in the manifest`)
|
|
197
|
+
}
|
|
198
|
+
return { $vidact: 'client', id: value.id, exportName: value.exportName }
|
|
199
|
+
}
|
|
200
|
+
if (isServerFunctionReference(value)) {
|
|
201
|
+
return {
|
|
202
|
+
$vidact: 'server-function',
|
|
203
|
+
id: value.id,
|
|
204
|
+
bound: encodeValue(value.bound, manifest, ancestors, budget, depth + 1),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (typeof value !== 'object') throw new TypeError('unsupported framework value')
|
|
208
|
+
if (ancestors.has(value)) throw new TypeError('framework values cannot contain cycles')
|
|
209
|
+
ancestors.add(value)
|
|
210
|
+
try {
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return value.map((item) => encodeValue(item, manifest, ancestors, budget, depth + 1))
|
|
213
|
+
}
|
|
214
|
+
if (!isPlainObject(value)) throw new TypeError('framework objects must use a plain prototype')
|
|
215
|
+
return {
|
|
216
|
+
$vidact: 'object',
|
|
217
|
+
entries: Object.entries(value).map(([key, item]) => {
|
|
218
|
+
assertSafeKey(key)
|
|
219
|
+
return [key, encodeValue(item, manifest, ancestors, budget, depth + 1)]
|
|
220
|
+
}),
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
ancestors.delete(value)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function decodeValue(
|
|
228
|
+
value: EncodedValue,
|
|
229
|
+
manifest: ClientModuleManifest | undefined,
|
|
230
|
+
budget: TraversalBudget,
|
|
231
|
+
depth: number,
|
|
232
|
+
): FrameworkValue {
|
|
233
|
+
consumeTraversalBudget(budget, depth)
|
|
234
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') return value
|
|
235
|
+
if (typeof value === 'number') {
|
|
236
|
+
if (!Number.isFinite(value)) throw new TypeError('framework values require finite numbers')
|
|
237
|
+
return value
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
return value.map((item) => decodeValue(item, manifest, budget, depth + 1))
|
|
241
|
+
}
|
|
242
|
+
if (!isPlainObject(value)) throw new TypeError('invalid framework value')
|
|
243
|
+
const tag = value.$vidact
|
|
244
|
+
if (tag === 'undefined') return undefined
|
|
245
|
+
if (tag === 'bigint' && typeof value.value === 'string') return BigInt(value.value)
|
|
246
|
+
if (tag === 'client' && typeof value.id === 'string' && typeof value.exportName === 'string') {
|
|
247
|
+
if (manifest !== undefined && !manifest.has(value.id, value.exportName)) {
|
|
248
|
+
throw new Error(`client reference ${value.id}#${value.exportName} is not in the manifest`)
|
|
249
|
+
}
|
|
250
|
+
return createClientReference(value.id, value.exportName)
|
|
251
|
+
}
|
|
252
|
+
if (tag === 'server-function' && typeof value.id === 'string' && Array.isArray(value.bound)) {
|
|
253
|
+
return createServerFunctionReference(
|
|
254
|
+
value.id,
|
|
255
|
+
value.bound.map((item) => decodeValue(item, manifest, budget, depth + 1)),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
if (tag === 'object' && Array.isArray(value.entries)) {
|
|
259
|
+
const output: Record<string, FrameworkValue> = Object.create(null) as Record<
|
|
260
|
+
string,
|
|
261
|
+
FrameworkValue
|
|
262
|
+
>
|
|
263
|
+
for (const entry of value.entries) {
|
|
264
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string') {
|
|
265
|
+
throw new TypeError('invalid framework object entry')
|
|
266
|
+
}
|
|
267
|
+
const [key, item] = entry
|
|
268
|
+
assertSafeKey(key)
|
|
269
|
+
output[key] = decodeValue(item, manifest, budget, depth + 1)
|
|
270
|
+
}
|
|
271
|
+
return output
|
|
272
|
+
}
|
|
273
|
+
if (tag !== undefined) throw new TypeError(`invalid framework value tag ${String(tag)}`)
|
|
274
|
+
const output: Record<string, FrameworkValue> = Object.create(null) as Record<
|
|
275
|
+
string,
|
|
276
|
+
FrameworkValue
|
|
277
|
+
>
|
|
278
|
+
for (const [key, item] of Object.entries(value)) {
|
|
279
|
+
assertSafeKey(key)
|
|
280
|
+
output[key] = decodeValue(item, manifest, budget, depth + 1)
|
|
281
|
+
}
|
|
282
|
+
return output
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function assertFrameworkValue(value: unknown): asserts value is FrameworkValue {
|
|
286
|
+
encodeFrameworkValue(value as FrameworkValue)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function isClientReference(value: unknown): value is ClientReference {
|
|
290
|
+
return typeof value === 'object' && value !== null && CLIENT_REFERENCE in value
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isServerFunctionReference(value: unknown): value is ServerFunctionReference {
|
|
294
|
+
return typeof value === 'object' && value !== null && SERVER_REFERENCE in value
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isPlainObject(value: unknown): value is Record<string, FrameworkValue> {
|
|
298
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
299
|
+
const prototype = Object.getPrototypeOf(value) as unknown
|
|
300
|
+
return prototype === Object.prototype || prototype === null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function assertSafeKey(key: string): void {
|
|
304
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
305
|
+
throw new TypeError(`unsafe framework object key ${key}`)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function consumeTraversalBudget(budget: TraversalBudget, depth: number): void {
|
|
310
|
+
if (depth > MAX_FRAMEWORK_VALUE_DEPTH) {
|
|
311
|
+
throw new RangeError(`framework values cannot exceed depth ${MAX_FRAMEWORK_VALUE_DEPTH}`)
|
|
312
|
+
}
|
|
313
|
+
budget.nodes += 1
|
|
314
|
+
if (budget.nodes > MAX_FRAMEWORK_VALUE_NODES) {
|
|
315
|
+
throw new RangeError(`framework values cannot exceed ${MAX_FRAMEWORK_VALUE_NODES} nodes`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertFrameworkPayloadSize(value: string, limit: number, label: string): void {
|
|
320
|
+
if (value.length > limit) {
|
|
321
|
+
throw new RangeError(`Vidact framework payload ${label} exceeds ${limit} characters`)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function assertReferencePart(value: string, label: string): void {
|
|
326
|
+
if (value.length === 0 || value.includes('\0')) throw new TypeError(`${label} must be non-empty`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function checksum(value: string): string {
|
|
330
|
+
let hash = 0x81_1c_9d_c5
|
|
331
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
332
|
+
hash = Math.imul(hash ^ byte, 0x01_00_01_93)
|
|
333
|
+
}
|
|
334
|
+
return (hash >>> 0).toString(16).padStart(8, '0')
|
|
335
|
+
}
|