@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.
Files changed (244) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +1 -1
  3. package/dist/actions.d.ts +32 -30
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +251 -266
  6. package/dist/actions.js.map +1 -1
  7. package/dist/async.d.ts +20 -17
  8. package/dist/async.d.ts.map +1 -1
  9. package/dist/async.js +18 -17
  10. package/dist/async.js.map +1 -1
  11. package/dist/compiled/async-resource.d.ts +9 -6
  12. package/dist/compiled/async-resource.d.ts.map +1 -1
  13. package/dist/compiled/async-resource.js +69 -77
  14. package/dist/compiled/async-resource.js.map +1 -1
  15. package/dist/compiled/core.d.ts +136 -147
  16. package/dist/compiled/core.d.ts.map +1 -1
  17. package/dist/compiled/core.js +2672 -2985
  18. package/dist/compiled/core.js.map +1 -1
  19. package/dist/compiled/types.d.ts +44 -56
  20. package/dist/compiled/types.d.ts.map +1 -1
  21. package/dist/compiled/types.js +10 -6
  22. package/dist/compiled/types.js.map +1 -1
  23. package/dist/compiled/validation.d.ts +4 -1
  24. package/dist/compiled/validation.d.ts.map +1 -1
  25. package/dist/compiled/validation.js +17 -20
  26. package/dist/compiled/validation.js.map +1 -1
  27. package/dist/component-spread.d.ts +5 -3
  28. package/dist/component-spread.d.ts.map +1 -1
  29. package/dist/component-spread.js +49 -67
  30. package/dist/component-spread.js.map +1 -1
  31. package/dist/concurrent.d.ts +20 -18
  32. package/dist/concurrent.d.ts.map +1 -1
  33. package/dist/concurrent.js +191 -210
  34. package/dist/concurrent.js.map +1 -1
  35. package/dist/direct-dom.d.ts +17 -12
  36. package/dist/direct-dom.d.ts.map +1 -1
  37. package/dist/direct-dom.js +187 -221
  38. package/dist/direct-dom.js.map +1 -1
  39. package/dist/dom/attributes.d.ts +9 -6
  40. package/dist/dom/attributes.d.ts.map +1 -1
  41. package/dist/dom/attributes.js +65 -47
  42. package/dist/dom/attributes.js.map +1 -1
  43. package/dist/dom/events.d.ts +5 -2
  44. package/dist/dom/events.d.ts.map +1 -1
  45. package/dist/dom/events.js +53 -65
  46. package/dist/dom/events.js.map +1 -1
  47. package/dist/dom/forms.d.ts +9 -6
  48. package/dist/dom/forms.d.ts.map +1 -1
  49. package/dist/dom/forms.js +152 -198
  50. package/dist/dom/forms.js.map +1 -1
  51. package/dist/dom/intrinsic.d.ts +17 -15
  52. package/dist/dom/intrinsic.d.ts.map +1 -1
  53. package/dist/dom/intrinsic.js +48 -50
  54. package/dist/dom/intrinsic.js.map +1 -1
  55. package/dist/dom/namespace.d.ts +4 -1
  56. package/dist/dom/namespace.d.ts.map +1 -1
  57. package/dist/dom/namespace.js +25 -28
  58. package/dist/dom/namespace.js.map +1 -1
  59. package/dist/dom/namespaces.d.ts +11 -8
  60. package/dist/dom/namespaces.d.ts.map +1 -1
  61. package/dist/dom/namespaces.js +14 -12
  62. package/dist/dom/namespaces.js.map +1 -1
  63. package/dist/dom/properties.d.ts +18 -16
  64. package/dist/dom/properties.d.ts.map +1 -1
  65. package/dist/dom/properties.js +273 -296
  66. package/dist/dom/properties.js.map +1 -1
  67. package/dist/dom/style-properties.d.ts +4 -1
  68. package/dist/dom/style-properties.d.ts.map +1 -1
  69. package/dist/dom/style-properties.js +49 -45
  70. package/dist/dom/style-properties.js.map +1 -1
  71. package/dist/dom/styles.d.ts +5 -2
  72. package/dist/dom/styles.d.ts.map +1 -1
  73. package/dist/dom/styles.js +47 -67
  74. package/dist/dom/styles.js.map +1 -1
  75. package/dist/dom-forms.d.ts +4 -1
  76. package/dist/dom-forms.d.ts.map +1 -1
  77. package/dist/dom-forms.js +16 -13
  78. package/dist/dom-forms.js.map +1 -1
  79. package/dist/dom-styles.d.ts +4 -1
  80. package/dist/dom-styles.d.ts.map +1 -1
  81. package/dist/dom-styles.js +10 -7
  82. package/dist/dom-styles.js.map +1 -1
  83. package/dist/framework-hydrate.d.ts +20 -17
  84. package/dist/framework-hydrate.d.ts.map +1 -1
  85. package/dist/framework-hydrate.js +128 -146
  86. package/dist/framework-hydrate.js.map +1 -1
  87. package/dist/framework-protocol.d.ts +27 -25
  88. package/dist/framework-protocol.d.ts.map +1 -1
  89. package/dist/framework-protocol.js +173 -211
  90. package/dist/framework-protocol.js.map +1 -1
  91. package/dist/framework.d.ts +34 -32
  92. package/dist/framework.d.ts.map +1 -1
  93. package/dist/framework.js +74 -93
  94. package/dist/framework.js.map +1 -1
  95. package/dist/hydrate.d.ts +11 -8
  96. package/dist/hydrate.d.ts.map +1 -1
  97. package/dist/hydrate.js +13 -9
  98. package/dist/hydrate.js.map +1 -1
  99. package/dist/hydration-bridge.d.ts +70 -58
  100. package/dist/hydration-bridge.d.ts.map +1 -1
  101. package/dist/hydration-bridge.js +77 -66
  102. package/dist/hydration-bridge.js.map +1 -1
  103. package/dist/hydration.d.ts +54 -28
  104. package/dist/hydration.d.ts.map +1 -1
  105. package/dist/hydration.js +450 -463
  106. package/dist/hydration.js.map +1 -1
  107. package/dist/index.d.ts +13 -13
  108. package/dist/index.js +11 -13
  109. package/dist/indexed-list.d.ts +8 -5
  110. package/dist/indexed-list.d.ts.map +1 -1
  111. package/dist/indexed-list.js +14 -7
  112. package/dist/indexed-list.js.map +1 -1
  113. package/dist/jsx-runtime.d.ts +10 -8
  114. package/dist/jsx-runtime.d.ts.map +1 -1
  115. package/dist/jsx-runtime.js +28 -23
  116. package/dist/jsx-runtime.js.map +1 -1
  117. package/dist/keyed-list.d.ts +13 -14
  118. package/dist/keyed-list.d.ts.map +1 -1
  119. package/dist/keyed-list.js +132 -160
  120. package/dist/keyed-list.js.map +1 -1
  121. package/dist/metadata.d.ts +6 -3
  122. package/dist/metadata.d.ts.map +1 -1
  123. package/dist/metadata.js +131 -164
  124. package/dist/metadata.js.map +1 -1
  125. package/dist/nested-prop.d.ts +4 -2
  126. package/dist/nested-prop.d.ts.map +1 -1
  127. package/dist/nested-prop.js +17 -16
  128. package/dist/nested-prop.js.map +1 -1
  129. package/dist/profiling.d.ts +11 -8
  130. package/dist/profiling.d.ts.map +1 -1
  131. package/dist/profiling.js +17 -17
  132. package/dist/profiling.js.map +1 -1
  133. package/dist/protocol.d.ts +5 -2
  134. package/dist/protocol.d.ts.map +1 -1
  135. package/dist/protocol.js +8 -5
  136. package/dist/protocol.js.map +1 -1
  137. package/dist/raw-html.d.ts +5 -2
  138. package/dist/raw-html.d.ts.map +1 -1
  139. package/dist/raw-html.js +86 -146
  140. package/dist/raw-html.js.map +1 -1
  141. package/dist/ref.d.ts +8 -3
  142. package/dist/ref.d.ts.map +1 -1
  143. package/dist/ref.js +10 -2
  144. package/dist/ref.js.map +1 -1
  145. package/dist/renderable-protocol.d.ts +26 -8
  146. package/dist/renderable-protocol.d.ts.map +1 -1
  147. package/dist/renderable-protocol.js +30 -9
  148. package/dist/renderable-protocol.js.map +1 -1
  149. package/dist/renderable.d.ts +27 -22
  150. package/dist/renderable.d.ts.map +1 -1
  151. package/dist/renderable.js +141 -133
  152. package/dist/renderable.js.map +1 -1
  153. package/dist/retained-ui.d.ts +9 -6
  154. package/dist/retained-ui.d.ts.map +1 -1
  155. package/dist/retained-ui.js +12 -10
  156. package/dist/retained-ui.js.map +1 -1
  157. package/dist/root.d.ts +18 -15
  158. package/dist/root.d.ts.map +1 -1
  159. package/dist/root.js +73 -85
  160. package/dist/root.js.map +1 -1
  161. package/dist/scheduler.d.ts +10 -8
  162. package/dist/scheduler.d.ts.map +1 -1
  163. package/dist/scheduler.js +97 -105
  164. package/dist/scheduler.js.map +1 -1
  165. package/dist/server-framework.d.ts +45 -42
  166. package/dist/server-framework.d.ts.map +1 -1
  167. package/dist/server-framework.js +209 -217
  168. package/dist/server-framework.js.map +1 -1
  169. package/dist/server.d.ts +130 -128
  170. package/dist/server.d.ts.map +1 -1
  171. package/dist/server.js +603 -710
  172. package/dist/server.js.map +1 -1
  173. package/dist/shared/promise.d.ts +6 -3
  174. package/dist/shared/promise.d.ts.map +1 -1
  175. package/dist/shared/promise.js +21 -19
  176. package/dist/shared/promise.js.map +1 -1
  177. package/dist/source-mask.d.ts +9 -6
  178. package/dist/source-mask.d.ts.map +1 -1
  179. package/dist/source-mask.js +45 -63
  180. package/dist/source-mask.js.map +1 -1
  181. package/dist/spread.d.ts +5 -2
  182. package/dist/spread.d.ts.map +1 -1
  183. package/dist/spread.js +79 -99
  184. package/dist/spread.js.map +1 -1
  185. package/dist/state-slot.d.ts +18 -15
  186. package/dist/state-slot.d.ts.map +1 -1
  187. package/dist/state-slot.js +43 -42
  188. package/dist/state-slot.js.map +1 -1
  189. package/dist/testing.d.ts +9 -6
  190. package/dist/testing.d.ts.map +1 -1
  191. package/dist/testing.js +16 -12
  192. package/dist/testing.js.map +1 -1
  193. package/package.json +8 -7
  194. package/src/actions.ts +415 -0
  195. package/src/async.ts +78 -0
  196. package/src/compiled/async-resource.ts +113 -0
  197. package/src/compiled/core.ts +4041 -0
  198. package/src/compiled/types.ts +96 -0
  199. package/src/compiled/validation.ts +23 -0
  200. package/src/component-spread.ts +86 -0
  201. package/src/concurrent.ts +310 -0
  202. package/src/direct-dom.ts +365 -0
  203. package/src/dom/attributes.ts +55 -0
  204. package/src/dom/events.ts +93 -0
  205. package/src/dom/forms.ts +220 -0
  206. package/src/dom/intrinsic.ts +94 -0
  207. package/src/dom/namespace.ts +30 -0
  208. package/src/dom/namespaces.ts +14 -0
  209. package/src/dom/properties.ts +355 -0
  210. package/src/dom/style-properties.ts +46 -0
  211. package/src/dom/styles.ts +86 -0
  212. package/src/dom-forms.ts +23 -0
  213. package/src/dom-styles.ts +11 -0
  214. package/src/env.d.ts +6 -0
  215. package/src/framework-hydrate.ts +224 -0
  216. package/src/framework-protocol.ts +335 -0
  217. package/src/framework.ts +161 -0
  218. package/src/hydrate.ts +38 -0
  219. package/src/hydration-bridge.ts +196 -0
  220. package/src/hydration.ts +642 -0
  221. package/src/index.ts +92 -0
  222. package/src/indexed-list.ts +20 -0
  223. package/src/jsx-runtime.ts +55 -0
  224. package/src/keyed-list.ts +233 -0
  225. package/src/metadata.ts +212 -0
  226. package/src/nested-prop.ts +27 -0
  227. package/src/profiling.ts +43 -0
  228. package/src/protocol.ts +8 -0
  229. package/src/raw-html.ts +206 -0
  230. package/src/ref.ts +13 -0
  231. package/src/renderable-protocol.ts +63 -0
  232. package/src/renderable.ts +283 -0
  233. package/src/retained-ui.ts +19 -0
  234. package/src/root.ts +122 -0
  235. package/src/scheduler.ts +136 -0
  236. package/src/server-framework.ts +397 -0
  237. package/src/server.ts +1160 -0
  238. package/src/shared/promise.ts +28 -0
  239. package/src/source-mask.ts +83 -0
  240. package/src/spread.ts +131 -0
  241. package/src/state-slot.ts +76 -0
  242. package/src/testing.ts +27 -0
  243. package/dist/index.d.ts.map +0 -1
  244. 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
+ }