@hops-ops/distributed 4.10.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 CHANGED
@@ -134,9 +134,52 @@ request-local replica. A single-surface application can omit the second entry.
134
134
  A component can own a sibling `Component.graphql` island. The adapter walks
135
135
  static Svelte imports and promotes `@load` work to the nearest page/layout.
136
136
  Route-local `+page.graphql` and `+layout.graphql` remain first-class. When a
137
- required variable cannot be proved from route/search/session/forwarded-prop
138
- sources, add one typed `boundaries` registration here; that generated binding
139
- is reused by SSR, hover prefetch, navigation, hydration, and live work.
137
+ surface needs its own authorization-specific copy, use a qualified route
138
+ document such as `+layout.public.graphql`; it still belongs to that layout.
139
+
140
+ Put stable values in GraphQL itself:
141
+
142
+ ```graphql
143
+ query ChatMessages(
144
+ $limit: Int! = 25
145
+ $offset: Int! = 0
146
+ ) @load @live {
147
+ chat_messages(limit: $limit, offset: $offset) { message_id body }
148
+ }
149
+ ```
150
+
151
+ The generated variable properties are optional, while the compiler-owned codec
152
+ canonicalizes the defaults before cache identity, SSR, live continuation, or
153
+ transport. An explicit `ChatMessages.use({ limit: 50, offset: 0 })` overrides
154
+ them; `ChatMessages.use()` uses the exact defaults.
155
+
156
+ Same-name route parameters are inferred. For external values, add one bounded
157
+ sidecar beside the document rather than putting screen behavior in
158
+ `distributed.config.js`:
159
+
160
+ ```js
161
+ // SearchResults.graphql.bindings.js
162
+ import {
163
+ defineGraphqlIslandBindings,
164
+ forwardedProp,
165
+ searchParam,
166
+ sessionClaim
167
+ } from '@hops-ops/distributed/sveltekit';
168
+
169
+ export default defineGraphqlIslandBindings({
170
+ query: searchParam('q'),
171
+ viewerId: sessionClaim('user', 'id'),
172
+ filters: forwardedProp('filters')
173
+ });
174
+ ```
175
+
176
+ The helper rejects unsafe shapes, generation checks the keys against the
177
+ operation, and the same binding is reused by SSR, hover prefetch, navigation,
178
+ hydration, and live work. Resolution precedence is explicit call variables,
179
+ then sidecar/route sources, then GraphQL defaults. A non-null variable without
180
+ any of those sources fails generation before transport. Central `boundaries`
181
+ registrations remain an explicit-placement escape hatch and cannot be combined
182
+ with a sidecar for the same operation.
140
183
 
141
184
  The Vite integration runs `distributed client` at startup/build, watches GraphQL
142
185
  documents, stages all surfaces, commits a rollback-capable multi-output
@@ -3,6 +3,7 @@ import type { ReplicaOperationArtifact, ReplicaVariableCodecArtifact, ReplicaVar
3
3
  export type VariableCodecRegistry = {
4
4
  readonly limits: ReplicaVariableCodecLimits;
5
5
  readonly variables: ReadonlyMap<string, ReplicaVariableInputRef>;
6
+ readonly defaults: ReadonlyMap<string, ReplicaValue>;
6
7
  readonly inputs: ReadonlyMap<string, ReplicaVariableInputDefinition>;
7
8
  };
8
9
  /**
@@ -24,6 +24,10 @@ export function canonicalizeOperationVariables(artifact, variables) {
24
24
  for (const [name, input] of [...registry.variables].sort(([left], [right]) => compareCodeUnits(left, right))) {
25
25
  const present = supplied.has(name) && supplied.get(name) !== undefined;
26
26
  if (!present) {
27
+ if (registry.defaults.has(name)) {
28
+ canonical.push([name, registry.defaults.get(name)]);
29
+ continue;
30
+ }
27
31
  if (!input.nullable) {
28
32
  variableValueInvalid(`variables.${name}`, 'required variable is missing');
29
33
  }
@@ -37,8 +41,8 @@ export function canonicalizeOperationVariables(artifact, variables) {
37
41
  return freezeRecord(canonical);
38
42
  }
39
43
  export function validateVariableCodec(codec) {
40
- const root = artifactRecord(codec, 'artifact.variableCodec', ['version', 'limits', 'variables', 'inputs']);
41
- if (root.version !== 1)
44
+ const root = artifactRecord(codec, 'artifact.variableCodec', ['version', 'limits', 'variables', 'defaults', 'inputs']);
45
+ if (root.version !== 2)
42
46
  variableCodecInvalid('artifact.variableCodec.version');
43
47
  const rawLimits = artifactRecord(root.limits, 'artifact.variableCodec.limits', ['maxDepth', 'maxBoolWidth', 'maxInList']);
44
48
  const limits = {
@@ -62,7 +66,16 @@ export function validateVariableCodec(codec) {
62
66
  for (const [name, definition] of inputs) {
63
67
  validateInputDefinition(definition, `artifact.variableCodec.inputs.${name}`, inputs, new Set(), 0);
64
68
  }
65
- return { limits, variables, inputs };
69
+ const defaults = new Map();
70
+ const registry = { limits, variables, defaults, inputs };
71
+ for (const [name, value] of artifactRecordEntries(root.defaults, 'artifact.variableCodec.defaults')) {
72
+ const input = variables.get(name);
73
+ if (input === undefined) {
74
+ variableCodecInvalid(`artifact.variableCodec.defaults.${name}`);
75
+ }
76
+ defaults.set(name, canonicalizeInputRef(input, value, registry, `artifact.variableCodec.defaults.${name}`, new Set(), 0));
77
+ }
78
+ return registry;
66
79
  }
67
80
  export function validateInputRef(value, path, inputs, limits, active, depth) {
68
81
  checkCodecDepth(depth, path);
@@ -150,9 +150,11 @@ export type ReplicaVariableCodecLimits = {
150
150
  };
151
151
  /** Exact variable codec emitted beside a generated operation artifact. */
152
152
  export type ReplicaVariableCodecArtifact = {
153
- readonly version: 1;
153
+ readonly version: 2;
154
154
  readonly limits: ReplicaVariableCodecLimits;
155
155
  readonly variables: Readonly<Record<string, ReplicaVariableInputRef>>;
156
+ /** Canonical GraphQL defaults, applied before cache identity or transport. */
157
+ readonly defaults: Readonly<Record<string, ReplicaValue>>;
156
158
  readonly inputs: Readonly<Record<string, ReplicaVariableInputDefinition>>;
157
159
  };
158
160
  export type ReplicaFilterOperator = '_eq' | '_neq' | '_gt' | '_gte' | '_lt' | '_lte' | '_in' | '_nin' | '_is_null' | '_like' | '_ilike' | '_contains' | '_contained_in' | '_has_key';
@@ -463,6 +465,7 @@ export type ReplicaIslandMetadata = {
463
465
  readonly variables: readonly {
464
466
  readonly name: string;
465
467
  readonly graphqlType: string;
468
+ readonly defaultValue?: ReplicaValue;
466
469
  }[];
467
470
  };
468
471
  readonly liveCoverage: {
@@ -96,9 +96,13 @@ function validateSources(artifact, sources) {
96
96
  throw new TypeError('Distributed boundary variable sources must be an object');
97
97
  }
98
98
  const definitions = artifact.variableCodec?.variables;
99
+ const defaults = artifact.variableCodec?.defaults;
99
100
  if (definitions === null || typeof definitions !== 'object' || Array.isArray(definitions)) {
100
101
  throw new TypeError('Distributed boundary artifact has no variable codec');
101
102
  }
103
+ if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
104
+ throw new TypeError('Distributed boundary artifact has no variable defaults');
105
+ }
102
106
  const names = Object.keys(sources);
103
107
  if (names.length > MAX_BINDING_VARIABLES) {
104
108
  throw new TypeError(`Distributed boundary binding exceeds ${MAX_BINDING_VARIABLES} variables`);
@@ -116,7 +120,7 @@ function validateSources(artifact, sources) {
116
120
  typeof definition === 'object' &&
117
121
  'nullable' in definition &&
118
122
  definition.nullable === false;
119
- if (required && !Object.hasOwn(sources, name)) {
123
+ if (required && !Object.hasOwn(defaults, name) && !Object.hasOwn(sources, name)) {
120
124
  throw new TypeError(`Distributed boundary binding is missing required variable ${name}; use an explicit binding, parent/boundary query, client-only execution, or a better read root`);
121
125
  }
122
126
  }
@@ -2,6 +2,7 @@ export { authFromPageData, type PageGraphqlData } from './auth.js';
2
2
  export { distributedReloadLifecycle, registerDistributedReloadClient, validateDistributedReloadLocation, validateDistributedReloadState, type DistributedReloadLifecycle, type DistributedReloadOptions, type DistributedReloadStateDeclaration } from './lifecycle.js';
3
3
  export { parseDistributedGenerationEnvelope, type DistributedGenerationEnvelope } from '../generation.js';
4
4
  export { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation, resolveDistributedBoundaryVariables, type DistributedBoundaryBinding, type DistributedBoundaryOperation, type DistributedBoundaryPlan, type DistributedBoundaryVariableContext, type DistributedBoundaryVariableSource, type DistributedBoundaryVariableSources } from './boundary-variables.js';
5
+ export { constant, defineGraphqlIslandBindings, forwardedProp, omitVariable, routeParam, searchParam, sessionClaim } from './island-bindings.js';
5
6
  export { DistributedSvelteKitBoundaryController, type DistributedSvelteKitBoundaryInstance, type DistributedSvelteKitBoundaryLocation, type DistributedSvelteKitLocationContext, type SveltekitBoundaryLifecycleDiagnostic, type SveltekitBoundaryRetention } from './boundary-lifecycle.js';
6
7
  export { defineDistributedSvelteKitOperation, provideDistributedSvelteKitClient, retainDistributedSvelteKitBoundary, useDistributedSvelteKitClient, useDistributedSvelteKitCommands } from './context.js';
7
8
  export { bindSveltekitOperation, createPageDataSessionSource, createDistributedSvelteKit, sessionSourceFromPageData, type CreateDistributedSvelteKitOptions, type DistributedSvelteKitClient, type SveltekitBoundOperation, type SveltekitBoundBoundaryOperation, type SveltekitCommandRuntimeFactory, type SveltekitCommandRuntimeFactoryOptions, type SveltekitCommandRuntimeLike, type SveltekitDistributedPageData, type SveltekitPageDataSessionSource, type SveltekitPageDataSource, type SveltekitQuerySnapshot, type SveltekitQueryStore, type SveltekitReplicaAuthority, type SveltekitReplicaHydration, type SveltekitSessionSource, type UseSveltekitOperationOptions } from './replica.js';
@@ -2,6 +2,7 @@ export { authFromPageData } from './auth.js';
2
2
  export { distributedReloadLifecycle, registerDistributedReloadClient, validateDistributedReloadLocation, validateDistributedReloadState } from './lifecycle.js';
3
3
  export { parseDistributedGenerationEnvelope } from '../generation.js';
4
4
  export { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation, resolveDistributedBoundaryVariables } from './boundary-variables.js';
5
+ export { constant, defineGraphqlIslandBindings, forwardedProp, omitVariable, routeParam, searchParam, sessionClaim } from './island-bindings.js';
5
6
  export { DistributedSvelteKitBoundaryController } from './boundary-lifecycle.js';
6
7
  export { defineDistributedSvelteKitOperation, provideDistributedSvelteKitClient, retainDistributedSvelteKitBoundary, useDistributedSvelteKitClient, useDistributedSvelteKitCommands } from './context.js';
7
8
  export { bindSveltekitOperation, createPageDataSessionSource, createDistributedSvelteKit, sessionSourceFromPageData } from './replica.js';
@@ -0,0 +1,17 @@
1
+ import type { GraphqlVariables } from '../types.js';
2
+ import type { DistributedBoundaryVariableSource, DistributedBoundaryVariableSources } from './boundary-variables.js';
3
+ /**
4
+ * Define the exceptional variables for one colocated GraphQL island.
5
+ *
6
+ * Save the default export as `<document>.bindings.js`. Route parameters with
7
+ * the same name and GraphQL variable defaults need no sidecar entry.
8
+ */
9
+ export declare function defineGraphqlIslandBindings<TVariables extends GraphqlVariables = GraphqlVariables>(sources: DistributedBoundaryVariableSources<TVariables>): DistributedBoundaryVariableSources<TVariables>;
10
+ /** @internal Validate a discovered sidecar without trusting its prototype. */
11
+ export declare function isGraphqlIslandBindings(value: unknown): value is DistributedBoundaryVariableSources<GraphqlVariables>;
12
+ export declare function routeParam(name: string): DistributedBoundaryVariableSource;
13
+ export declare function searchParam(name: string, mode?: 'first' | 'all'): DistributedBoundaryVariableSource;
14
+ export declare function sessionClaim(...path: string[]): DistributedBoundaryVariableSource;
15
+ export declare function forwardedProp(...path: string[]): DistributedBoundaryVariableSource;
16
+ export declare function constant<TValue>(value: TValue): DistributedBoundaryVariableSource<TValue>;
17
+ export declare function omitVariable(): DistributedBoundaryVariableSource;
@@ -0,0 +1,58 @@
1
+ const GRAPHQL_ISLAND_BINDINGS = Symbol.for('@hops-ops/distributed/graphql-island-bindings');
2
+ const HOSTILE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
3
+ /**
4
+ * Define the exceptional variables for one colocated GraphQL island.
5
+ *
6
+ * Save the default export as `<document>.bindings.js`. Route parameters with
7
+ * the same name and GraphQL variable defaults need no sidecar entry.
8
+ */
9
+ export function defineGraphqlIslandBindings(sources) {
10
+ if (sources === null ||
11
+ typeof sources !== 'object' ||
12
+ Array.isArray(sources) ||
13
+ (Object.getPrototypeOf(sources) !== Object.prototype &&
14
+ Object.getPrototypeOf(sources) !== null)) {
15
+ throw new TypeError('GraphQL island bindings must be an object');
16
+ }
17
+ const bindings = {};
18
+ for (const key of Object.keys(sources)) {
19
+ if (HOSTILE_KEYS.has(key)) {
20
+ throw new TypeError(`GraphQL island binding name ${key} is unsafe`);
21
+ }
22
+ const descriptor = Object.getOwnPropertyDescriptor(sources, key);
23
+ if (descriptor === undefined || !('value' in descriptor)) {
24
+ throw new TypeError(`GraphQL island binding ${key} must be a data property`);
25
+ }
26
+ bindings[key] = descriptor.value;
27
+ }
28
+ Object.defineProperty(bindings, GRAPHQL_ISLAND_BINDINGS, {
29
+ value: true,
30
+ enumerable: false
31
+ });
32
+ return Object.freeze(bindings);
33
+ }
34
+ /** @internal Validate a discovered sidecar without trusting its prototype. */
35
+ export function isGraphqlIslandBindings(value) {
36
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
37
+ return false;
38
+ const descriptor = Object.getOwnPropertyDescriptor(value, GRAPHQL_ISLAND_BINDINGS);
39
+ return descriptor !== undefined && 'value' in descriptor && descriptor.value === true;
40
+ }
41
+ export function routeParam(name) {
42
+ return Object.freeze({ kind: 'route_param', name });
43
+ }
44
+ export function searchParam(name, mode = 'first') {
45
+ return Object.freeze({ kind: 'search_param', name, mode });
46
+ }
47
+ export function sessionClaim(...path) {
48
+ return Object.freeze({ kind: 'trusted_session', path: Object.freeze(path) });
49
+ }
50
+ export function forwardedProp(...path) {
51
+ return Object.freeze({ kind: 'forwarded_prop', path: Object.freeze(path) });
52
+ }
53
+ export function constant(value) {
54
+ return Object.freeze({ kind: 'constant', value });
55
+ }
56
+ export function omitVariable() {
57
+ return Object.freeze({ kind: 'omit' });
58
+ }
@@ -34,6 +34,7 @@ export type DistributedIslandPlanInput = Readonly<{
34
34
  variables: readonly Readonly<{
35
35
  name: string;
36
36
  graphqlType: string;
37
+ defaultValue?: unknown;
37
38
  }>[];
38
39
  }>;
39
40
  }>;
@@ -98,7 +99,9 @@ export type DistributedSvelteKitBoundaryAnalysisOptions = Readonly<{
98
99
  /** Validate a persisted adapter plan before check/dev treats it as coherent. */
99
100
  export declare function validateDistributedSvelteKitBoundaryPlan(value: unknown, module?: string): DistributedSvelteKitBoundaryPlan;
100
101
  /**
101
- * Analyze Svelte component reachability without evaluating application code.
102
- * The returned plan is deterministic and contains project-relative paths only.
102
+ * Analyze Svelte component reachability without evaluating application
103
+ * components. Colocated, bounded GraphQL binding sidecars are the only app
104
+ * modules evaluated. The returned plan is deterministic and contains
105
+ * project-relative paths only.
103
106
  */
104
107
  export declare function analyzeDistributedSvelteKitBoundaries(options: DistributedSvelteKitBoundaryAnalysisOptions): Promise<readonly DistributedSvelteKitBoundaryPlan[]>;
@@ -1,10 +1,15 @@
1
- import { lstat, readFile, readdir } from 'node:fs/promises';
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
2
3
  import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
3
5
  import { parse } from 'svelte/compiler';
6
+ import { isGraphqlIslandBindings } from '../island-bindings.js';
4
7
  const BOUNDARY_PLAN_VERSION = 1;
8
+ const VARIABLE_CODEC_VERSION = 2;
5
9
  const MAX_COMPONENTS = 4_096;
6
10
  const MAX_ISLANDS = 4_096;
7
11
  const MAX_COMPONENT_BYTES = 2 * 1024 * 1024;
12
+ const MAX_BINDINGS_BYTES = 256 * 1024;
8
13
  const MAX_GRAPH_EDGES = 32_768;
9
14
  /** Validate a persisted adapter plan before check/dev treats it as coherent. */
10
15
  export function validateDistributedSvelteKitBoundaryPlan(value, module) {
@@ -21,8 +26,10 @@ export function validateDistributedSvelteKitBoundaryPlan(value, module) {
21
26
  return value;
22
27
  }
23
28
  /**
24
- * Analyze Svelte component reachability without evaluating application code.
25
- * The returned plan is deterministic and contains project-relative paths only.
29
+ * Analyze Svelte component reachability without evaluating application
30
+ * components. Colocated, bounded GraphQL binding sidecars are the only app
31
+ * modules evaluated. The returned plan is deterministic and contains
32
+ * project-relative paths only.
26
33
  */
27
34
  export async function analyzeDistributedSvelteKitBoundaries(options) {
28
35
  const cwd = resolve(options.cwd);
@@ -52,6 +59,7 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
52
59
  }
53
60
  }
54
61
  const plans = [];
62
+ const colocatedBindings = await loadColocatedBindings(cwd, sourceOwners.keys());
55
63
  for (const client of [...options.clients].sort((left, right) => left.module.localeCompare(right.module))) {
56
64
  const loadIslands = client.inventory.islands
57
65
  .filter((island) => island.directives.load)
@@ -115,11 +123,11 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
115
123
  const explicitEntries = explicitByBoundary.get(root.id) ?? [];
116
124
  const explicitAtBoundary = new Set(explicitEntries.map(({ entry }) => entry.island.id));
117
125
  for (const { entry, registration } of explicitEntries) {
118
- occurrences.push(occurrence(cwd, entry, root, root.path, 'explicit', false, registration.variables));
126
+ occurrences.push(occurrence(cwd, entry, root, root.path, 'explicit', false, bindingSources(entry, registration.variables, colocatedBindings)));
119
127
  placed.add(entry.island.id);
120
128
  }
121
129
  for (const entry of routeIslands.get(routeBoundaryKey(root.kind, dirname(root.path))) ?? []) {
122
- occurrences.push(occurrence(cwd, entry, root, root.path, 'route_document', false, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables));
130
+ occurrences.push(occurrence(cwd, entry, root, root.path, 'route_document', false, bindingSources(entry, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables, colocatedBindings)));
123
131
  placed.add(entry.island.id);
124
132
  }
125
133
  const dynamicComponentIslands = new Map([...componentIslands.entries()]
@@ -131,7 +139,7 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
131
139
  const reachable = traverse(root, components, aliases, dynamicComponentIslands, cwd);
132
140
  for (const component of reachable) {
133
141
  for (const entry of componentIslands.get(component) ?? []) {
134
- occurrences.push(occurrence(cwd, entry, root, component, 'static_component_import', true, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables));
142
+ occurrences.push(occurrence(cwd, entry, root, component, 'static_component_import', true, bindingSources(entry, explicitByIdentity.get(`${root.id}\u0000${entry.island.operation}`)?.variables, colocatedBindings)));
135
143
  placed.add(entry.island.id);
136
144
  }
137
145
  }
@@ -164,6 +172,49 @@ export async function analyzeDistributedSvelteKitBoundaries(options) {
164
172
  }
165
173
  return Object.freeze(plans);
166
174
  }
175
+ function bindingSources(entry, explicit, colocated) {
176
+ const sidecar = colocated.get(entry.source);
177
+ if (explicit !== undefined && sidecar !== undefined) {
178
+ throw diagnostic('distributed.island.variable_binding_conflict', entry.source, entry.island.source.line, entry.island.source.column, `operation ${entry.island.operation} has both centralized and colocated variable bindings; remove the screen behavior from distributed.config and keep ${entry.source}.bindings.js`);
179
+ }
180
+ return sidecar ?? explicit;
181
+ }
182
+ async function loadColocatedBindings(cwd, sources) {
183
+ const canonicalRoot = await realpath(cwd);
184
+ const bindings = new Map();
185
+ for (const source of [...new Set(sources)].sort()) {
186
+ const path = contained(cwd, `${source}.bindings.js`, 'GraphQL island bindings');
187
+ let metadata;
188
+ try {
189
+ metadata = await lstat(path);
190
+ }
191
+ catch (error) {
192
+ if (isMissing(error))
193
+ continue;
194
+ throw error;
195
+ }
196
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
197
+ throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must be a regular project-local file`);
198
+ }
199
+ if (metadata.size > MAX_BINDINGS_BYTES) {
200
+ throw new Error(`[distributed.island.bindings_too_large] ${source}.bindings.js exceeds ${MAX_BINDINGS_BYTES} bytes`);
201
+ }
202
+ const canonicalPath = await realpath(path);
203
+ if (!isWithin(canonicalRoot, canonicalPath)) {
204
+ throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must stay within the project root`);
205
+ }
206
+ const bytes = await readFile(canonicalPath);
207
+ const url = pathToFileURL(canonicalPath);
208
+ url.searchParams.set('distributed-binding', createHash('sha256').update(bytes).digest('hex'));
209
+ const loaded = await import(url.href);
210
+ const value = ownDataValue(loaded, 'default');
211
+ if (!isGraphqlIslandBindings(value)) {
212
+ throw new Error(`[distributed.island.bindings_invalid] ${source}.bindings.js must default-export defineGraphqlIslandBindings({...})`);
213
+ }
214
+ bindings.set(source, value);
215
+ }
216
+ return bindings;
217
+ }
167
218
  function occurrence(cwd, entry, root, component, reason, conservative, explicitSources) {
168
219
  if (root.kind === 'layout' &&
169
220
  entry.island.directives.live &&
@@ -222,7 +273,7 @@ function boundaryBinding(entry, root, explicitSources) {
222
273
  ]);
223
274
  hasRoute = true;
224
275
  }
225
- else if (!variable.graphqlType.endsWith('!')) {
276
+ else if (Object.hasOwn(variable, 'defaultValue') || !variable.graphqlType.endsWith('!')) {
226
277
  sources.push([variable.name, Object.freeze({ kind: 'omit' })]);
227
278
  }
228
279
  else {
@@ -546,10 +597,11 @@ function islandOwnerComponent(cwd, source) {
546
597
  const suffix = extname(absolute);
547
598
  const base = absolute.slice(0, -suffix.length);
548
599
  const name = posix.basename(portable(base));
549
- if (name === '+page' || name === '+layout') {
600
+ const routeDocument = /^\+(page|layout)(?:\.[_A-Za-z][_0-9A-Za-z-]*)?$/.exec(name);
601
+ if (routeDocument !== null) {
550
602
  return {
551
603
  kind: 'route',
552
- key: routeBoundaryKey(name === '+page' ? 'page' : 'layout', dirname(base))
604
+ key: routeBoundaryKey(routeDocument[1] === 'page' ? 'page' : 'layout', dirname(base))
553
605
  };
554
606
  }
555
607
  return { kind: 'component', path: `${base}.svelte` };
@@ -590,6 +642,8 @@ function validateInventory(module, inventory) {
590
642
  Array.isArray(variableSchema) ||
591
643
  typeof variableSchema.reference !== 'string' ||
592
644
  !Number.isSafeInteger(variableSchema.codecVersion) ||
645
+ variableSchema.codecVersion !== VARIABLE_CODEC_VERSION ||
646
+ !variableSchema.reference.endsWith(`#variable-codec-v${VARIABLE_CODEC_VERSION}`) ||
593
647
  !Array.isArray(variableSchema.variables) ||
594
648
  variableSchema.variables.some((variable) => variable === null ||
595
649
  typeof variable !== 'object' ||
@@ -62,7 +62,7 @@ export async function checkDistributedSvelteKit(options) {
62
62
  * coalesced, abortable, and always invoked without a shell.
63
63
  */
64
64
  export function distributedSvelteKit(options) {
65
- const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_DIR !== undefined;
65
+ const lifecycleOwnsCompile = process.env.DISTRIBUTED_LIFECYCLE_OWNS_CLIENT_COMPILE === '1';
66
66
  let resolved;
67
67
  let dirty = false;
68
68
  let running;
@@ -135,14 +135,11 @@ export function distributedSvelteKit(options) {
135
135
  frameworkDist = localFrameworkDist(config.root);
136
136
  await validateResolvedPaths(resolved);
137
137
  /*
138
- * `distributed dev` has already staged this generation and owns every
139
- * compiler input through its application watcher. Recompiling here would
140
- * race the API process for Cargo and mutate generated output outside the
141
- * lifecycle transaction. The virtual modules still resolve the staged
142
- * files, while compiler inputs below are held for the supervisor reload.
138
+ * The lifecycle has already built the active generation before it starts
139
+ * this UI process. The supervisor activates later generations atomically
140
+ * while this server stays available; compiling again here would race that
141
+ * owner and can prevent the UI from reaching its readiness probe.
143
142
  */
144
- if (lifecycleOwnsCompile)
145
- return;
146
143
  /*
147
144
  * SvelteKit post-build analysis loads Vite config in an isolated
148
145
  * worker marked with SVELTEKIT_FORK. That pass reads framework
@@ -151,6 +148,8 @@ export function distributedSvelteKit(options) {
151
148
  */
152
149
  if (!isMainThread && process.env.SVELTEKIT_FORK === 'true')
153
150
  return;
151
+ if (lifecycleOwnsCompile)
152
+ return;
154
153
  lock = await acquireCompilerLock(resolved.cwd);
155
154
  try {
156
155
  await compile('Vite startup', true);
@@ -229,7 +228,7 @@ export function distributedSvelteKit(options) {
229
228
  context.server.moduleGraph.invalidateModule(module);
230
229
  }
231
230
  }
232
- if (process.env.DISTRIBUTED_LIFECYCLE_DIR === undefined) {
231
+ if (!lifecycleOwnsCompile) {
233
232
  context.server.ws.send({ type: 'full-reload', path: '*' });
234
233
  }
235
234
  reloadedGeneration = completedGeneration;
@@ -665,12 +664,15 @@ function isCompilerInput(file, integration) {
665
664
  }
666
665
  function isGraphqlInput(file, integration) {
667
666
  const absolute = resolve(integration.cwd, file);
667
+ const isDocument = absolute.endsWith('.graphql') || absolute.endsWith('.gql');
668
+ const isBindings = absolute.endsWith('.graphql.bindings.js') ||
669
+ absolute.endsWith('.gql.bindings.js');
668
670
  if (absolute.endsWith('.svelte') &&
669
671
  (isWithin(integration.routesDir, absolute) ||
670
672
  isWithin(integration.libDir, absolute))) {
671
673
  return true;
672
674
  }
673
- if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
675
+ if ((!isDocument && !isBindings) ||
674
676
  !isWithin(integration.cwd, absolute)) {
675
677
  return false;
676
678
  }
@@ -728,7 +730,7 @@ async function compileTransaction(integration, children, signal) {
728
730
  hadAdapterOutput: await realDirectoryExists(client.adapterOut)
729
731
  });
730
732
  }
731
- const plans = await analyzeStagedBoundaries(integration, staged);
733
+ const plans = await analyzeStagedBoundaries(integration, transaction, staged);
732
734
  const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
733
735
  for (const item of staged) {
734
736
  const plan = plansByModule.get(item.client.module);
@@ -775,7 +777,7 @@ async function checkTransaction(integration, children, signal) {
775
777
  await validateGeneratedEntrypoint(integration.cwd, output, client.module);
776
778
  staged.push(Object.freeze({ client, output }));
777
779
  }
778
- const plans = await analyzeStagedBoundaries(integration, staged);
780
+ const plans = await analyzeStagedBoundaries(integration, transaction, staged);
779
781
  const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
780
782
  for (const [index, client] of integration.clients.entries()) {
781
783
  const output = staged[index].output;
@@ -829,7 +831,7 @@ async function validateAdapterBoundaryPlan(client, plan) {
829
831
  throw new Error(`Distributed SvelteKit boundary plan for ${client.module} is stale; run generation without check`);
830
832
  }
831
833
  }
832
- async function analyzeStagedBoundaries(integration, staged) {
834
+ async function analyzeStagedBoundaries(integration, transactionRoot, staged) {
833
835
  return await analyzeDistributedSvelteKitBoundaries({
834
836
  cwd: integration.cwd,
835
837
  routesDir: portablePath(relative(integration.cwd, integration.routesDir)),
@@ -837,18 +839,18 @@ async function analyzeStagedBoundaries(integration, staged) {
837
839
  aliases: integration.aliases,
838
840
  clients: await Promise.all(staged.map(async ({ client, output }) => ({
839
841
  module: client.module,
840
- inventory: await readIslandInventory(integration.cwd, output),
842
+ inventory: await readIslandInventory(transactionRoot, output),
841
843
  explicitBoundaries: client.boundaries
842
844
  })))
843
845
  });
844
846
  }
845
- async function readIslandInventory(cwd, output) {
847
+ async function readIslandInventory(transactionRoot, output) {
846
848
  const path = join(output, 'islands.json');
847
849
  const metadata = await lstat(path);
848
850
  if (metadata.isSymbolicLink() || !metadata.isFile()) {
849
- throw new Error(`Distributed island inventory ${portablePath(relative(cwd, path))} must be a regular file`);
851
+ throw new Error(`Distributed island inventory ${portablePath(relative(transactionRoot, path))} must be a regular file`);
850
852
  }
851
- const canonicalRoot = await realpath(cwd);
853
+ const canonicalRoot = await realpath(transactionRoot);
852
854
  const canonical = await realpath(path);
853
855
  if (!isWithin(canonicalRoot, canonical)) {
854
856
  throw new Error('Distributed island inventory escaped the project root');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hops-ops/distributed",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "Typed GraphQL client, causal replica, command runtime, and framework adapters for Distributed services",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",