@hops-ops/distributed 4.9.0 → 4.11.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 +89 -8
- 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/identity/codec.d.ts +1 -0
- package/dist/replica/identity/codec.js +16 -3
- package/dist/replica/index.d.ts +1 -1
- package/dist/replica/types.d.ts +42 -1
- 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 +294 -0
- package/dist/sveltekit/context.d.ts +3 -0
- package/dist/sveltekit/context.js +8 -0
- package/dist/sveltekit/index.d.ts +6 -3
- package/dist/sveltekit/index.js +5 -2
- package/dist/sveltekit/island-bindings.d.ts +17 -0
- package/dist/sveltekit/island-bindings.js +58 -0
- package/dist/sveltekit/islands/boundaries.d.ts +107 -0
- package/dist/sveltekit/islands/boundaries.js +788 -0
- package/dist/sveltekit/operation-identity.d.ts +4 -0
- package/dist/sveltekit/operation-identity.js +10 -0
- package/dist/sveltekit/replica.d.ts +24 -0
- package/dist/sveltekit/replica.js +72 -5
- package/dist/sveltekit/server-replica.d.ts +10 -26
- package/dist/sveltekit/server-replica.js +159 -132
- package/dist/sveltekit/vite.d.ts +10 -3
- package/dist/sveltekit/vite.js +310 -39
- package/package.json +2 -2
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReplicaOperationArtifact } from '../replica/index.js';
|
|
2
|
+
import type { GraphqlVariables } from '../types.js';
|
|
3
|
+
/** Build the canonical identity shared by SSR scheduling and browser retention. */
|
|
4
|
+
export declare function boundaryOperationIdentity(artifact: ReplicaOperationArtifact<unknown, GraphqlVariables>, variables: GraphqlVariables): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Build the canonical identity shared by SSR scheduling and browser retention. */
|
|
2
|
+
export function boundaryOperationIdentity(artifact, variables) {
|
|
3
|
+
return JSON.stringify([
|
|
4
|
+
artifact.protocol.version,
|
|
5
|
+
artifact.protocol.schemaHash,
|
|
6
|
+
artifact.protocol.surface,
|
|
7
|
+
artifact.id,
|
|
8
|
+
variables
|
|
9
|
+
]);
|
|
10
|
+
}
|
|
@@ -3,6 +3,8 @@ import type { GqlAuth, GqlError, GraphqlVariables } from '../types.js';
|
|
|
3
3
|
import type { WebSocketConstructor } from '../websocket.js';
|
|
4
4
|
import type { FetchLike } from '../request.js';
|
|
5
5
|
import { type PageGraphqlData } from './auth.js';
|
|
6
|
+
import { type DistributedBoundaryPlan, type DistributedBoundaryOperation, type DistributedBoundaryVariableSources, type DistributedBoundaryVariableContext } from './boundary-variables.js';
|
|
7
|
+
import { type DistributedSvelteKitBoundaryInstance, type DistributedSvelteKitBoundaryLocation, type DistributedSvelteKitLocationContext, type SveltekitBoundaryLifecycleDiagnostic, type SveltekitBoundaryRetention } from './boundary-lifecycle.js';
|
|
6
8
|
import { type DistributedReloadOptions } from './lifecycle.js';
|
|
7
9
|
type UnknownCommandEntries = Readonly<Record<string, never>>;
|
|
8
10
|
export type SveltekitSessionSource = Readonly<{
|
|
@@ -28,6 +30,8 @@ export type SveltekitReplicaHydration = Readonly<{
|
|
|
28
30
|
version: 1;
|
|
29
31
|
state: import('../replica/index.js').ReplicaDehydratedState;
|
|
30
32
|
readonly operations?: readonly string[];
|
|
33
|
+
/** Exact variable-binding fingerprints used to create this SSR seed. */
|
|
34
|
+
readonly bindings?: readonly string[];
|
|
31
35
|
}>;
|
|
32
36
|
/**
|
|
33
37
|
* Independently trusted SSR/session authority for one hydration transfer.
|
|
@@ -64,6 +68,10 @@ export type CreateDistributedSvelteKitOptions<TCommands = Readonly<Record<never,
|
|
|
64
68
|
authority?: SveltekitReplicaAuthority;
|
|
65
69
|
createCommands?: SveltekitCommandRuntimeFactory<TCommands>;
|
|
66
70
|
replica?: Omit<DistributedReplicaOptions, 'transport'>;
|
|
71
|
+
/** Generated boundary operations accepted by this component-tree client. */
|
|
72
|
+
boundaries: readonly DistributedBoundaryOperation[];
|
|
73
|
+
/** Redacted structural lifecycle events for boundary ownership diagnostics. */
|
|
74
|
+
onBoundaryDiagnostic?: (event: SveltekitBoundaryLifecycleDiagnostic) => void;
|
|
67
75
|
onAuthError?: (error: unknown) => void;
|
|
68
76
|
/** Generated clients supply the surface key; apps may declare safe state partitions. */
|
|
69
77
|
reload?: DistributedReloadOptions;
|
|
@@ -110,12 +118,21 @@ export type SveltekitBoundOperation<TData, TVariables extends GraphqlVariables>
|
|
|
110
118
|
read(variables: TVariables): ReplicaSnapshot<TData>;
|
|
111
119
|
/** Client-side hover/nav warmup; no-ops when the replica already has a complete snapshot. */
|
|
112
120
|
prefetch(variables: TVariables): Promise<void>;
|
|
121
|
+
/** One typed binding shared by SSR, navigation, prefetch, hydration, and live use. */
|
|
122
|
+
boundary<TSession = unknown, TProps = Readonly<Record<string, unknown>>>(plan: DistributedBoundaryPlan, sources: DistributedBoundaryVariableSources<TVariables>): DistributedBoundaryOperation<TData, TVariables, TSession, TProps>;
|
|
113
123
|
}>;
|
|
114
124
|
export type DistributedSvelteKitClient<TCommands> = Readonly<{
|
|
115
125
|
replica: DistributedReplica;
|
|
116
126
|
transport: ReplicaGraphqlTransport;
|
|
117
127
|
commands: TCommands;
|
|
118
128
|
operation<TData, TVariables extends GraphqlVariables>(artifact: ReplicaOperationArtifact<TData, TVariables>): SveltekitBoundOperation<TData, TVariables>;
|
|
129
|
+
boundary<TData, TVariables extends GraphqlVariables, TSession, TProps>(operation: DistributedBoundaryOperation<TData, TVariables, TSession, TProps>): SveltekitBoundBoundaryOperation<TData, TVariables, TSession, TProps>;
|
|
130
|
+
/** Retain every generated selection owned by one mounted page/layout instance. */
|
|
131
|
+
retainBoundary<TSession, TProps>(instance: DistributedSvelteKitBoundaryInstance, context: DistributedBoundaryVariableContext<TSession, TProps>): SveltekitBoundaryRetention;
|
|
132
|
+
/** Retain the nearest generated page/layout boundary at this location. */
|
|
133
|
+
retainLocation<TSession, TProps>(location: DistributedSvelteKitBoundaryLocation, context: DistributedSvelteKitLocationContext<TSession, TProps>): SveltekitBoundaryRetention;
|
|
134
|
+
/** Prefetch the generated page and owning layout selections for a target URL. */
|
|
135
|
+
prefetchLocation<TSession, TProps>(pathname: string, context: DistributedSvelteKitLocationContext<TSession, TProps>): Promise<void>;
|
|
119
136
|
/**
|
|
120
137
|
* Apply a server seed. A malformed or mismatched seed closes the old
|
|
121
138
|
* generation and returns false so the bound operation refetches.
|
|
@@ -125,6 +142,13 @@ export type DistributedSvelteKitClient<TCommands> = Readonly<{
|
|
|
125
142
|
invalidateAuthorization(): void;
|
|
126
143
|
destroy(): void;
|
|
127
144
|
}>;
|
|
145
|
+
export type SveltekitBoundBoundaryOperation<TData, TVariables extends GraphqlVariables, TSession, TProps> = Readonly<{
|
|
146
|
+
operation: DistributedBoundaryOperation<TData, TVariables, TSession, TProps>;
|
|
147
|
+
variables(context: DistributedBoundaryVariableContext<TSession, TProps>): TVariables;
|
|
148
|
+
use(context: DistributedBoundaryVariableContext<TSession, TProps>, options?: UseSveltekitOperationOptions): SveltekitQueryStore<TData>;
|
|
149
|
+
read(context: DistributedBoundaryVariableContext<TSession, TProps>): ReplicaSnapshot<TData>;
|
|
150
|
+
prefetch(context: DistributedBoundaryVariableContext<TSession, TProps>): Promise<void>;
|
|
151
|
+
}>;
|
|
128
152
|
/**
|
|
129
153
|
* Bind the framework-neutral replica to Svelte's readable-store lifecycle.
|
|
130
154
|
*
|
|
@@ -2,6 +2,8 @@ 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';
|
|
5
7
|
import { distributedReloadLifecycle, registerDistributedReloadClient } from './lifecycle.js';
|
|
6
8
|
/**
|
|
7
9
|
* Bind the framework-neutral replica to Svelte's readable-store lifecycle.
|
|
@@ -15,12 +17,20 @@ export function createDistributedSvelteKit(options) {
|
|
|
15
17
|
typeof options.session.getAuth !== 'function') {
|
|
16
18
|
throw new TypeError('Distributed SvelteKit requires one session source');
|
|
17
19
|
}
|
|
20
|
+
if (!Array.isArray(options.boundaries)) {
|
|
21
|
+
throw new TypeError('Distributed SvelteKit requires generated boundary operations');
|
|
22
|
+
}
|
|
18
23
|
if ((options.hydration === undefined) !==
|
|
19
24
|
(options.authority === undefined)) {
|
|
20
25
|
throw new TypeError('Distributed SvelteKit hydration requires separate trusted SSR authority');
|
|
21
26
|
}
|
|
22
27
|
let replica;
|
|
23
|
-
|
|
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);
|
|
24
34
|
const configuredUrl = options.url;
|
|
25
35
|
const transport = createReplicaGraphqlTransport({
|
|
26
36
|
getUrl: typeof configuredUrl === 'function'
|
|
@@ -34,8 +44,13 @@ export function createDistributedSvelteKit(options) {
|
|
|
34
44
|
});
|
|
35
45
|
replica = createDistributedReplica({
|
|
36
46
|
transport,
|
|
37
|
-
...(options.replica ?? {})
|
|
47
|
+
...(options.replica ?? {}),
|
|
48
|
+
onAuthorizationGenerationDispose: () => {
|
|
49
|
+
boundaryController?.disposeScope();
|
|
50
|
+
options.replica?.onAuthorizationGenerationDispose?.();
|
|
51
|
+
}
|
|
38
52
|
});
|
|
53
|
+
boundaryController = new DistributedSvelteKitBoundaryController(replica, options.boundaries, options.onBoundaryDiagnostic);
|
|
39
54
|
const stores = new Set();
|
|
40
55
|
const pending = new PendingReceiptStore();
|
|
41
56
|
let destroyed = false;
|
|
@@ -43,6 +58,10 @@ export function createDistributedSvelteKit(options) {
|
|
|
43
58
|
let accepted = false;
|
|
44
59
|
try {
|
|
45
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
|
+
}
|
|
46
65
|
const active = replica.scope;
|
|
47
66
|
if (active !== undefined && !sameReplicaScope(active, expected)) {
|
|
48
67
|
throw new TypeError('Distributed SvelteKit hydration authority changed before session invalidation');
|
|
@@ -54,8 +73,10 @@ export function createDistributedSvelteKit(options) {
|
|
|
54
73
|
catch {
|
|
55
74
|
accepted = false;
|
|
56
75
|
}
|
|
57
|
-
if (!accepted)
|
|
76
|
+
if (!accepted) {
|
|
77
|
+
boundaryController.disposeScope();
|
|
58
78
|
replica.invalidateAuthorization();
|
|
79
|
+
}
|
|
59
80
|
return accepted;
|
|
60
81
|
};
|
|
61
82
|
if (options.hydration !== undefined && options.authority !== undefined) {
|
|
@@ -108,24 +129,67 @@ export function createDistributedSvelteKit(options) {
|
|
|
108
129
|
stores.delete(store);
|
|
109
130
|
}
|
|
110
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
|
+
};
|
|
111
153
|
return Object.freeze({
|
|
112
154
|
replica,
|
|
113
155
|
transport,
|
|
114
156
|
commands,
|
|
115
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
|
+
},
|
|
116
177
|
hydrate,
|
|
117
178
|
prefetch(artifact, variables) {
|
|
118
179
|
return prefetchReplicaOperation(replica, artifact, variables);
|
|
119
180
|
},
|
|
120
181
|
invalidateAuthorization() {
|
|
121
|
-
if (!destroyed)
|
|
182
|
+
if (!destroyed) {
|
|
183
|
+
boundaryController.disposeScope();
|
|
122
184
|
replica.invalidateAuthorization();
|
|
185
|
+
}
|
|
123
186
|
},
|
|
124
187
|
destroy() {
|
|
125
188
|
if (destroyed)
|
|
126
189
|
return;
|
|
127
190
|
destroyed = true;
|
|
128
191
|
auth.dispose();
|
|
192
|
+
boundaryController.destroy();
|
|
129
193
|
for (const store of [...stores])
|
|
130
194
|
store.destroy();
|
|
131
195
|
stores.clear();
|
|
@@ -204,7 +268,10 @@ function bindOperation(replica, artifact, pending, lifecycle) {
|
|
|
204
268
|
artifact,
|
|
205
269
|
use,
|
|
206
270
|
read: (variables) => replica.read(artifact, variables),
|
|
207
|
-
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
|
+
}
|
|
208
275
|
});
|
|
209
276
|
}
|
|
210
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)
|