@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.
- package/CHANGELOG.md +73 -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 +148 -147
- package/dist/compiled/core.d.ts.map +1 -1
- package/dist/compiled/core.js +3064 -2987
- 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 +279 -222
- 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 +8 -2
- package/dist/dom/events.d.ts.map +1 -1
- package/dist/dom/events.js +118 -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 +17 -14
- package/dist/keyed-list.d.ts.map +1 -1
- package/dist/keyed-list.js +244 -161
- 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 +28 -22
- package/dist/renderable.d.ts.map +1 -1
- package/dist/renderable.js +152 -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 +10 -6
- package/dist/source-mask.d.ts.map +1 -1
- package/dist/source-mask.js +60 -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 +29 -15
- package/dist/state-slot.d.ts.map +1 -1
- package/dist/state-slot.js +73 -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 +4647 -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 +491 -0
- package/src/dom/attributes.ts +55 -0
- package/src/dom/events.ts +179 -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 +202 -0
- package/src/hydration.ts +644 -0
- package/src/index.ts +94 -0
- package/src/indexed-list.ts +20 -0
- package/src/jsx-runtime.ts +55 -0
- package/src/keyed-list.ts +379 -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 +313 -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 +103 -0
- package/src/spread.ts +131 -0
- package/src/state-slot.ts +132 -0
- package/src/testing.ts +27 -0
- package/dist/index.d.ts.map +0 -1
- 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
|
+
}
|