@superpanel-sdk/runtime 0.2.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/dist/builder/codegen/emit.d.ts +29 -11
- package/dist/builder/codegen/emit.js +328 -61
- package/dist/builder/codegen/generate.d.ts +21 -0
- package/dist/builder/codegen/generate.js +152 -20
- package/dist/builder/company-import-graph.d.ts +2 -0
- package/dist/builder/company-import-graph.js +40 -8
- package/dist/builder/company-imports.d.ts +24 -0
- package/dist/builder/company-imports.js +114 -0
- package/dist/builder/compile/compile-sequence.d.ts +59 -8
- package/dist/builder/compile/compile-sequence.js +279 -77
- package/dist/builder/compile/function-code.d.ts +12 -2
- package/dist/builder/compile/function-code.js +87 -39
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +24 -23
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +250 -61
- package/dist/builder/paths.d.ts +3 -0
- package/dist/builder/paths.js +16 -0
- package/dist/builder/schedules.d.ts +23 -0
- package/dist/builder/schedules.js +102 -0
- package/dist/builder/slot-schema-validation.d.ts +4 -0
- package/dist/builder/slot-schema-validation.js +10 -0
- package/dist/builder/typecheck.d.ts +8 -0
- package/dist/builder/typecheck.js +105 -0
- package/dist/builder/types.d.ts +91 -19
- package/dist/builder/types.js +2 -1
- package/dist/cli/auth/auth.d.ts +37 -0
- package/dist/cli/auth/auth.js +211 -0
- package/dist/cli/auth/backend-url.d.ts +5 -0
- package/dist/cli/auth/backend-url.js +25 -0
- package/dist/cli/auth/clipboard.d.ts +9 -0
- package/dist/cli/auth/clipboard.js +38 -0
- package/dist/cli/auth/http.d.ts +20 -0
- package/dist/cli/auth/http.js +27 -0
- package/dist/cli/auth/index.d.ts +10 -0
- package/dist/cli/auth/index.js +27 -0
- package/dist/cli/auth/progress.d.ts +12 -0
- package/dist/cli/auth/progress.js +15 -0
- package/dist/cli/auth/serve-login.d.ts +8 -0
- package/dist/cli/auth/serve-login.js +24 -0
- package/dist/cli/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/index.js +28 -8
- package/dist/cli/serve/index.d.ts +73 -3
- package/dist/cli/serve/index.js +410 -113
- package/dist/cli/type-gen/index.js +9 -8
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +15 -23
- package/dist/compute-host/compute-host.js +25 -28
- package/dist/compute-host/index.d.ts +2 -2
- package/dist/compute-host/index.js +1 -1
- package/dist/compute-host/source-compute-host.d.ts +11 -9
- package/dist/compute-host/source-compute-host.js +50 -31
- package/dist/core/case-orchestrator/contact-hours.d.ts +50 -0
- package/dist/core/case-orchestrator/contact-hours.js +108 -0
- package/dist/core/case-orchestrator/follow-ups.d.ts +31 -0
- package/dist/core/case-orchestrator/follow-ups.js +103 -0
- package/dist/core/case-orchestrator/index.d.ts +6 -0
- package/dist/core/case-orchestrator/index.js +6 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +239 -0
- package/dist/core/case-orchestrator/time.d.ts +11 -0
- package/dist/core/case-orchestrator/time.js +29 -0
- package/dist/core/case-orchestrator/tools.d.ts +29 -0
- package/dist/core/case-orchestrator/tools.js +31 -0
- package/dist/core/case-orchestrator/types.d.ts +753 -0
- package/dist/core/case-orchestrator/types.js +46 -0
- package/dist/core/case-orchestrator/validate.d.ts +11 -0
- package/dist/core/case-orchestrator/validate.js +63 -0
- package/dist/core/context/call.d.ts +291 -0
- package/dist/core/context/call.js +94 -0
- package/dist/core/context/channels.d.ts +60 -0
- package/dist/core/context/channels.js +35 -0
- package/dist/core/context/contact.d.ts +96 -6
- package/dist/core/context/contact.js +48 -2
- package/dist/core/context/email-message.d.ts +159 -0
- package/dist/core/context/email-message.js +59 -0
- package/dist/core/context/email.d.ts +97 -0
- package/dist/core/context/email.js +91 -0
- package/dist/core/context/exchanges.d.ts +142 -0
- package/dist/core/context/exchanges.js +63 -0
- package/dist/core/context/instances.d.ts +278 -0
- package/dist/core/context/instances.js +123 -0
- package/dist/core/context/kv.d.ts +113 -0
- package/dist/core/context/kv.js +60 -0
- package/dist/core/context/platform-registry.d.ts +509 -4
- package/dist/core/context/platform-registry.js +14 -2
- package/dist/core/context/platform.d.ts +2 -1
- package/dist/core/context/sms.d.ts +147 -0
- package/dist/core/context/sms.js +57 -0
- package/dist/core/context/types.d.ts +51 -30
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +31 -0
- package/dist/core/conversation/index.js +14 -0
- package/dist/core/functions/define/bind.d.ts +25 -0
- package/dist/core/functions/define/bind.js +14 -0
- package/dist/core/functions/define/case.d.ts +9 -0
- package/dist/core/functions/define/case.js +13 -0
- package/dist/core/functions/define/events.d.ts +20 -0
- package/dist/core/functions/define/events.js +21 -0
- package/dist/core/functions/define/no-case.d.ts +4 -0
- package/dist/core/functions/define/no-case.js +9 -0
- package/dist/core/functions/events.d.ts +247 -0
- package/dist/core/functions/events.js +32 -0
- package/dist/core/functions/func-ref.d.ts +11 -2
- package/dist/core/functions/schedules.d.ts +51 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +62 -51
- package/dist/core/functions/types.js +6 -1
- package/dist/core/office-hours/index.d.ts +39 -0
- package/dist/core/office-hours/index.js +187 -0
- package/dist/core/page/define.d.ts +834 -0
- package/dist/core/page/define.js +477 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/render.d.ts +253 -0
- package/dist/core/page/render.js +840 -0
- package/dist/core/page/time-format.d.ts +19 -0
- package/dist/core/page/time-format.js +41 -0
- package/dist/core/page/timeline.d.ts +40 -0
- package/dist/core/page/timeline.js +2 -0
- package/dist/core/page/typed.d.ts +642 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +33 -0
- package/dist/core/page/workspace.js +231 -0
- package/dist/core/party/index.d.ts +3 -0
- package/dist/core/party/index.js +3 -0
- package/dist/core/party/schema.d.ts +137 -0
- package/dist/core/party/schema.js +80 -0
- package/dist/core/party/types.d.ts +40 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +57 -0
- package/dist/core/sequence/define.js +10 -0
- package/dist/core/sequence/index.d.ts +63 -0
- package/dist/core/sequence/index.js +231 -0
- package/dist/core/slot/binding.d.ts +40 -0
- package/dist/core/slot/binding.js +2 -0
- package/dist/core/slot/company-metadata.d.ts +113 -0
- package/dist/core/slot/company-metadata.js +563 -0
- package/dist/core/slot/compile-schema.d.ts +19 -13
- package/dist/core/slot/compile-schema.js +51 -248
- package/dist/core/slot/contact-timezone.d.ts +110 -0
- package/dist/core/slot/contact-timezone.js +186 -0
- package/dist/core/slot/field.d.ts +2 -24
- package/dist/core/slot/field.js +0 -69
- package/dist/core/slot/id-ref.d.ts +17 -2
- package/dist/core/slot/id-ref.js +16 -8
- package/dist/core/slot/index.d.ts +12 -13
- package/dist/core/slot/index.js +11 -12
- package/dist/core/slot/naming.d.ts +15 -0
- package/dist/core/slot/naming.js +96 -0
- package/dist/core/slot/platform-fields.d.ts +119 -0
- package/dist/core/slot/platform-fields.js +121 -0
- package/dist/core/slot/ref.d.ts +16 -0
- package/dist/core/slot/ref.js +14 -0
- package/dist/core/slot/row.d.ts +2 -9
- package/dist/core/slot/row.js +13 -23
- package/dist/core/slot/schema-compat.d.ts +16 -0
- package/dist/core/slot/schema-compat.js +112 -0
- package/dist/core/slot/schema.d.ts +409 -76
- package/dist/core/slot/schema.js +178 -10
- package/dist/core/slot/types.d.ts +27 -92
- package/dist/core/slot/value.d.ts +26 -3
- package/dist/core/slot/value.js +89 -57
- package/dist/dev-overlay/protocol.d.ts +41 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +20 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +11 -4
- package/dist/fn-context/fake/index.js +27 -2
- package/dist/fn-context/fn-params.d.ts +6 -0
- package/dist/fn-context/fn-params.js +13 -0
- package/dist/fn-context/index.d.ts +5 -5
- package/dist/fn-context/index.js +4 -4
- package/dist/fn-context/log.js +2 -2
- package/dist/fn-context/make-ctx.d.ts +11 -0
- package/dist/fn-context/make-ctx.js +18 -8
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +24 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +13 -0
- package/dist/fn-context/rpc/index.d.ts +3 -1
- package/dist/fn-context/rpc/index.js +3 -1
- package/dist/fn-context/rpc/integration/index.d.ts +4 -4
- package/dist/fn-context/rpc/integration/index.js +1 -1
- package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
- package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
- package/dist/fn-context/rpc/platform.d.ts +2 -2
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +78 -0
- package/dist/fn-context/rpc/records.js +26 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-client.js +89 -5
- package/dist/fn-context/rpc/trpc-init.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +49 -3
- package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
- package/dist/fn-context/run-scope.d.ts +4 -0
- package/dist/fn-context/run-scope.js +9 -0
- package/dist/fn-context/types.d.ts +36 -3
- package/dist/index.d.ts +37 -46
- package/dist/index.js +33 -33
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +41 -0
- package/dist/platform.d.ts +76 -0
- package/dist/platform.js +56 -0
- package/dist/voice-service/actions.d.ts +71 -4
- package/dist/voice-service/types.d.ts +42 -2
- package/dist/worker.d.ts +10 -0
- package/dist/worker.js +10 -0
- package/package.json +38 -19
- package/dist/builder/compile/compile-agenda.d.ts +0 -15
- package/dist/builder/compile/compile-agenda.js +0 -59
- package/dist/builder/merge.d.ts +0 -4
- package/dist/builder/merge.js +0 -19
- package/dist/core/context/db.d.ts +0 -69
- package/dist/core/context/db.js +0 -44
- package/dist/core/context/docusign.d.ts +0 -134
- package/dist/core/context/docusign.js +0 -108
- package/dist/core/context/filevine.d.ts +0 -323
- package/dist/core/context/filevine.js +0 -254
- package/dist/core/context/http.d.ts +0 -9
- package/dist/core/context/http.js +0 -4
- package/dist/core/context/hubspot.d.ts +0 -250
- package/dist/core/context/hubspot.js +0 -191
- package/dist/core/context/twilio.d.ts +0 -50
- package/dist/core/context/twilio.js +0 -42
- package/dist/core/functions/define/case-context.d.ts +0 -14
- package/dist/core/functions/define/case-context.js +0 -15
- package/dist/core/functions/define/compute.d.ts +0 -7
- package/dist/core/functions/define/compute.js +0 -17
- package/dist/core/functions/define/custom.d.ts +0 -4
- package/dist/core/functions/define/custom.js +0 -8
- package/dist/core/functions/define/system.d.ts +0 -7
- package/dist/core/functions/define/system.js +0 -12
- package/dist/core/slot/deps.d.ts +0 -15
- package/dist/core/slot/deps.js +0 -88
- package/dist/core/slot/derived.d.ts +0 -32
- package/dist/core/slot/derived.js +0 -115
- package/dist/core/slot/platform-session-fields.d.ts +0 -38
- package/dist/core/slot/platform-session-fields.js +0 -48
- package/dist/core/slot/sf-schema.d.ts +0 -23
- package/dist/core/slot/sf-schema.js +0 -125
- package/dist/core/slot/sf-validate.d.ts +0 -14
- package/dist/core/slot/sf-validate.js +0 -132
- package/dist/core/slot/snapshot.d.ts +0 -18
- package/dist/core/slot/snapshot.js +0 -138
- package/dist/core/slot/write.d.ts +0 -41
- package/dist/core/slot/write.js +0 -319
- package/dist/core/stage/builders.d.ts +0 -36
- package/dist/core/stage/builders.js +0 -28
- package/dist/core/stage/condition.d.ts +0 -17
- package/dist/core/stage/condition.js +0 -198
- package/dist/core/stage/drive.d.ts +0 -43
- package/dist/core/stage/drive.js +0 -77
- package/dist/core/stage/finish-when.d.ts +0 -20
- package/dist/core/stage/finish-when.js +0 -82
- package/dist/core/stage/flatten.d.ts +0 -27
- package/dist/core/stage/flatten.js +0 -464
- package/dist/core/stage/index.d.ts +0 -11
- package/dist/core/stage/index.js +0 -10
- package/dist/core/stage/simulate.d.ts +0 -24
- package/dist/core/stage/simulate.js +0 -33
- package/dist/core/stage/transfer-fields.d.ts +0 -14
- package/dist/core/stage/transfer-fields.js +0 -88
- package/dist/core/stage/types.d.ts +0 -177
- package/dist/core/stage/types.js +0 -2
- package/dist/core/stage/validate.d.ts +0 -11
- package/dist/core/stage/validate.js +0 -226
- package/dist/core/stage/walk.d.ts +0 -34
- package/dist/core/stage/walk.js +0 -96
- package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
- package/dist/fn-context/system-args.d.ts +0 -6
- package/dist/fn-context/system-args.js +0 -14
package/dist/cli/serve/index.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { cancel, isCancel, select } from '@clack/prompts';
|
|
2
3
|
import chokidar from 'chokidar';
|
|
3
|
-
import { compileAll } from '../../builder/compile/compile-sequence.js';
|
|
4
|
+
import { compileAll, compileCompanyMetadataFromRepo, compileCompanyPart, runtimePackageSource, runtimePackageVersion, } from '../../builder/compile/compile-sequence.js';
|
|
4
5
|
import { withCompanyImportGraph } from '../../builder/company-import-graph.js';
|
|
5
|
-
import { findTsModule } from '../../builder/paths.js';
|
|
6
|
-
import { listSequenceIds,
|
|
6
|
+
import { findTsModule, requireCompanyResolveModule, requireResolveModule } from '../../builder/paths.js';
|
|
7
|
+
import { listSequenceIds, loadCompanyWebhook, loadSequenceCaseOrchestrator, WEBHOOK_MODULE, loadCompanyResolver, loadSequenceResolver, loadSequenceEvents, loadSequenceFunctions, } from '../../builder/load.js';
|
|
8
|
+
import { loadSchedules } from '../../builder/schedules.js';
|
|
9
|
+
import { ORCHESTRATOR_CONTACTS_FUNCTION, ORCHESTRATOR_FUNCTION } from '../../core/case-orchestrator/types.js';
|
|
10
|
+
import { WEBHOOK_RECEIVED_FUNCTION } from '../../core/functions/events.js';
|
|
11
|
+
import { COMPANY_RESOLVE_FUNCTION, RESOLVE_FUNCTION } from '../../core/party/types.js';
|
|
12
|
+
import { COMPANY_SCOPE } from '../../builder/types.js';
|
|
7
13
|
import { createSourceComputeHost } from '../../compute-host/source-compute-host.js';
|
|
14
|
+
import { formatRuntimeVersion } from '../../fn-context/rpc/request-id.js';
|
|
8
15
|
import { encodeOverlayMessage, OVERLAY_REPLACED_CLOSE_CODE, parseOverlayMessage, } from '../../dev-overlay/protocol.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
16
|
+
import { getLogger } from '../../logger.js';
|
|
17
|
+
import { listServeEnvironments, loadCachedServeAuthorization, resolveBackendUrl, ServeAuthorizationRejectedError, } from '../auth/auth.js';
|
|
18
|
+
import { forgetServeAuthorization, loginServe } from '../auth/serve-login.js';
|
|
19
|
+
import { toHttpUrl, toWsUrl } from '../auth/backend-url.js';
|
|
20
|
+
import { progressIndicator } from '../auth/progress.js';
|
|
21
|
+
const logger = getLogger(['runtime', 'overlay']);
|
|
14
22
|
export function overlayWebSocketUrl(raw) {
|
|
15
|
-
const url =
|
|
16
|
-
if (url.protocol === 'http:')
|
|
17
|
-
url.protocol = 'ws:';
|
|
18
|
-
else if (url.protocol === 'https:')
|
|
19
|
-
url.protocol = 'wss:';
|
|
20
|
-
else if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
21
|
-
throw new Error(`unsupported backend url: ${raw}`);
|
|
22
|
-
}
|
|
23
|
+
const url = toWsUrl(raw);
|
|
23
24
|
const path = url.pathname.replace(/\/$/, '');
|
|
24
25
|
if (path === '')
|
|
25
26
|
url.pathname = '/overlay';
|
|
@@ -27,15 +28,33 @@ export function overlayWebSocketUrl(raw) {
|
|
|
27
28
|
url.hash = '';
|
|
28
29
|
return url.toString();
|
|
29
30
|
}
|
|
31
|
+
function overlayRpcUrl(raw) {
|
|
32
|
+
const url = toHttpUrl(raw);
|
|
33
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/trpc`;
|
|
34
|
+
url.search = '';
|
|
35
|
+
url.hash = '';
|
|
36
|
+
return url.toString();
|
|
37
|
+
}
|
|
30
38
|
/** `sequences/<id>/…` → sequence id */
|
|
31
39
|
function sequenceIdFromPath(repoRoot, file) {
|
|
32
40
|
const rel = path.relative(repoRoot, file);
|
|
33
41
|
const match = /^sequences[/\\]([^/\\]+)(?:[/\\]|$)/.exec(rel);
|
|
34
42
|
return match?.[1] ?? null;
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
/** Not the repository's code: dependencies, git, and what compiling writes. */
|
|
45
|
+
export function isIgnoredServePath(repoRoot, file) {
|
|
46
|
+
const parts = path.relative(repoRoot, file).split(/[/\\]/);
|
|
47
|
+
return parts.some((part) => part === 'node_modules' || part === '__gen' || part === 'build' || part === 'dist' || part.startsWith('.'));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What a changed file recompiles (22-sequence-as-business-unit §6): a file of one sequence, that
|
|
51
|
+
* sequence; anything else (`schema/`, `resolve.ts`, shared code) can reach every sequence, so all.
|
|
52
|
+
*/
|
|
53
|
+
export function reloadScopeForPath(repoRoot, file) {
|
|
54
|
+
if (isIgnoredServePath(repoRoot, file))
|
|
55
|
+
return null;
|
|
56
|
+
const sequenceId = sequenceIdFromPath(repoRoot, file);
|
|
57
|
+
return sequenceId ? { sequenceId } : 'all';
|
|
39
58
|
}
|
|
40
59
|
async function loadOne(repoRoot, sequenceId, generation) {
|
|
41
60
|
return withCompanyImportGraph(repoRoot, async () => {
|
|
@@ -52,54 +71,148 @@ async function loadOne(repoRoot, sequenceId, generation) {
|
|
|
52
71
|
if (!functionsPath) {
|
|
53
72
|
throw new Error(`No functions module for sequence ${sequenceId}`);
|
|
54
73
|
}
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
74
|
+
const caseOrchestratorPath = findTsModule(sequenceDir, 'orchestrator');
|
|
75
|
+
const authored = await loadSequenceFunctions(functionsPath);
|
|
76
|
+
const functions = {
|
|
77
|
+
...authored,
|
|
78
|
+
[RESOLVE_FUNCTION]: await loadSequenceResolver(requireResolveModule(sequenceDir, sequenceId)),
|
|
79
|
+
...(caseOrchestratorPath ? await caseOrchestratorFunctions(caseOrchestratorPath) : {}),
|
|
80
|
+
// The jobs of its schedules.ts run here too while serving (25 §6.4).
|
|
81
|
+
...(await loadSchedules(sequenceDir, `sequences/${sequenceId}/schedules.ts`))?.functions,
|
|
82
|
+
};
|
|
83
|
+
const eventsPath = findTsModule(sequenceDir, 'events');
|
|
84
|
+
const events = eventsPath ? await loadSequenceEvents(eventsPath) : {};
|
|
85
|
+
return { build: compiled.build, functions, events, generation };
|
|
59
86
|
});
|
|
60
87
|
}
|
|
88
|
+
export async function loadCompany(repoRoot) {
|
|
89
|
+
return withCompanyImportGraph(repoRoot, async () => {
|
|
90
|
+
const webhookPath = findTsModule(repoRoot, WEBHOOK_MODULE);
|
|
91
|
+
return {
|
|
92
|
+
part: await compileCompanyPart(repoRoot, { includeCode: false }),
|
|
93
|
+
companyMetadata: await compileCompanyMetadataFromRepo(repoRoot),
|
|
94
|
+
functions: {
|
|
95
|
+
[COMPANY_RESOLVE_FUNCTION]: await loadCompanyResolver(requireCompanyResolveModule(repoRoot)),
|
|
96
|
+
...(webhookPath ? { [WEBHOOK_RECEIVED_FUNCTION]: await loadCompanyWebhook(webhookPath) } : {}),
|
|
97
|
+
...(await loadSchedules(repoRoot, 'schedules.ts'))?.functions,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** orchestrator.ts's two functions, as the build names them. */
|
|
103
|
+
async function caseOrchestratorFunctions(path) {
|
|
104
|
+
const { orchestrator, contacts } = await loadSequenceCaseOrchestrator(path);
|
|
105
|
+
return {
|
|
106
|
+
[ORCHESTRATOR_FUNCTION]: { kind: 'orchestrator', main: orchestrator },
|
|
107
|
+
[ORCHESTRATOR_CONTACTS_FUNCTION]: { kind: 'orchestrator', main: contacts },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** The repository's sequences, compiled; a listed sequence missing from the repository is left out. */
|
|
61
111
|
async function loadSequences(repoRoot, sequenceIds, generation) {
|
|
62
112
|
return withCompanyImportGraph(repoRoot, async () => {
|
|
113
|
+
const present = new Set(listSequenceIds(repoRoot));
|
|
63
114
|
const out = new Map();
|
|
64
115
|
for (const sequenceId of sequenceIds) {
|
|
65
|
-
|
|
116
|
+
if (present.has(sequenceId))
|
|
117
|
+
out.set(sequenceId, await loadOne(repoRoot, sequenceId, generation));
|
|
66
118
|
}
|
|
67
119
|
return out;
|
|
68
120
|
});
|
|
69
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The changes waiting to be reloaded. A reload that fails keeps its scope (`fail`) for the next one,
|
|
124
|
+
* which a new file change starts (no retry on its own: the failure is in the code until it is
|
|
125
|
+
* edited); it is cleared only once a reload of it loads.
|
|
126
|
+
*/
|
|
127
|
+
export class ReloadQueue {
|
|
128
|
+
pending = { all: false, ids: new Set() };
|
|
129
|
+
failed = { all: false, ids: new Set() };
|
|
130
|
+
add(scope) {
|
|
131
|
+
if (scope === 'all')
|
|
132
|
+
this.pending.all = true;
|
|
133
|
+
else
|
|
134
|
+
this.pending.ids.add(scope.sequenceId);
|
|
135
|
+
}
|
|
136
|
+
/** Something changed since the last reload. */
|
|
137
|
+
hasChanges() {
|
|
138
|
+
return this.pending.all || this.pending.ids.size > 0;
|
|
139
|
+
}
|
|
140
|
+
/** The next reload: the new changes plus what failed before. */
|
|
141
|
+
take() {
|
|
142
|
+
const all = this.pending.all || this.failed.all;
|
|
143
|
+
const job = { all, ids: all ? new Set() : new Set([...this.failed.ids, ...this.pending.ids]) };
|
|
144
|
+
this.pending = { all: false, ids: new Set() };
|
|
145
|
+
this.failed = { all: false, ids: new Set() };
|
|
146
|
+
return job;
|
|
147
|
+
}
|
|
148
|
+
fail(job) {
|
|
149
|
+
this.failed = { all: this.failed.all || job.all, ids: new Set([...this.failed.ids, ...job.ids]) };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The state after a reload (`all`: the whole repository; else the sequences `ids` names), built
|
|
154
|
+
* aside: if any part fails to load it throws and the current state stays whole, never new company
|
|
155
|
+
* code with old sequences.
|
|
156
|
+
*/
|
|
157
|
+
export async function reloadServeState(repoRoot, current, job, generation) {
|
|
158
|
+
// Recompiling everything also picks up sequence directories added or removed since.
|
|
159
|
+
const ids = job.all ? listSequenceIds(repoRoot) : [...job.ids];
|
|
160
|
+
// The company's code is outside `sequences/`, so only a recompile of everything reaches it.
|
|
161
|
+
const company = job.all ? await loadCompany(repoRoot) : current.company;
|
|
162
|
+
const loaded = await loadSequences(repoRoot, ids, generation);
|
|
163
|
+
const sequences = job.all ? new Map() : new Map(current.sequences);
|
|
164
|
+
for (const id of ids) {
|
|
165
|
+
const next = loaded.get(id);
|
|
166
|
+
if (next)
|
|
167
|
+
sequences.set(id, next);
|
|
168
|
+
else
|
|
169
|
+
sequences.delete(id);
|
|
170
|
+
}
|
|
171
|
+
return { company, sequences };
|
|
172
|
+
}
|
|
70
173
|
function send(ws, message) {
|
|
71
174
|
ws.send(encodeOverlayMessage(message));
|
|
72
175
|
}
|
|
73
176
|
function logField(label, value) {
|
|
74
|
-
|
|
177
|
+
logger.info(label, value);
|
|
75
178
|
}
|
|
76
|
-
async function handleExecute(loaded, message) {
|
|
179
|
+
async function handleExecute(loaded, message, rpcUrl) {
|
|
180
|
+
const data = message.data
|
|
181
|
+
? {
|
|
182
|
+
...message.data,
|
|
183
|
+
config: { ...message.data.config, backendUrl: rpcUrl },
|
|
184
|
+
}
|
|
185
|
+
: undefined;
|
|
77
186
|
const host = createSourceComputeHost({
|
|
78
187
|
functions: loaded.functions,
|
|
79
|
-
|
|
80
|
-
data:
|
|
188
|
+
events: loaded.events,
|
|
189
|
+
data: data,
|
|
81
190
|
runToken: message.runToken,
|
|
82
|
-
runtimeVersion: loaded.
|
|
83
|
-
runtimeSource: loaded.
|
|
191
|
+
runtimeVersion: loaded.runtimeVersion,
|
|
192
|
+
runtimeSource: loaded.runtimeSource,
|
|
84
193
|
getRequestId: () => message.logRequestId,
|
|
85
194
|
getTags: () => message.logTags,
|
|
86
195
|
});
|
|
87
196
|
try {
|
|
88
|
-
return await host.execute(message.name, message.
|
|
197
|
+
return await host.execute(message.name, message.invocation);
|
|
89
198
|
}
|
|
90
199
|
finally {
|
|
91
200
|
host.dispose();
|
|
92
201
|
}
|
|
93
202
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
203
|
+
/**
|
|
204
|
+
* Push the local company build: the company's functions and every loaded sequence, as one overlay.
|
|
205
|
+
* With no sequence left it is still pushed, so the environment drops the ones it had (22 §11).
|
|
206
|
+
*/
|
|
207
|
+
export function pushOverlay(ws, company, sequences, generation) {
|
|
208
|
+
const build = {
|
|
209
|
+
company: company.part,
|
|
210
|
+
sequences: Object.fromEntries([...sequences].map(([sequenceId, sequence]) => [sequenceId, sequence.build])),
|
|
211
|
+
};
|
|
212
|
+
send(ws, { type: 'overlay', generation, build, companyMetadata: company.companyMetadata });
|
|
213
|
+
}
|
|
214
|
+
function closeDetail(event) {
|
|
215
|
+
return event.reason ? `${event.code} ${event.reason}` : String(event.code);
|
|
103
216
|
}
|
|
104
217
|
function waitOpen(ws) {
|
|
105
218
|
return new Promise((resolve, reject) => {
|
|
@@ -107,8 +220,27 @@ function waitOpen(ws) {
|
|
|
107
220
|
resolve();
|
|
108
221
|
return;
|
|
109
222
|
}
|
|
110
|
-
|
|
111
|
-
|
|
223
|
+
let settled = false;
|
|
224
|
+
const finish = (fn) => {
|
|
225
|
+
if (settled)
|
|
226
|
+
return;
|
|
227
|
+
settled = true;
|
|
228
|
+
cleanup();
|
|
229
|
+
fn();
|
|
230
|
+
};
|
|
231
|
+
const onOpen = () => finish(() => resolve());
|
|
232
|
+
const onClose = (event) => finish(() => reject(new Error(`websocket closed ${closeDetail(event)}`)));
|
|
233
|
+
const onError = () => {
|
|
234
|
+
setTimeout(() => finish(() => reject(new Error('websocket error'))), 250);
|
|
235
|
+
};
|
|
236
|
+
const cleanup = () => {
|
|
237
|
+
ws.removeEventListener('open', onOpen);
|
|
238
|
+
ws.removeEventListener('close', onClose);
|
|
239
|
+
ws.removeEventListener('error', onError);
|
|
240
|
+
};
|
|
241
|
+
ws.addEventListener('open', onOpen);
|
|
242
|
+
ws.addEventListener('close', onClose);
|
|
243
|
+
ws.addEventListener('error', onError);
|
|
112
244
|
});
|
|
113
245
|
}
|
|
114
246
|
class OverlayReplacedError extends Error {
|
|
@@ -117,6 +249,36 @@ class OverlayReplacedError extends Error {
|
|
|
117
249
|
this.name = 'OverlayReplacedError';
|
|
118
250
|
}
|
|
119
251
|
}
|
|
252
|
+
/** Transient: an infra hiccup, or a lease that was just replaced. Retry with backoff. */
|
|
253
|
+
class OverlayUnavailableError extends Error {
|
|
254
|
+
constructor(message) {
|
|
255
|
+
super(message);
|
|
256
|
+
this.name = 'OverlayUnavailableError';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Permanent: the request itself is invalid (missing or unknown environment, prod). Fatal. */
|
|
260
|
+
class OverlayRequestError extends Error {
|
|
261
|
+
constructor(message) {
|
|
262
|
+
super(message);
|
|
263
|
+
this.name = 'OverlayRequestError';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export function toOverlayError(message) {
|
|
267
|
+
switch (message.code) {
|
|
268
|
+
case 'authorization':
|
|
269
|
+
return new ServeAuthorizationRejectedError(message.error);
|
|
270
|
+
case 'request':
|
|
271
|
+
return new OverlayRequestError(message.error);
|
|
272
|
+
case 'unavailable':
|
|
273
|
+
return new OverlayUnavailableError(message.error);
|
|
274
|
+
default: {
|
|
275
|
+
// The wire format is not type-checked: a code this CLI doesn't know is a protocol bug, so
|
|
276
|
+
// stop and say so instead of reconnecting forever on an `undefined` error.
|
|
277
|
+
const unknown = message.code;
|
|
278
|
+
return new OverlayRequestError(`unknown auth.error code ${JSON.stringify(unknown)}: ${message.error}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
120
282
|
function waitAuthOk(ws) {
|
|
121
283
|
return new Promise((resolve, reject) => {
|
|
122
284
|
const onMessage = (event) => {
|
|
@@ -134,7 +296,7 @@ function waitAuthOk(ws) {
|
|
|
134
296
|
}
|
|
135
297
|
if (message.type === 'auth.error') {
|
|
136
298
|
cleanup();
|
|
137
|
-
reject(
|
|
299
|
+
reject(toOverlayError(message));
|
|
138
300
|
}
|
|
139
301
|
}
|
|
140
302
|
catch (error) {
|
|
@@ -146,7 +308,7 @@ function waitAuthOk(ws) {
|
|
|
146
308
|
cleanup();
|
|
147
309
|
reject(event.code === OVERLAY_REPLACED_CLOSE_CODE
|
|
148
310
|
? new OverlayReplacedError()
|
|
149
|
-
: new Error(
|
|
311
|
+
: new Error(`websocket closed before auth.ok ${closeDetail(event)}`));
|
|
150
312
|
};
|
|
151
313
|
const cleanup = () => {
|
|
152
314
|
ws.removeEventListener('message', onMessage);
|
|
@@ -156,7 +318,7 @@ function waitAuthOk(ws) {
|
|
|
156
318
|
ws.addEventListener('close', onClose);
|
|
157
319
|
});
|
|
158
320
|
}
|
|
159
|
-
function attachExecuteHandler(ws,
|
|
321
|
+
function attachExecuteHandler(ws, find, rpcUrl) {
|
|
160
322
|
ws.addEventListener('message', (event) => {
|
|
161
323
|
void (async () => {
|
|
162
324
|
let message;
|
|
@@ -164,57 +326,88 @@ function attachExecuteHandler(ws, sequences) {
|
|
|
164
326
|
message = parseOverlayMessage(String(event.data));
|
|
165
327
|
}
|
|
166
328
|
catch (error) {
|
|
167
|
-
|
|
329
|
+
logger.error('bad server message', error);
|
|
168
330
|
return;
|
|
169
331
|
}
|
|
170
332
|
if (message.type !== 'execute')
|
|
171
333
|
return;
|
|
172
|
-
const
|
|
173
|
-
const loaded = sequences.get(message.sequenceId);
|
|
334
|
+
const loaded = find(message.sequenceId);
|
|
174
335
|
if (!loaded) {
|
|
175
336
|
const error = `unknown sequence ${message.sequenceId}`;
|
|
176
|
-
console.error(`execute ${call} failed: ${error}`);
|
|
177
337
|
send(ws, {
|
|
178
338
|
type: 'execute.result',
|
|
179
339
|
requestId: message.requestId,
|
|
180
340
|
ok: false,
|
|
181
341
|
error,
|
|
182
342
|
});
|
|
343
|
+
logger.error('execute failed', { sequenceId: message.sequenceId, name: message.name, error });
|
|
183
344
|
return;
|
|
184
345
|
}
|
|
185
346
|
const started = Date.now();
|
|
186
|
-
|
|
347
|
+
let result;
|
|
187
348
|
try {
|
|
188
|
-
|
|
189
|
-
console.log(`execute ${call} ok ${Date.now() - started}ms`);
|
|
190
|
-
send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
|
|
349
|
+
result = await handleExecute(loaded, message, rpcUrl);
|
|
191
350
|
}
|
|
192
351
|
catch (error) {
|
|
193
352
|
const text = error instanceof Error ? error.message : String(error);
|
|
194
|
-
console.error(`execute ${call} failed: ${text}`);
|
|
195
353
|
send(ws, {
|
|
196
354
|
type: 'execute.result',
|
|
197
355
|
requestId: message.requestId,
|
|
198
356
|
ok: false,
|
|
199
357
|
error: text,
|
|
200
358
|
});
|
|
359
|
+
logger.error('execute failed', {
|
|
360
|
+
sequenceId: message.sequenceId,
|
|
361
|
+
name: message.name,
|
|
362
|
+
durationMs: Date.now() - started,
|
|
363
|
+
error: text,
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
201
366
|
}
|
|
367
|
+
send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
|
|
368
|
+
logger.info('execute ok', {
|
|
369
|
+
sequenceId: message.sequenceId,
|
|
370
|
+
name: message.name,
|
|
371
|
+
durationMs: Date.now() - started,
|
|
372
|
+
});
|
|
202
373
|
})();
|
|
203
374
|
});
|
|
204
375
|
}
|
|
376
|
+
function attachNoticeHandler(ws) {
|
|
377
|
+
ws.addEventListener('message', (event) => {
|
|
378
|
+
let message;
|
|
379
|
+
try {
|
|
380
|
+
message = parseOverlayMessage(String(event.data));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (message.type !== 'notice')
|
|
386
|
+
return;
|
|
387
|
+
if (message.details.length > 0)
|
|
388
|
+
logger.warn(message.message, message.details);
|
|
389
|
+
else
|
|
390
|
+
logger.warn(message.message);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
205
393
|
function waitSessionEnd(ws) {
|
|
206
|
-
return new Promise((resolve) => {
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
207
395
|
if (ws.readyState === WebSocket.CLOSED) {
|
|
208
|
-
resolve('closed');
|
|
396
|
+
resolve({ closed: 'already closed' });
|
|
209
397
|
return;
|
|
210
398
|
}
|
|
211
399
|
const onMessage = (event) => {
|
|
212
400
|
try {
|
|
213
401
|
const message = parseOverlayMessage(String(event.data));
|
|
214
402
|
if (message.type === 'replaced') {
|
|
403
|
+
logger.warn('another serve took the environment');
|
|
215
404
|
cleanup();
|
|
216
405
|
resolve('replaced');
|
|
217
406
|
}
|
|
407
|
+
else if (message.type === 'auth.error') {
|
|
408
|
+
cleanup();
|
|
409
|
+
reject(toOverlayError(message));
|
|
410
|
+
}
|
|
218
411
|
}
|
|
219
412
|
catch {
|
|
220
413
|
return;
|
|
@@ -222,7 +415,7 @@ function waitSessionEnd(ws) {
|
|
|
222
415
|
};
|
|
223
416
|
const onClose = (event) => {
|
|
224
417
|
cleanup();
|
|
225
|
-
resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' :
|
|
418
|
+
resolve(event.code === OVERLAY_REPLACED_CLOSE_CODE ? 'replaced' : { closed: closeDetail(event) });
|
|
226
419
|
};
|
|
227
420
|
const cleanup = () => {
|
|
228
421
|
ws.removeEventListener('message', onMessage);
|
|
@@ -232,30 +425,128 @@ function waitSessionEnd(ws) {
|
|
|
232
425
|
ws.addEventListener('close', onClose);
|
|
233
426
|
});
|
|
234
427
|
}
|
|
428
|
+
function exitOnCancel() {
|
|
429
|
+
cancel('Cancelled.');
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Only prompt when there's an actual person at a terminal to answer — a CI runner, a piped/
|
|
434
|
+
* redirected stdin, or any other non-interactive invocation must keep failing fast on a missing
|
|
435
|
+
* flag instead of hanging on a readline prompt with undefined behavior off a real TTY.
|
|
436
|
+
*/
|
|
437
|
+
function requireInteractiveOrThrow(flag) {
|
|
438
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
439
|
+
throw new Error(`${flag} is required (not running in an interactive terminal to prompt for it)`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** The token's non-prod environments. A rejected token surfaces as `ServeAuthorizationRejectedError`. */
|
|
443
|
+
async function fetchEnvironments(backendUrl, authorization) {
|
|
444
|
+
const progress = progressIndicator();
|
|
445
|
+
const company = authorization.companyName ? ` in ${authorization.companyName}` : '';
|
|
446
|
+
progress.start(`Fetching environments${company}…`);
|
|
447
|
+
let environments;
|
|
448
|
+
try {
|
|
449
|
+
environments = await listServeEnvironments(backendUrl, authorization.token);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (error instanceof ServeAuthorizationRejectedError) {
|
|
453
|
+
progress.error('Login was rejected by the backend.');
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
progress.error('Could not fetch environments.');
|
|
457
|
+
throw new Error(`Could not list environments: ${error instanceof Error ? error.message : String(error)}`);
|
|
458
|
+
}
|
|
459
|
+
progress.stop(`Found ${environments.length} environment${environments.length === 1 ? '' : 's'}${company}.`);
|
|
460
|
+
return environments;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Login plus the company's environments. Listing them doubles as checking the cached token against
|
|
464
|
+
* the backend: it can be unexpired locally yet rejected (revoked, re-keyed, access removed), and
|
|
465
|
+
* then it is dropped for a fresh browser login. Rejections after startup stop serve instead.
|
|
466
|
+
*/
|
|
467
|
+
async function authorizeWithEnvironments(backendUrl, openBrowser) {
|
|
468
|
+
const cached = await loadCachedServeAuthorization(backendUrl);
|
|
469
|
+
if (cached) {
|
|
470
|
+
logger.info('using cached serve authorization');
|
|
471
|
+
try {
|
|
472
|
+
return { authorization: cached, environments: await fetchEnvironments(backendUrl, cached) };
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (!(error instanceof ServeAuthorizationRejectedError))
|
|
476
|
+
throw error;
|
|
477
|
+
logger.warn('cached serve authorization is no longer valid; authorizing again', { error: error.message });
|
|
478
|
+
await forgetServeAuthorization(backendUrl, cached);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const authorization = await loginServe(backendUrl, { openBrowser });
|
|
482
|
+
return { authorization, environments: await fetchEnvironments(backendUrl, authorization) };
|
|
483
|
+
}
|
|
484
|
+
/** Prompts for one non-prod environment when `--environment` was not given. Shows id and name. */
|
|
485
|
+
async function promptForEnvironment(environments) {
|
|
486
|
+
if (environments.length === 0) {
|
|
487
|
+
throw new Error('No non-prod environments available for this company.');
|
|
488
|
+
}
|
|
489
|
+
const picked = await select({
|
|
490
|
+
message: 'Which environment should Local Serve overlay?',
|
|
491
|
+
options: environments.map((environment) => ({
|
|
492
|
+
value: environment.id,
|
|
493
|
+
label: environment.name,
|
|
494
|
+
hint: environment.id,
|
|
495
|
+
})),
|
|
496
|
+
});
|
|
497
|
+
if (isCancel(picked))
|
|
498
|
+
exitOnCancel();
|
|
499
|
+
return picked;
|
|
500
|
+
}
|
|
235
501
|
export async function runServe(repoRoot, options = {}) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (!
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const sequenceIds = options.sequenceIds ?? listSequenceIds(repoRoot);
|
|
245
|
-
if (sequenceIds.length === 0) {
|
|
246
|
-
throw new Error(`No sequences with slots found under ${path.join(repoRoot, 'sequences')}`);
|
|
502
|
+
const backendUrl = resolveBackendUrl(repoRoot, options.url);
|
|
503
|
+
// A non-interactive caller cannot answer the prompt. Check this before authorization so a CI job
|
|
504
|
+
// with a missing flag fails instead of waiting on a browser-based device-auth flow.
|
|
505
|
+
let environment = options.environment?.trim();
|
|
506
|
+
if (!environment)
|
|
507
|
+
requireInteractiveOrThrow('--environment <id>');
|
|
508
|
+
if (listSequenceIds(repoRoot).length === 0) {
|
|
509
|
+
throw new Error(`No sequences found under ${path.join(repoRoot, 'sequences')}`);
|
|
247
510
|
}
|
|
511
|
+
// Login is the first interactive step: the environment prompt needs the company-scoped token.
|
|
512
|
+
const { authorization, environments } = await authorizeWithEnvironments(backendUrl, options.openBrowser !== false);
|
|
513
|
+
if (!environment)
|
|
514
|
+
environment = await promptForEnvironment(environments);
|
|
515
|
+
// The whole repository serves the environment (22 §6): every sequence, and only these.
|
|
248
516
|
let generation = 1;
|
|
249
|
-
|
|
517
|
+
let company = await loadCompany(repoRoot);
|
|
518
|
+
let sequences = await loadSequences(repoRoot, listSequenceIds(repoRoot), generation);
|
|
519
|
+
const runtimeSource = runtimePackageSource(repoRoot);
|
|
520
|
+
const find = (scope) => {
|
|
521
|
+
if (scope === COMPANY_SCOPE) {
|
|
522
|
+
return { functions: company.functions, events: {}, runtimeVersion: runtimePackageVersion(), runtimeSource };
|
|
523
|
+
}
|
|
524
|
+
const loaded = sequences.get(scope);
|
|
525
|
+
return loaded
|
|
526
|
+
? {
|
|
527
|
+
functions: loaded.functions,
|
|
528
|
+
events: loaded.events,
|
|
529
|
+
runtimeVersion: loaded.build.engineVersion,
|
|
530
|
+
runtimeSource: loaded.build.runtimeSource,
|
|
531
|
+
}
|
|
532
|
+
: undefined;
|
|
533
|
+
};
|
|
534
|
+
const url = overlayWebSocketUrl(backendUrl);
|
|
535
|
+
const rpcUrl = overlayRpcUrl(backendUrl);
|
|
250
536
|
let socket = null;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
logField('
|
|
254
|
-
logField('
|
|
537
|
+
const firstBuild = sequences.values().next().value?.build;
|
|
538
|
+
logger.info('Local overlay');
|
|
539
|
+
logField('user', authorization.user.email);
|
|
540
|
+
logField('company', authorization.companyId);
|
|
541
|
+
logField('environment', environment);
|
|
542
|
+
logField('sequences', [...sequences.keys()].join(', '));
|
|
543
|
+
if (firstBuild) {
|
|
544
|
+
logField('runtime', formatRuntimeVersion(firstBuild.engineVersion, firstBuild.runtimeSource));
|
|
545
|
+
}
|
|
255
546
|
logField('backend', backendUrl);
|
|
256
547
|
logField('websocket', url);
|
|
257
548
|
let timer;
|
|
258
|
-
|
|
549
|
+
const queue = new ReloadQueue();
|
|
259
550
|
let flushing = false;
|
|
260
551
|
const flush = () => {
|
|
261
552
|
if (flushing)
|
|
@@ -263,54 +554,51 @@ export async function runServe(repoRoot, options = {}) {
|
|
|
263
554
|
flushing = true;
|
|
264
555
|
void (async () => {
|
|
265
556
|
try {
|
|
266
|
-
while (
|
|
267
|
-
const job =
|
|
268
|
-
pending = { all: false, ids: new Set() };
|
|
557
|
+
while (queue.hasChanges()) {
|
|
558
|
+
const job = queue.take();
|
|
269
559
|
generation += 1;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
sequences
|
|
274
|
-
|
|
275
|
-
|
|
560
|
+
// Swapped in only whole: a failed reload leaves the code that runs (and is pushed) as it was.
|
|
561
|
+
let next;
|
|
562
|
+
try {
|
|
563
|
+
next = await reloadServeState(repoRoot, { company, sequences }, job, generation);
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
// Kept for the next change: fixing one sequence then also reloads what this one covered.
|
|
567
|
+
queue.fail(job);
|
|
568
|
+
logger.error('reload failed', error);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
company = next.company;
|
|
572
|
+
sequences = next.sequences;
|
|
276
573
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
277
|
-
|
|
278
|
-
|
|
574
|
+
pushOverlay(socket, company, sequences, generation);
|
|
575
|
+
logger.info('reloaded sequences', job.all ? 'all' : [...job.ids]);
|
|
279
576
|
}
|
|
280
577
|
}
|
|
281
578
|
}
|
|
282
579
|
catch (error) {
|
|
283
|
-
|
|
580
|
+
logger.error('reload failed', error);
|
|
284
581
|
}
|
|
285
582
|
finally {
|
|
286
583
|
flushing = false;
|
|
287
|
-
if (
|
|
584
|
+
if (queue.hasChanges())
|
|
288
585
|
flush();
|
|
289
586
|
}
|
|
290
587
|
})();
|
|
291
588
|
};
|
|
292
589
|
chokidar
|
|
293
|
-
.watch(
|
|
590
|
+
.watch(repoRoot, {
|
|
294
591
|
ignoreInitial: true,
|
|
295
|
-
ignored: (p) =>
|
|
592
|
+
ignored: (p) => isIgnoredServePath(repoRoot, p),
|
|
296
593
|
})
|
|
297
594
|
.on('all', (event, file) => {
|
|
298
595
|
if (!file)
|
|
299
596
|
return;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (file.includes(`${path.sep}build${path.sep}`))
|
|
303
|
-
return;
|
|
304
|
-
const rel = path.relative(repoRoot, file);
|
|
305
|
-
const schemaHit = isSchemaPath(repoRoot, file);
|
|
306
|
-
const sequenceId = sequenceIdFromPath(repoRoot, file);
|
|
307
|
-
if (!schemaHit && !sequenceId)
|
|
597
|
+
const scope = reloadScopeForPath(repoRoot, file);
|
|
598
|
+
if (!scope)
|
|
308
599
|
return;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
pending.all = true;
|
|
312
|
-
else if (sequenceId)
|
|
313
|
-
pending.ids.add(sequenceId);
|
|
600
|
+
logger.info('changed', path.relative(repoRoot, file));
|
|
601
|
+
queue.add(scope);
|
|
314
602
|
clearTimeout(timer);
|
|
315
603
|
timer = setTimeout(flush, 100);
|
|
316
604
|
});
|
|
@@ -321,26 +609,35 @@ export async function runServe(repoRoot, options = {}) {
|
|
|
321
609
|
try {
|
|
322
610
|
await waitOpen(ws);
|
|
323
611
|
const authOk = waitAuthOk(ws);
|
|
324
|
-
send(ws, {
|
|
325
|
-
type: 'auth',
|
|
326
|
-
apiKey,
|
|
327
|
-
companyId,
|
|
328
|
-
names: [...sequences.keys()],
|
|
329
|
-
});
|
|
612
|
+
send(ws, { type: 'auth', token: authorization.token, environment });
|
|
330
613
|
await authOk;
|
|
331
|
-
attachExecuteHandler(ws,
|
|
332
|
-
|
|
614
|
+
attachExecuteHandler(ws, find, rpcUrl);
|
|
615
|
+
attachNoticeHandler(ws);
|
|
616
|
+
pushOverlay(ws, company, sequences, generation);
|
|
333
617
|
delayMs = 500;
|
|
334
|
-
|
|
618
|
+
logger.info('connected', { url });
|
|
335
619
|
const end = await waitSessionEnd(ws);
|
|
336
620
|
if (end === 'replaced')
|
|
337
621
|
throw new OverlayReplacedError();
|
|
338
|
-
|
|
622
|
+
logger.warn('disconnected; retrying', { detail: end.closed });
|
|
339
623
|
}
|
|
340
624
|
catch (error) {
|
|
341
625
|
if (error instanceof OverlayReplacedError)
|
|
342
626
|
throw error;
|
|
343
|
-
|
|
627
|
+
if (error instanceof OverlayRequestError)
|
|
628
|
+
throw error;
|
|
629
|
+
if (error instanceof ServeAuthorizationRejectedError) {
|
|
630
|
+
// Mid-session the identity is fixed: stop rather than log in again, and drop the cached
|
|
631
|
+
// token so the user's next `serve` starts with a fresh login.
|
|
632
|
+
await forgetServeAuthorization(backendUrl, authorization);
|
|
633
|
+
throw new Error(`Serve authorization was rejected (${error.message}). Run serve again to log in.`);
|
|
634
|
+
}
|
|
635
|
+
if (error instanceof OverlayUnavailableError) {
|
|
636
|
+
logger.warn('serve is temporarily unavailable; retrying', { error: error.message });
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
logger.error('connection failed', error);
|
|
640
|
+
}
|
|
344
641
|
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
345
642
|
ws.close();
|
|
346
643
|
}
|