@async/framework 0.6.0 → 0.8.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/CHANGELOG.md +31 -0
- package/README.md +93 -15
- package/{framework.d.ts → browser.d.ts} +71 -4
- package/{framework.js → browser.js} +791 -147
- package/browser.min.js +1 -0
- package/browser.ts +4781 -0
- package/{framework.umd.js → browser.umd.js} +791 -147
- package/browser.umd.min.js +1 -0
- package/package.json +45 -30
- package/server.d.ts +640 -0
- package/src/app.js +143 -12
- package/src/async-signal.js +32 -4
- package/src/browser.js +15 -0
- package/src/cache.js +27 -3
- package/src/component.js +42 -7
- package/src/index.js +5 -2
- package/src/loader.js +42 -10
- package/src/request-context.js +40 -0
- package/src/router.js +113 -16
- package/src/scheduler.js +296 -0
- package/src/server-entry.js +20 -0
- package/src/server-registry.js +97 -0
- package/src/server.js +49 -89
- package/src/signals.js +38 -6
- package/framework.min.js +0 -3648
- package/framework.ts +0 -3
- package/framework.umd.min.js +0 -3671
package/src/scheduler.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const defaultPhases = ["binding", "lifecycle", "effect", "async", "post"];
|
|
2
|
+
|
|
3
|
+
export function createScheduler(options = {}) {
|
|
4
|
+
const phases = [...(options.phases ?? defaultPhases)];
|
|
5
|
+
const queues = new Map(phases.map((phase) => [phase, []]));
|
|
6
|
+
const keyedJobs = new Map();
|
|
7
|
+
const destroyedScopes = new Set();
|
|
8
|
+
const objectScopeIds = new WeakMap();
|
|
9
|
+
const onError = typeof options.onError === "function" ? options.onError : undefined;
|
|
10
|
+
const maxDepth = options.maxDepth ?? 100;
|
|
11
|
+
const strategy = options.strategy ?? "microtask";
|
|
12
|
+
let destroyed = false;
|
|
13
|
+
let flushing = false;
|
|
14
|
+
let scheduled = false;
|
|
15
|
+
let batchDepth = 0;
|
|
16
|
+
let jobCounter = 0;
|
|
17
|
+
let scopeCounter = 0;
|
|
18
|
+
|
|
19
|
+
const api = {
|
|
20
|
+
strategy,
|
|
21
|
+
phases,
|
|
22
|
+
|
|
23
|
+
batch(fn) {
|
|
24
|
+
if (typeof fn !== "function") {
|
|
25
|
+
throw new TypeError("scheduler.batch(fn) requires a function.");
|
|
26
|
+
}
|
|
27
|
+
assertActive();
|
|
28
|
+
batchDepth += 1;
|
|
29
|
+
let asyncBatch = false;
|
|
30
|
+
try {
|
|
31
|
+
const value = fn();
|
|
32
|
+
if (value && typeof value.then === "function") {
|
|
33
|
+
asyncBatch = true;
|
|
34
|
+
return value.finally(() => {
|
|
35
|
+
batchDepth -= 1;
|
|
36
|
+
requestFlush();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
} finally {
|
|
41
|
+
if (!asyncBatch && batchDepth > 0) {
|
|
42
|
+
batchDepth -= 1;
|
|
43
|
+
requestFlush();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
enqueue(phase, fn, options = {}) {
|
|
49
|
+
assertActive();
|
|
50
|
+
assertPhase(phase);
|
|
51
|
+
if (typeof fn !== "function") {
|
|
52
|
+
throw new TypeError("scheduler.enqueue(phase, fn) requires a function.");
|
|
53
|
+
}
|
|
54
|
+
const scope = options.scope;
|
|
55
|
+
if (scope !== undefined && destroyedScopes.has(scope)) {
|
|
56
|
+
return noop;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dedupeKey = options.key === undefined ? undefined : `${phase}:${scopeKey(scope)}:${String(options.key)}`;
|
|
60
|
+
if (dedupeKey && keyedJobs.has(dedupeKey)) {
|
|
61
|
+
return keyedJobs.get(dedupeKey).cancel;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const job = {
|
|
65
|
+
id: ++jobCounter,
|
|
66
|
+
phase,
|
|
67
|
+
fn,
|
|
68
|
+
scope,
|
|
69
|
+
boundary: options.boundary,
|
|
70
|
+
key: dedupeKey,
|
|
71
|
+
canceled: false,
|
|
72
|
+
cancel() {
|
|
73
|
+
job.canceled = true;
|
|
74
|
+
if (job.key) {
|
|
75
|
+
keyedJobs.delete(job.key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
queues.get(phase).push(job);
|
|
80
|
+
if (job.key) {
|
|
81
|
+
keyedJobs.set(job.key, job);
|
|
82
|
+
}
|
|
83
|
+
requestFlush();
|
|
84
|
+
return job.cancel;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
afterFlush(fn, options = {}) {
|
|
88
|
+
return api.enqueue("post", fn, options);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async flush() {
|
|
92
|
+
assertActive();
|
|
93
|
+
if (flushing) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
scheduled = false;
|
|
97
|
+
flushing = true;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
try {
|
|
100
|
+
while (hasJobs()) {
|
|
101
|
+
depth += 1;
|
|
102
|
+
if (depth > maxDepth) {
|
|
103
|
+
throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
|
|
104
|
+
}
|
|
105
|
+
for (const phase of phases) {
|
|
106
|
+
await flushPhase(phase);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
flushing = false;
|
|
111
|
+
if (hasJobs()) {
|
|
112
|
+
requestFlush();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async flushScope(scope) {
|
|
118
|
+
assertActive();
|
|
119
|
+
if (flushing) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
scheduled = false;
|
|
123
|
+
flushing = true;
|
|
124
|
+
let depth = 0;
|
|
125
|
+
try {
|
|
126
|
+
while (hasJobsForScope(scope)) {
|
|
127
|
+
depth += 1;
|
|
128
|
+
if (depth > maxDepth) {
|
|
129
|
+
throw new Error(`Scheduler exceeded maxDepth ${maxDepth}.`);
|
|
130
|
+
}
|
|
131
|
+
for (const phase of phases) {
|
|
132
|
+
await flushPhase(phase, scope);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
flushing = false;
|
|
137
|
+
if (hasJobs()) {
|
|
138
|
+
requestFlush();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
cancelScope(scope) {
|
|
144
|
+
if (scope === undefined) {
|
|
145
|
+
return api;
|
|
146
|
+
}
|
|
147
|
+
for (const queue of queues.values()) {
|
|
148
|
+
for (const job of queue) {
|
|
149
|
+
if (job.scope === scope) {
|
|
150
|
+
job.cancel();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return api;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
markScopeDestroyed(scope) {
|
|
158
|
+
if (scope !== undefined) {
|
|
159
|
+
destroyedScopes.add(scope);
|
|
160
|
+
api.cancelScope(scope);
|
|
161
|
+
}
|
|
162
|
+
return api;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
inspect() {
|
|
166
|
+
const counts = {};
|
|
167
|
+
for (const [phase, queue] of queues) {
|
|
168
|
+
counts[phase] = queue.filter((job) => !job.canceled).length;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
strategy,
|
|
172
|
+
phases: [...phases],
|
|
173
|
+
pending: counts,
|
|
174
|
+
scopesDestroyed: destroyedScopes.size,
|
|
175
|
+
flushing,
|
|
176
|
+
scheduled
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
destroy() {
|
|
181
|
+
destroyed = true;
|
|
182
|
+
for (const queue of queues.values()) {
|
|
183
|
+
for (const job of queue) {
|
|
184
|
+
job.cancel();
|
|
185
|
+
}
|
|
186
|
+
queue.length = 0;
|
|
187
|
+
}
|
|
188
|
+
keyedJobs.clear();
|
|
189
|
+
destroyedScopes.clear();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return api;
|
|
194
|
+
|
|
195
|
+
function requestFlush() {
|
|
196
|
+
if (strategy === "manual" || destroyed || flushing || batchDepth > 0 || scheduled) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
scheduled = true;
|
|
200
|
+
scheduleMicrotask(() => {
|
|
201
|
+
if (!destroyed) {
|
|
202
|
+
void api.flush();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function flushPhase(phase, scope) {
|
|
208
|
+
const queue = queues.get(phase);
|
|
209
|
+
const remaining = [];
|
|
210
|
+
const runnable = [];
|
|
211
|
+
|
|
212
|
+
for (const job of queue.splice(0)) {
|
|
213
|
+
if (job.canceled) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (scope !== undefined && job.scope !== scope) {
|
|
217
|
+
remaining.push(job);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
runnable.push(job);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
queue.push(...remaining);
|
|
224
|
+
|
|
225
|
+
for (const job of runnable) {
|
|
226
|
+
if (job.key) {
|
|
227
|
+
keyedJobs.delete(job.key);
|
|
228
|
+
}
|
|
229
|
+
if (job.canceled || (job.scope !== undefined && destroyedScopes.has(job.scope))) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await job.fn();
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (onError) {
|
|
236
|
+
onError(error, job);
|
|
237
|
+
} else {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hasJobs() {
|
|
245
|
+
for (const queue of queues.values()) {
|
|
246
|
+
if (queue.some((job) => !job.canceled)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function hasJobsForScope(scope) {
|
|
254
|
+
for (const queue of queues.values()) {
|
|
255
|
+
if (queue.some((job) => !job.canceled && job.scope === scope)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function assertActive() {
|
|
263
|
+
if (destroyed) {
|
|
264
|
+
throw new Error("Scheduler has been destroyed.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function assertPhase(phase) {
|
|
269
|
+
if (!queues.has(phase)) {
|
|
270
|
+
throw new Error(`Unknown scheduler phase "${phase}".`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function scopeKey(scope) {
|
|
275
|
+
if (scope === undefined) {
|
|
276
|
+
return "global";
|
|
277
|
+
}
|
|
278
|
+
if ((typeof scope === "object" && scope !== null) || typeof scope === "function") {
|
|
279
|
+
if (!objectScopeIds.has(scope)) {
|
|
280
|
+
objectScopeIds.set(scope, `scope:${++scopeCounter}`);
|
|
281
|
+
}
|
|
282
|
+
return objectScopeIds.get(scope);
|
|
283
|
+
}
|
|
284
|
+
return String(scope);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function scheduleMicrotask(fn) {
|
|
289
|
+
if (typeof queueMicrotask === "function") {
|
|
290
|
+
queueMicrotask(fn);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
Promise.resolve().then(fn);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function noop() {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApp as createBaseApp,
|
|
3
|
+
defineApp as defineBaseApp,
|
|
4
|
+
readSnapshot
|
|
5
|
+
} from "./app.js";
|
|
6
|
+
import { createServerRegistry } from "./server-registry.js";
|
|
7
|
+
|
|
8
|
+
export function createApp(appOrDefinition = Async, options = {}) {
|
|
9
|
+
return createBaseApp(appOrDefinition, {
|
|
10
|
+
serverFactory: createServerRegistry,
|
|
11
|
+
...options
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function defineApp(initial) {
|
|
16
|
+
return defineBaseApp(initial, { createRuntime: createApp });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Async = defineApp();
|
|
20
|
+
export { readSnapshot };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readRequestContext } from "./request-context.js";
|
|
2
|
+
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
3
|
+
import { assertServerId, createServerNamespace, createSignalReader } from "./server.js";
|
|
4
|
+
|
|
5
|
+
export function createServerRegistry(initialMap = {}, options = {}) {
|
|
6
|
+
const registryStore = options.registry ?? createRegistryStore();
|
|
7
|
+
const type = options.type ?? "server";
|
|
8
|
+
const entries = registryStore._map(type);
|
|
9
|
+
const defaults = {};
|
|
10
|
+
|
|
11
|
+
const registry = attachRegistryInspection({
|
|
12
|
+
register(id, fn) {
|
|
13
|
+
assertServerId(id);
|
|
14
|
+
if (typeof fn !== "function") {
|
|
15
|
+
throw new TypeError(`Server function "${id}" must be a function.`);
|
|
16
|
+
}
|
|
17
|
+
if (entries.has(id)) {
|
|
18
|
+
throw new Error(`Server function "${id}" is already registered.`);
|
|
19
|
+
}
|
|
20
|
+
entries.set(id, fn);
|
|
21
|
+
return id;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
registerMany(map) {
|
|
25
|
+
for (const [id, fn] of Object.entries(map ?? {})) {
|
|
26
|
+
registry.register(id, fn);
|
|
27
|
+
}
|
|
28
|
+
return registry;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
unregister(id) {
|
|
32
|
+
assertServerId(id);
|
|
33
|
+
return entries.delete(id);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
resolve(id) {
|
|
37
|
+
assertServerId(id);
|
|
38
|
+
return entries.get(id);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async run(id, args = [], context = {}) {
|
|
42
|
+
assertServerId(id);
|
|
43
|
+
const fn = registry.resolve(id);
|
|
44
|
+
if (!fn) {
|
|
45
|
+
throw new Error(`Server function "${id}" is not registered.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let runContext;
|
|
49
|
+
const server = createServerNamespace((childId, childArgs, childContext = {}) => {
|
|
50
|
+
return registry.run(childId, childArgs, { ...runContext, ...childContext });
|
|
51
|
+
}, {}, () => runContext);
|
|
52
|
+
|
|
53
|
+
const mergedContext = mergeRequestContext({
|
|
54
|
+
...defaults,
|
|
55
|
+
...context,
|
|
56
|
+
cache: defaults.cache ?? context.cache
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
runContext = {
|
|
60
|
+
...mergedContext,
|
|
61
|
+
id,
|
|
62
|
+
args,
|
|
63
|
+
input: mergedContext.input,
|
|
64
|
+
signals: createSignalReader(mergedContext.signals),
|
|
65
|
+
abort: mergedContext.abort,
|
|
66
|
+
cache: mergedContext.cache,
|
|
67
|
+
server
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return fn.call(runContext, ...args);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
_setContext(context = {}) {
|
|
74
|
+
Object.assign(defaults, context);
|
|
75
|
+
return registry;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
_adoptMany() {
|
|
79
|
+
return registry;
|
|
80
|
+
}
|
|
81
|
+
}, registryStore, type);
|
|
82
|
+
|
|
83
|
+
registry.registerMany(initialMap);
|
|
84
|
+
return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeRequestContext(context) {
|
|
88
|
+
const requestContext = readRequestContext(context.requestContext);
|
|
89
|
+
return {
|
|
90
|
+
...context,
|
|
91
|
+
requestContext,
|
|
92
|
+
request: requestContext.request ?? context.request,
|
|
93
|
+
headers: requestContext.headers ?? context.headers,
|
|
94
|
+
cookies: requestContext.cookies ?? context.cookies,
|
|
95
|
+
locals: requestContext.locals ?? context.locals
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/server.js
CHANGED
|
@@ -1,88 +1,6 @@
|
|
|
1
|
-
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
2
|
-
|
|
3
1
|
const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const registryStore = options.registry ?? createRegistryStore();
|
|
7
|
-
const type = options.type ?? "server";
|
|
8
|
-
const entries = registryStore._map(type);
|
|
9
|
-
const defaults = {};
|
|
10
|
-
|
|
11
|
-
const registry = attachRegistryInspection({
|
|
12
|
-
register(id, fn) {
|
|
13
|
-
assertServerId(id);
|
|
14
|
-
if (typeof fn !== "function") {
|
|
15
|
-
throw new TypeError(`Server function "${id}" must be a function.`);
|
|
16
|
-
}
|
|
17
|
-
if (entries.has(id)) {
|
|
18
|
-
throw new Error(`Server function "${id}" is already registered.`);
|
|
19
|
-
}
|
|
20
|
-
entries.set(id, fn);
|
|
21
|
-
return id;
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
registerMany(map) {
|
|
25
|
-
for (const [id, fn] of Object.entries(map ?? {})) {
|
|
26
|
-
registry.register(id, fn);
|
|
27
|
-
}
|
|
28
|
-
return registry;
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
unregister(id) {
|
|
32
|
-
assertServerId(id);
|
|
33
|
-
return entries.delete(id);
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
resolve(id) {
|
|
37
|
-
assertServerId(id);
|
|
38
|
-
return entries.get(id);
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
async run(id, args = [], context = {}) {
|
|
42
|
-
assertServerId(id);
|
|
43
|
-
const fn = registry.resolve(id);
|
|
44
|
-
if (!fn) {
|
|
45
|
-
throw new Error(`Server function "${id}" is not registered.`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let runContext;
|
|
49
|
-
const server = createServerNamespace((childId, childArgs, childContext = {}) => {
|
|
50
|
-
return registry.run(childId, childArgs, { ...runContext, ...childContext });
|
|
51
|
-
}, {}, () => runContext);
|
|
52
|
-
|
|
53
|
-
const mergedContext = {
|
|
54
|
-
...defaults,
|
|
55
|
-
...context,
|
|
56
|
-
cache: defaults.cache ?? context.cache
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
runContext = {
|
|
60
|
-
...mergedContext,
|
|
61
|
-
id,
|
|
62
|
-
args,
|
|
63
|
-
input: mergedContext.input,
|
|
64
|
-
signals: createSignalReader(mergedContext.signals),
|
|
65
|
-
abort: mergedContext.abort,
|
|
66
|
-
cache: mergedContext.cache,
|
|
67
|
-
server
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return fn.call(runContext, ...args);
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
_setContext(context = {}) {
|
|
74
|
-
Object.assign(defaults, context);
|
|
75
|
-
return registry;
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
_adoptMany() {
|
|
79
|
-
return registry;
|
|
80
|
-
}
|
|
81
|
-
}, registryStore, type);
|
|
82
|
-
|
|
83
|
-
registry.registerMany(initialMap);
|
|
84
|
-
return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
|
|
85
|
-
}
|
|
2
|
+
const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
|
|
3
|
+
const appliedServerValues = new WeakSet();
|
|
86
4
|
|
|
87
5
|
export function createServerProxy({
|
|
88
6
|
endpoint = "/__async/server",
|
|
@@ -91,13 +9,14 @@ export function createServerProxy({
|
|
|
91
9
|
loader,
|
|
92
10
|
router,
|
|
93
11
|
cache,
|
|
12
|
+
scheduler,
|
|
94
13
|
headers = {}
|
|
95
14
|
} = {}) {
|
|
96
15
|
if (typeof fetchImpl !== "function") {
|
|
97
16
|
throw new TypeError("createServerProxy(...) requires fetch to be available.");
|
|
98
17
|
}
|
|
99
18
|
|
|
100
|
-
const defaults = { signals, loader, router, cache };
|
|
19
|
+
const defaults = { signals, loader, router, cache, scheduler };
|
|
101
20
|
|
|
102
21
|
async function run(id, args = [], context = {}) {
|
|
103
22
|
assertServerId(id);
|
|
@@ -107,6 +26,7 @@ export function createServerProxy({
|
|
|
107
26
|
input: context.input ?? defaultInput(runContext),
|
|
108
27
|
signals: context.signalValues ?? snapshotSignalPaths(context.signalPaths, runContext.signals)
|
|
109
28
|
};
|
|
29
|
+
assertJsonTransportable(body);
|
|
110
30
|
|
|
111
31
|
const response = await fetchImpl(joinEndpoint(endpoint, id), {
|
|
112
32
|
method: "POST",
|
|
@@ -124,7 +44,7 @@ export function createServerProxy({
|
|
|
124
44
|
|
|
125
45
|
const result = await readServerResponse(response);
|
|
126
46
|
await applyServerResult(result, runContext);
|
|
127
|
-
return unwrapServerResult(result);
|
|
47
|
+
return markAppliedServerValue(unwrapServerResult(result));
|
|
128
48
|
}
|
|
129
49
|
|
|
130
50
|
return createServerNamespace(run, {
|
|
@@ -159,6 +79,9 @@ export async function applyServerResult(result, context = {}) {
|
|
|
159
79
|
if (!isServerEnvelope(result)) {
|
|
160
80
|
return result;
|
|
161
81
|
}
|
|
82
|
+
if (result[appliedServerResult] || appliedServerValues.has(result)) {
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
162
85
|
|
|
163
86
|
if (result.signals && context.signals) {
|
|
164
87
|
for (const [path, value] of Object.entries(result.signals)) {
|
|
@@ -182,6 +105,12 @@ export async function applyServerResult(result, context = {}) {
|
|
|
182
105
|
throw toError(result.error);
|
|
183
106
|
}
|
|
184
107
|
|
|
108
|
+
Object.defineProperty(result, appliedServerResult, {
|
|
109
|
+
configurable: true,
|
|
110
|
+
enumerable: false,
|
|
111
|
+
value: true
|
|
112
|
+
});
|
|
113
|
+
|
|
185
114
|
return result;
|
|
186
115
|
}
|
|
187
116
|
|
|
@@ -192,6 +121,13 @@ export function unwrapServerResult(result) {
|
|
|
192
121
|
return result;
|
|
193
122
|
}
|
|
194
123
|
|
|
124
|
+
function markAppliedServerValue(value) {
|
|
125
|
+
if (value && typeof value === "object") {
|
|
126
|
+
appliedServerValues.add(value);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
195
131
|
export function defaultInput(context = {}) {
|
|
196
132
|
const form = findForm(context);
|
|
197
133
|
if (form) {
|
|
@@ -210,7 +146,7 @@ export function defaultInput(context = {}) {
|
|
|
210
146
|
};
|
|
211
147
|
}
|
|
212
148
|
|
|
213
|
-
function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
|
|
149
|
+
export function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
|
|
214
150
|
const cache = new Map();
|
|
215
151
|
|
|
216
152
|
function namespace(parts) {
|
|
@@ -292,7 +228,7 @@ function readSignal(signals, path) {
|
|
|
292
228
|
return signals.get(path);
|
|
293
229
|
}
|
|
294
230
|
|
|
295
|
-
function createSignalReader(signals) {
|
|
231
|
+
export function createSignalReader(signals) {
|
|
296
232
|
if (!signals || typeof signals.get === "function") {
|
|
297
233
|
return signals;
|
|
298
234
|
}
|
|
@@ -369,6 +305,30 @@ function formDataToObject(formData) {
|
|
|
369
305
|
return output;
|
|
370
306
|
}
|
|
371
307
|
|
|
308
|
+
function assertJsonTransportable(value, seen = new Set()) {
|
|
309
|
+
if (value == null || typeof value !== "object") {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (seen.has(value)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
seen.add(value);
|
|
316
|
+
|
|
317
|
+
const tag = Object.prototype.toString.call(value);
|
|
318
|
+
if (tag === "[object File]" || tag === "[object Blob]" || tag === "[object FormData]") {
|
|
319
|
+
throw new Error("Server proxy JSON transport does not support File, Blob, or FormData values yet.");
|
|
320
|
+
}
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
for (const item of value) {
|
|
323
|
+
assertJsonTransportable(item, seen);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
for (const item of Object.values(value)) {
|
|
328
|
+
assertJsonTransportable(item, seen);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
372
332
|
function joinEndpoint(endpoint, id) {
|
|
373
333
|
return `${String(endpoint).replace(/\/$/, "")}/${encodeURIComponent(id)}`;
|
|
374
334
|
}
|
|
@@ -390,7 +350,7 @@ function toError(value) {
|
|
|
390
350
|
return new Error(String(value));
|
|
391
351
|
}
|
|
392
352
|
|
|
393
|
-
function assertServerId(id) {
|
|
353
|
+
export function assertServerId(id) {
|
|
394
354
|
if (typeof id !== "string" || id.length === 0) {
|
|
395
355
|
throw new TypeError("Server function id must be a non-empty string.");
|
|
396
356
|
}
|