@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.
@@ -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
- const auth = createAuthorizationFence(options.session, () => replica?.invalidateAuthorization(), options.onAuthError);
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
- routes: readonly DistributedRouteOperation[];
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
- /** Variables for routed operations that do not accept `{}`. */
39
- variables?: DistributedRouteVariables<TEvent>;
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 routes = validateRoutes(options.routes);
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 selected = routes.filter(({ plan }) => plan.route === routeId);
29
- if (selected.length === 0) {
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
- ...(fetchImpl === undefined ? {} : { fetch: fetchImpl })
56
+ ...(requestFetch === undefined ? {} : { fetch: requestFetch })
40
57
  });
41
58
  const replica = createDistributedReplica({ transport });
42
- const watches = [];
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
- for (const binding of selected) {
45
- let variables;
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
- variables =
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 (error) {
59
- if (options.variables?.[binding.plan.operation] === undefined) {
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
- await Promise.all(watches.map(({ watch }) => watch.refresh()));
66
- const errors = watches.flatMap(({ watch }) => watch.get().errors);
67
- for (const { artifact, variables, watch } of watches) {
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(), selected.map(({ plan }) => plan.operation));
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]?.message ??
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
- for (const { watch } of watches)
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 requires generated DISTRIBUTED_ROUTE_OPERATIONS');
168
+ throw new TypeError('createDistributedSvelteKitServer boundaries must be an array');
191
169
  }
192
170
  const identities = new Set();
193
- return Object.freeze(value.map((binding, index) => {
194
- if (binding === null ||
195
- typeof binding !== 'object' ||
196
- binding.plan === null ||
197
- typeof binding.plan !== 'object' ||
198
- binding.artifact === null ||
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 identity = `${route}\u0000${operation}`;
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 route operation ${operation} at ${route}`);
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
- plan: Object.freeze({
214
- ...binding.plan,
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)