@superpanel-sdk/runtime 0.2.1 → 0.2.3

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 (55) hide show
  1. package/dist/builder/codegen/generate.js +15 -9
  2. package/dist/builder/company-import-graph.d.ts +11 -0
  3. package/dist/builder/company-import-graph.js +105 -0
  4. package/dist/builder/compile/compile-agenda.js +9 -4
  5. package/dist/builder/compile/compile-sequence.d.ts +4 -1
  6. package/dist/builder/compile/compile-sequence.js +28 -9
  7. package/dist/builder/load.js +3 -3
  8. package/dist/builder/types.d.ts +5 -0
  9. package/dist/cli/serve/index.js +107 -47
  10. package/dist/compute-host/index.d.ts +2 -0
  11. package/dist/compute-host/index.js +1 -0
  12. package/dist/compute-host/read-only-cache.d.ts +20 -0
  13. package/dist/compute-host/read-only-cache.js +61 -0
  14. package/dist/compute-host/source-compute-host.d.ts +4 -0
  15. package/dist/compute-host/source-compute-host.js +12 -2
  16. package/dist/core/context/types.d.ts +19 -1
  17. package/dist/core/functions/define/custom.d.ts +1 -1
  18. package/dist/core/functions/types.d.ts +4 -4
  19. package/dist/core/slot/index.d.ts +1 -1
  20. package/dist/core/slot/index.js +1 -1
  21. package/dist/core/slot/types.d.ts +2 -0
  22. package/dist/core/slot/write.d.ts +8 -1
  23. package/dist/core/slot/write.js +145 -38
  24. package/dist/core/stage/builders.d.ts +23 -5
  25. package/dist/core/stage/builders.js +3 -0
  26. package/dist/core/stage/drive.d.ts +1 -1
  27. package/dist/core/stage/drive.js +9 -8
  28. package/dist/core/stage/flatten.js +13 -11
  29. package/dist/core/stage/index.d.ts +8 -4
  30. package/dist/core/stage/index.js +5 -2
  31. package/dist/core/stage/interrupts.d.ts +28 -0
  32. package/dist/core/stage/interrupts.js +120 -0
  33. package/dist/core/stage/ivr-fields.d.ts +9 -0
  34. package/dist/core/stage/ivr-fields.js +75 -0
  35. package/dist/core/stage/transfer-fields.d.ts +6 -1
  36. package/dist/core/stage/transfer-fields.js +44 -3
  37. package/dist/core/stage/transfer-spec.d.ts +48 -0
  38. package/dist/core/stage/transfer-spec.js +113 -0
  39. package/dist/core/stage/types.d.ts +45 -14
  40. package/dist/core/stage/validate.js +105 -1
  41. package/dist/core/stage/walk.d.ts +5 -0
  42. package/dist/core/stage/walk.js +33 -10
  43. package/dist/fn-context/index.d.ts +1 -1
  44. package/dist/fn-context/index.js +1 -1
  45. package/dist/fn-context/rpc/index.d.ts +1 -1
  46. package/dist/fn-context/rpc/index.js +1 -1
  47. package/dist/fn-context/rpc/request-id.d.ts +5 -0
  48. package/dist/fn-context/rpc/request-id.js +11 -0
  49. package/dist/fn-context/rpc/trpc-client.d.ts +2 -0
  50. package/dist/fn-context/rpc/trpc-client.js +5 -1
  51. package/dist/fn-context/rpc/trpc-init.d.ts +1 -0
  52. package/dist/index.d.ts +7 -7
  53. package/dist/index.js +4 -4
  54. package/dist/voice-service/types.d.ts +3 -0
  55. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { withCompanyImportGraph } from '../company-import-graph.js';
3
4
  import { listSequenceIds, loadCompanySchema, loadSequenceSlots } from '../load.js';
4
5
  import { mergeEffectiveSchema } from '../merge.js';
5
6
  import { findTsModule, resolveTsModule } from '../paths.js';
@@ -12,6 +13,9 @@ function sequenceCaseContextGenPath(repoRoot, sequenceId) {
12
13
  return path.join(repoRoot, 'sequences', sequenceId, '__gen', 'case-context.type.ts');
13
14
  }
14
15
  export async function generateSequence(repoRoot, sequenceId, options = {}) {
16
+ return withCompanyImportGraph(repoRoot, () => generateSequenceInGraph(repoRoot, sequenceId, options));
17
+ }
18
+ async function generateSequenceInGraph(repoRoot, sequenceId, options) {
15
19
  const schemaPath = resolveTsModule(repoRoot, 'schema', `No company schema found. Expected schema.ts or schema/index.ts under ${repoRoot}`);
16
20
  const slotsPath = resolveTsModule(path.join(repoRoot, 'sequences', sequenceId), 'slots', `No sequence slots found. Expected sequences/${sequenceId}/slots.ts or sequences/${sequenceId}/slots/index.ts`);
17
21
  const company = await loadCompanySchema(schemaPath);
@@ -38,15 +42,17 @@ export async function generateSequence(repoRoot, sequenceId, options = {}) {
38
42
  };
39
43
  }
40
44
  export async function generateAll(repoRoot, options = {}) {
41
- const ids = options.sequenceIds ?? listSequenceIds(repoRoot);
42
- if (ids.length === 0) {
43
- throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')} (expected slots.ts or slots/index.ts)`);
44
- }
45
- const results = [];
46
- for (const sequenceId of ids) {
47
- results.push(await generateSequence(repoRoot, sequenceId, options));
48
- }
49
- return results;
45
+ return withCompanyImportGraph(repoRoot, async () => {
46
+ const ids = options.sequenceIds ?? listSequenceIds(repoRoot);
47
+ if (ids.length === 0) {
48
+ throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')} (expected slots.ts or slots/index.ts)`);
49
+ }
50
+ const results = [];
51
+ for (const sequenceId of ids) {
52
+ results.push(await generateSequence(repoRoot, sequenceId, options));
53
+ }
54
+ return results;
55
+ });
50
56
  }
51
57
  function assertFileMatches(outPath, next, repoRoot) {
52
58
  let current = '';
@@ -0,0 +1,11 @@
1
+ /** Stamp a company-repo entry URL, or return it unchanged outside a graph. */
2
+ export declare function companyImportGraphHref(fileHref: string): string;
3
+ /**
4
+ * Load company-repo modules as one ESM graph.
5
+ *
6
+ * Nested calls (same async context) reuse the graph so compile + overlay
7
+ * `loadSequenceFunctions` share object identity. Overlapping outer calls wait
8
+ * their turn, then bump generation so watch sees new source.
9
+ */
10
+ export declare function withCompanyImportGraph<T>(repoRoot: string, fn: () => Promise<T>): Promise<T>;
11
+ //# sourceMappingURL=company-import-graph.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { realpathSync } from 'node:fs';
3
+ import { registerHooks } from 'node:module';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const COMPANY_GRAPH_GEN_QUERY = 'spgen';
7
+ const COMPANY_GRAPH_ROOT_QUERY = 'sproot';
8
+ const graph = new AsyncLocalStorage();
9
+ let generation = 0;
10
+ let registered = false;
11
+ let outerTail = Promise.resolve();
12
+ function filePathFromUrl(url) {
13
+ const parsed = new URL(url);
14
+ parsed.search = '';
15
+ parsed.hash = '';
16
+ return fileURLToPath(parsed);
17
+ }
18
+ function realPath(filePath) {
19
+ try {
20
+ return realpathSync(filePath);
21
+ }
22
+ catch {
23
+ return path.resolve(filePath);
24
+ }
25
+ }
26
+ function underRoot(filePath, root) {
27
+ const resolved = realPath(filePath);
28
+ const base = realPath(root);
29
+ return resolved === base || resolved.startsWith(`${base}${path.sep}`);
30
+ }
31
+ function inNodeModules(filePath, url) {
32
+ return (url.includes('/node_modules/') ||
33
+ url.includes('\\node_modules\\') ||
34
+ filePath.includes(`${path.sep}node_modules${path.sep}`));
35
+ }
36
+ function stampCompanyResolve(specifier, context, nextResolve) {
37
+ const result = nextResolve(specifier, context);
38
+ if (!result.url.startsWith('file:'))
39
+ return result;
40
+ const resolved = new URL(result.url);
41
+ if (resolved.searchParams.has(COMPANY_GRAPH_GEN_QUERY))
42
+ return result;
43
+ const parent = context.parentURL ? new URL(context.parentURL) : null;
44
+ const graphGeneration = parent?.searchParams.get(COMPANY_GRAPH_GEN_QUERY);
45
+ const repoRoot = parent?.searchParams.get(COMPANY_GRAPH_ROOT_QUERY);
46
+ if (!graphGeneration || !repoRoot)
47
+ return result;
48
+ let resolvedFile;
49
+ try {
50
+ resolvedFile = filePathFromUrl(result.url);
51
+ }
52
+ catch {
53
+ return result;
54
+ }
55
+ if (inNodeModules(resolvedFile, result.url))
56
+ return result;
57
+ if (!underRoot(resolvedFile, repoRoot))
58
+ return result;
59
+ resolved.searchParams.set(COMPANY_GRAPH_GEN_QUERY, graphGeneration);
60
+ resolved.searchParams.set(COMPANY_GRAPH_ROOT_QUERY, repoRoot);
61
+ return { ...result, url: resolved.href };
62
+ }
63
+ function ensureHookRegistered() {
64
+ if (registered)
65
+ return;
66
+ registered = true;
67
+ registerHooks({ resolve: stampCompanyResolve });
68
+ }
69
+ /** Stamp a company-repo entry URL, or return it unchanged outside a graph. */
70
+ export function companyImportGraphHref(fileHref) {
71
+ const store = graph.getStore();
72
+ if (!store)
73
+ return fileHref;
74
+ const url = new URL(fileHref);
75
+ url.searchParams.set(COMPANY_GRAPH_GEN_QUERY, String(store.generation));
76
+ url.searchParams.set(COMPANY_GRAPH_ROOT_QUERY, store.repoRoot);
77
+ return url.href;
78
+ }
79
+ /**
80
+ * Load company-repo modules as one ESM graph.
81
+ *
82
+ * Nested calls (same async context) reuse the graph so compile + overlay
83
+ * `loadSequenceFunctions` share object identity. Overlapping outer calls wait
84
+ * their turn, then bump generation so watch sees new source.
85
+ */
86
+ export async function withCompanyImportGraph(repoRoot, fn) {
87
+ ensureHookRegistered();
88
+ if (graph.getStore())
89
+ return fn();
90
+ const root = realpathSync(path.resolve(repoRoot));
91
+ let release;
92
+ const previous = outerTail;
93
+ outerTail = new Promise((resolve) => {
94
+ release = resolve;
95
+ });
96
+ await previous;
97
+ generation += 1;
98
+ try {
99
+ return await graph.run({ generation, repoRoot: root }, fn);
100
+ }
101
+ finally {
102
+ release();
103
+ }
104
+ }
105
+ //# sourceMappingURL=company-import-graph.js.map
@@ -1,5 +1,5 @@
1
1
  import { toEffectiveSchema } from '../../core/slot/compile-schema.js';
2
- import { collectTransferBlocks, fatalAgendaIssues, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, registerTransferSessionFields, validateCompiledAgenda, } from '../../core/stage/index.js';
2
+ import { collectIvrBlocks, collectTransferBlocks, collectTransferInterruptHeaders, fatalAgendaIssues, flattenAgendaPlacements, flattenAgendaTree, isAgendaTree, isSequenceAgendas, normalizeSequenceAgendas, registerIvrSessionFields, registerTransferSessionFields, validateCompiledAgenda, } from '../../core/stage/index.js';
3
3
  function asAgendaTrees(raw) {
4
4
  if (isAgendaTree(raw))
5
5
  return [raw];
@@ -27,8 +27,11 @@ export function compileSequenceAgendas(schema, raw, options = {}) {
27
27
  }
28
28
  const trees = asAgendaTrees(raw);
29
29
  if (trees) {
30
- const transferIds = collectTransferBlocks({}, trees.flatMap((tree) => tree.tree)).map((block) => block.id);
31
- const withTransfer = registerTransferSessionFields(schema, transferIds);
30
+ const nodes = trees.flatMap((tree) => tree.tree);
31
+ const transferIds = collectTransferBlocks({}, nodes).map((block) => block.id);
32
+ const withTransfer = registerIvrSessionFields(registerTransferSessionFields(schema, transferIds, {
33
+ lastResult: collectTransferInterruptHeaders(trees.map((tree) => tree.header)),
34
+ }), collectIvrBlocks({}, nodes).map((block) => block.id));
32
35
  const compiled = {};
33
36
  for (const tree of trees) {
34
37
  compiled[tree.id] = compileOneFlat(tree.id, tree.header, flattenAgendaTree(tree.tree, { functions: options.functions }), withTransfer, options.functions, issues);
@@ -48,7 +51,9 @@ export function compileSequenceAgendas(schema, raw, options = {}) {
48
51
  return { schema, effectiveSchema: toEffectiveSchema(schema), agendas: {}, issues };
49
52
  }
50
53
  const transferIds = collectTransferBlocks(agendasInput.blocks).map((block) => block.id);
51
- const withTransfer = registerTransferSessionFields(schema, transferIds);
54
+ const withTransfer = registerIvrSessionFields(registerTransferSessionFields(schema, transferIds, {
55
+ lastResult: collectTransferInterruptHeaders(Object.values(agendasInput.agendas).map((config) => config.header)),
56
+ }), collectIvrBlocks(agendasInput.blocks).map((block) => block.id));
52
57
  const compiled = {};
53
58
  for (const [agendaId, config] of Object.entries(agendasInput.agendas)) {
54
59
  compiled[agendaId] = compileOneFlat(agendaId, config.header, flattenAgendaPlacements(config.placements, agendasInput.blocks, { functions: options.functions }), withTransfer, options.functions, issues);
@@ -1,8 +1,9 @@
1
1
  import type { FuncMap } from '../../core/functions/types.js';
2
2
  import type { CompanySchema } from '../../core/slot/schema.js';
3
- import type { SequenceBuild } from '../types.js';
3
+ import type { RuntimePackageSource, SequenceBuild } from '../types.js';
4
4
  /** SequenceBuild / SequenceConfigBuild cache key. Bump when compiled `code` shape changes. */
5
5
  export declare function runtimePackageVersion(): string;
6
+ export declare function runtimePackageSource(repoRoot: string): RuntimePackageSource;
6
7
  /** Deep-clone company schema with derived computeFn normalized to string names. */
7
8
  export declare function snapshotCompanySchema(company: CompanySchema, functions?: FuncMap): CompanySchema;
8
9
  export declare function sequenceBuildPath(repoRoot: string, sequenceId: string): string;
@@ -12,6 +13,8 @@ export type CompileSequenceOptions = {
12
13
  includeCode?: boolean;
13
14
  /** Persist on SequenceBuild.sha. Defaults to `local`. */
14
15
  sha?: string;
16
+ /** Dependency source captured before deployment rewrites package.json. */
17
+ runtimeSource?: RuntimePackageSource;
15
18
  };
16
19
  export type CompileSequenceResult = {
17
20
  sequenceId: string;
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { resolveCaseContextViewFn } from '../../core/functions/define/case-context.js';
5
5
  import { resolveFuncName } from '../../core/functions/func-ref.js';
6
6
  import { compileSlotSchema } from '../../core/slot/compile-schema.js';
7
+ import { withCompanyImportGraph } from '../company-import-graph.js';
7
8
  import { listSequenceIds, loadCompanySchema, loadSequenceAgendas, loadSequenceCaseContextView, loadSequenceFunctions, loadSequenceSlots, } from '../load.js';
8
9
  import { findTsModule, resolveTsModule } from '../paths.js';
9
10
  import { compileSequenceAgendas, fatalAgendaIssues } from './compile-agenda.js';
@@ -27,6 +28,18 @@ export function runtimePackageVersion() {
27
28
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
28
29
  return pkg.version ?? '0.0.0';
29
30
  }
31
+ export function runtimePackageSource(repoRoot) {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
34
+ const spec = pkg.dependencies?.['@superpanel-sdk/runtime'] ??
35
+ pkg.devDependencies?.['@superpanel-sdk/runtime'] ??
36
+ pkg.optionalDependencies?.['@superpanel-sdk/runtime'];
37
+ return spec?.startsWith('file:') || spec?.startsWith('workspace:') ? 'local-file' : 'npm';
38
+ }
39
+ catch {
40
+ return 'npm';
41
+ }
42
+ }
30
43
  /** Deep-clone company schema with derived computeFn normalized to string names. */
31
44
  export function snapshotCompanySchema(company, functions) {
32
45
  const tables = {};
@@ -51,6 +64,9 @@ export function sequenceBuildPath(repoRoot, sequenceId) {
51
64
  return path.join(repoRoot, 'sequences', sequenceId, 'build', 'sequence-build.json');
52
65
  }
53
66
  export async function compileSequence(repoRoot, sequenceId, options = {}) {
67
+ return withCompanyImportGraph(repoRoot, () => compileSequenceInGraph(repoRoot, sequenceId, options));
68
+ }
69
+ async function compileSequenceInGraph(repoRoot, sequenceId, options) {
54
70
  const schemaPath = resolveTsModule(repoRoot, 'schema', `No company schema found. Expected schema.ts or schema/index.ts under ${repoRoot}`);
55
71
  const sequenceDir = path.join(repoRoot, 'sequences', sequenceId);
56
72
  const slotsPath = resolveTsModule(sequenceDir, 'slots', `No sequence slots found. Expected sequences/${sequenceId}/slots.ts or sequences/${sequenceId}/slots/index.ts`);
@@ -92,6 +108,7 @@ export async function compileSequence(repoRoot, sequenceId, options = {}) {
92
108
  sequenceId,
93
109
  sha: options.sha ?? 'local',
94
110
  engineVersion: runtimePackageVersion(),
111
+ runtimeSource: options.runtimeSource ?? runtimePackageSource(repoRoot),
95
112
  compiledAt: new Date().toISOString(),
96
113
  companySchemaSnapshot: snapshotCompanySchema(company, functions),
97
114
  schema: compiledAgendas.schema,
@@ -108,14 +125,16 @@ export async function compileSequence(repoRoot, sequenceId, options = {}) {
108
125
  return { sequenceId, outPath, build, wrote: !options.dryRun };
109
126
  }
110
127
  export async function compileAll(repoRoot, options = {}) {
111
- const ids = options.sequenceIds ?? listSequenceIds(repoRoot);
112
- if (ids.length === 0) {
113
- throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')} (expected slots.ts or slots/index.ts)`);
114
- }
115
- const results = [];
116
- for (const sequenceId of ids) {
117
- results.push(await compileSequence(repoRoot, sequenceId, options));
118
- }
119
- return results;
128
+ return withCompanyImportGraph(repoRoot, async () => {
129
+ const ids = options.sequenceIds ?? listSequenceIds(repoRoot);
130
+ if (ids.length === 0) {
131
+ throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')} (expected slots.ts or slots/index.ts)`);
132
+ }
133
+ const results = [];
134
+ for (const sequenceId of ids) {
135
+ results.push(await compileSequence(repoRoot, sequenceId, options));
136
+ }
137
+ return results;
138
+ });
120
139
  }
121
140
  //# sourceMappingURL=compile-sequence.js.map
@@ -3,11 +3,11 @@ import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { resolveCaseContextViewFn } from '../core/functions/define/case-context.js';
5
5
  import { isAgendaTree, isSequenceAgendas } from '../core/stage/flatten.js';
6
+ import { companyImportGraphHref } from './company-import-graph.js';
6
7
  import { findTsModule } from './paths.js';
7
8
  async function importModule(filePath) {
8
- const href = pathToFileURL(filePath).href;
9
- // Cache-bust so --watch reloads edited TS modules.
10
- const mod = await import(`${href}?t=${Date.now()}`);
9
+ const href = companyImportGraphHref(pathToFileURL(filePath).href);
10
+ const mod = await import(href);
11
11
  return mod;
12
12
  }
13
13
  function isFuncMap(value) {
@@ -2,6 +2,7 @@ import type { CaseContextViewRegistryEntry, FunctionRegistry } from '../core/fun
2
2
  import type { CompanySchema, EffectiveSchema } from '../core/slot/schema.js';
3
3
  import type { CompiledSlotSchema } from '../core/slot/types.js';
4
4
  import type { CompiledAgenda } from '../core/stage/types.js';
5
+ export type RuntimePackageSource = 'local-file' | 'npm';
5
6
  /**
6
7
  * Compiled sequence artifact (Manual §9 subset; grows over time).
7
8
  * Omitted for now: paramsDefaults, schedulers, caseManual.
@@ -13,6 +14,10 @@ export type SequenceBuild = {
13
14
  sequenceId: string;
14
15
  sha: string;
15
16
  engineVersion: string;
17
+ /** Installed @superpanel-sdk/runtime version resolved from the company repo. */
18
+ runtimeVersion?: string;
19
+ /** Dependency source declared by the company repo. Optional for legacy persisted builds. */
20
+ runtimeSource?: RuntimePackageSource;
16
21
  compiledAt: string;
17
22
  companySchemaSnapshot: CompanySchema;
18
23
  /** SlotEngine runtime schema (tables + derivedOrder / reverseDeps). */
@@ -1,9 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import chokidar from 'chokidar';
3
3
  import { compileAll } from '../../builder/compile/compile-sequence.js';
4
+ import { withCompanyImportGraph } from '../../builder/company-import-graph.js';
4
5
  import { findTsModule } from '../../builder/paths.js';
5
6
  import { listSequenceIds, loadSequenceCaseContextView, loadSequenceFunctions } from '../../builder/load.js';
6
7
  import { createSourceComputeHost } from '../../compute-host/source-compute-host.js';
8
+ import { formatRuntimeVersion } from '../../fn-context/rpc/request-id.js';
7
9
  import { encodeOverlayMessage, OVERLAY_REPLACED_CLOSE_CODE, parseOverlayMessage, } from '../../dev-overlay/protocol.js';
8
10
  const DEFAULT_BACKEND_URL = 'http://127.0.0.1:8100';
9
11
  function env(name) {
@@ -26,6 +28,20 @@ export function overlayWebSocketUrl(raw) {
26
28
  url.hash = '';
27
29
  return url.toString();
28
30
  }
31
+ function overlayRpcUrl(raw) {
32
+ const url = new URL(raw);
33
+ if (url.protocol === 'ws:')
34
+ url.protocol = 'http:';
35
+ else if (url.protocol === 'wss:')
36
+ url.protocol = 'https:';
37
+ else if (url.protocol !== 'http:' && url.protocol !== 'https:') {
38
+ throw new Error(`unsupported backend url: ${raw}`);
39
+ }
40
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/trpc`;
41
+ url.search = '';
42
+ url.hash = '';
43
+ return url.toString();
44
+ }
29
45
  /** `sequences/<id>/…` → sequence id */
30
46
  function sequenceIdFromPath(repoRoot, file) {
31
47
  const rel = path.relative(repoRoot, file);
@@ -37,30 +53,34 @@ function isSchemaPath(repoRoot, file) {
37
53
  return rel === 'schema.ts' || rel.startsWith(`schema${path.sep}`) || rel.startsWith('schema/');
38
54
  }
39
55
  async function loadOne(repoRoot, sequenceId, generation) {
40
- const [compiled] = await compileAll(repoRoot, {
41
- sequenceIds: [sequenceId],
42
- dryRun: true,
43
- includeCode: false,
56
+ return withCompanyImportGraph(repoRoot, async () => {
57
+ const [compiled] = await compileAll(repoRoot, {
58
+ sequenceIds: [sequenceId],
59
+ dryRun: true,
60
+ includeCode: false,
61
+ });
62
+ if (!compiled) {
63
+ throw new Error(`Failed to compile sequence ${sequenceId}`);
64
+ }
65
+ const sequenceDir = path.join(repoRoot, 'sequences', sequenceId);
66
+ const functionsPath = findTsModule(sequenceDir, 'functions');
67
+ if (!functionsPath) {
68
+ throw new Error(`No functions module for sequence ${sequenceId}`);
69
+ }
70
+ const functions = await loadSequenceFunctions(functionsPath);
71
+ const viewPath = findTsModule(sequenceDir, 'view');
72
+ const view = viewPath ? await loadSequenceCaseContextView(viewPath) : undefined;
73
+ return { build: compiled.build, functions, view, generation };
44
74
  });
45
- if (!compiled) {
46
- throw new Error(`Failed to compile sequence ${sequenceId}`);
47
- }
48
- const sequenceDir = path.join(repoRoot, 'sequences', sequenceId);
49
- const functionsPath = findTsModule(sequenceDir, 'functions');
50
- if (!functionsPath) {
51
- throw new Error(`No functions module for sequence ${sequenceId}`);
52
- }
53
- const functions = await loadSequenceFunctions(functionsPath);
54
- const viewPath = findTsModule(sequenceDir, 'view');
55
- const view = viewPath ? await loadSequenceCaseContextView(viewPath) : undefined;
56
- return { build: compiled.build, functions, view, generation };
57
75
  }
58
76
  async function loadSequences(repoRoot, sequenceIds, generation) {
59
- const out = new Map();
60
- for (const sequenceId of sequenceIds) {
61
- out.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
62
- }
63
- return out;
77
+ return withCompanyImportGraph(repoRoot, async () => {
78
+ const out = new Map();
79
+ for (const sequenceId of sequenceIds) {
80
+ out.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
81
+ }
82
+ return out;
83
+ });
64
84
  }
65
85
  function send(ws, message) {
66
86
  ws.send(encodeOverlayMessage(message));
@@ -68,12 +88,20 @@ function send(ws, message) {
68
88
  function logField(label, value) {
69
89
  console.log(` ${label.padEnd(10)} ${value}`);
70
90
  }
71
- async function handleExecute(loaded, message) {
91
+ async function handleExecute(loaded, message, rpcUrl) {
92
+ const data = message.data
93
+ ? {
94
+ ...message.data,
95
+ config: { ...message.data.config, backendUrl: rpcUrl },
96
+ }
97
+ : undefined;
72
98
  const host = createSourceComputeHost({
73
99
  functions: loaded.functions,
74
100
  caseContextView: loaded.view,
75
- data: message.data,
101
+ data: data,
76
102
  runToken: message.runToken,
103
+ runtimeVersion: loaded.build.engineVersion,
104
+ runtimeSource: loaded.build.runtimeSource ?? 'npm',
77
105
  getRequestId: () => message.logRequestId,
78
106
  getTags: () => message.logTags,
79
107
  });
@@ -94,14 +122,36 @@ function pushOverlays(ws, sequences) {
94
122
  });
95
123
  }
96
124
  }
125
+ function closeDetail(event) {
126
+ return event.reason ? `${event.code} ${event.reason}` : String(event.code);
127
+ }
97
128
  function waitOpen(ws) {
98
129
  return new Promise((resolve, reject) => {
99
130
  if (ws.readyState === WebSocket.OPEN) {
100
131
  resolve();
101
132
  return;
102
133
  }
103
- ws.addEventListener('open', () => resolve(), { once: true });
104
- ws.addEventListener('error', () => reject(new Error('websocket error')), { once: true });
134
+ let settled = false;
135
+ const finish = (fn) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ cleanup();
140
+ fn();
141
+ };
142
+ const onOpen = () => finish(() => resolve());
143
+ const onClose = (event) => finish(() => reject(new Error(`websocket closed ${closeDetail(event)}`)));
144
+ const onError = () => {
145
+ setTimeout(() => finish(() => reject(new Error('websocket error'))), 250);
146
+ };
147
+ const cleanup = () => {
148
+ ws.removeEventListener('open', onOpen);
149
+ ws.removeEventListener('close', onClose);
150
+ ws.removeEventListener('error', onError);
151
+ };
152
+ ws.addEventListener('open', onOpen);
153
+ ws.addEventListener('close', onClose);
154
+ ws.addEventListener('error', onError);
105
155
  });
106
156
  }
107
157
  class OverlayReplacedError extends Error {
@@ -139,7 +189,7 @@ function waitAuthOk(ws) {
139
189
  cleanup();
140
190
  reject(event.code === OVERLAY_REPLACED_CLOSE_CODE
141
191
  ? new OverlayReplacedError()
142
- : new Error('websocket closed before auth.ok'));
192
+ : new Error(`websocket closed before auth.ok ${closeDetail(event)}`));
143
193
  };
144
194
  const cleanup = () => {
145
195
  ws.removeEventListener('message', onMessage);
@@ -149,7 +199,7 @@ function waitAuthOk(ws) {
149
199
  ws.addEventListener('close', onClose);
150
200
  });
151
201
  }
152
- function attachExecuteHandler(ws, sequences) {
202
+ function attachExecuteHandler(ws, sequences, rpcUrl) {
153
203
  ws.addEventListener('message', (event) => {
154
204
  void (async () => {
155
205
  let message;
@@ -178,7 +228,7 @@ function attachExecuteHandler(ws, sequences) {
178
228
  const started = Date.now();
179
229
  console.log(`execute ${call}`);
180
230
  try {
181
- const result = await handleExecute(loaded, message);
231
+ const result = await handleExecute(loaded, message, rpcUrl);
182
232
  console.log(`execute ${call} ok ${Date.now() - started}ms`);
183
233
  send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
184
234
  }
@@ -198,7 +248,7 @@ function attachExecuteHandler(ws, sequences) {
198
248
  function waitSessionEnd(ws) {
199
249
  return new Promise((resolve) => {
200
250
  if (ws.readyState === WebSocket.CLOSED) {
201
- resolve('closed');
251
+ resolve({ closed: 'already closed' });
202
252
  return;
203
253
  }
204
254
  const onMessage = (event) => {
@@ -215,7 +265,7 @@ function waitSessionEnd(ws) {
215
265
  };
216
266
  const onClose = (event) => {
217
267
  cleanup();
218
- resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' : 'closed');
268
+ resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' : { closed: closeDetail(event) });
219
269
  };
220
270
  const cleanup = () => {
221
271
  ws.removeEventListener('message', onMessage);
@@ -234,6 +284,7 @@ export async function runServe(repoRoot, options = {}) {
234
284
  throw new Error('SUPERPANEL_SDK_API_KEY or --api-key is required');
235
285
  const backendUrl = options.url ?? env('SUPERPANEL_SDK_BACKEND_URL') ?? DEFAULT_BACKEND_URL;
236
286
  const url = overlayWebSocketUrl(backendUrl);
287
+ const rpcUrl = overlayRpcUrl(backendUrl);
237
288
  const sequenceIds = options.sequenceIds ?? listSequenceIds(repoRoot);
238
289
  if (sequenceIds.length === 0) {
239
290
  throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')}`);
@@ -242,39 +293,48 @@ export async function runServe(repoRoot, options = {}) {
242
293
  const sequences = await loadSequences(repoRoot, sequenceIds, generation);
243
294
  let socket = null;
244
295
  const names = [...sequences.keys()];
296
+ const firstBuild = sequences.values().next().value?.build;
245
297
  console.log('Local overlay');
246
298
  logField('company', companyId);
247
299
  logField('sequences', names.join(', '));
300
+ if (firstBuild) {
301
+ logField('runtime', formatRuntimeVersion(firstBuild.engineVersion, firstBuild.runtimeSource ?? 'npm'));
302
+ }
248
303
  logField('backend', backendUrl);
249
304
  logField('websocket', url);
250
305
  let timer;
251
306
  let pending = { all: false, ids: new Set() };
307
+ let flushing = false;
252
308
  const flush = () => {
253
- const job = pending;
254
- pending = { all: false, ids: new Set() };
309
+ if (flushing)
310
+ return;
311
+ flushing = true;
255
312
  void (async () => {
256
313
  try {
257
- generation += 1;
258
- const ids = job.all ? sequenceIds : [...job.ids];
259
- if (job.all) {
260
- const next = await loadSequences(repoRoot, sequenceIds, generation);
261
- sequences.clear();
314
+ while (pending.all || pending.ids.size > 0) {
315
+ const job = pending;
316
+ pending = { all: false, ids: new Set() };
317
+ generation += 1;
318
+ const ids = job.all ? sequenceIds : [...job.ids];
319
+ const next = await loadSequences(repoRoot, ids, generation);
320
+ if (job.all)
321
+ sequences.clear();
262
322
  for (const [id, loaded] of next)
263
323
  sequences.set(id, loaded);
264
- }
265
- else {
266
- for (const sequenceId of ids) {
267
- sequences.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
324
+ if (socket && socket.readyState === WebSocket.OPEN) {
325
+ pushOverlays(socket, sequences);
326
+ console.log(`reloaded sequences ${ids.join(', ')}`);
268
327
  }
269
328
  }
270
- if (socket && socket.readyState === WebSocket.OPEN) {
271
- pushOverlays(socket, sequences);
272
- console.log(`reloaded sequences ${ids.join(', ')}`);
273
- }
274
329
  }
275
330
  catch (error) {
276
331
  console.error('reload failed:', error instanceof Error ? error.message : error);
277
332
  }
333
+ finally {
334
+ flushing = false;
335
+ if (pending.all || pending.ids.size > 0)
336
+ flush();
337
+ }
278
338
  })();
279
339
  };
280
340
  chokidar
@@ -316,14 +376,14 @@ export async function runServe(repoRoot, options = {}) {
316
376
  names: [...sequences.keys()],
317
377
  });
318
378
  await authOk;
319
- attachExecuteHandler(ws, sequences);
379
+ attachExecuteHandler(ws, sequences, rpcUrl);
320
380
  pushOverlays(ws, sequences);
321
381
  delayMs = 500;
322
382
  console.log(`connected overlay ${url}`);
323
383
  const end = await waitSessionEnd(ws);
324
384
  if (end === 'replaced')
325
385
  throw new OverlayReplacedError();
326
- console.log('disconnected retrying');
386
+ console.log(`disconnected retrying ${end.closed}`);
327
387
  }
328
388
  catch (error) {
329
389
  if (error instanceof OverlayReplacedError)
@@ -2,4 +2,6 @@ export { createLocalComputeHost, resolveComputeHost } from './compute-host.js';
2
2
  export type { ComputeFnMap, ComputeHost, ComputeHostOrFns, LocalComputeHostOptions } from './compute-host.js';
3
3
  export { createSourceComputeHost } from './source-compute-host.js';
4
4
  export type { SourceComputeHost, SourceComputeHostOptions } from './source-compute-host.js';
5
+ export { readOnlyComputeNames, withReadOnlyComputeCache } from './read-only-cache.js';
6
+ export type { ReadOnlyComputeCacheOptions } from './read-only-cache.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
1
  export { createLocalComputeHost, resolveComputeHost } from './compute-host.js';
2
2
  export { createSourceComputeHost } from './source-compute-host.js';
3
+ export { readOnlyComputeNames, withReadOnlyComputeCache } from './read-only-cache.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ import type { ComputeHost } from './compute-host.js';
2
+ /** Names safe to cache: `caseContextView` plus every compute / boolCompute. */
3
+ export declare function readOnlyComputeNames(functions?: Record<string, {
4
+ kind: string;
5
+ }>): Set<string>;
6
+ export type ReadOnlyComputeCacheOptions = {
7
+ /** Defaults to `caseContextView` only. Pass {@link readOnlyComputeNames} for compute / boolCompute too. */
8
+ readOnly?: Iterable<string> | ((name: string) => boolean);
9
+ };
10
+ /**
11
+ * Cache read-only `execute` results by function name + the single input object.
12
+ * Calls with extra args bypass the cache so a shared snapshot cannot alias
13
+ * two different argument lists.
14
+ *
15
+ * Do not wrap a host used by derived recompute: that path mutates one snapshot
16
+ * in place between `compute` calls. Stage walk / when-eval is safe (new snapshot
17
+ * after writes).
18
+ */
19
+ export declare function withReadOnlyComputeCache(host: ComputeHost, options?: ReadOnlyComputeCacheOptions): ComputeHost;
20
+ //# sourceMappingURL=read-only-cache.d.ts.map