@vidact/runtime 0.2.0-beta.0 → 0.2.0-beta.11

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 +73 -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 +148 -147
  16. package/dist/compiled/core.d.ts.map +1 -1
  17. package/dist/compiled/core.js +3064 -2987
  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 +279 -222
  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 +8 -2
  44. package/dist/dom/events.d.ts.map +1 -1
  45. package/dist/dom/events.js +118 -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 +17 -14
  118. package/dist/keyed-list.d.ts.map +1 -1
  119. package/dist/keyed-list.js +244 -161
  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 +28 -22
  150. package/dist/renderable.d.ts.map +1 -1
  151. package/dist/renderable.js +152 -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 +10 -6
  178. package/dist/source-mask.d.ts.map +1 -1
  179. package/dist/source-mask.js +60 -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 +29 -15
  186. package/dist/state-slot.d.ts.map +1 -1
  187. package/dist/state-slot.js +73 -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 +4647 -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 +491 -0
  203. package/src/dom/attributes.ts +55 -0
  204. package/src/dom/events.ts +179 -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 +202 -0
  220. package/src/hydration.ts +644 -0
  221. package/src/index.ts +94 -0
  222. package/src/indexed-list.ts +20 -0
  223. package/src/jsx-runtime.ts +55 -0
  224. package/src/keyed-list.ts +379 -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 +313 -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 +103 -0
  240. package/src/spread.ts +131 -0
  241. package/src/state-slot.ts +132 -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
package/src/actions.ts ADDED
@@ -0,0 +1,415 @@
1
+ import {
2
+ captureCompiledTask,
3
+ createContext,
4
+ createCompiledExternalStore,
5
+ createCompiledState,
6
+ registerCompiledCleanup,
7
+ runWithCompiledContext,
8
+ useContext,
9
+ } from './compiled/core.ts'
10
+ import type { CompiledScope } from './compiled/types.ts'
11
+ import {
12
+ registerTransitionAborter,
13
+ registerTransitionFinalizer,
14
+ registerTransitionSettlement,
15
+ runUrgentUpdate,
16
+ startIndependentTransition,
17
+ } from './concurrent.ts'
18
+ import { h, type DirectChild, type DirectProps } from './direct-dom.ts'
19
+ import { resetActionForm } from './dom/forms.ts'
20
+ import { installDomPropHandler } from './dom/properties.ts'
21
+ import type { SourceMask } from './source-mask.ts'
22
+ import type { StateSlot } from './state-slot.ts'
23
+
24
+ const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
25
+ const FORM_ACTION_EXECUTE = Symbol(DEV ? 'Vidact.FormActionExecute' : undefined)
26
+
27
+ export interface FormStatus {
28
+ readonly pending: boolean
29
+ readonly data: FormData | null
30
+ readonly method: 'get' | 'post'
31
+ readonly action: FunctionFormAction | null
32
+ }
33
+
34
+ export type FunctionFormAction = (data: FormData) => unknown | PromiseLike<unknown>
35
+
36
+ type FormStatusStore = {
37
+ current: FormStatus
38
+ readonly listeners: Set<() => void>
39
+ }
40
+
41
+ type ActionFrame = {
42
+ readonly settlements: Set<() => void>
43
+ }
44
+
45
+ type ActionDispatch<Payload> = {
46
+ (payload: Payload): void
47
+ readonly permalink?: string
48
+ readonly [FORM_ACTION_EXECUTE]: (payload: Payload) => Promise<void>
49
+ }
50
+
51
+ type ActionQueueEntry<Payload> = {
52
+ readonly payload: Payload
53
+ readonly reportError: boolean
54
+ readonly resolve: () => void
55
+ readonly reject: (error: unknown) => void
56
+ }
57
+
58
+ type OptimisticLayer<Update> = {
59
+ readonly frame: object
60
+ readonly update: Update
61
+ settling: boolean
62
+ }
63
+
64
+ export interface OptimisticSlot<Value, Update> {
65
+ readonly get: () => Value
66
+ readonly set: (update: Update) => void
67
+ }
68
+
69
+ const defaultFormStatus: FormStatus = {
70
+ pending: false,
71
+ data: null,
72
+ method: 'get',
73
+ action: null,
74
+ }
75
+ const functionActions = new WeakMap<Element, FunctionFormAction>()
76
+ const formStatusContext = createContext<FormStatusStore | undefined>(undefined)
77
+ let actionDomInstalled = false
78
+ let activeActionFrame: ActionFrame | undefined
79
+
80
+ export function createCompiledActionState<State, Payload>(
81
+ scope: CompiledScope,
82
+ stateSource: SourceMask,
83
+ pendingSource: SourceMask,
84
+ action: (previousState: State, payload: Payload) => State | PromiseLike<State>,
85
+ initialState: State,
86
+ permalink?: string,
87
+ ): {
88
+ readonly get: () => { readonly value: State; readonly pending: boolean }
89
+ readonly set: ActionDispatch<Payload>
90
+ } {
91
+ const state = createCompiledState(scope, stateSource, () => initialState)
92
+ const pending = createCompiledState(scope, pendingSource, false)
93
+ const task = captureCompiledTask(scope)
94
+ const queue: ActionQueueEntry<Payload>[] = []
95
+ let running = false
96
+
97
+ const enqueue = (payload: Payload, reportError: boolean): Promise<void> => {
98
+ runUrgentUpdate(() => pending.replace(true))
99
+ return new Promise<void>((resolve, reject) => {
100
+ queue.push({ payload, reportError, resolve, reject })
101
+ processNext()
102
+ })
103
+ }
104
+
105
+ const dispatch = ((payload: Payload): void => {
106
+ void enqueue(payload, true)
107
+ }) as ActionDispatch<Payload>
108
+ Object.defineProperty(dispatch, FORM_ACTION_EXECUTE, {
109
+ value: (payload: Payload) => enqueue(payload, false),
110
+ })
111
+ if (permalink !== undefined) Object.defineProperty(dispatch, 'permalink', { value: permalink })
112
+
113
+ const processNext = (): void => {
114
+ if (running) return
115
+ const entry = queue.shift()
116
+ if (entry === undefined) {
117
+ runUrgentUpdate(() => pending.replace(false))
118
+ return
119
+ }
120
+ running = true
121
+ const frame = createActionFrame()
122
+ let result: State | PromiseLike<State>
123
+ const previousFrame = activeActionFrame
124
+ activeActionFrame = frame
125
+ try {
126
+ result = action(state.get(), entry.payload)
127
+ } catch (error) {
128
+ activeActionFrame = previousFrame
129
+ finishFailure(entry, frame, error)
130
+ return
131
+ }
132
+ activeActionFrame = previousFrame
133
+ void Promise.resolve(result).then(
134
+ (nextState) => {
135
+ if (task.disposed()) {
136
+ entry.resolve()
137
+ running = false
138
+ return
139
+ }
140
+ runActionTransition(
141
+ () => {
142
+ state.replace(nextState)
143
+ settleActionFrame(frame)
144
+ },
145
+ () => {
146
+ entry.resolve()
147
+ running = false
148
+ processNext()
149
+ },
150
+ )
151
+ },
152
+ (error: unknown) => finishFailure(entry, frame, error),
153
+ )
154
+ }
155
+
156
+ const finishFailure = (
157
+ entry: ActionQueueEntry<Payload>,
158
+ frame: ActionFrame,
159
+ error: unknown,
160
+ ): void => {
161
+ if (task.disposed()) {
162
+ entry.resolve()
163
+ running = false
164
+ return
165
+ }
166
+ runActionTransition(
167
+ () => settleActionFrame(frame),
168
+ () => {
169
+ running = false
170
+ if (entry.reportError) {
171
+ entry.resolve()
172
+ task.report(error)
173
+ } else {
174
+ entry.reject(error)
175
+ }
176
+ processNext()
177
+ },
178
+ )
179
+ }
180
+
181
+ return {
182
+ get: () => ({ value: state.get(), pending: pending.get() }),
183
+ set: dispatch,
184
+ }
185
+ }
186
+
187
+ export function createCompiledOptimistic<Value, Update = Value>(
188
+ scope: CompiledScope,
189
+ reads: SourceMask,
190
+ writes: SourceMask,
191
+ evaluate: () => Value,
192
+ reducer: (current: Value, update: Update) => Value = (_current, update) =>
193
+ update as unknown as Value,
194
+ ): OptimisticSlot<Value, Update> {
195
+ const slot = createCompiledState(scope, writes, () => evaluate())
196
+ let layers: OptimisticLayer<Update>[] = []
197
+ const compute = (items = layers): Value =>
198
+ items.reduce(
199
+ (value, layer) => (layer.settling ? value : reducer(value, layer.update)),
200
+ evaluate(),
201
+ )
202
+ scope[0](reads, () => slot.replace(compute()))
203
+
204
+ const add = (update: Update): void => {
205
+ const frame = activeActionFrame ?? {}
206
+ const layer: OptimisticLayer<Update> = { frame, update, settling: false }
207
+ layers.push(layer)
208
+ try {
209
+ runUrgentUpdate(() => slot.replace(compute()))
210
+ } catch (error) {
211
+ layers = layers.filter((item) => item !== layer)
212
+ throw error
213
+ }
214
+ const settle = (): void => settleOptimisticFrame(frame)
215
+ if (activeActionFrame !== undefined) {
216
+ activeActionFrame.settlements.add(settle)
217
+ return
218
+ }
219
+ if (registerTransitionSettlement(() => startIndependentTransition(settle))) return
220
+ layers = layers.filter((item) => item !== layer)
221
+ runUrgentUpdate(() => slot.replace(compute()))
222
+ throw new Error(DEV ? 'optimistic updates must run inside an Action or transition' : 'V035')
223
+ }
224
+
225
+ const settleOptimisticFrame = (frame: object): void => {
226
+ const settling = layers.filter((layer) => layer.frame === frame)
227
+ if (settling.length === 0) return
228
+ for (const layer of settling) layer.settling = true
229
+ slot.replace(compute())
230
+ if (
231
+ registerTransitionFinalizer(() => {
232
+ layers = layers.filter((layer) => layer.frame !== frame)
233
+ })
234
+ ) {
235
+ registerTransitionAborter(() => {
236
+ for (const layer of settling) layer.settling = false
237
+ })
238
+ return
239
+ }
240
+ layers = layers.filter((layer) => layer.frame !== frame)
241
+ }
242
+
243
+ return { get: slot.get, set: add }
244
+ }
245
+
246
+ export function createCompiledFormStatus(
247
+ scope: CompiledScope,
248
+ sourceMask: SourceMask,
249
+ ): StateSlot<FormStatus> {
250
+ const store = useContext(formStatusContext)
251
+ if (store === undefined) return createCompiledState(scope, sourceMask, () => defaultFormStatus)
252
+ return createCompiledExternalStore(
253
+ scope,
254
+ sourceMask,
255
+ (notify) => {
256
+ store.listeners.add(notify)
257
+ return () => store.listeners.delete(notify)
258
+ },
259
+ () => store.current,
260
+ )
261
+ }
262
+
263
+ export function ActionForm(props: DirectProps): DirectChild {
264
+ ensureActionDom()
265
+ const task = captureCompiledTask()
266
+ const store: FormStatusStore = { current: defaultFormStatus, listeners: new Set() }
267
+ const children = normalizeChildren(props?.children)
268
+ const form = runWithCompiledContext(formStatusContext, store, () =>
269
+ h('form', props, ...children),
270
+ ) as HTMLFormElement
271
+ let activeSubmissions = 0
272
+ let latestSubmission = 0
273
+ let latestSucceeded = false
274
+ const submit = (event: SubmitEvent): void => {
275
+ const submitter = event.submitter
276
+ const hasSubmitterOverride =
277
+ submitter instanceof Element &&
278
+ (functionActions.has(submitter) || submitter.hasAttribute('formaction'))
279
+ const action = hasSubmitterOverride
280
+ ? functionActions.get(submitter as Element)
281
+ : functionActions.get(form)
282
+ if (action === undefined) return
283
+ event.preventDefault()
284
+ const submission = latestSubmission + 1
285
+ latestSubmission = submission
286
+ activeSubmissions += 1
287
+ const data = createFormData(form, submitter)
288
+ publishFormStatus(store, {
289
+ pending: true,
290
+ data,
291
+ method: 'post',
292
+ action,
293
+ })
294
+ const frame = createActionFrame()
295
+ let result: unknown
296
+ const previousFrame = activeActionFrame
297
+ activeActionFrame = frame
298
+ try {
299
+ result = executeFormAction(action, data)
300
+ } catch (error) {
301
+ activeActionFrame = previousFrame
302
+ finishFormAction(submission, false, frame, error)
303
+ return
304
+ }
305
+ activeActionFrame = previousFrame
306
+ void Promise.resolve(result).then(
307
+ () => finishFormAction(submission, true, frame),
308
+ (error: unknown) => finishFormAction(submission, false, frame, error),
309
+ )
310
+ }
311
+ const finishFormAction = (
312
+ submission: number,
313
+ succeeded: boolean,
314
+ frame: ActionFrame,
315
+ error?: unknown,
316
+ ): void => {
317
+ if (task.disposed()) return
318
+ runActionTransition(
319
+ () => settleActionFrame(frame),
320
+ () => {
321
+ activeSubmissions -= 1
322
+ if (submission === latestSubmission) latestSucceeded = succeeded
323
+ if (activeSubmissions === 0) {
324
+ if (latestSucceeded) resetActionForm(form)
325
+ publishFormStatus(store, defaultFormStatus)
326
+ }
327
+ if (!succeeded) task.report(error)
328
+ },
329
+ )
330
+ }
331
+ form.addEventListener('submit', submit)
332
+ registerCompiledCleanup(() => form.removeEventListener('submit', submit))
333
+ return form
334
+ }
335
+
336
+ export function compiledFormAction<Value>(value: Value): Value {
337
+ ensureActionDom()
338
+ return value
339
+ }
340
+
341
+ export function useActionState(): never {
342
+ throw new Error(DEV ? 'useActionState requires compiler lowering' : 'V036')
343
+ }
344
+
345
+ export function useOptimistic(): never {
346
+ throw new Error(DEV ? 'useOptimistic requires compiler lowering' : 'V037')
347
+ }
348
+
349
+ export function useFormStatus(): never {
350
+ throw new Error(DEV ? 'useFormStatus requires compiler lowering' : 'V038')
351
+ }
352
+
353
+ function createActionFrame(): ActionFrame {
354
+ return { settlements: new Set() }
355
+ }
356
+
357
+ function settleActionFrame(frame: ActionFrame): void {
358
+ for (const settle of frame.settlements) settle()
359
+ if (!registerTransitionFinalizer(() => frame.settlements.clear())) frame.settlements.clear()
360
+ }
361
+
362
+ function runActionTransition(stage: () => void, complete: () => void): void {
363
+ let committed = false
364
+ startIndependentTransition(() => {
365
+ stage()
366
+ registerTransitionFinalizer(() => {
367
+ committed = true
368
+ })
369
+ registerTransitionSettlement(() => {
370
+ if (committed) {
371
+ complete()
372
+ } else {
373
+ queueMicrotask(() => runActionTransition(stage, complete))
374
+ }
375
+ })
376
+ })
377
+ }
378
+
379
+ function ensureActionDom(): void {
380
+ if (actionDomInstalled) return
381
+ actionDomInstalled = true
382
+ installDomPropHandler((element, name, value) => {
383
+ const valid =
384
+ (name === 'action' && element instanceof HTMLFormElement) ||
385
+ (name === 'formAction' &&
386
+ (element instanceof HTMLButtonElement || element instanceof HTMLInputElement))
387
+ if (!valid) return false
388
+ if (typeof value !== 'function') {
389
+ functionActions.delete(element)
390
+ return false
391
+ }
392
+ functionActions.set(element, value as FunctionFormAction)
393
+ element.removeAttribute(name === 'action' ? 'action' : 'formaction')
394
+ return true
395
+ })
396
+ }
397
+
398
+ function executeFormAction(action: FunctionFormAction, data: FormData): unknown {
399
+ const specialized = (action as Partial<ActionDispatch<FormData>>)[FORM_ACTION_EXECUTE]
400
+ return specialized === undefined ? action(data) : specialized(data)
401
+ }
402
+
403
+ function createFormData(form: HTMLFormElement, submitter: HTMLElement | null): FormData {
404
+ return submitter === null ? new FormData(form) : new FormData(form, submitter)
405
+ }
406
+
407
+ function publishFormStatus(store: FormStatusStore, status: FormStatus): void {
408
+ store.current = status
409
+ for (const listener of store.listeners) listener()
410
+ }
411
+
412
+ function normalizeChildren(value: unknown): DirectChild[] {
413
+ if (value === undefined) return []
414
+ return (Array.isArray(value) ? value : [value]) as DirectChild[]
415
+ }
package/src/async.ts ADDED
@@ -0,0 +1,78 @@
1
+ // oxlint-disable-next-line typescript/triple-slash-reference -- Include compiler feature defines.
2
+ /// <reference path="./env.d.ts" />
3
+
4
+ import {
5
+ createResource as createAsyncResource,
6
+ lazy as createLazyComponent,
7
+ useAsync,
8
+ } from './compiled/async-resource.ts'
9
+ import {
10
+ Suspense as createSuspenseComponent,
11
+ createCompiledAsync as createAsyncSlot,
12
+ suspense as createSuspenseBinding,
13
+ } from './compiled/core.ts'
14
+ import {
15
+ ASYNC_RESOURCE,
16
+ type AsyncResourceState,
17
+ type CompiledContext,
18
+ type CompiledRenderValue,
19
+ type CompiledRenderValue as LazyRenderValue,
20
+ type CompiledScope,
21
+ type StructuralBinding,
22
+ } from './compiled/types.ts'
23
+ import type { SourceMask } from './source-mask.ts'
24
+ import type { StateSlot } from './state-slot.ts'
25
+
26
+ export interface AsyncResource<Value> {
27
+ readonly [ASYNC_RESOURCE]: AsyncResourceState<Value>
28
+ }
29
+
30
+ export interface ResourceOptions {
31
+ readonly cancel?: () => void
32
+ }
33
+
34
+ export interface LazyModule<Props extends Record<string, unknown>> {
35
+ readonly default: (props: Props) => LazyRenderValue
36
+ }
37
+
38
+ export function createResource<Value>(
39
+ input: PromiseLike<Value>,
40
+ options: ResourceOptions = {},
41
+ ): AsyncResource<Value> {
42
+ return createAsyncResource(input, options)
43
+ }
44
+
45
+ export function use<Value>(
46
+ input: CompiledContext<Value> | AsyncResource<Value> | PromiseLike<Value>,
47
+ ): Value {
48
+ return useAsync(input)
49
+ }
50
+
51
+ export function lazy<Props extends Record<string, unknown>>(
52
+ load: () => PromiseLike<LazyModule<Props>>,
53
+ ): (props: Props) => CompiledRenderValue {
54
+ return createLazyComponent(load)
55
+ }
56
+
57
+ export function suspense(
58
+ render: () => CompiledRenderValue,
59
+ fallback: () => CompiledRenderValue,
60
+ ): StructuralBinding {
61
+ return createSuspenseBinding(render, fallback)
62
+ }
63
+
64
+ export function createCompiledAsync<Value>(
65
+ scope: CompiledScope,
66
+ reads: SourceMask,
67
+ writes: SourceMask,
68
+ evaluate: () => CompiledContext<Value> | AsyncResource<Value> | PromiseLike<Value>,
69
+ ): StateSlot<Value> {
70
+ return createAsyncSlot(scope, reads, writes, evaluate)
71
+ }
72
+
73
+ export function Suspense(props: {
74
+ readonly children?: (() => CompiledRenderValue) | readonly [() => CompiledRenderValue]
75
+ readonly fallback: () => CompiledRenderValue
76
+ }): StructuralBinding {
77
+ return createSuspenseComponent(props)
78
+ }
@@ -0,0 +1,113 @@
1
+ import { isPromiseLike } from '../shared/promise.ts'
2
+ import { DEV, isCompiledContext, useContext } from './core.ts'
3
+ import {
4
+ ASYNC_RESOURCE,
5
+ SUSPENSION,
6
+ type AsyncResource,
7
+ type AsyncResourceState,
8
+ type CompiledContext,
9
+ type CompiledRenderValue,
10
+ type LazyModule,
11
+ type ResourceOptions,
12
+ type Suspension,
13
+ } from './types.ts'
14
+
15
+ const noop = (): void => {}
16
+ const promiseResources = new WeakMap<object, AsyncResource<unknown>>()
17
+
18
+ export function createResource<Value>(
19
+ input: PromiseLike<Value>,
20
+ options: ResourceOptions = {},
21
+ ): AsyncResource<Value> {
22
+ if (!isPromiseLike(input)) {
23
+ throw new TypeError(DEV ? 'createResource requires a promise-like value' : 'V029')
24
+ }
25
+ const existing = promiseResources.get(input as object) as AsyncResource<Value> | undefined
26
+ if (existing !== undefined) return existing
27
+ const state: AsyncResourceState<Value> = {
28
+ status: 'pending',
29
+ value: undefined,
30
+ reason: undefined,
31
+ listeners: new Set(),
32
+ cancel: options.cancel,
33
+ subscribers: 0,
34
+ }
35
+ const resource: AsyncResource<Value> = { [ASYNC_RESOURCE]: state }
36
+ promiseResources.set(input as object, resource as AsyncResource<unknown>)
37
+ void Promise.resolve(input).then(
38
+ (value) => settleResource(state, 'fulfilled', value),
39
+ (reason: unknown) => settleResource(state, 'rejected', reason),
40
+ )
41
+ return resource
42
+ }
43
+
44
+ export function useAsync<Value>(
45
+ input: CompiledContext<Value> | AsyncResource<Value> | PromiseLike<Value>,
46
+ ): Value {
47
+ if (isCompiledContext<Value>(input)) return useContext(input)
48
+ return readResource(isAsyncResource<Value>(input) ? input : createResource(input))
49
+ }
50
+
51
+ export function lazy<Props extends Record<string, unknown>>(
52
+ load: () => PromiseLike<LazyModule<Props>>,
53
+ ): (props: Props) => CompiledRenderValue {
54
+ let resource: AsyncResource<LazyModule<Props>> | undefined
55
+ return (props) => {
56
+ resource ??= createResource(load())
57
+ const module = readResource(resource)
58
+ if (typeof module?.default !== 'function') {
59
+ throw new TypeError(DEV ? 'lazy loader must resolve to a default component export' : 'V030')
60
+ }
61
+ return module.default(props)
62
+ }
63
+ }
64
+
65
+ function isAsyncResource<Value>(value: unknown): value is AsyncResource<Value> {
66
+ return typeof value === 'object' && value !== null && ASYNC_RESOURCE in value
67
+ }
68
+
69
+ export function isSuspension(value: unknown): value is Suspension {
70
+ return typeof value === 'object' && value !== null && SUSPENSION in value
71
+ }
72
+
73
+ function readResource<Value>(resource: AsyncResource<Value>): Value {
74
+ const state = resource[ASYNC_RESOURCE]
75
+ if (state.status === 'fulfilled') return state.value as Value
76
+ if (state.status === 'rejected') throw state.reason
77
+ throw { [SUSPENSION]: true, resource } satisfies Suspension
78
+ }
79
+
80
+ export function subscribeResource(
81
+ resource: AsyncResource<unknown>,
82
+ listener: () => void,
83
+ ): () => void {
84
+ const state = resource[ASYNC_RESOURCE]
85
+ if (state.status !== 'pending') {
86
+ queueMicrotask(listener)
87
+ return noop
88
+ }
89
+ let active = true
90
+ state.subscribers += 1
91
+ state.listeners.add(listener)
92
+ return () => {
93
+ if (!active) return
94
+ active = false
95
+ if (state.listeners.delete(listener)) state.subscribers -= 1
96
+ if (state.status === 'pending' && state.subscribers === 0) state.cancel?.()
97
+ }
98
+ }
99
+
100
+ function settleResource<Value>(
101
+ state: AsyncResourceState<Value>,
102
+ status: 'fulfilled' | 'rejected',
103
+ result: Value | unknown,
104
+ ): void {
105
+ if (state.status !== 'pending') return
106
+ state.status = status
107
+ if (status === 'fulfilled') state.value = result as Value
108
+ else state.reason = result
109
+ const listeners = [...state.listeners]
110
+ state.listeners.clear()
111
+ state.subscribers = 0
112
+ for (const listener of listeners) listener()
113
+ }