@sveltejs/kit 2.26.0 → 2.27.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/README.md +1 -1
- package/package.json +3 -2
- package/src/core/adapt/builder.js +6 -1
- package/src/core/config/options.js +4 -0
- package/src/core/generate_manifest/index.js +3 -0
- package/src/core/postbuild/analyse.js +25 -1
- package/src/core/postbuild/fallback.js +2 -1
- package/src/core/postbuild/prerender.js +41 -10
- package/src/core/sync/create_manifest_data/index.js +35 -1
- package/src/core/sync/write_server.js +4 -2
- package/src/core/sync/write_types/index.js +12 -5
- package/src/exports/index.js +1 -1
- package/src/exports/internal/index.js +3 -1
- package/src/exports/internal/remote-functions.js +21 -0
- package/src/exports/public.d.ts +162 -2
- package/src/exports/vite/build/build_remote.js +129 -0
- package/src/exports/vite/dev/index.js +7 -0
- package/src/exports/vite/index.js +123 -8
- package/src/exports/vite/module_ids.js +3 -2
- package/src/exports/vite/preview/index.js +3 -1
- package/src/runtime/app/navigation.js +1 -0
- package/src/runtime/app/server/index.js +2 -0
- package/src/runtime/app/server/remote/command.js +91 -0
- package/src/runtime/app/server/remote/form.js +124 -0
- package/src/runtime/app/server/remote/index.js +4 -0
- package/src/runtime/app/server/remote/prerender.js +163 -0
- package/src/runtime/app/server/remote/query.js +115 -0
- package/src/runtime/app/server/remote/shared.js +153 -0
- package/src/runtime/client/client.js +107 -39
- package/src/runtime/client/fetcher.js +1 -1
- package/src/runtime/client/remote-functions/command.js +71 -0
- package/src/runtime/client/remote-functions/form.svelte.js +312 -0
- package/src/runtime/client/remote-functions/index.js +4 -0
- package/src/runtime/client/remote-functions/prerender.svelte.js +166 -0
- package/src/runtime/client/remote-functions/query.svelte.js +219 -0
- package/src/runtime/client/remote-functions/shared.svelte.js +143 -0
- package/src/runtime/client/types.d.ts +2 -0
- package/src/runtime/server/data/index.js +6 -4
- package/src/runtime/server/event-state.js +41 -0
- package/src/runtime/server/index.js +12 -3
- package/src/runtime/server/page/actions.js +1 -1
- package/src/runtime/server/page/index.js +10 -3
- package/src/runtime/server/page/load_data.js +18 -12
- package/src/runtime/server/page/render.js +31 -5
- package/src/runtime/server/page/serialize_data.js +1 -1
- package/src/runtime/server/remote.js +237 -0
- package/src/runtime/server/respond.js +57 -36
- package/src/runtime/shared.js +61 -0
- package/src/types/global-private.d.ts +2 -0
- package/src/types/internal.d.ts +51 -4
- package/src/types/synthetic/$env+static+private.md +1 -1
- package/src/utils/routing.js +1 -1
- package/src/version.js +1 -1
- package/types/index.d.ts +266 -3
- package/types/index.d.ts.map +14 -1
- /package/src/{runtime → utils}/hash.js +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
|
+
/** @import { Query } from './query.svelte.js' */
|
|
4
|
+
import { app_dir } from '__sveltekit/paths';
|
|
5
|
+
import * as devalue from 'devalue';
|
|
6
|
+
import { HttpError } from '@sveltejs/kit/internal';
|
|
7
|
+
import { app } from '../client.js';
|
|
8
|
+
import { stringify_remote_arg } from '../../shared.js';
|
|
9
|
+
import { refresh_queries, release_overrides } from './shared.svelte.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Client-version of the `command` function from `$app/server`.
|
|
13
|
+
* @param {string} id
|
|
14
|
+
* @returns {RemoteCommand<any, any>}
|
|
15
|
+
*/
|
|
16
|
+
export function command(id) {
|
|
17
|
+
// Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method.
|
|
18
|
+
// If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
|
|
19
|
+
return (arg) => {
|
|
20
|
+
/** @type {Array<Query<any> | RemoteQueryOverride>} */
|
|
21
|
+
let updates = [];
|
|
22
|
+
|
|
23
|
+
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
|
|
24
|
+
const promise = (async () => {
|
|
25
|
+
// Wait a tick to give room for the `updates` method to be called
|
|
26
|
+
await Promise.resolve();
|
|
27
|
+
|
|
28
|
+
const response = await fetch(`/${app_dir}/remote/${id}`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
payload: stringify_remote_arg(arg, app.hooks.transport),
|
|
32
|
+
refreshes: updates.map((u) => u._key)
|
|
33
|
+
}),
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json'
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
release_overrides(updates);
|
|
41
|
+
// We only end up here in case of a network error or if the server has an internal error
|
|
42
|
+
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
|
|
43
|
+
throw new Error('Failed to execute remote function');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
|
|
47
|
+
if (result.type === 'redirect') {
|
|
48
|
+
release_overrides(updates);
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Redirects are not allowed in commands. Return a result instead and use goto on the client'
|
|
51
|
+
);
|
|
52
|
+
} else if (result.type === 'error') {
|
|
53
|
+
release_overrides(updates);
|
|
54
|
+
throw new HttpError(result.status ?? 500, result.error);
|
|
55
|
+
} else {
|
|
56
|
+
refresh_queries(result.refreshes, updates);
|
|
57
|
+
|
|
58
|
+
return devalue.parse(result.result, app.decoders);
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
promise.updates = (/** @type {any} */ ...args) => {
|
|
63
|
+
updates = args;
|
|
64
|
+
// @ts-expect-error Don't allow updates to be called multiple times
|
|
65
|
+
delete promise.updates;
|
|
66
|
+
return promise;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return promise;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/** @import { RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
|
|
2
|
+
/** @import { RemoteFunctionResponse } from 'types' */
|
|
3
|
+
/** @import { Query } from './query.svelte.js' */
|
|
4
|
+
import { app_dir } from '__sveltekit/paths';
|
|
5
|
+
import * as devalue from 'devalue';
|
|
6
|
+
import { DEV } from 'esm-env';
|
|
7
|
+
import { HttpError } from '@sveltejs/kit/internal';
|
|
8
|
+
import { app, remote_responses, started, goto, set_nearest_error_page } from '../client.js';
|
|
9
|
+
import { create_remote_cache_key } from '../../shared.js';
|
|
10
|
+
import { tick } from 'svelte';
|
|
11
|
+
import { refresh_queries, release_overrides } from './shared.svelte.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Client-version of the `form` function from `$app/server`.
|
|
15
|
+
* @template T
|
|
16
|
+
* @param {string} id
|
|
17
|
+
* @returns {RemoteForm<T>}
|
|
18
|
+
*/
|
|
19
|
+
export function form(id) {
|
|
20
|
+
/** @type {Map<any, { count: number, instance: RemoteForm<T> }>} */
|
|
21
|
+
const instances = new Map();
|
|
22
|
+
|
|
23
|
+
/** @param {string | number | boolean} [key] */
|
|
24
|
+
function create_instance(key) {
|
|
25
|
+
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
|
|
26
|
+
const action = '?/remote=' + encodeURIComponent(action_id);
|
|
27
|
+
|
|
28
|
+
/** @type {any} */
|
|
29
|
+
let result = $state(
|
|
30
|
+
!started ? (remote_responses[create_remote_cache_key(action, '')] ?? undefined) : undefined
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {FormData} data
|
|
35
|
+
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
|
|
36
|
+
*/
|
|
37
|
+
function submit(data) {
|
|
38
|
+
// Store a reference to the current instance and increment the usage count for the duration
|
|
39
|
+
// of the request. This ensures that the instance is not deleted in case of an optimistic update
|
|
40
|
+
// (e.g. when deleting an item in a list) that fails and wants to surface an error to the user afterwards.
|
|
41
|
+
// If the instance would be deleted in the meantime, the error property would be assigned to the old,
|
|
42
|
+
// no-longer-visible instance, so it would never be shown to the user.
|
|
43
|
+
const entry = instances.get(key);
|
|
44
|
+
if (entry) {
|
|
45
|
+
entry.count++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {Array<Query<any> | RemoteQueryOverride>} */
|
|
49
|
+
let updates = [];
|
|
50
|
+
|
|
51
|
+
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
|
|
52
|
+
const promise = (async () => {
|
|
53
|
+
try {
|
|
54
|
+
await Promise.resolve();
|
|
55
|
+
|
|
56
|
+
if (updates.length > 0) {
|
|
57
|
+
if (DEV) {
|
|
58
|
+
if (data.get('sveltekit:remote_refreshes')) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'The FormData key `sveltekit:remote_refreshes` is reserved for internal use and should not be set manually'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
data.set('sveltekit:remote_refreshes', JSON.stringify(updates.map((u) => u._key)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const response = await fetch(`/${app_dir}/remote/${action_id}`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: data
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
// We only end up here in case of a network error or if the server has an internal error
|
|
74
|
+
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
|
|
75
|
+
result = undefined;
|
|
76
|
+
throw new Error('Failed to execute remote function');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
|
|
80
|
+
|
|
81
|
+
if (form_result.type === 'result') {
|
|
82
|
+
result = devalue.parse(form_result.result, app.decoders);
|
|
83
|
+
|
|
84
|
+
refresh_queries(form_result.refreshes, updates);
|
|
85
|
+
} else if (form_result.type === 'redirect') {
|
|
86
|
+
const refreshes = form_result.refreshes ?? '';
|
|
87
|
+
const invalidateAll = !refreshes && updates.length === 0;
|
|
88
|
+
if (!invalidateAll) {
|
|
89
|
+
refresh_queries(refreshes, updates);
|
|
90
|
+
}
|
|
91
|
+
void goto(form_result.location, { invalidateAll });
|
|
92
|
+
} else {
|
|
93
|
+
result = undefined;
|
|
94
|
+
throw new HttpError(500, form_result.error);
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
release_overrides(updates);
|
|
98
|
+
throw e;
|
|
99
|
+
} finally {
|
|
100
|
+
void tick().then(() => {
|
|
101
|
+
if (entry) {
|
|
102
|
+
entry.count--;
|
|
103
|
+
if (entry.count === 0) {
|
|
104
|
+
instances.delete(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
promise.updates = (...args) => {
|
|
112
|
+
updates = args;
|
|
113
|
+
return promise;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return promise;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @type {RemoteForm<T>} */
|
|
120
|
+
const instance = {};
|
|
121
|
+
|
|
122
|
+
instance.method = 'POST';
|
|
123
|
+
instance.action = action;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {HTMLFormElement} form_element
|
|
127
|
+
* @param {HTMLElement | null} submitter
|
|
128
|
+
*/
|
|
129
|
+
function create_form_data(form_element, submitter) {
|
|
130
|
+
const form_data = new FormData(form_element);
|
|
131
|
+
|
|
132
|
+
if (DEV) {
|
|
133
|
+
const enctype = submitter?.hasAttribute('formenctype')
|
|
134
|
+
? /** @type {HTMLButtonElement | HTMLInputElement} */ (submitter).formEnctype
|
|
135
|
+
: clone(form_element).enctype;
|
|
136
|
+
if (enctype !== 'multipart/form-data') {
|
|
137
|
+
for (const value of form_data.values()) {
|
|
138
|
+
if (value instanceof File) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const submitter_name = submitter?.getAttribute('name');
|
|
148
|
+
if (submitter_name) {
|
|
149
|
+
form_data.append(submitter_name, submitter?.getAttribute('value') ?? '');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return form_data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @param {Parameters<RemoteForm<any>['enhance']>[0]} callback */
|
|
156
|
+
const form_onsubmit = (callback) => {
|
|
157
|
+
/** @param {SubmitEvent} event */
|
|
158
|
+
return async (event) => {
|
|
159
|
+
const form = /** @type {HTMLFormElement} */ (event.target);
|
|
160
|
+
const method = event.submitter?.hasAttribute('formmethod')
|
|
161
|
+
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
|
|
162
|
+
: clone(form).method;
|
|
163
|
+
|
|
164
|
+
if (method !== 'post') return;
|
|
165
|
+
|
|
166
|
+
const action = new URL(
|
|
167
|
+
// We can't do submitter.formAction directly because that property is always set
|
|
168
|
+
event.submitter?.hasAttribute('formaction')
|
|
169
|
+
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
|
|
170
|
+
: clone(form).action
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (action.searchParams.get('/remote') !== action_id) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
|
|
179
|
+
const data = create_form_data(form, event.submitter);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await callback({
|
|
183
|
+
form,
|
|
184
|
+
data,
|
|
185
|
+
submit: () => submit(data)
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
const error =
|
|
189
|
+
e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
|
190
|
+
const status = e instanceof HttpError ? e.status : 500;
|
|
191
|
+
void set_nearest_error_page(error, status);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
instance.onsubmit = form_onsubmit(({ submit }) => submit());
|
|
197
|
+
|
|
198
|
+
/** @param {Parameters<RemoteForm<any>['buttonProps']['enhance']>[0]} callback */
|
|
199
|
+
const form_action_onclick = (callback) => {
|
|
200
|
+
/** @param {Event} event */
|
|
201
|
+
return async (event) => {
|
|
202
|
+
const target = /** @type {HTMLButtonElement} */ (event.target);
|
|
203
|
+
const form = target.form;
|
|
204
|
+
if (!form) return;
|
|
205
|
+
|
|
206
|
+
// Prevent this from firing the form's submit event
|
|
207
|
+
event.stopPropagation();
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
|
|
210
|
+
const data = create_form_data(form, target);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await callback({
|
|
214
|
+
form,
|
|
215
|
+
data,
|
|
216
|
+
submit: () => submit(data)
|
|
217
|
+
});
|
|
218
|
+
} catch (e) {
|
|
219
|
+
const error =
|
|
220
|
+
e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
|
|
221
|
+
const status = e instanceof HttpError ? e.status : 500;
|
|
222
|
+
void set_nearest_error_page(error, status);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** @type {RemoteForm<any>['buttonProps']} */
|
|
228
|
+
// @ts-expect-error we gotta set enhance as a non-enumerable property
|
|
229
|
+
const button_props = {
|
|
230
|
+
type: 'submit',
|
|
231
|
+
formmethod: 'POST',
|
|
232
|
+
formaction: action,
|
|
233
|
+
onclick: form_action_onclick(({ submit }) => submit())
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
Object.defineProperty(button_props, 'enhance', {
|
|
237
|
+
/** @type {RemoteForm<any>['buttonProps']['enhance']} */
|
|
238
|
+
value: (callback) => {
|
|
239
|
+
return {
|
|
240
|
+
type: 'submit',
|
|
241
|
+
formmethod: 'POST',
|
|
242
|
+
formaction: action,
|
|
243
|
+
onclick: form_action_onclick(callback)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
Object.defineProperties(instance, {
|
|
249
|
+
buttonProps: {
|
|
250
|
+
value: button_props
|
|
251
|
+
},
|
|
252
|
+
result: {
|
|
253
|
+
get: () => result
|
|
254
|
+
},
|
|
255
|
+
enhance: {
|
|
256
|
+
/** @type {RemoteForm<any>['enhance']} */
|
|
257
|
+
value: (callback) => {
|
|
258
|
+
return {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
action,
|
|
261
|
+
onsubmit: form_onsubmit(callback)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return instance;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const instance = create_instance();
|
|
271
|
+
|
|
272
|
+
Object.defineProperty(instance, 'for', {
|
|
273
|
+
/** @type {RemoteForm<any>['for']} */
|
|
274
|
+
value: (key) => {
|
|
275
|
+
const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
$effect.pre(() => {
|
|
279
|
+
return () => {
|
|
280
|
+
entry.count--;
|
|
281
|
+
|
|
282
|
+
void tick().then(() => {
|
|
283
|
+
if (entry.count === 0) {
|
|
284
|
+
instances.delete(key);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
entry.count += 1;
|
|
291
|
+
instances.set(key, entry);
|
|
292
|
+
} catch {
|
|
293
|
+
// not in an effect context
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return entry.instance;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return instance;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Shallow clone an element, so that we can access e.g. `form.action` without worrying
|
|
305
|
+
* that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593)
|
|
306
|
+
* @template {HTMLElement} T
|
|
307
|
+
* @param {T} element
|
|
308
|
+
* @returns {T}
|
|
309
|
+
*/
|
|
310
|
+
function clone(element) {
|
|
311
|
+
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
|
|
312
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** @import { RemoteFunctionResponse } from 'types' */
|
|
2
|
+
import { app_dir } from '__sveltekit/paths';
|
|
3
|
+
import { version } from '__sveltekit/environment';
|
|
4
|
+
import * as devalue from 'devalue';
|
|
5
|
+
import { DEV } from 'esm-env';
|
|
6
|
+
import { app, remote_responses, started } from '../client.js';
|
|
7
|
+
import { create_remote_function, remote_request } from './shared.svelte.js';
|
|
8
|
+
|
|
9
|
+
// Initialize Cache API for prerender functions
|
|
10
|
+
const CACHE_NAME = `sveltekit:${version}`;
|
|
11
|
+
/** @type {Cache | undefined} */
|
|
12
|
+
let prerender_cache;
|
|
13
|
+
|
|
14
|
+
void (async () => {
|
|
15
|
+
if (!DEV && typeof caches !== 'undefined') {
|
|
16
|
+
try {
|
|
17
|
+
prerender_cache = await caches.open(CACHE_NAME);
|
|
18
|
+
|
|
19
|
+
// Clean up old cache versions
|
|
20
|
+
const cache_names = await caches.keys();
|
|
21
|
+
for (const cache_name of cache_names) {
|
|
22
|
+
if (cache_name.startsWith('sveltekit:') && cache_name !== CACHE_NAME) {
|
|
23
|
+
await caches.delete(cache_name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn('Failed to initialize SvelteKit cache:', error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template T
|
|
34
|
+
* @implements {Partial<Promise<T>>}
|
|
35
|
+
*/
|
|
36
|
+
class Prerender {
|
|
37
|
+
/** @type {Promise<T>} */
|
|
38
|
+
#promise;
|
|
39
|
+
|
|
40
|
+
#loading = $state(true);
|
|
41
|
+
#ready = $state(false);
|
|
42
|
+
|
|
43
|
+
/** @type {T | undefined} */
|
|
44
|
+
#current = $state.raw();
|
|
45
|
+
|
|
46
|
+
#error = $state.raw(undefined);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {() => Promise<T>} fn
|
|
50
|
+
*/
|
|
51
|
+
constructor(fn) {
|
|
52
|
+
this.#promise = fn().then(
|
|
53
|
+
(value) => {
|
|
54
|
+
this.#loading = false;
|
|
55
|
+
this.#ready = true;
|
|
56
|
+
this.#current = value;
|
|
57
|
+
return value;
|
|
58
|
+
},
|
|
59
|
+
(error) => {
|
|
60
|
+
this.#loading = false;
|
|
61
|
+
this.#error = error;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
*
|
|
69
|
+
* @param {((value: any) => any) | null | undefined} onfulfilled
|
|
70
|
+
* @param {((reason: any) => any) | null | undefined} [onrejected]
|
|
71
|
+
* @returns
|
|
72
|
+
*/
|
|
73
|
+
then(onfulfilled, onrejected) {
|
|
74
|
+
return this.#promise.then(onfulfilled, onrejected);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {((reason: any) => any) | null | undefined} onrejected
|
|
79
|
+
*/
|
|
80
|
+
catch(onrejected) {
|
|
81
|
+
return this.#promise.catch(onrejected);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {(() => any) | null | undefined} onfinally
|
|
86
|
+
*/
|
|
87
|
+
finally(onfinally) {
|
|
88
|
+
return this.#promise.finally(onfinally);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get current() {
|
|
92
|
+
return this.#current;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get error() {
|
|
96
|
+
return this.#error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns true if the resource is loading.
|
|
101
|
+
*/
|
|
102
|
+
get loading() {
|
|
103
|
+
return this.#loading;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns true once the resource has been loaded.
|
|
108
|
+
*/
|
|
109
|
+
get ready() {
|
|
110
|
+
return this.#ready;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} id
|
|
116
|
+
*/
|
|
117
|
+
export function prerender(id) {
|
|
118
|
+
return create_remote_function(id, (cache_key, payload) => {
|
|
119
|
+
return new Prerender(async () => {
|
|
120
|
+
if (!started) {
|
|
121
|
+
const result = remote_responses[cache_key];
|
|
122
|
+
if (result) {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const url = `/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
|
|
128
|
+
|
|
129
|
+
// Check the Cache API first
|
|
130
|
+
if (prerender_cache) {
|
|
131
|
+
try {
|
|
132
|
+
const cached_response = await prerender_cache.match(url);
|
|
133
|
+
if (cached_response) {
|
|
134
|
+
const cached_result = /** @type { RemoteFunctionResponse & { type: 'result' } } */ (
|
|
135
|
+
await cached_response.json()
|
|
136
|
+
);
|
|
137
|
+
return devalue.parse(cached_result.result, app.decoders);
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Nothing we can do here
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await remote_request(url);
|
|
145
|
+
|
|
146
|
+
// For successful prerender requests, save to cache
|
|
147
|
+
if (prerender_cache) {
|
|
148
|
+
try {
|
|
149
|
+
await prerender_cache.put(
|
|
150
|
+
url,
|
|
151
|
+
// We need to create a new response because the original response is already consumed
|
|
152
|
+
new Response(JSON.stringify(result), {
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'application/json'
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
} catch {
|
|
159
|
+
// Nothing we can do here
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|