@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.
Files changed (37) hide show
  1. package/README.md +46 -8
  2. package/dist/generation.d.ts +15 -0
  3. package/dist/generation.js +41 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/protocol.d.ts +2 -0
  7. package/dist/protocol.js +5 -0
  8. package/dist/replica/command-runtime/create.js +11 -0
  9. package/dist/replica/command-runtime/errors.js +2 -0
  10. package/dist/replica/command-runtime/types.d.ts +7 -1
  11. package/dist/replica/distributed-replica/impl-protocol.d.ts +1 -0
  12. package/dist/replica/distributed-replica/impl-protocol.js +1 -0
  13. package/dist/replica/distributed-replica/impl.js +11 -0
  14. package/dist/replica/distributed-replica/watch.js +13 -1
  15. package/dist/replica/index.d.ts +1 -1
  16. package/dist/replica/types.d.ts +38 -0
  17. package/dist/sveltekit/boundary-lifecycle.d.ts +37 -0
  18. package/dist/sveltekit/boundary-lifecycle.js +355 -0
  19. package/dist/sveltekit/boundary-variables.d.ts +57 -0
  20. package/dist/sveltekit/boundary-variables.js +290 -0
  21. package/dist/sveltekit/context.d.ts +3 -0
  22. package/dist/sveltekit/context.js +8 -0
  23. package/dist/sveltekit/index.d.ts +7 -3
  24. package/dist/sveltekit/index.js +6 -2
  25. package/dist/sveltekit/islands/boundaries.d.ts +104 -0
  26. package/dist/sveltekit/islands/boundaries.js +734 -0
  27. package/dist/sveltekit/lifecycle.d.ts +57 -0
  28. package/dist/sveltekit/lifecycle.js +454 -0
  29. package/dist/sveltekit/operation-identity.d.ts +4 -0
  30. package/dist/sveltekit/operation-identity.js +10 -0
  31. package/dist/sveltekit/replica.d.ts +29 -2
  32. package/dist/sveltekit/replica.js +102 -6
  33. package/dist/sveltekit/server-replica.d.ts +10 -26
  34. package/dist/sveltekit/server-replica.js +159 -132
  35. package/dist/sveltekit/vite.d.ts +46 -3
  36. package/dist/sveltekit/vite.js +643 -36
  37. 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
- 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);
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
- 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)