@hops-ops/distributed 4.9.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.
@@ -1,3 +1,5 @@
1
+ import { type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
2
+ export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan, type DistributedIslandInventory, type DistributedIslandPlanInput, type DistributedSvelteKitBoundary, type DistributedSvelteKitBoundaryAnalysisClient, type DistributedSvelteKitBoundaryAnalysisOptions, type DistributedSvelteKitBoundaryOccurrence, type DistributedSvelteKitBoundaryPlan, type DistributedSvelteKitBoundaryRegistration } from './islands/boundaries.js';
1
3
  export type DistributedGraphqlProxyOptions = {
2
4
  /** Absolute Distributed API origin, for example `http://127.0.0.1:8791`. */
3
5
  target: string;
@@ -28,8 +30,8 @@ export type DistributedSvelteKitClientCompiler = Readonly<{
28
30
  surface?: string;
29
31
  /** GraphQL globs passed verbatim as repeated `distributed client --documents`. */
30
32
  documents: readonly string[];
31
- /** Explicit `OPERATION=/route` fallbacks. */
32
- routes?: readonly string[];
33
+ /** Typed fallback when static component ownership cannot be proven. */
34
+ boundaries?: readonly DistributedSvelteKitBoundaryRegistration[];
33
35
  /** Compiler-owned artifact directory, relative to `cwd` by default. */
34
36
  out: string;
35
37
  }>;
@@ -40,6 +42,12 @@ export type DistributedSvelteKitViteOptions = Readonly<{
40
42
  command?: string;
41
43
  /** Prefix argv, e.g. `cargo run ... --`; never interpreted by a shell. */
42
44
  commandArgs?: readonly string[];
45
+ /** SvelteKit route source root. Defaults to `src/routes`. */
46
+ routesDir?: string;
47
+ /** SvelteKit library source root. Defaults to `src/lib`. */
48
+ libDir?: string;
49
+ /** Additional project-local `$name` aliases used by static component imports. */
50
+ aliases?: Readonly<Record<string, string>>;
43
51
  clients: readonly DistributedSvelteKitClientCompiler[];
44
52
  }>;
45
53
  type ViteWebSocketLike = Readonly<{
@@ -136,4 +144,3 @@ export declare function distributedSvelteKit(options: DistributedSvelteKitViteOp
136
144
  * generation and is safe to call from `svelte.config.js`.
137
145
  */
138
146
  export declare function distributedSvelteKitAliases(options: Pick<DistributedSvelteKitViteOptions, 'cwd' | 'clients'>): Readonly<Record<string, string>>;
139
- export {};
@@ -1,10 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
4
- import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
4
+ import { lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, unlink, writeFile } from 'node:fs/promises';
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
6
  import { isMainThread } from 'node:worker_threads';
7
+ import { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
8
+ export { analyzeDistributedSvelteKitBoundaries, validateDistributedSvelteKitBoundaryPlan } from './islands/boundaries.js';
7
9
  const GENERATED_SVELTEKIT_MODULE = 'sveltekit.ts';
10
+ const GENERATED_BOUNDARIES_MODULE = 'boundaries.ts';
8
11
  const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
9
12
  const MAX_GENERATED_COMPARE_FILES = 20_000;
10
13
  const MAX_GENERATED_COMPARE_BYTES = 128 * 1024 * 1024;
@@ -162,10 +165,14 @@ export function distributedSvelteKit(options) {
162
165
  configureLifecycleServer(server);
163
166
  const integration = requireResolved(resolved);
164
167
  if (!lifecycleOwnsCompile) {
165
- const roots = integration.clients.flatMap((client) => [
166
- ...client.watchRoots,
167
- ...client.manifestWatchRoots
168
- ]);
168
+ const roots = [
169
+ integration.routesDir,
170
+ integration.libDir,
171
+ ...integration.clients.flatMap((client) => [
172
+ ...client.watchRoots,
173
+ ...client.manifestWatchRoots
174
+ ])
175
+ ];
169
176
  if (roots.length > 0)
170
177
  server.watcher.add(roots);
171
178
  }
@@ -177,6 +184,8 @@ export function distributedSvelteKit(options) {
177
184
  const integration = requireResolved(resolved);
178
185
  if (lifecycleOwnsCompile)
179
186
  return;
187
+ this.addWatchFile(integration.routesDir);
188
+ this.addWatchFile(integration.libDir);
180
189
  for (const client of integration.clients) {
181
190
  for (const root of [...client.watchRoots, ...client.manifestWatchRoots])
182
191
  this.addWatchFile(root);
@@ -203,7 +212,7 @@ export function distributedSvelteKit(options) {
203
212
  if (lifecycleOwnsCompile)
204
213
  return [];
205
214
  try {
206
- await compile(`GraphQL change ${context.file}`);
215
+ await compile(`GraphQL/Svelte change ${context.file}`);
207
216
  }
208
217
  catch (error) {
209
218
  context.server.ws.send({
@@ -231,7 +240,7 @@ export function distributedSvelteKit(options) {
231
240
  if (lifecycleOwnsCompile)
232
241
  return;
233
242
  if (isCompilerInput(id, integration)) {
234
- await compile(`GraphQL watch change ${id}`);
243
+ await compile(`GraphQL/Svelte watch change ${id}`);
235
244
  }
236
245
  },
237
246
  closeBundle: stop
@@ -488,6 +497,14 @@ function resolveIntegration(options, fallbackCwd) {
488
497
  }
489
498
  const modules = new Set();
490
499
  const outputs = [];
500
+ const routesDir = containedPath(cwd, options.routesDir ?? 'src/routes', 'routesDir');
501
+ const libDir = containedPath(cwd, options.libDir ?? 'src/lib', 'libDir');
502
+ const aliases = Object.freeze(Object.fromEntries(Object.entries(options.aliases ?? {}).map(([key, value]) => {
503
+ if (!/^\$[A-Za-z0-9_-]+$/.test(key)) {
504
+ throw new TypeError(`Distributed SvelteKit alias \`${key}\` must be a single $name segment`);
505
+ }
506
+ return [key, portablePath(relative(cwd, containedPath(cwd, value, `alias ${key}`)))];
507
+ })));
491
508
  const clients = options.clients.map((client, index) => {
492
509
  if (client === null || typeof client !== 'object') {
493
510
  throw new TypeError(`Distributed client[${index}] must be an object`);
@@ -515,7 +532,21 @@ function resolveIntegration(options, fallbackCwd) {
515
532
  throw new TypeError(`Distributed client \`${client.module}\` requires at least one GraphQL document glob`);
516
533
  }
517
534
  const documents = client.documents.map((document, documentIndex) => nonempty(document, `${client.module} documents[${documentIndex}]`));
518
- const routes = (client.routes ?? []).map((route, routeIndex) => nonempty(route, `${client.module} routes[${routeIndex}]`));
535
+ const boundaries = (client.boundaries ?? []).map((boundary, boundaryIndex) => {
536
+ if (boundary === null ||
537
+ typeof boundary !== 'object' ||
538
+ (boundary.kind !== 'page' && boundary.kind !== 'layout')) {
539
+ throw new TypeError(`${client.module} boundaries[${boundaryIndex}] is invalid`);
540
+ }
541
+ return Object.freeze({
542
+ operation: nonempty(boundary.operation, `${client.module} boundaries[${boundaryIndex}].operation`),
543
+ route: nonempty(boundary.route, `${client.module} boundaries[${boundaryIndex}].route`),
544
+ kind: boundary.kind,
545
+ ...(boundary.variables === undefined
546
+ ? {}
547
+ : { variables: boundary.variables })
548
+ });
549
+ });
519
550
  validateManifestSource(client.module, client.manifest);
520
551
  const out = containedPath(cwd, client.out, `${client.module} generated output`);
521
552
  if (out === cwd) {
@@ -527,14 +558,16 @@ function resolveIntegration(options, fallbackCwd) {
527
558
  }
528
559
  }
529
560
  outputs.push(out);
561
+ const adapterOut = join(cwd, '.svelte-kit', 'distributed', 'clients', Buffer.from(client.module).toString('base64url'));
530
562
  return Object.freeze({
531
563
  module: client.module,
532
564
  manifest: client.manifest,
533
565
  selector,
534
566
  documents: Object.freeze(documents),
535
- routes: Object.freeze(routes),
567
+ boundaries: Object.freeze(boundaries),
536
568
  out,
537
569
  entry: join(out, GENERATED_SVELTEKIT_MODULE),
570
+ adapterOut,
538
571
  watchRoots: Object.freeze(documentWatchRoots(cwd, documents, out)),
539
572
  manifestWatchRoots: Object.freeze(manifestWatchRoots(cwd, client.manifest))
540
573
  });
@@ -548,6 +581,9 @@ function resolveIntegration(options, fallbackCwd) {
548
581
  }
549
582
  return argument;
550
583
  })),
584
+ routesDir,
585
+ libDir,
586
+ aliases,
551
587
  clients: Object.freeze(clients)
552
588
  });
553
589
  }
@@ -629,6 +665,11 @@ function isCompilerInput(file, integration) {
629
665
  }
630
666
  function isGraphqlInput(file, integration) {
631
667
  const absolute = resolve(integration.cwd, file);
668
+ if (absolute.endsWith('.svelte') &&
669
+ (isWithin(integration.routesDir, absolute) ||
670
+ isWithin(integration.libDir, absolute))) {
671
+ return true;
672
+ }
632
673
  if ((!absolute.endsWith('.graphql') && !absolute.endsWith('.gql')) ||
633
674
  !isWithin(integration.cwd, absolute)) {
634
675
  return false;
@@ -656,17 +697,12 @@ async function compileTransaction(integration, children, signal) {
656
697
  throw new Error(`generated output ${client.out} must be a real directory`);
657
698
  }
658
699
  hadOutput = true;
659
- await cp(client.out, output, {
660
- recursive: true,
661
- errorOnExist: true,
662
- force: false,
663
- dereference: false
664
- });
665
700
  }
666
701
  catch (error) {
667
702
  if (!isMissing(error))
668
703
  throw error;
669
704
  }
705
+ await mkdir(output, { recursive: true });
670
706
  const args = [
671
707
  'client',
672
708
  '--manifest',
@@ -677,7 +713,6 @@ async function compileTransaction(integration, children, signal) {
677
713
  '--documents',
678
714
  document
679
715
  ]),
680
- ...client.routes.flatMap((route) => ['--route', route]),
681
716
  '--out',
682
717
  output
683
718
  ];
@@ -687,9 +722,24 @@ async function compileTransaction(integration, children, signal) {
687
722
  client,
688
723
  output,
689
724
  backup: join(transaction, `backup-${index}`),
690
- hadOutput
725
+ hadOutput,
726
+ adapterOutput: join(transaction, `adapter-${index}`),
727
+ adapterBackup: join(transaction, `adapter-backup-${index}`),
728
+ hadAdapterOutput: await realDirectoryExists(client.adapterOut)
691
729
  });
692
730
  }
731
+ const plans = await analyzeStagedBoundaries(integration, staged);
732
+ const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
733
+ for (const item of staged) {
734
+ const plan = plansByModule.get(item.client.module);
735
+ if (plan === undefined) {
736
+ throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${item.client.module}`);
737
+ }
738
+ await writeFile(join(item.output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
739
+ await exposeBoundaryModule(item.output);
740
+ await mkdir(item.adapterOutput, { recursive: true });
741
+ await writeFile(join(item.adapterOutput, 'boundaries.json'), boundaryPlanSource(plan), { encoding: 'utf8', flag: 'wx' });
742
+ }
693
743
  throwIfAborted(signal);
694
744
  await commitOutputs(integration, staged, signal);
695
745
  }
@@ -703,12 +753,14 @@ async function checkTransaction(integration, children, signal) {
703
753
  const transactionRoot = await compilerTransactionRoot(integration);
704
754
  const transaction = await mkdtemp(join(transactionRoot, '.distributed-sveltekit-check-'));
705
755
  try {
756
+ const staged = [];
706
757
  for (const [index, client] of integration.clients.entries()) {
707
758
  throwIfAborted(signal);
708
759
  const manifest = await materializeManifest(integration, client, transaction, index, children, signal);
760
+ const output = join(transaction, `output-${index}`);
761
+ await mkdir(output, { recursive: true });
709
762
  await runCommand(integration, [
710
763
  'client',
711
- '--check',
712
764
  '--manifest',
713
765
  manifest,
714
766
  client.selector[0],
@@ -717,10 +769,24 @@ async function checkTransaction(integration, children, signal) {
717
769
  '--documents',
718
770
  document
719
771
  ]),
720
- ...client.routes.flatMap((route) => ['--route', route]),
721
772
  '--out',
722
- client.out
773
+ output
723
774
  ], children, signal);
775
+ await validateGeneratedEntrypoint(integration.cwd, output, client.module);
776
+ staged.push(Object.freeze({ client, output }));
777
+ }
778
+ const plans = await analyzeStagedBoundaries(integration, staged);
779
+ const plansByModule = new Map(plans.map((plan) => [plan.module, plan]));
780
+ for (const [index, client] of integration.clients.entries()) {
781
+ const output = staged[index].output;
782
+ const plan = plansByModule.get(client.module);
783
+ if (plan === undefined) {
784
+ throw new Error(`Distributed SvelteKit boundary analysis returned no plan for ${client.module}`);
785
+ }
786
+ await writeFile(join(output, GENERATED_BOUNDARIES_MODULE), boundaryModuleSource(plan), { encoding: 'utf8', flag: 'wx' });
787
+ await exposeBoundaryModule(output);
788
+ await compareGeneratedTrees(client.out, output, client.module);
789
+ await validateAdapterBoundaryPlan(client, plan);
724
790
  }
725
791
  }
726
792
  finally {
@@ -735,6 +801,195 @@ async function compilerTransactionRoot(integration) {
735
801
  await mkdir(root, { recursive: true });
736
802
  return root;
737
803
  }
804
+ async function validateAdapterBoundaryPlan(client, plan) {
805
+ let actual;
806
+ try {
807
+ actual = await readFile(join(client.adapterOut, 'boundaries.json'), 'utf8');
808
+ }
809
+ catch (error) {
810
+ /*
811
+ * `.svelte-kit` is SvelteKit-owned build state. A production build may
812
+ * replace it after our Vite startup generation, while the durable client
813
+ * tree (including boundaries.ts) remains current and was checked above.
814
+ */
815
+ if (isMissing(error))
816
+ return;
817
+ throw error;
818
+ }
819
+ let persisted;
820
+ try {
821
+ persisted = JSON.parse(actual);
822
+ }
823
+ catch {
824
+ throw new Error(`[distributed.island.boundary_plan_invalid] ${client.module} boundaries.json is not valid JSON`);
825
+ }
826
+ validateDistributedSvelteKitBoundaryPlan(persisted, client.module);
827
+ const expected = boundaryPlanSource(plan);
828
+ if (actual !== expected) {
829
+ throw new Error(`Distributed SvelteKit boundary plan for ${client.module} is stale; run generation without check`);
830
+ }
831
+ }
832
+ async function analyzeStagedBoundaries(integration, staged) {
833
+ return await analyzeDistributedSvelteKitBoundaries({
834
+ cwd: integration.cwd,
835
+ routesDir: portablePath(relative(integration.cwd, integration.routesDir)),
836
+ libDir: portablePath(relative(integration.cwd, integration.libDir)),
837
+ aliases: integration.aliases,
838
+ clients: await Promise.all(staged.map(async ({ client, output }) => ({
839
+ module: client.module,
840
+ inventory: await readIslandInventory(integration.cwd, output),
841
+ explicitBoundaries: client.boundaries
842
+ })))
843
+ });
844
+ }
845
+ async function readIslandInventory(cwd, output) {
846
+ const path = join(output, 'islands.json');
847
+ const metadata = await lstat(path);
848
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
849
+ throw new Error(`Distributed island inventory ${portablePath(relative(cwd, path))} must be a regular file`);
850
+ }
851
+ const canonicalRoot = await realpath(cwd);
852
+ const canonical = await realpath(path);
853
+ if (!isWithin(canonicalRoot, canonical)) {
854
+ throw new Error('Distributed island inventory escaped the project root');
855
+ }
856
+ return JSON.parse(await readFile(canonical, 'utf8'));
857
+ }
858
+ function boundaryPlanSource(plan) {
859
+ return `${JSON.stringify(plan, null, 2)}\n`;
860
+ }
861
+ function boundaryModuleSource(plan) {
862
+ validateDistributedSvelteKitBoundaryPlan(plan, plan.module);
863
+ const occurrences = plan.boundaries.flatMap((boundary) => boundary.islands.map((island) => ({ boundary, island })));
864
+ const artifacts = new Map();
865
+ for (const { island } of occurrences) {
866
+ if (!/^operations\/[A-Za-z0-9._-]+\.ts$/.test(island.modulePath) ||
867
+ !/^[_A-Za-z][_0-9A-Za-z]*$/.test(island.exportName)) {
868
+ throw new Error(`[distributed.island.boundary_plan_invalid] ${island.graphqlSource} has an unsafe generated artifact reference`);
869
+ }
870
+ const key = `${island.modulePath}\u0000${island.exportName}`;
871
+ if (!artifacts.has(key)) {
872
+ artifacts.set(key, Object.freeze({
873
+ alias: `DistributedBoundaryArtifact_${artifacts.size}`,
874
+ module: island.modulePath.slice(0, -3),
875
+ exportName: island.exportName
876
+ }));
877
+ }
878
+ }
879
+ const imports = [...artifacts.values()].map(({ alias, module, exportName }) => `import { ${exportName} as ${alias} } from './${module}.js';`);
880
+ const definitions = occurrences.map(({ boundary, island }, index) => {
881
+ const artifact = artifacts.get(`${island.modulePath}\u0000${island.exportName}`);
882
+ const discovery = island.reason === 'static_component_import' ? 'component' : island.reason;
883
+ return [
884
+ `const DistributedBoundaryBinding_${index} = defineDistributedBoundaryBinding(`,
885
+ ` ${artifact.alias},`,
886
+ ` ${JSON.stringify(island.binding.sources, null, 2)} as const`,
887
+ `);`,
888
+ `const DistributedBoundaryOperation_${index} = defineDistributedBoundaryOperation(`,
889
+ ` ${JSON.stringify({
890
+ operation: island.operation,
891
+ route: boundary.route,
892
+ kind: boundary.kind,
893
+ sourcePath: island.graphqlSource,
894
+ discovery
895
+ }, null, 2)} as const,`,
896
+ ` ${artifact.alias},`,
897
+ ` DistributedBoundaryBinding_${index}`,
898
+ `);`
899
+ ].join('\n');
900
+ });
901
+ const operations = occurrences.map((_, index) => ` DistributedBoundaryOperation_${index}`);
902
+ return [
903
+ '/** GENERATED by the Distributed SvelteKit boundary planner. Do not edit. */',
904
+ "import { defineDistributedBoundaryBinding, defineDistributedBoundaryOperation } from '@hops-ops/distributed/sveltekit';",
905
+ ...imports,
906
+ '',
907
+ `export const DISTRIBUTED_BOUNDARY_PLAN = ${JSON.stringify(plan, null, 2)} as const;`,
908
+ '',
909
+ ...definitions,
910
+ '',
911
+ '/** Executable SSR/browser ownership assembled from the same boundary plan. */',
912
+ `export const DISTRIBUTED_BOUNDARY_OPERATIONS = ${operations.length === 0 ? '[]' : `[\n${operations.join(',\n')}\n]`} as const;`,
913
+ ''
914
+ ].join('\n');
915
+ }
916
+ async function exposeBoundaryModule(output) {
917
+ const path = join(output, GENERATED_SVELTEKIT_MODULE);
918
+ const source = await readFile(path, 'utf8');
919
+ if (!source.startsWith('/** GENERATED by distributed client. Do not edit. */')) {
920
+ throw new Error('generated SvelteKit entrypoint is missing its ownership marker');
921
+ }
922
+ await writeFile(path, `${source.trimEnd()}\n\nexport { DISTRIBUTED_BOUNDARY_OPERATIONS, DISTRIBUTED_BOUNDARY_PLAN } from './boundaries.js';\n`, 'utf8');
923
+ }
924
+ async function compareGeneratedTrees(actualRoot, expectedRoot, module) {
925
+ const [actual, expected] = await Promise.all([
926
+ readGeneratedTree(actualRoot),
927
+ readGeneratedTree(expectedRoot)
928
+ ]);
929
+ const drift = [];
930
+ for (const [path, contents] of expected) {
931
+ const current = actual.get(path);
932
+ if (current === undefined)
933
+ drift.push(`missing ${path}`);
934
+ else if (current !== contents)
935
+ drift.push(`changed ${path}`);
936
+ }
937
+ for (const path of actual.keys()) {
938
+ if (!expected.has(path))
939
+ drift.push(`unexpected ${path}`);
940
+ }
941
+ if (drift.length > 0) {
942
+ throw new Error(`Distributed SvelteKit client ${module} is stale:\n ${drift.sort().join('\n ')}\nrun generation without check`);
943
+ }
944
+ }
945
+ async function readGeneratedTree(root) {
946
+ const rootMetadata = await lstat(root);
947
+ if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) {
948
+ throw new Error(`generated output ${root} must be a real directory`);
949
+ }
950
+ const files = new Map();
951
+ const pending = [
952
+ { absolute: root, relative: '' }
953
+ ];
954
+ while (pending.length > 0) {
955
+ const directory = pending.pop();
956
+ for (const entry of await readdir(directory.absolute, { withFileTypes: true })) {
957
+ const relativePath = directory.relative.length === 0
958
+ ? entry.name
959
+ : `${directory.relative}/${entry.name}`;
960
+ const absolutePath = join(directory.absolute, entry.name);
961
+ if (entry.isSymbolicLink()) {
962
+ throw new Error(`generated output contains unsupported symlink ${relativePath}`);
963
+ }
964
+ if (entry.isDirectory()) {
965
+ pending.push({ absolute: absolutePath, relative: relativePath });
966
+ continue;
967
+ }
968
+ if (!entry.isFile()) {
969
+ throw new Error(`generated output contains unsupported entry ${relativePath}`);
970
+ }
971
+ files.set(relativePath, await readFile(absolutePath, 'utf8'));
972
+ if (files.size > 8_192) {
973
+ throw new Error('generated output exceeds 8192 files');
974
+ }
975
+ }
976
+ }
977
+ return files;
978
+ }
979
+ async function realDirectoryExists(path) {
980
+ try {
981
+ const metadata = await lstat(path);
982
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
983
+ throw new Error(`Distributed SvelteKit adapter output ${path} must be a real directory`);
984
+ }
985
+ return true;
986
+ }
987
+ catch (error) {
988
+ if (isMissing(error))
989
+ return false;
990
+ throw error;
991
+ }
992
+ }
738
993
  async function materializeManifest(integration, client, transaction, index, children, signal) {
739
994
  throwIfAborted(signal);
740
995
  if (typeof client.manifest === 'string') {
@@ -778,23 +1033,37 @@ async function validateGeneratedEntrypoint(cwd, output, module) {
778
1033
  async function commitOutputs(integration, staged, signal) {
779
1034
  throwIfAborted(signal);
780
1035
  await validateResolvedPaths(integration);
1036
+ const outputs = staged.flatMap((item) => [
1037
+ {
1038
+ target: item.client.out,
1039
+ output: item.output,
1040
+ backup: item.backup,
1041
+ hadOutput: item.hadOutput
1042
+ },
1043
+ {
1044
+ target: item.client.adapterOut,
1045
+ output: item.adapterOutput,
1046
+ backup: item.adapterBackup,
1047
+ hadOutput: item.hadAdapterOutput
1048
+ }
1049
+ ]);
781
1050
  const applied = [];
782
1051
  try {
783
- for (const item of staged) {
1052
+ for (const item of outputs) {
784
1053
  throwIfAborted(signal);
785
- await mkdir(dirname(item.client.out), { recursive: true });
786
- await validateNearestExistingParent(integration.cwd, item.client.out);
787
- if (item.hadOutput && await generatedTreesEqual(item.client.out, item.output)) {
1054
+ await mkdir(dirname(item.target), { recursive: true });
1055
+ await validateNearestExistingParent(integration.cwd, item.target);
1056
+ if (item.hadOutput && await generatedTreesEqual(item.target, item.output)) {
788
1057
  continue;
789
1058
  }
790
1059
  if (item.hadOutput)
791
- await rename(item.client.out, item.backup);
1060
+ await rename(item.target, item.backup);
792
1061
  try {
793
- await rename(item.output, item.client.out);
1062
+ await rename(item.output, item.target);
794
1063
  }
795
1064
  catch (error) {
796
1065
  if (item.hadOutput)
797
- await rename(item.backup, item.client.out);
1066
+ await rename(item.backup, item.target);
798
1067
  throw error;
799
1068
  }
800
1069
  applied.push(item);
@@ -803,9 +1072,9 @@ async function commitOutputs(integration, staged, signal) {
803
1072
  }
804
1073
  catch (error) {
805
1074
  for (const item of [...applied].reverse()) {
806
- await rm(item.client.out, { recursive: true, force: true });
1075
+ await rm(item.target, { recursive: true, force: true });
807
1076
  if (item.hadOutput)
808
- await rename(item.backup, item.client.out);
1077
+ await rename(item.backup, item.target);
809
1078
  }
810
1079
  throw error;
811
1080
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hops-ops/distributed",
3
- "version": "4.9.0",
3
+ "version": "4.10.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",
@@ -79,7 +79,7 @@
79
79
  "typescript": "5.9.3"
80
80
  },
81
81
  "engines": {
82
- "node": ">=20.0.0"
82
+ "node": ">=20.3.0"
83
83
  },
84
84
  "publishConfig": {
85
85
  "access": "public"