@hops-ops/distributed 4.8.0 → 4.10.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 +46 -8
- package/dist/generation.d.ts +15 -0
- package/dist/generation.js +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +5 -0
- package/dist/replica/command-runtime/create.js +11 -0
- package/dist/replica/command-runtime/errors.js +2 -0
- package/dist/replica/command-runtime/types.d.ts +7 -1
- package/dist/replica/distributed-replica/impl-protocol.d.ts +1 -0
- package/dist/replica/distributed-replica/impl-protocol.js +1 -0
- package/dist/replica/distributed-replica/impl.js +11 -0
- package/dist/replica/distributed-replica/watch.js +13 -1
- package/dist/replica/index.d.ts +1 -1
- package/dist/replica/types.d.ts +38 -0
- package/dist/sveltekit/boundary-lifecycle.d.ts +37 -0
- package/dist/sveltekit/boundary-lifecycle.js +355 -0
- package/dist/sveltekit/boundary-variables.d.ts +57 -0
- package/dist/sveltekit/boundary-variables.js +290 -0
- package/dist/sveltekit/context.d.ts +3 -0
- package/dist/sveltekit/context.js +8 -0
- package/dist/sveltekit/index.d.ts +7 -3
- package/dist/sveltekit/index.js +6 -2
- package/dist/sveltekit/islands/boundaries.d.ts +104 -0
- package/dist/sveltekit/islands/boundaries.js +734 -0
- package/dist/sveltekit/lifecycle.d.ts +57 -0
- package/dist/sveltekit/lifecycle.js +454 -0
- package/dist/sveltekit/operation-identity.d.ts +4 -0
- package/dist/sveltekit/operation-identity.js +10 -0
- package/dist/sveltekit/replica.d.ts +29 -2
- package/dist/sveltekit/replica.js +102 -6
- package/dist/sveltekit/server-replica.d.ts +10 -26
- package/dist/sveltekit/server-replica.js +159 -132
- package/dist/sveltekit/vite.d.ts +46 -3
- package/dist/sveltekit/vite.js +643 -36
- package/package.json +4 -3
|
@@ -2,6 +2,9 @@ import { sameAuthCredential, snapshotAuthCredential } from '../identity.js';
|
|
|
2
2
|
import { createDistributedReplica, createReplicaGraphqlTransport } from '../replica/index.js';
|
|
3
3
|
import { replicaCommandProjectedLifecycleOf } from '../replica/command-runtime.js';
|
|
4
4
|
import { authFromPageData } from './auth.js';
|
|
5
|
+
import { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation } from './boundary-variables.js';
|
|
6
|
+
import { DistributedSvelteKitBoundaryController } from './boundary-lifecycle.js';
|
|
7
|
+
import { distributedReloadLifecycle, registerDistributedReloadClient } from './lifecycle.js';
|
|
5
8
|
/**
|
|
6
9
|
* Bind the framework-neutral replica to Svelte's readable-store lifecycle.
|
|
7
10
|
*
|
|
@@ -14,12 +17,20 @@ export function createDistributedSvelteKit(options) {
|
|
|
14
17
|
typeof options.session.getAuth !== 'function') {
|
|
15
18
|
throw new TypeError('Distributed SvelteKit requires one session source');
|
|
16
19
|
}
|
|
20
|
+
if (!Array.isArray(options.boundaries)) {
|
|
21
|
+
throw new TypeError('Distributed SvelteKit requires generated boundary operations');
|
|
22
|
+
}
|
|
17
23
|
if ((options.hydration === undefined) !==
|
|
18
24
|
(options.authority === undefined)) {
|
|
19
25
|
throw new TypeError('Distributed SvelteKit hydration requires separate trusted SSR authority');
|
|
20
26
|
}
|
|
21
27
|
let replica;
|
|
22
|
-
|
|
28
|
+
let boundaryController;
|
|
29
|
+
const boundaryIds = Object.freeze([...new Set(options.boundaries.map(({ binding }) => binding.id))].sort());
|
|
30
|
+
const auth = createAuthorizationFence(options.session, () => {
|
|
31
|
+
boundaryController?.disposeScope();
|
|
32
|
+
replica?.invalidateAuthorization();
|
|
33
|
+
}, options.onAuthError);
|
|
23
34
|
const configuredUrl = options.url;
|
|
24
35
|
const transport = createReplicaGraphqlTransport({
|
|
25
36
|
getUrl: typeof configuredUrl === 'function'
|
|
@@ -33,8 +44,13 @@ export function createDistributedSvelteKit(options) {
|
|
|
33
44
|
});
|
|
34
45
|
replica = createDistributedReplica({
|
|
35
46
|
transport,
|
|
36
|
-
...(options.replica ?? {})
|
|
47
|
+
...(options.replica ?? {}),
|
|
48
|
+
onAuthorizationGenerationDispose: () => {
|
|
49
|
+
boundaryController?.disposeScope();
|
|
50
|
+
options.replica?.onAuthorizationGenerationDispose?.();
|
|
51
|
+
}
|
|
37
52
|
});
|
|
53
|
+
boundaryController = new DistributedSvelteKitBoundaryController(replica, options.boundaries, options.onBoundaryDiagnostic);
|
|
38
54
|
const stores = new Set();
|
|
39
55
|
const pending = new PendingReceiptStore();
|
|
40
56
|
let destroyed = false;
|
|
@@ -42,6 +58,10 @@ export function createDistributedSvelteKit(options) {
|
|
|
42
58
|
let accepted = false;
|
|
43
59
|
try {
|
|
44
60
|
const expected = validatedHydrationAuthority(authority);
|
|
61
|
+
const hydrationBindings = [...(hydration.bindings ?? [])].sort();
|
|
62
|
+
if (hydrationBindings.some((value) => !boundaryIds.includes(value))) {
|
|
63
|
+
throw new TypeError('Distributed SvelteKit hydration boundary binding fingerprint changed');
|
|
64
|
+
}
|
|
45
65
|
const active = replica.scope;
|
|
46
66
|
if (active !== undefined && !sameReplicaScope(active, expected)) {
|
|
47
67
|
throw new TypeError('Distributed SvelteKit hydration authority changed before session invalidation');
|
|
@@ -53,8 +73,10 @@ export function createDistributedSvelteKit(options) {
|
|
|
53
73
|
catch {
|
|
54
74
|
accepted = false;
|
|
55
75
|
}
|
|
56
|
-
if (!accepted)
|
|
76
|
+
if (!accepted) {
|
|
77
|
+
boundaryController.disposeScope();
|
|
57
78
|
replica.invalidateAuthorization();
|
|
79
|
+
}
|
|
58
80
|
return accepted;
|
|
59
81
|
};
|
|
60
82
|
if (options.hydration !== undefined && options.authority !== undefined) {
|
|
@@ -65,8 +87,35 @@ export function createDistributedSvelteKit(options) {
|
|
|
65
87
|
const commandRuntime = options.createCommands?.(replica, transport, Object.freeze({
|
|
66
88
|
...(options.replica?.diagnostics === undefined
|
|
67
89
|
? {}
|
|
68
|
-
: { diagnostics: options.replica.diagnostics })
|
|
90
|
+
: { diagnostics: options.replica.diagnostics }),
|
|
91
|
+
...(options.browser === true && options.reload !== undefined
|
|
92
|
+
? { lifecycle: distributedReloadLifecycle() }
|
|
93
|
+
: {})
|
|
69
94
|
}));
|
|
95
|
+
let unregisterReload;
|
|
96
|
+
try {
|
|
97
|
+
unregisterReload =
|
|
98
|
+
options.browser === true && options.reload !== undefined
|
|
99
|
+
? registerDistributedReloadClient(replica, commandRuntime, options.reload)
|
|
100
|
+
: undefined;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Client construction is transactional: a malformed reload declaration
|
|
104
|
+
// must not retain its session subscription or command authority.
|
|
105
|
+
try {
|
|
106
|
+
commandRuntime?.dispose();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Preserve the construction failure; disposal is best effort here.
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
auth.dispose();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Preserve the construction failure; disposal is best effort here.
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
70
119
|
const commands = commandRuntime === undefined
|
|
71
120
|
? Object.freeze({})
|
|
72
121
|
: wrapCommandTree(commandRuntime.commands, pending);
|
|
@@ -80,28 +129,72 @@ export function createDistributedSvelteKit(options) {
|
|
|
80
129
|
stores.delete(store);
|
|
81
130
|
}
|
|
82
131
|
});
|
|
132
|
+
const boundary = (binding) => {
|
|
133
|
+
if (!boundaryIds.includes(binding.binding.id)) {
|
|
134
|
+
throw new TypeError('Distributed boundary operation was not registered with this SvelteKit client');
|
|
135
|
+
}
|
|
136
|
+
const bound = operation(binding.artifact);
|
|
137
|
+
const variables = (context) => binding.binding.resolve(context);
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
operation: binding,
|
|
140
|
+
variables,
|
|
141
|
+
use(context, useOptions) {
|
|
142
|
+
const invoke = bound.use;
|
|
143
|
+
return invoke(variables(context), useOptions);
|
|
144
|
+
},
|
|
145
|
+
read(context) {
|
|
146
|
+
return bound.read(variables(context));
|
|
147
|
+
},
|
|
148
|
+
prefetch(context) {
|
|
149
|
+
return bound.prefetch(variables(context));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
83
153
|
return Object.freeze({
|
|
84
154
|
replica,
|
|
85
155
|
transport,
|
|
86
156
|
commands,
|
|
87
157
|
operation,
|
|
158
|
+
boundary,
|
|
159
|
+
retainBoundary(instance, context) {
|
|
160
|
+
if (destroyed) {
|
|
161
|
+
throw new Error('Distributed SvelteKit client is destroyed');
|
|
162
|
+
}
|
|
163
|
+
return boundaryController.retain(instance, context);
|
|
164
|
+
},
|
|
165
|
+
retainLocation(location, context) {
|
|
166
|
+
if (destroyed) {
|
|
167
|
+
throw new Error('Distributed SvelteKit client is destroyed');
|
|
168
|
+
}
|
|
169
|
+
return boundaryController.retainLocation(location, context);
|
|
170
|
+
},
|
|
171
|
+
prefetchLocation(pathname, context) {
|
|
172
|
+
if (destroyed) {
|
|
173
|
+
return Promise.reject(new Error('Distributed SvelteKit client is destroyed'));
|
|
174
|
+
}
|
|
175
|
+
return boundaryController.prefetchLocation(pathname, context);
|
|
176
|
+
},
|
|
88
177
|
hydrate,
|
|
89
178
|
prefetch(artifact, variables) {
|
|
90
179
|
return prefetchReplicaOperation(replica, artifact, variables);
|
|
91
180
|
},
|
|
92
181
|
invalidateAuthorization() {
|
|
93
|
-
if (!destroyed)
|
|
182
|
+
if (!destroyed) {
|
|
183
|
+
boundaryController.disposeScope();
|
|
94
184
|
replica.invalidateAuthorization();
|
|
185
|
+
}
|
|
95
186
|
},
|
|
96
187
|
destroy() {
|
|
97
188
|
if (destroyed)
|
|
98
189
|
return;
|
|
99
190
|
destroyed = true;
|
|
100
191
|
auth.dispose();
|
|
192
|
+
boundaryController.destroy();
|
|
101
193
|
for (const store of [...stores])
|
|
102
194
|
store.destroy();
|
|
103
195
|
stores.clear();
|
|
104
196
|
pending.clear();
|
|
197
|
+
unregisterReload?.();
|
|
105
198
|
commandRuntime?.dispose();
|
|
106
199
|
}
|
|
107
200
|
});
|
|
@@ -175,7 +268,10 @@ function bindOperation(replica, artifact, pending, lifecycle) {
|
|
|
175
268
|
artifact,
|
|
176
269
|
use,
|
|
177
270
|
read: (variables) => replica.read(artifact, variables),
|
|
178
|
-
prefetch: (variables) => prefetchReplicaOperation(replica, artifact, variables)
|
|
271
|
+
prefetch: (variables) => prefetchReplicaOperation(replica, artifact, variables),
|
|
272
|
+
boundary(plan, sources) {
|
|
273
|
+
return defineDistributedBoundaryOperation(plan, artifact, defineDistributedBoundaryBinding(artifact, sources));
|
|
274
|
+
}
|
|
179
275
|
});
|
|
180
276
|
}
|
|
181
277
|
function prefetchReplicaOperation(replica, artifact, variables) {
|
|
@@ -1,24 +1,19 @@
|
|
|
1
|
-
import { type ReplicaOperationArtifact } from '../replica/index.js';
|
|
2
1
|
import type { FetchLike } from '../request.js';
|
|
3
2
|
import type { GqlAuth, GraphqlVariables } from '../types.js';
|
|
4
3
|
import { type PageGraphqlData } from './auth.js';
|
|
4
|
+
import { type DistributedBoundaryOperation } from './boundary-variables.js';
|
|
5
5
|
import type { SveltekitDistributedPageData } from './replica.js';
|
|
6
|
-
export type DistributedRoutePlan = Readonly<{
|
|
7
|
-
operation: string;
|
|
8
|
-
route: string;
|
|
9
|
-
source_path?: string;
|
|
10
|
-
discovery: 'convention' | 'explicit';
|
|
11
|
-
}>;
|
|
12
|
-
export type DistributedRouteOperation = Readonly<{
|
|
13
|
-
plan: DistributedRoutePlan;
|
|
14
|
-
artifact: ReplicaOperationArtifact<unknown, GraphqlVariables>;
|
|
15
|
-
}>;
|
|
16
6
|
export type SveltekitServerLoadEventLike<TLocals = unknown> = Readonly<{
|
|
17
7
|
locals: TLocals;
|
|
18
8
|
route?: Readonly<{
|
|
19
9
|
id?: string | null;
|
|
20
10
|
}>;
|
|
21
11
|
url?: URL;
|
|
12
|
+
params?: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
parent?(): Promise<Readonly<Record<string, unknown>>>;
|
|
14
|
+
request?: Readonly<{
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
}>;
|
|
22
17
|
fetch?: FetchLike;
|
|
23
18
|
/**
|
|
24
19
|
* SvelteKit client-side navigation and hover preload (`__data.json`).
|
|
@@ -27,16 +22,16 @@ export type SveltekitServerLoadEventLike<TLocals = unknown> = Readonly<{
|
|
|
27
22
|
*/
|
|
28
23
|
isDataRequest?: boolean;
|
|
29
24
|
}>;
|
|
30
|
-
export type DistributedRouteVariables<TEvent extends SveltekitServerLoadEventLike> = Readonly<Record<string, (event: TEvent) => GraphqlVariables | Promise<GraphqlVariables>>>;
|
|
31
25
|
export type CreateDistributedSvelteKitServerOptions<TSession extends NonNullable<PageGraphqlData['session']>, TEvent extends SveltekitServerLoadEventLike = SveltekitServerLoadEventLike> = Readonly<{
|
|
32
|
-
|
|
26
|
+
/** One executable binding per promoted page/layout island. */
|
|
27
|
+
boundaries: readonly DistributedBoundaryOperation<unknown, GraphqlVariables, TSession, Readonly<Record<string, unknown>>>[];
|
|
33
28
|
getSession(event: TEvent): Promise<TSession | null>;
|
|
34
29
|
getRole(session: TSession | null, event: TEvent): string | null | undefined;
|
|
35
30
|
getAuth?(pageData: PageGraphqlData, event: TEvent): GqlAuth;
|
|
36
31
|
/** Private API origin or same-origin `/graphql`; defaults to `/graphql`. */
|
|
37
32
|
getUrl?(event: TEvent): string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
33
|
+
/** Maximum simultaneous SSR island refreshes. Defaults to 8, maximum 32. */
|
|
34
|
+
maxConcurrency?: number;
|
|
40
35
|
}>;
|
|
41
36
|
export type DistributedSvelteKitServer<TEvent> = Readonly<{
|
|
42
37
|
load(event: TEvent): Promise<SveltekitDistributedPageData & {
|
|
@@ -49,14 +44,3 @@ export type DistributedSvelteKitServer<TEvent> = Readonly<{
|
|
|
49
44
|
* across requests.
|
|
50
45
|
*/
|
|
51
46
|
export declare function createDistributedSvelteKitServer<TSession extends NonNullable<PageGraphqlData['session']>, TEvent extends SveltekitServerLoadEventLike = SveltekitServerLoadEventLike>(options: CreateDistributedSvelteKitServerOptions<TSession, TEvent>): DistributedSvelteKitServer<TEvent>;
|
|
52
|
-
/**
|
|
53
|
-
* Explicit one-line fallback when the compiler cannot discover route ownership.
|
|
54
|
-
*
|
|
55
|
-
* Prefer co-locating `+page.graphql`; the compiler diagnostic includes the
|
|
56
|
-
* equivalent `--route Operation=/route-id` registration.
|
|
57
|
-
*/
|
|
58
|
-
export declare function registerDistributedRoute<TData, TVariables extends GraphqlVariables>(route: string, operation: string, artifact: ReplicaOperationArtifact<TData, TVariables>): DistributedRouteOperation;
|
|
59
|
-
/**
|
|
60
|
-
* Match a SvelteKit route id (`/blob/[[gameId]]`) to a browser pathname.
|
|
61
|
-
*/
|
|
62
|
-
export declare function matchDistributedRoute(routeId: string, pathname: string): boolean;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createDistributedReplica, createReplicaGraphqlTransport } from '../replica/index.js';
|
|
2
2
|
import { authFromPageData } from './auth.js';
|
|
3
|
+
import { resolveDistributedBoundaryVariables } from './boundary-variables.js';
|
|
4
|
+
import { boundaryOperationIdentity } from './operation-identity.js';
|
|
3
5
|
/**
|
|
4
6
|
* One app-level root layout loader for every compiler-discovered `@load`
|
|
5
7
|
* operation. Each invocation creates a fresh replica and never shares SSR data
|
|
6
8
|
* across requests.
|
|
7
9
|
*/
|
|
8
10
|
export function createDistributedSvelteKitServer(options) {
|
|
9
|
-
const
|
|
11
|
+
const boundaries = validateBoundaryOperations(options.boundaries);
|
|
12
|
+
const maxConcurrency = validateConcurrency(options.maxConcurrency ?? 8);
|
|
10
13
|
return Object.freeze({
|
|
11
14
|
async load(event) {
|
|
12
15
|
const session = await options.getSession(event);
|
|
@@ -25,54 +28,102 @@ export function createDistributedSvelteKitServer(options) {
|
|
|
25
28
|
}
|
|
26
29
|
const auth = options.getAuth?.(pageData, event) ?? authFromPageData(pageData);
|
|
27
30
|
const routeId = routeIdentity(event);
|
|
28
|
-
const
|
|
29
|
-
|
|
31
|
+
const selectedBoundaries = boundaries
|
|
32
|
+
.filter(({ plan }) => plan.kind === 'page'
|
|
33
|
+
? plan.route === routeId
|
|
34
|
+
: layoutOwnsRoute(plan.route, routeId))
|
|
35
|
+
.sort(compareBoundaryOperations);
|
|
36
|
+
if (selectedBoundaries.length === 0) {
|
|
30
37
|
return {
|
|
31
38
|
...pageData,
|
|
32
39
|
gqlError: null
|
|
33
40
|
};
|
|
34
41
|
}
|
|
42
|
+
const requestSignal = event.request?.signal;
|
|
35
43
|
const fetchImpl = event.fetch;
|
|
44
|
+
const requestFetch = fetchImpl === undefined || requestSignal === undefined
|
|
45
|
+
? fetchImpl
|
|
46
|
+
: ((input, init) => {
|
|
47
|
+
const transportSignal = init?.signal;
|
|
48
|
+
const signal = transportSignal === undefined || transportSignal === null
|
|
49
|
+
? requestSignal
|
|
50
|
+
: AbortSignal.any([requestSignal, transportSignal]);
|
|
51
|
+
return fetchImpl(input, { ...init, signal });
|
|
52
|
+
});
|
|
36
53
|
const transport = createReplicaGraphqlTransport({
|
|
37
54
|
getUrl: () => options.getUrl?.(event) ?? '/graphql',
|
|
38
55
|
getAuth: () => auth,
|
|
39
|
-
...(
|
|
56
|
+
...(requestFetch === undefined ? {} : { fetch: requestFetch })
|
|
40
57
|
});
|
|
41
58
|
const replica = createDistributedReplica({ transport });
|
|
42
|
-
const
|
|
59
|
+
const activeWatches = new Set();
|
|
60
|
+
const executions = [];
|
|
61
|
+
let aborted = requestSignal?.aborted ?? false;
|
|
62
|
+
const abort = () => {
|
|
63
|
+
aborted = true;
|
|
64
|
+
for (const watch of activeWatches)
|
|
65
|
+
watch.destroy();
|
|
66
|
+
};
|
|
67
|
+
requestSignal?.addEventListener('abort', abort, { once: true });
|
|
43
68
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
const needsParent = selectedBoundaries.some(({ binding }) => Object.values(binding.sources).some((source) => source?.kind === 'forwarded_prop'));
|
|
70
|
+
const props = needsParent && event.parent !== undefined
|
|
71
|
+
? await settleWithRequestAbort(() => event.parent(), requestSignal)
|
|
72
|
+
: Object.freeze({});
|
|
73
|
+
if (aborted)
|
|
74
|
+
throw requestAborted();
|
|
75
|
+
const boundaryContext = Object.freeze({
|
|
76
|
+
params: event.params ?? Object.freeze({}),
|
|
77
|
+
search: event.url?.searchParams ?? new URLSearchParams(),
|
|
78
|
+
session,
|
|
79
|
+
props
|
|
80
|
+
});
|
|
81
|
+
const scheduled = new Map();
|
|
82
|
+
for (const binding of selectedBoundaries) {
|
|
83
|
+
if (aborted)
|
|
84
|
+
throw requestAborted();
|
|
85
|
+
const variables = resolveDistributedBoundaryVariables(binding.artifact, binding.binding.sources, boundaryContext);
|
|
86
|
+
const identity = boundaryOperationIdentity(binding.artifact, variables);
|
|
87
|
+
if (scheduled.has(identity))
|
|
88
|
+
continue;
|
|
89
|
+
scheduled.set(identity, {
|
|
90
|
+
identity,
|
|
91
|
+
operation: binding.plan.operation,
|
|
92
|
+
artifact: binding.artifact,
|
|
93
|
+
variables
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const completed = await mapBounded([...scheduled.values()], maxConcurrency, async (item) => {
|
|
97
|
+
if (aborted)
|
|
98
|
+
throw requestAborted();
|
|
99
|
+
const watch = replica.watch(item.artifact, item.variables, { live: false });
|
|
100
|
+
activeWatches.add(watch);
|
|
101
|
+
let failure = null;
|
|
46
102
|
try {
|
|
47
|
-
|
|
48
|
-
(await options.variables?.[binding.plan.operation]?.(event)) ??
|
|
49
|
-
{};
|
|
50
|
-
const watch = replica.watch(binding.artifact, variables, { live: false });
|
|
51
|
-
watches.push({
|
|
52
|
-
operation: binding.plan.operation,
|
|
53
|
-
artifact: binding.artifact,
|
|
54
|
-
variables,
|
|
55
|
-
watch
|
|
56
|
-
});
|
|
103
|
+
await watch.refresh();
|
|
57
104
|
}
|
|
58
|
-
catch
|
|
59
|
-
|
|
60
|
-
throw new Error(`Distributed @load operation \`${binding.plan.operation}\` needs route variables; configure variables.${binding.plan.operation}(event) in createDistributedSvelteKitServer`, { cause: error });
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
105
|
+
catch {
|
|
106
|
+
failure = 'Distributed GraphQL island refresh failed';
|
|
63
107
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
if (watch.get().errors.length > 0) {
|
|
109
|
+
failure = 'Distributed GraphQL island refresh failed';
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze({ ...item, watch, failure });
|
|
112
|
+
});
|
|
113
|
+
executions.push(...completed);
|
|
114
|
+
if (aborted)
|
|
115
|
+
throw requestAborted();
|
|
116
|
+
const errors = executions.flatMap(({ failure }) => failure === null ? [] : [failure]);
|
|
117
|
+
for (const { artifact, variables, watch } of executions) {
|
|
68
118
|
// Preserve exact rendered-operation reachability after the
|
|
69
119
|
// temporary watch is released.
|
|
70
120
|
replica.read(artifact, variables);
|
|
71
121
|
watch.destroy();
|
|
122
|
+
activeWatches.delete(watch);
|
|
72
123
|
}
|
|
73
124
|
const transfer = replica.scope === undefined
|
|
74
125
|
? undefined
|
|
75
|
-
: hydrationTransfer(replica.dehydrate(),
|
|
126
|
+
: hydrationTransfer(replica.dehydrate(), selectedBoundaries.map(({ plan }) => plan.operation), selectedBoundaries.map(({ binding }) => binding.id));
|
|
76
127
|
return {
|
|
77
128
|
...pageData,
|
|
78
129
|
...(transfer === undefined
|
|
@@ -81,41 +132,30 @@ export function createDistributedSvelteKitServer(options) {
|
|
|
81
132
|
distributed: transfer.hydration,
|
|
82
133
|
distributedAuthority: transfer.authority
|
|
83
134
|
}),
|
|
84
|
-
gqlError: errors[0]
|
|
135
|
+
gqlError: errors[0] ??
|
|
85
136
|
(transfer === undefined
|
|
86
137
|
? 'Distributed GraphQL response did not establish an authoritative cache scope'
|
|
87
138
|
: null)
|
|
88
139
|
};
|
|
89
140
|
}
|
|
90
141
|
finally {
|
|
91
|
-
|
|
142
|
+
requestSignal?.removeEventListener('abort', abort);
|
|
143
|
+
for (const watch of activeWatches)
|
|
92
144
|
watch.destroy();
|
|
145
|
+
activeWatches.clear();
|
|
93
146
|
}
|
|
94
147
|
}
|
|
95
148
|
});
|
|
96
149
|
}
|
|
97
|
-
|
|
98
|
-
* Explicit one-line fallback when the compiler cannot discover route ownership.
|
|
99
|
-
*
|
|
100
|
-
* Prefer co-locating `+page.graphql`; the compiler diagnostic includes the
|
|
101
|
-
* equivalent `--route Operation=/route-id` registration.
|
|
102
|
-
*/
|
|
103
|
-
export function registerDistributedRoute(route, operation, artifact) {
|
|
104
|
-
return Object.freeze({
|
|
105
|
-
plan: Object.freeze({
|
|
106
|
-
operation: nonEmpty(operation, 'operation'),
|
|
107
|
-
route: normalizeRoute(route),
|
|
108
|
-
discovery: 'explicit'
|
|
109
|
-
}),
|
|
110
|
-
artifact: artifact
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
function hydrationTransfer(state, operations) {
|
|
150
|
+
function hydrationTransfer(state, operations, bindings = []) {
|
|
114
151
|
return Object.freeze({
|
|
115
152
|
hydration: Object.freeze({
|
|
116
153
|
version: 1,
|
|
117
154
|
state,
|
|
118
|
-
operations: Object.freeze([...operations])
|
|
155
|
+
operations: Object.freeze([...operations]),
|
|
156
|
+
...(bindings.length === 0
|
|
157
|
+
? {}
|
|
158
|
+
: { bindings: Object.freeze([...new Set(bindings)].sort()) })
|
|
119
159
|
}),
|
|
120
160
|
authority: Object.freeze({
|
|
121
161
|
version: 1,
|
|
@@ -123,102 +163,89 @@ function hydrationTransfer(state, operations) {
|
|
|
123
163
|
})
|
|
124
164
|
});
|
|
125
165
|
}
|
|
126
|
-
|
|
127
|
-
* Match a SvelteKit route id (`/blob/[[gameId]]`) to a browser pathname.
|
|
128
|
-
*/
|
|
129
|
-
export function matchDistributedRoute(routeId, pathname) {
|
|
130
|
-
const route = normalizeRoute(routeId);
|
|
131
|
-
const path = normalizePathname(pathname);
|
|
132
|
-
if (route === path)
|
|
133
|
-
return true;
|
|
134
|
-
const routeParts = route
|
|
135
|
-
.split('/')
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.filter((part) => !(part.startsWith('(') && part.endsWith(')')));
|
|
138
|
-
const pathParts = path.split('/').filter(Boolean);
|
|
139
|
-
const failed = new Set();
|
|
140
|
-
const matches = (routeIndex, pathIndex) => {
|
|
141
|
-
const state = routeIndex + ':' + pathIndex;
|
|
142
|
-
if (failed.has(state))
|
|
143
|
-
return false;
|
|
144
|
-
if (routeIndex === routeParts.length) {
|
|
145
|
-
return pathIndex === pathParts.length;
|
|
146
|
-
}
|
|
147
|
-
const part = routeParts[routeIndex];
|
|
148
|
-
const optionalRest = part.startsWith('[[...') && part.endsWith(']]');
|
|
149
|
-
const rest = part.startsWith('[...') && part.endsWith(']');
|
|
150
|
-
if (optionalRest || rest) {
|
|
151
|
-
for (let next = pathIndex; next <= pathParts.length; next += 1) {
|
|
152
|
-
if (matches(routeIndex + 1, next))
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
failed.add(state);
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
const optional = part.startsWith('[[') && part.endsWith(']]');
|
|
159
|
-
if (optional) {
|
|
160
|
-
if (matches(routeIndex + 1, pathIndex))
|
|
161
|
-
return true;
|
|
162
|
-
if (pathIndex < pathParts.length &&
|
|
163
|
-
matches(routeIndex + 1, pathIndex + 1)) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
failed.add(state);
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
const parameter = part.startsWith('[') && part.endsWith(']');
|
|
170
|
-
if ((parameter && pathIndex < pathParts.length) ||
|
|
171
|
-
(!parameter &&
|
|
172
|
-
pathIndex < pathParts.length &&
|
|
173
|
-
pathParts[pathIndex] === part)) {
|
|
174
|
-
if (matches(routeIndex + 1, pathIndex + 1))
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
failed.add(state);
|
|
178
|
-
return false;
|
|
179
|
-
};
|
|
180
|
-
return matches(0, 0);
|
|
181
|
-
}
|
|
182
|
-
function normalizePathname(pathname) {
|
|
183
|
-
if (typeof pathname !== 'string' || pathname.length === 0)
|
|
184
|
-
return '/';
|
|
185
|
-
const trimmed = pathname.replace(/\/+$/, '');
|
|
186
|
-
return trimmed.length === 0 ? '/' : trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
187
|
-
}
|
|
188
|
-
function validateRoutes(value) {
|
|
166
|
+
function validateBoundaryOperations(value) {
|
|
189
167
|
if (!Array.isArray(value)) {
|
|
190
|
-
throw new TypeError('createDistributedSvelteKitServer
|
|
168
|
+
throw new TypeError('createDistributedSvelteKitServer boundaries must be an array');
|
|
191
169
|
}
|
|
192
170
|
const identities = new Set();
|
|
193
|
-
return Object.freeze(value.map((
|
|
194
|
-
if (
|
|
195
|
-
typeof
|
|
196
|
-
binding
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
typeof binding.artifact !== 'object') {
|
|
200
|
-
throw new TypeError(`invalid Distributed route binding at index ${index}`);
|
|
201
|
-
}
|
|
202
|
-
const operation = nonEmpty(binding.plan.operation, 'route operation');
|
|
203
|
-
const route = normalizeRoute(binding.plan.route);
|
|
204
|
-
if (binding.artifact.id.length === 0) {
|
|
205
|
-
throw new TypeError(`invalid Distributed route artifact for ${operation}`);
|
|
171
|
+
return Object.freeze(value.map((operation, index) => {
|
|
172
|
+
if (operation === null ||
|
|
173
|
+
typeof operation !== 'object' ||
|
|
174
|
+
operation.binding?.version !== 1 ||
|
|
175
|
+
operation.binding.artifactId !== operation.artifact?.id) {
|
|
176
|
+
throw new TypeError(`invalid Distributed boundary operation at index ${index}`);
|
|
206
177
|
}
|
|
207
|
-
const
|
|
178
|
+
const route = normalizeRoute(operation.plan.route);
|
|
179
|
+
const identity = `${operation.plan.kind}\u0000${route}\u0000${operation.plan.operation}`;
|
|
208
180
|
if (identities.has(identity)) {
|
|
209
|
-
throw new TypeError(`duplicate Distributed
|
|
181
|
+
throw new TypeError(`duplicate Distributed boundary operation ${operation.plan.operation} at ${route}`);
|
|
210
182
|
}
|
|
211
183
|
identities.add(identity);
|
|
212
184
|
return Object.freeze({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
operation,
|
|
216
|
-
route
|
|
217
|
-
}),
|
|
218
|
-
artifact: binding.artifact
|
|
185
|
+
...operation,
|
|
186
|
+
plan: Object.freeze({ ...operation.plan, route })
|
|
219
187
|
});
|
|
220
188
|
}));
|
|
221
189
|
}
|
|
190
|
+
function compareBoundaryOperations(left, right) {
|
|
191
|
+
const leftDepth = left.plan.route.split('/').filter(Boolean).length;
|
|
192
|
+
const rightDepth = right.plan.route.split('/').filter(Boolean).length;
|
|
193
|
+
return (leftDepth - rightDepth ||
|
|
194
|
+
left.plan.kind.localeCompare(right.plan.kind) ||
|
|
195
|
+
left.plan.route.localeCompare(right.plan.route) ||
|
|
196
|
+
left.plan.operation.localeCompare(right.plan.operation) ||
|
|
197
|
+
left.binding.id.localeCompare(right.binding.id));
|
|
198
|
+
}
|
|
199
|
+
function layoutOwnsRoute(layout, route) {
|
|
200
|
+
const owner = normalizeRoute(layout);
|
|
201
|
+
const selected = normalizeRoute(route);
|
|
202
|
+
return owner === '/' || selected === owner || selected.startsWith(`${owner}/`);
|
|
203
|
+
}
|
|
204
|
+
function validateConcurrency(value) {
|
|
205
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 32) {
|
|
206
|
+
throw new TypeError('Distributed SvelteKit maxConcurrency must be an integer from 1 through 32');
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
async function mapBounded(values, concurrency, map) {
|
|
211
|
+
const results = new Array(values.length);
|
|
212
|
+
let next = 0;
|
|
213
|
+
const worker = async () => {
|
|
214
|
+
while (next < values.length) {
|
|
215
|
+
const index = next;
|
|
216
|
+
next += 1;
|
|
217
|
+
results[index] = await map(values[index], index);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
function requestAborted() {
|
|
224
|
+
const error = new Error('Distributed SvelteKit request was aborted');
|
|
225
|
+
error.name = 'AbortError';
|
|
226
|
+
return error;
|
|
227
|
+
}
|
|
228
|
+
async function settleWithRequestAbort(start, signal) {
|
|
229
|
+
if (signal === undefined)
|
|
230
|
+
return await start();
|
|
231
|
+
if (signal.aborted)
|
|
232
|
+
throw requestAborted();
|
|
233
|
+
let rejectAbort;
|
|
234
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
235
|
+
rejectAbort = reject;
|
|
236
|
+
});
|
|
237
|
+
const abort = () => rejectAbort?.(requestAborted());
|
|
238
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
239
|
+
try {
|
|
240
|
+
if (signal.aborted)
|
|
241
|
+
throw requestAborted();
|
|
242
|
+
return await Promise.race([start(), aborted]);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
signal.removeEventListener('abort', abort);
|
|
246
|
+
rejectAbort = undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
222
249
|
function routeIdentity(event) {
|
|
223
250
|
const route = event.route?.id;
|
|
224
251
|
if (typeof route === 'string' && route.length > 0)
|