@statorjs/stator 1.3.0 → 1.4.0
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/package.json +2 -2
- package/src/client/css.d.ts +6 -0
- package/src/client/inspector-flash.css +50 -0
- package/src/client/inspector.css +196 -0
- package/src/client/inspector.ts +31 -72
- package/src/client/runtime.ts +5 -1
- package/src/compiler/compile.ts +35 -8
- package/src/compiler/lower.ts +0 -0
- package/src/compiler/virtual-code.ts +1 -1
- package/src/server/http.ts +10 -3
- package/src/server/recompute.ts +97 -10
- package/src/server/render-context.ts +123 -6
- package/src/server/render.ts +77 -4
- package/src/template/client-shell.ts +35 -9
- package/src/template/conditional.ts +2 -1
- package/src/template/defer.ts +72 -0
- package/src/template/directives/list-attr.ts +16 -0
- package/src/template/each.ts +89 -9
- package/src/template/html.ts +65 -1
- package/src/template/index.ts +3 -1
- package/src/template/read.ts +28 -4
- package/src/template/resource.ts +70 -0
package/src/template/each.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ErasedItemRenderer,
|
|
4
4
|
type ErasedKeyFn,
|
|
5
5
|
type ErasedSelector,
|
|
6
|
+
type ItemBinding,
|
|
6
7
|
keyedScopePrefix,
|
|
7
8
|
keyToken,
|
|
8
9
|
popListScope,
|
|
@@ -37,6 +38,42 @@ export function isEachResult(v: unknown): v is EachResult {
|
|
|
37
38
|
)
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* SPIKE (finding #5 / option C): the value of an item-dependent interpolation
|
|
43
|
+
* inside an `each` row — what the compiler will lower `{item.field}` to. It
|
|
44
|
+
* evaluates the selector against the *current* row's item now, and registers a
|
|
45
|
+
* per-row binding so a later content change patches this one slot in place
|
|
46
|
+
* (rather than re-rendering the row, so islands/focus survive).
|
|
47
|
+
*/
|
|
48
|
+
export interface ItemReadResult {
|
|
49
|
+
readonly __isItemRead: true
|
|
50
|
+
readonly selector: (item: unknown, index: number) => unknown
|
|
51
|
+
readonly value: unknown
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isItemReadResult(v: unknown): v is ItemReadResult {
|
|
55
|
+
return typeof v === 'object' && v !== null && (v as Record<string, unknown>).__isItemRead === true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// biome-ignore lint/suspicious/noExplicitAny: erased item selector — the item type is the each callback's, recovered at the call site
|
|
59
|
+
export function itemBind(selector: (item: any, index: number) => unknown): ItemReadResult {
|
|
60
|
+
const state = requireCurrentRenderState()
|
|
61
|
+
if (!state.currentRowBindings) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'stator: read(item, …) (itemBind) called outside an each() row render — an item ' +
|
|
64
|
+
'binding is owned by its row, which supplies the item and re-diffs the binding. ' +
|
|
65
|
+
'This happens when an item read sits inside a when()/match()/defer() arm (an arm ' +
|
|
66
|
+
're-renders on its own schedule, without the row) or outside each() entirely. ' +
|
|
67
|
+
'Use a machine read there instead.',
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
// Compute the value now (for initial render); the per-row binding is registered
|
|
71
|
+
// by html.ts, which knows the position (text span vs attribute) — same split as
|
|
72
|
+
// read() → handleRead.
|
|
73
|
+
const value = selector(state.currentItem, state.currentItemIndex ?? 0)
|
|
74
|
+
return { __isItemRead: true, selector, value }
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
export function each<T>(
|
|
41
78
|
items: readonly T[] | ReadResult<readonly T[]>,
|
|
42
79
|
fn: (item: T, index: number) => HtmlFragment,
|
|
@@ -63,8 +100,10 @@ export function each<T>(
|
|
|
63
100
|
let innerHtml: string
|
|
64
101
|
if (keyFn) {
|
|
65
102
|
const keys = coerceKeys(array, keyFn as ErasedKeyFn, slotId)
|
|
66
|
-
|
|
103
|
+
const body = renderKeyedListBody(state, slotId, array, keys, fn)
|
|
104
|
+
innerHtml = body.html
|
|
67
105
|
if (machineName && selector) {
|
|
106
|
+
const hasItemBindings = [...body.rowsByKey.values()].some((b) => b.length > 0)
|
|
68
107
|
registerBinding(state, {
|
|
69
108
|
slotId,
|
|
70
109
|
machineName,
|
|
@@ -74,10 +113,13 @@ export function each<T>(
|
|
|
74
113
|
itemRenderer: fn as ErasedItemRenderer,
|
|
75
114
|
keyFn: keyFn as ErasedKeyFn,
|
|
76
115
|
lastKeys: keys,
|
|
116
|
+
// Same opt-in as non-keyed: only track rows when there ARE item bindings.
|
|
117
|
+
rowsByKey: hasItemBindings ? body.rowsByKey : undefined,
|
|
77
118
|
})
|
|
78
119
|
}
|
|
79
120
|
} else {
|
|
80
|
-
|
|
121
|
+
const body = renderListBody(state, slotId, array, fn)
|
|
122
|
+
innerHtml = body.html
|
|
81
123
|
if (machineName && selector) {
|
|
82
124
|
registerBinding(state, {
|
|
83
125
|
slotId,
|
|
@@ -86,6 +128,10 @@ export function each<T>(
|
|
|
86
128
|
lastValue: array,
|
|
87
129
|
kind: 'list',
|
|
88
130
|
itemRenderer: fn as ErasedItemRenderer,
|
|
131
|
+
// Only opt into the granular path when there ARE item bindings —
|
|
132
|
+
// otherwise (static-capture rows) leave it undefined so recompute
|
|
133
|
+
// wholesale-re-renders as before, keeping content fresh.
|
|
134
|
+
rows: body.rows.some((r) => r.length > 0) ? body.rows : undefined,
|
|
89
135
|
})
|
|
90
136
|
}
|
|
91
137
|
}
|
|
@@ -111,23 +157,39 @@ export function renderListBody<T>(
|
|
|
111
157
|
listSlotId: SlotId,
|
|
112
158
|
items: readonly T[],
|
|
113
159
|
fn: (item: T, index: number) => HtmlFragment,
|
|
114
|
-
): string {
|
|
160
|
+
): { html: string; rows: ItemBinding[][] } {
|
|
115
161
|
unregisterBindingsForScope(state, listSlotId)
|
|
116
162
|
|
|
163
|
+
// Save/restore the ambient row context around the whole body so a nested
|
|
164
|
+
// each() (a row rendering its own list) doesn't clobber this list's row.
|
|
165
|
+
const prevItem = state.currentItem
|
|
166
|
+
const prevIndex = state.currentItemIndex
|
|
167
|
+
const prevRow = state.currentRowBindings
|
|
168
|
+
|
|
117
169
|
const chunks: string[] = []
|
|
170
|
+
const rows: ItemBinding[][] = []
|
|
118
171
|
for (let i = 0; i < items.length; i++) {
|
|
119
172
|
pushListScope(state, listSlotId, i)
|
|
173
|
+
const rowBindings: ItemBinding[] = []
|
|
174
|
+
state.currentItem = items[i]
|
|
175
|
+
state.currentItemIndex = i
|
|
176
|
+
state.currentRowBindings = rowBindings
|
|
120
177
|
try {
|
|
121
178
|
const fragment = fn(items[i]!, i)
|
|
122
179
|
if (!isHtmlFragment(fragment)) {
|
|
123
180
|
throw new Error('stator: each() callback must return an html`...` result')
|
|
124
181
|
}
|
|
125
182
|
chunks.push(fragment.html)
|
|
183
|
+
rows.push(rowBindings)
|
|
126
184
|
} finally {
|
|
127
185
|
popListScope(state)
|
|
128
186
|
}
|
|
129
187
|
}
|
|
130
|
-
|
|
188
|
+
|
|
189
|
+
state.currentItem = prevItem
|
|
190
|
+
state.currentItemIndex = prevIndex
|
|
191
|
+
state.currentRowBindings = prevRow
|
|
192
|
+
return { html: chunks.join(''), rows }
|
|
131
193
|
}
|
|
132
194
|
|
|
133
195
|
/**
|
|
@@ -173,13 +235,17 @@ export function renderKeyedListBody<T>(
|
|
|
173
235
|
items: readonly T[],
|
|
174
236
|
keys: readonly string[],
|
|
175
237
|
fn: (item: T, index: number) => HtmlFragment,
|
|
176
|
-
): string {
|
|
238
|
+
): { html: string; rowsByKey: Map<string, ItemBinding[]> } {
|
|
177
239
|
unregisterBindingsForScope(state, listSlotId)
|
|
178
240
|
const chunks: string[] = []
|
|
241
|
+
const rowsByKey = new Map<string, ItemBinding[]>()
|
|
179
242
|
for (let i = 0; i < items.length; i++) {
|
|
180
|
-
|
|
243
|
+
const key = keys[i]!
|
|
244
|
+
const row = renderKeyedItem(state, listSlotId, items[i] as T, i, key, fn)
|
|
245
|
+
chunks.push(row.html)
|
|
246
|
+
rowsByKey.set(key, row.bindings)
|
|
181
247
|
}
|
|
182
|
-
return chunks.join('')
|
|
248
|
+
return { html: chunks.join(''), rowsByKey }
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
/**
|
|
@@ -195,10 +261,21 @@ export function renderKeyedItem<T>(
|
|
|
195
261
|
index: number,
|
|
196
262
|
key: string,
|
|
197
263
|
fn: (item: T, index: number) => HtmlFragment,
|
|
198
|
-
): string {
|
|
264
|
+
): { html: string; bindings: ItemBinding[] } {
|
|
199
265
|
const token = keyToken(key)
|
|
200
266
|
unregisterBindingsForScope(state, keyedScopePrefix(listSlotId, token))
|
|
201
267
|
pushKeyedScope(state, listSlotId, token)
|
|
268
|
+
|
|
269
|
+
// Item-value binding context for this row (option C, keyed), save/restore
|
|
270
|
+
// around the body so a nested each() doesn't clobber it.
|
|
271
|
+
const prevItem = state.currentItem
|
|
272
|
+
const prevIndex = state.currentItemIndex
|
|
273
|
+
const prevRow = state.currentRowBindings
|
|
274
|
+
const bindings: ItemBinding[] = []
|
|
275
|
+
state.currentItem = item
|
|
276
|
+
state.currentItemIndex = index
|
|
277
|
+
state.currentRowBindings = bindings
|
|
278
|
+
|
|
202
279
|
let html: string
|
|
203
280
|
try {
|
|
204
281
|
const fragment = fn(item, index)
|
|
@@ -207,6 +284,9 @@ export function renderKeyedItem<T>(
|
|
|
207
284
|
}
|
|
208
285
|
html = fragment.html
|
|
209
286
|
} finally {
|
|
287
|
+
state.currentItem = prevItem
|
|
288
|
+
state.currentItemIndex = prevIndex
|
|
289
|
+
state.currentRowBindings = prevRow
|
|
210
290
|
popListScope(state)
|
|
211
291
|
}
|
|
212
292
|
if (!isSingleRootElement(html)) {
|
|
@@ -216,7 +296,7 @@ export function renderKeyedItem<T>(
|
|
|
216
296
|
`items would corrupt sibling indices`,
|
|
217
297
|
)
|
|
218
298
|
}
|
|
219
|
-
return html
|
|
299
|
+
return { html, bindings }
|
|
220
300
|
}
|
|
221
301
|
|
|
222
302
|
const VOID_TAGS = new Set([
|
package/src/template/html.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
allocSlotId,
|
|
2
3
|
type RenderState,
|
|
3
4
|
registerBinding,
|
|
4
5
|
requireCurrentRenderState,
|
|
@@ -22,12 +23,13 @@ export function raw(html: string): HtmlFragment {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
import { isBranchResult } from './conditional.ts'
|
|
26
|
+
import { isDeferResult } from './defer.ts'
|
|
25
27
|
import {
|
|
26
28
|
type DirectiveContext,
|
|
27
29
|
type DirectiveInvocation,
|
|
28
30
|
isDirectiveInvocation,
|
|
29
31
|
} from './directives/core.ts'
|
|
30
|
-
import { isEachResult } from './each.ts'
|
|
32
|
+
import { type ItemReadResult, isEachResult, isItemReadResult } from './each.ts'
|
|
31
33
|
import { isReadResult, type ReadResult } from './read.ts'
|
|
32
34
|
|
|
33
35
|
export function html(strings: TemplateStringsArray, ...values: unknown[]): HtmlFragment {
|
|
@@ -100,11 +102,26 @@ function processValue(builder: HtmlBuilder, state: RenderState, value: unknown):
|
|
|
100
102
|
return
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
if (isDeferResult(value)) {
|
|
106
|
+
if (pos.kind !== 'text') {
|
|
107
|
+
throw new Error('stator: cannot inline a defer() result outside text position')
|
|
108
|
+
}
|
|
109
|
+
builder.pushRaw(value.html)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
if (isReadResult(value)) {
|
|
104
114
|
handleRead(builder, state, value, pos)
|
|
105
115
|
return
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
// read(item, …) → itemBind: register the per-row binding by position (text span
|
|
119
|
+
// or attribute), the item analog of handleRead.
|
|
120
|
+
if (isItemReadResult(value)) {
|
|
121
|
+
handleItemRead(builder, state, value, pos)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
if (pos.kind === 'text') {
|
|
109
126
|
builder.pushRaw(escapeText(stringifyValue(value)))
|
|
110
127
|
return
|
|
@@ -180,6 +197,53 @@ function handleRead(
|
|
|
180
197
|
throw new Error(`stator: read() result cannot be interpolated at ${pos.kind} position`)
|
|
181
198
|
}
|
|
182
199
|
|
|
200
|
+
/** Register a `read(item, …)` per-row binding by position — the item analog of
|
|
201
|
+
* handleRead. Pushes onto the row's binding list (owned by the ListBinding), not
|
|
202
|
+
* state.bindings; recompute diffs it per row and emits text- or attr-op patches. */
|
|
203
|
+
function handleItemRead(
|
|
204
|
+
builder: HtmlBuilder,
|
|
205
|
+
state: RenderState,
|
|
206
|
+
r: ItemReadResult,
|
|
207
|
+
pos: ValuePosition,
|
|
208
|
+
): void {
|
|
209
|
+
const row = state.currentRowBindings
|
|
210
|
+
if (!row) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
'stator: read(item, …) interpolated outside an each() row render — an item binding ' +
|
|
213
|
+
'is owned by its row (see the itemBind ownership rule). Use a machine read here.',
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
if (pos.kind === 'text') {
|
|
217
|
+
const slotId = allocSlotId(state)
|
|
218
|
+
row.push({ kind: 'text', slotId, selector: r.selector, lastValue: r.value })
|
|
219
|
+
builder.pushRaw(`<span data-slot="${slotId}">${escapeText(stringifyValue(r.value))}</span>`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (pos.kind === 'attr-value') {
|
|
223
|
+
if (pos.hasLiteralText) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`stator: attribute "${pos.attrName}" mixes literal text with a read(item, …). ` +
|
|
226
|
+
`An attribute value must come from a single source.`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
row.push({
|
|
230
|
+
kind: 'attr',
|
|
231
|
+
attrName: pos.attrName,
|
|
232
|
+
parentId: pos.elementId,
|
|
233
|
+
selector: r.selector,
|
|
234
|
+
lastValue: r.value,
|
|
235
|
+
})
|
|
236
|
+
// Same boolean semantics as a machine attr read (see handleRead).
|
|
237
|
+
if (r.value === false || r.value === null || r.value === undefined) {
|
|
238
|
+
builder.omitCurrentAttribute()
|
|
239
|
+
} else if (r.value !== true) {
|
|
240
|
+
builder.pushRaw(escapeAttribute(sanitizeAttrValue(pos.attrName, stringifyValue(r.value))))
|
|
241
|
+
}
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`stator: read(item, …) cannot be interpolated at ${pos.kind} position`)
|
|
245
|
+
}
|
|
246
|
+
|
|
183
247
|
function stringifyValue(v: unknown): string {
|
|
184
248
|
if (v == null) return ''
|
|
185
249
|
if (typeof v === 'string') return v
|
package/src/template/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export {
|
|
|
6
6
|
renderBranchBody,
|
|
7
7
|
when,
|
|
8
8
|
} from './conditional.ts'
|
|
9
|
+
export type { DeferArms, DeferResult } from './defer.ts'
|
|
10
|
+
export { defer, isDeferResult } from './defer.ts'
|
|
9
11
|
export type {
|
|
10
12
|
Directive,
|
|
11
13
|
DirectiveContext,
|
|
@@ -21,7 +23,7 @@ export type { ClassListSpec, StyleListSpec } from './directives/list-attr.ts'
|
|
|
21
23
|
export { classList, styleList } from './directives/list-attr.ts'
|
|
22
24
|
export { on } from './directives/on.ts'
|
|
23
25
|
export type { EachResult } from './each.ts'
|
|
24
|
-
export { each, isEachResult, renderListBody } from './each.ts'
|
|
26
|
+
export { each, isEachResult, itemBind, renderListBody } from './each.ts'
|
|
25
27
|
export { html, raw } from './html.ts'
|
|
26
28
|
export type { ReadResult } from './read.ts'
|
|
27
29
|
export { isReadResult, read } from './read.ts'
|
package/src/template/read.ts
CHANGED
|
@@ -29,22 +29,46 @@ export function isReadResult(v: unknown): v is ReadResult {
|
|
|
29
29
|
export function read<TDef extends AnyMachineDef, TResult>(
|
|
30
30
|
instance: InstanceOf<TDef>,
|
|
31
31
|
selector: (instance: InstanceOf<TDef>) => TResult,
|
|
32
|
-
): ReadResult<TResult>
|
|
32
|
+
): ReadResult<TResult>
|
|
33
|
+
/**
|
|
34
|
+
* Live field of an `each` row — `read(item, (i) => i.field)`. `read()` is the
|
|
35
|
+
* one marker for live data, so an item field that changes over time is read the
|
|
36
|
+
* same way a machine value is. The compiler lowers this form to a per-row
|
|
37
|
+
* `itemBind`; it is valid only where `item` is the each callback's item param.
|
|
38
|
+
*/
|
|
39
|
+
export function read<TItem, TResult>(
|
|
40
|
+
item: TItem,
|
|
41
|
+
selector: (item: TItem) => TResult,
|
|
42
|
+
): ReadResult<TResult>
|
|
43
|
+
export function read(
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: overload implementation — the typed surface is the two signatures above
|
|
45
|
+
instance: any,
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: overload implementation — see above
|
|
47
|
+
selector: (instance: any) => unknown,
|
|
48
|
+
): ReadResult {
|
|
33
49
|
const state = requireCurrentRenderState()
|
|
34
50
|
const def = defForProxy(instance as unknown as object)
|
|
35
51
|
if (!def) {
|
|
52
|
+
// The item-read overload exists only for typing — the compiler rewrites
|
|
53
|
+
// read(item, …) to itemBind before runtime, so a non-machine here is a bug.
|
|
36
54
|
throw new Error(
|
|
37
55
|
'stator: read() must be called with a machine instance produced by the framework',
|
|
38
56
|
)
|
|
39
57
|
}
|
|
40
58
|
const slotId = allocSlotId(state)
|
|
41
|
-
|
|
59
|
+
// During a recompute-driven re-render (fan-out), resolve the CURRENT proxy for
|
|
60
|
+
// this machine so arm/list interiors see fresh state — the closure `instance`
|
|
61
|
+
// was frozen at connect-time by `rehydrate()` (FINDINGS #3). On the initial
|
|
62
|
+
// render `currentRuntime` is null and `instance` already IS the current proxy.
|
|
63
|
+
const live = state.currentRuntime?.proxyFor(def.name)
|
|
64
|
+
const source = live ?? instance
|
|
65
|
+
const value = selector(source)
|
|
42
66
|
return {
|
|
43
67
|
__isReadResult: true,
|
|
44
68
|
slotId,
|
|
45
69
|
machineName: def.name,
|
|
46
70
|
selector: selector as ErasedSelector,
|
|
47
|
-
instance,
|
|
48
|
-
value
|
|
71
|
+
instance: source,
|
|
72
|
+
value,
|
|
49
73
|
}
|
|
50
74
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type ResourceStatus = 'pending' | 'fulfilled' | 'rejected'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A peekable async value. You cannot synchronously read a native promise's
|
|
5
|
+
* settled state (`.then` is always a microtask), so `defer` wraps a thunk's
|
|
6
|
+
* result in a resource that records its settlement as it happens — letting the
|
|
7
|
+
* fill pass read `status` synchronously ("peek") at the moment it splices HTML.
|
|
8
|
+
*
|
|
9
|
+
* A non-promise thunk result fulfills immediately, so `defer` degrades to plain
|
|
10
|
+
* inline render for synchronous data.
|
|
11
|
+
*/
|
|
12
|
+
export interface Resource<T = unknown> {
|
|
13
|
+
readonly status: ResourceStatus
|
|
14
|
+
readonly value?: T
|
|
15
|
+
readonly reason?: unknown
|
|
16
|
+
/**
|
|
17
|
+
* Resolves when the resource settles (fulfilled OR rejected). Never rejects —
|
|
18
|
+
* a rejection is recorded as `status: 'rejected'` + `reason`, and `settled`
|
|
19
|
+
* still resolves, so the resolve phase can `await` a batch of resources with a
|
|
20
|
+
* plain `Promise.all` and never has to catch.
|
|
21
|
+
*/
|
|
22
|
+
readonly settled: Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPromiseLike(v: unknown): v is PromiseLike<unknown> {
|
|
26
|
+
return (
|
|
27
|
+
(typeof v === 'object' || typeof v === 'function') &&
|
|
28
|
+
v !== null &&
|
|
29
|
+
typeof (v as { then?: unknown }).then === 'function'
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a thunk's result in a peekable {@link Resource}. Synchronous values (and
|
|
35
|
+
* synchronous throws) settle immediately; a promise records its settlement on a
|
|
36
|
+
* single attached continuation.
|
|
37
|
+
*/
|
|
38
|
+
export function createResource<T>(produce: () => T | Promise<T>): Resource<T> {
|
|
39
|
+
let result: T | Promise<T>
|
|
40
|
+
try {
|
|
41
|
+
result = produce()
|
|
42
|
+
} catch (reason) {
|
|
43
|
+
// A thunk that throws synchronously is a rejected resource, not a crash.
|
|
44
|
+
return { status: 'rejected', reason, settled: Promise.resolve() }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isPromiseLike(result)) {
|
|
48
|
+
return { status: 'fulfilled', value: result, settled: Promise.resolve() }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resource: {
|
|
52
|
+
status: ResourceStatus
|
|
53
|
+
value?: T
|
|
54
|
+
reason?: unknown
|
|
55
|
+
settled: Promise<void>
|
|
56
|
+
} = { status: 'pending', settled: Promise.resolve() }
|
|
57
|
+
|
|
58
|
+
resource.settled = Promise.resolve(result).then(
|
|
59
|
+
(value) => {
|
|
60
|
+
resource.status = 'fulfilled'
|
|
61
|
+
resource.value = value
|
|
62
|
+
},
|
|
63
|
+
(reason) => {
|
|
64
|
+
resource.status = 'rejected'
|
|
65
|
+
resource.reason = reason
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return resource
|
|
70
|
+
}
|