@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.
- package/dist/builder/codegen/generate.js +15 -9
- package/dist/builder/company-import-graph.d.ts +11 -0
- package/dist/builder/company-import-graph.js +105 -0
- package/dist/builder/compile/compile-agenda.js +9 -4
- package/dist/builder/compile/compile-sequence.d.ts +4 -1
- package/dist/builder/compile/compile-sequence.js +28 -9
- package/dist/builder/load.js +3 -3
- package/dist/builder/types.d.ts +5 -0
- package/dist/cli/serve/index.js +107 -47
- package/dist/compute-host/index.d.ts +2 -0
- package/dist/compute-host/index.js +1 -0
- package/dist/compute-host/read-only-cache.d.ts +20 -0
- package/dist/compute-host/read-only-cache.js +61 -0
- package/dist/compute-host/source-compute-host.d.ts +4 -0
- package/dist/compute-host/source-compute-host.js +12 -2
- package/dist/core/context/types.d.ts +19 -1
- package/dist/core/functions/define/custom.d.ts +1 -1
- package/dist/core/functions/types.d.ts +4 -4
- package/dist/core/slot/index.d.ts +1 -1
- package/dist/core/slot/index.js +1 -1
- package/dist/core/slot/types.d.ts +2 -0
- package/dist/core/slot/write.d.ts +8 -1
- package/dist/core/slot/write.js +145 -38
- package/dist/core/stage/builders.d.ts +23 -5
- package/dist/core/stage/builders.js +3 -0
- package/dist/core/stage/drive.d.ts +1 -1
- package/dist/core/stage/drive.js +9 -8
- package/dist/core/stage/flatten.js +13 -11
- package/dist/core/stage/index.d.ts +8 -4
- package/dist/core/stage/index.js +5 -2
- package/dist/core/stage/interrupts.d.ts +28 -0
- package/dist/core/stage/interrupts.js +120 -0
- package/dist/core/stage/ivr-fields.d.ts +9 -0
- package/dist/core/stage/ivr-fields.js +75 -0
- package/dist/core/stage/transfer-fields.d.ts +6 -1
- package/dist/core/stage/transfer-fields.js +44 -3
- package/dist/core/stage/transfer-spec.d.ts +48 -0
- package/dist/core/stage/transfer-spec.js +113 -0
- package/dist/core/stage/types.d.ts +45 -14
- package/dist/core/stage/validate.js +105 -1
- package/dist/core/stage/walk.d.ts +5 -0
- package/dist/core/stage/walk.js +33 -10
- package/dist/fn-context/index.d.ts +1 -1
- package/dist/fn-context/index.js +1 -1
- package/dist/fn-context/rpc/index.d.ts +1 -1
- package/dist/fn-context/rpc/index.js +1 -1
- package/dist/fn-context/rpc/request-id.d.ts +5 -0
- package/dist/fn-context/rpc/request-id.js +11 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +2 -0
- package/dist/fn-context/rpc/trpc-client.js +5 -1
- package/dist/fn-context/rpc/trpc-init.d.ts +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -4
- package/dist/voice-service/types.d.ts +3 -0
- 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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
31
|
-
const
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
package/dist/builder/load.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/builder/types.d.ts
CHANGED
|
@@ -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). */
|
package/dist/cli/serve/index.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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
|
-
|
|
104
|
-
|
|
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(
|
|
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' :
|
|
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
|
-
|
|
254
|
-
|
|
309
|
+
if (flushing)
|
|
310
|
+
return;
|
|
311
|
+
flushing = true;
|
|
255
312
|
void (async () => {
|
|
256
313
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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(
|
|
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
|