sdn-flow 0.2.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.
Files changed (69) hide show
  1. package/.claude/SKILLS.md +7 -0
  2. package/.claude/skills/sdn-plugin-abi-compliance/SKILL.md +56 -0
  3. package/.claude/todo/001-js-host-startup-and-deno.md +85 -0
  4. package/LICENSE +21 -0
  5. package/README.md +223 -0
  6. package/bin/sdn-flow-host.js +169 -0
  7. package/docs/.nojekyll +0 -0
  8. package/docs/ARCHITECTURE.md +200 -0
  9. package/docs/HOST_CAPABILITY_MODEL.md +317 -0
  10. package/docs/PLUGIN_ARCHITECTURE.md +145 -0
  11. package/docs/PLUGIN_COMPATIBILITY.md +61 -0
  12. package/docs/PLUGIN_COMPLIANCE_CHECKS.md +82 -0
  13. package/docs/PLUGIN_MANIFEST.md +94 -0
  14. package/docs/css/style.css +465 -0
  15. package/docs/index.html +218 -0
  16. package/docs/js/app.mjs +751 -0
  17. package/docs/js/editor-panel.mjs +203 -0
  18. package/docs/js/flow-canvas.mjs +515 -0
  19. package/docs/js/flow-model.mjs +391 -0
  20. package/docs/js/workers/emception.worker.js +146 -0
  21. package/docs/js/workers/pyodide.worker.js +134 -0
  22. package/native/flow_source_generator.cpp +1958 -0
  23. package/package.json +67 -0
  24. package/schemas/FlowRuntimeAbi.fbs +91 -0
  25. package/src/auth/canonicalize.js +5 -0
  26. package/src/auth/index.js +11 -0
  27. package/src/auth/permissions.js +8 -0
  28. package/src/compiler/CppFlowSourceGenerator.js +475 -0
  29. package/src/compiler/EmceptionCompilerAdapter.js +244 -0
  30. package/src/compiler/SignedArtifactCatalog.js +152 -0
  31. package/src/compiler/index.js +8 -0
  32. package/src/compiler/nativeFlowSourceGeneratorTool.js +144 -0
  33. package/src/compliance/index.js +13 -0
  34. package/src/compliance/pluginCompliance.js +11 -0
  35. package/src/deploy/FlowDeploymentClient.js +532 -0
  36. package/src/deploy/index.js +8 -0
  37. package/src/designer/FlowDesignerSession.js +158 -0
  38. package/src/designer/index.js +2 -0
  39. package/src/designer/requirements.js +184 -0
  40. package/src/generated/runtimeAbiLayouts.js +544 -0
  41. package/src/host/appHost.js +105 -0
  42. package/src/host/autoHost.js +113 -0
  43. package/src/host/browserHostAdapters.js +108 -0
  44. package/src/host/compiledFlowRuntimeHost.js +703 -0
  45. package/src/host/constants.js +55 -0
  46. package/src/host/dependencyRuntime.js +227 -0
  47. package/src/host/descriptorAbi.js +351 -0
  48. package/src/host/fetchService.js +237 -0
  49. package/src/host/httpHostAdapters.js +280 -0
  50. package/src/host/index.js +91 -0
  51. package/src/host/installedFlowHost.js +885 -0
  52. package/src/host/invocationAbi.js +440 -0
  53. package/src/host/normalize.js +372 -0
  54. package/src/host/packageManagers.js +369 -0
  55. package/src/host/profile.js +134 -0
  56. package/src/host/runtimeAbi.js +106 -0
  57. package/src/host/workspace.js +895 -0
  58. package/src/index.js +8 -0
  59. package/src/runtime/FlowRuntime.js +273 -0
  60. package/src/runtime/MethodRegistry.js +295 -0
  61. package/src/runtime/constants.js +44 -0
  62. package/src/runtime/index.js +19 -0
  63. package/src/runtime/normalize.js +377 -0
  64. package/src/transport/index.js +7 -0
  65. package/src/transport/pki.js +7 -0
  66. package/src/utils/crypto.js +7 -0
  67. package/src/utils/encoding.js +65 -0
  68. package/src/utils/wasmCrypto.js +69 -0
  69. package/tools/run-plugin-compliance-check.mjs +153 -0
@@ -0,0 +1,885 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ import { summarizeProgramRequirements } from "../designer/requirements.js";
6
+ import { findManifestFiles } from "../compliance/index.js";
7
+ import {
8
+ FlowRuntime,
9
+ MethodRegistry,
10
+ TriggerKind,
11
+ normalizeFrame,
12
+ normalizeManifest,
13
+ normalizeProgram,
14
+ } from "../runtime/index.js";
15
+ import {
16
+ HostedRuntimeAdapter,
17
+ HostedRuntimeAuthority,
18
+ HostedRuntimeEngine,
19
+ HostedRuntimeKind,
20
+ HostedRuntimeStartupPhase,
21
+ } from "./constants.js";
22
+ import { normalizeHostedRuntimePlan } from "./normalize.js";
23
+
24
+ function normalizeString(value, fallback = null) {
25
+ if (typeof value !== "string") {
26
+ return fallback;
27
+ }
28
+ const normalized = value.trim();
29
+ return normalized.length > 0 ? normalized : fallback;
30
+ }
31
+
32
+ function normalizeStringArray(values) {
33
+ if (!Array.isArray(values)) {
34
+ return [];
35
+ }
36
+ return values
37
+ .map((value) => normalizeString(value, null))
38
+ .filter((value) => value !== null);
39
+ }
40
+
41
+ async function fileExists(filePath) {
42
+ try {
43
+ await access(filePath);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ async function readJsonFile(filePath) {
51
+ return JSON.parse(await readFile(filePath, "utf8"));
52
+ }
53
+
54
+ function isObject(value) {
55
+ return value !== null && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
58
+ function toSortedUniqueStrings(values) {
59
+ return Array.from(new Set(normalizeStringArray(values))).sort();
60
+ }
61
+
62
+ function normalizeMetadata(value) {
63
+ return isObject(value) ? { ...value } : {};
64
+ }
65
+
66
+ function firstAcceptedTypeForTrigger(trigger) {
67
+ return Array.isArray(trigger?.acceptedTypes) && trigger.acceptedTypes.length > 0
68
+ ? trigger.acceptedTypes[0]
69
+ : null;
70
+ }
71
+
72
+ function buildBaseTriggerFrame(trigger, input = {}) {
73
+ const normalizedMetadata = normalizeMetadata(input.metadata);
74
+ return normalizeFrame({
75
+ typeRef: input.typeRef ?? firstAcceptedTypeForTrigger(trigger) ?? {},
76
+ traceId: input.traceId ?? null,
77
+ streamId: Number(input.streamId ?? 0),
78
+ sequence: Number(input.sequence ?? 0),
79
+ payload: input.payload ?? null,
80
+ metadata: {
81
+ triggerId: trigger.triggerId,
82
+ triggerKind: trigger.kind,
83
+ triggerSource: trigger.source ?? null,
84
+ ...normalizedMetadata,
85
+ },
86
+ });
87
+ }
88
+
89
+ function buildTimerTriggerFrame(trigger, input = {}) {
90
+ const firedAt = Number(input.firedAt ?? Date.now());
91
+ return buildBaseTriggerFrame(trigger, {
92
+ ...input,
93
+ traceId: input.traceId ?? `timer:${trigger.triggerId}:${firedAt}`,
94
+ sequence: input.sequence ?? firedAt,
95
+ metadata: {
96
+ firedAt,
97
+ description: trigger.description ?? null,
98
+ ...(input.metadata ?? {}),
99
+ },
100
+ });
101
+ }
102
+
103
+ function buildHttpRequestTriggerFrame(trigger, request = {}) {
104
+ const method = normalizeString(request.method, "GET");
105
+ const pathName =
106
+ normalizeString(request.path, null) ?? trigger.source ?? "/";
107
+ const requestId =
108
+ normalizeString(request.requestId, null) ??
109
+ `http:${method}:${pathName}:${Date.now()}`;
110
+ return buildBaseTriggerFrame(trigger, {
111
+ ...request,
112
+ traceId: request.traceId ?? requestId,
113
+ sequence: request.sequence ?? 1,
114
+ payload: request.payload ?? request.body ?? null,
115
+ metadata: {
116
+ requestId,
117
+ method,
118
+ path: pathName,
119
+ headers: isObject(request.headers) ? { ...request.headers } : {},
120
+ query: isObject(request.query) ? { ...request.query } : {},
121
+ description: trigger.description ?? null,
122
+ ...(request.metadata ?? {}),
123
+ },
124
+ });
125
+ }
126
+
127
+ function resolveHandlerRecord(moduleExports, manifest, pluginPackage, options = {}) {
128
+ if (isObject(pluginPackage.handlers)) {
129
+ return pluginPackage.handlers;
130
+ }
131
+ if (typeof pluginPackage.createHandlers === "function") {
132
+ return pluginPackage.createHandlers({
133
+ manifest,
134
+ pluginPackage,
135
+ context: options.context ?? null,
136
+ });
137
+ }
138
+
139
+ const defaultExport = moduleExports?.default;
140
+ if (isObject(moduleExports?.handlers)) {
141
+ return moduleExports.handlers;
142
+ }
143
+ if (typeof moduleExports?.createHandlers === "function") {
144
+ return moduleExports.createHandlers({
145
+ manifest,
146
+ pluginPackage,
147
+ context: options.context ?? null,
148
+ });
149
+ }
150
+ if (isObject(defaultExport?.handlers)) {
151
+ return defaultExport.handlers;
152
+ }
153
+ if (typeof defaultExport?.createHandlers === "function") {
154
+ return defaultExport.createHandlers({
155
+ manifest,
156
+ pluginPackage,
157
+ context: options.context ?? null,
158
+ });
159
+ }
160
+ return null;
161
+ }
162
+
163
+ async function resolvePluginModule(pluginPackage, options = {}) {
164
+ if (pluginPackage.module) {
165
+ return pluginPackage.module;
166
+ }
167
+ if (!pluginPackage.modulePath) {
168
+ return null;
169
+ }
170
+ const importModule =
171
+ options.importModule ??
172
+ (async (specifier) => import(specifier));
173
+ return importModule(pathToFileURL(pluginPackage.modulePath).href);
174
+ }
175
+
176
+ export function normalizeInstalledPluginPackage(pluginPackage = {}) {
177
+ const manifest =
178
+ pluginPackage.manifest && isObject(pluginPackage.manifest)
179
+ ? normalizeManifest(pluginPackage.manifest)
180
+ : null;
181
+ const packageRoot =
182
+ normalizeString(pluginPackage.packageRoot, null) ??
183
+ (pluginPackage.manifestPath
184
+ ? path.dirname(path.resolve(pluginPackage.manifestPath))
185
+ : null);
186
+ const rawModulePath = normalizeString(pluginPackage.modulePath, null);
187
+ const modulePath = rawModulePath
188
+ ? path.resolve(packageRoot ?? process.cwd(), rawModulePath)
189
+ : null;
190
+ const manifestPath = normalizeString(pluginPackage.manifestPath, null)
191
+ ? path.resolve(pluginPackage.manifestPath)
192
+ : null;
193
+
194
+ return {
195
+ packageId:
196
+ normalizeString(
197
+ pluginPackage.packageId ??
198
+ pluginPackage.packageName ??
199
+ pluginPackage.pluginId ??
200
+ manifest?.pluginId,
201
+ null,
202
+ ) ?? "plugin-package",
203
+ packageName: normalizeString(
204
+ pluginPackage.packageName ?? pluginPackage.packageJson?.name,
205
+ null,
206
+ ),
207
+ pluginId: normalizeString(pluginPackage.pluginId ?? manifest?.pluginId, ""),
208
+ packageRoot: packageRoot ? path.resolve(packageRoot) : null,
209
+ manifestPath,
210
+ modulePath,
211
+ runtimeTargets: toSortedUniqueStrings(
212
+ pluginPackage.runtimeTargets ?? manifest?.runtimeTargets,
213
+ ),
214
+ capabilities: toSortedUniqueStrings(
215
+ pluginPackage.capabilities ?? manifest?.capabilities,
216
+ ),
217
+ startupPhase:
218
+ normalizeString(
219
+ pluginPackage.startupPhase ?? pluginPackage.startup_phase,
220
+ null,
221
+ ) ?? HostedRuntimeStartupPhase.ON_DEMAND,
222
+ autoStart: Boolean(pluginPackage.autoStart ?? pluginPackage.auto_start),
223
+ manifest,
224
+ module: pluginPackage.module ?? null,
225
+ handlers: pluginPackage.handlers ?? null,
226
+ metadata: normalizeMetadata(pluginPackage.metadata),
227
+ };
228
+ }
229
+
230
+ export async function discoverInstalledPluginPackages(options = {}) {
231
+ const rootDirectories = toSortedUniqueStrings(
232
+ options.rootDirectories ??
233
+ options.pluginRootDirectories ??
234
+ options.directories,
235
+ );
236
+ const moduleCandidates = Array.isArray(options.moduleCandidates)
237
+ ? options.moduleCandidates
238
+ : ["plugin.js", "index.js", "mod.js"];
239
+ const packages = [];
240
+
241
+ for (const rootDirectory of rootDirectories) {
242
+ const manifestPaths = await findManifestFiles(rootDirectory);
243
+ for (const manifestPath of manifestPaths) {
244
+ const packageRoot = path.dirname(manifestPath);
245
+ const manifest = await readJsonFile(manifestPath);
246
+ const packageJsonPath = path.join(packageRoot, "package.json");
247
+ const packageJson = (await fileExists(packageJsonPath))
248
+ ? await readJsonFile(packageJsonPath)
249
+ : null;
250
+
251
+ let modulePath = null;
252
+ const packageEntrypoints = [
253
+ packageJson?.module,
254
+ packageJson?.main,
255
+ ...moduleCandidates,
256
+ ]
257
+ .map((value) => normalizeString(value, null))
258
+ .filter(Boolean);
259
+
260
+ for (const candidate of packageEntrypoints) {
261
+ const resolved = path.resolve(packageRoot, candidate);
262
+ if (await fileExists(resolved)) {
263
+ modulePath = resolved;
264
+ break;
265
+ }
266
+ }
267
+
268
+ packages.push(
269
+ normalizeInstalledPluginPackage({
270
+ packageName: packageJson?.name ?? manifest.pluginId,
271
+ pluginId: manifest.pluginId,
272
+ packageRoot,
273
+ manifestPath,
274
+ modulePath,
275
+ manifest,
276
+ metadata: {
277
+ packageJson,
278
+ },
279
+ }),
280
+ );
281
+ }
282
+ }
283
+
284
+ packages.sort((left, right) =>
285
+ `${left.packageName ?? ""}:${left.pluginId}`.localeCompare(
286
+ `${right.packageName ?? ""}:${right.pluginId}`,
287
+ ),
288
+ );
289
+ return packages;
290
+ }
291
+
292
+ export async function loadInstalledPluginPackage(pluginPackage, options = {}) {
293
+ const normalizedPackage = normalizeInstalledPluginPackage(pluginPackage);
294
+ let manifest = normalizedPackage.manifest;
295
+ if (!manifest && normalizedPackage.manifestPath) {
296
+ manifest = normalizeManifest(await readJsonFile(normalizedPackage.manifestPath));
297
+ }
298
+
299
+ const moduleExports = await resolvePluginModule(normalizedPackage, options);
300
+ if (!manifest && isObject(moduleExports?.manifest)) {
301
+ manifest = normalizeManifest(moduleExports.manifest);
302
+ } else if (!manifest && isObject(moduleExports?.default?.manifest)) {
303
+ manifest = normalizeManifest(moduleExports.default.manifest);
304
+ }
305
+
306
+ if (!manifest?.pluginId) {
307
+ throw new Error(
308
+ `Installed plugin package "${normalizedPackage.packageId}" could not resolve a plugin manifest.`,
309
+ );
310
+ }
311
+
312
+ const resolvedHandlers = await resolveHandlerRecord(
313
+ moduleExports,
314
+ manifest,
315
+ normalizedPackage,
316
+ options,
317
+ );
318
+ if (!isObject(resolvedHandlers)) {
319
+ throw new Error(
320
+ `Installed plugin package "${manifest.pluginId}" did not expose handlers or createHandlers().`,
321
+ );
322
+ }
323
+
324
+ return {
325
+ pluginPackage: {
326
+ ...normalizedPackage,
327
+ pluginId: manifest.pluginId,
328
+ runtimeTargets: toSortedUniqueStrings(
329
+ normalizedPackage.runtimeTargets.length > 0
330
+ ? normalizedPackage.runtimeTargets
331
+ : manifest.runtimeTargets,
332
+ ),
333
+ capabilities: toSortedUniqueStrings(
334
+ normalizedPackage.capabilities.length > 0
335
+ ? normalizedPackage.capabilities
336
+ : manifest.capabilities,
337
+ ),
338
+ manifest,
339
+ module: moduleExports,
340
+ handlers: resolvedHandlers,
341
+ },
342
+ manifest,
343
+ handlers: resolvedHandlers,
344
+ module: moduleExports,
345
+ };
346
+ }
347
+
348
+ export async function registerInstalledPluginPackage({
349
+ registry,
350
+ pluginPackage,
351
+ importModule,
352
+ context = null,
353
+ } = {}) {
354
+ if (!(registry instanceof MethodRegistry)) {
355
+ throw new TypeError(
356
+ "registerInstalledPluginPackage requires a MethodRegistry instance.",
357
+ );
358
+ }
359
+
360
+ const loaded = await loadInstalledPluginPackage(pluginPackage, {
361
+ importModule,
362
+ context,
363
+ });
364
+ registry.registerPlugin({
365
+ manifest: loaded.manifest,
366
+ handlers: loaded.handlers,
367
+ plugin: loaded.module ?? loaded.pluginPackage,
368
+ });
369
+ return loaded;
370
+ }
371
+
372
+ export async function registerInstalledPluginPackages({
373
+ registry,
374
+ pluginPackages = [],
375
+ importModule,
376
+ context = null,
377
+ } = {}) {
378
+ const loadedPackages = [];
379
+ for (const pluginPackage of Array.isArray(pluginPackages) ? pluginPackages : []) {
380
+ loadedPackages.push(
381
+ await registerInstalledPluginPackage({
382
+ registry,
383
+ pluginPackage,
384
+ importModule,
385
+ context,
386
+ }),
387
+ );
388
+ }
389
+ return loadedPackages;
390
+ }
391
+
392
+ export function createInstalledFlowHostedRuntimePlan(options = {}) {
393
+ const program = normalizeProgram(options.program ?? {});
394
+ const requirements = summarizeProgramRequirements({
395
+ program,
396
+ manifests: options.manifests ?? [],
397
+ registry: options.registry ?? null,
398
+ });
399
+
400
+ return normalizeHostedRuntimePlan({
401
+ hostId: options.hostId ?? "sdn-js-local",
402
+ hostKind: options.hostKind ?? "sdn-js",
403
+ adapter: options.adapter ?? HostedRuntimeAdapter.SDN_JS,
404
+ engine: options.engine ?? HostedRuntimeEngine.DENO,
405
+ description:
406
+ options.description ??
407
+ `Auto-start flow host for ${program.programId ?? "flow-program"}.`,
408
+ runtimes: [
409
+ {
410
+ runtimeId:
411
+ options.runtimeId ??
412
+ `${program.programId ?? "flow-program"}:runtime`,
413
+ kind: HostedRuntimeKind.FLOW,
414
+ programId: program.programId ?? null,
415
+ description: options.runtimeDescription ?? program.description ?? null,
416
+ execution: options.execution ?? "compiled-wasm",
417
+ authority: options.authority ?? HostedRuntimeAuthority.LOCAL,
418
+ startupPhase:
419
+ options.startupPhase ?? HostedRuntimeStartupPhase.SESSION,
420
+ autoStart: options.autoStart ?? true,
421
+ dependsOn: options.dependsOn ?? [],
422
+ requiredCapabilities:
423
+ options.requiredCapabilities ?? requirements.capabilities,
424
+ bindings: options.bindings ?? [],
425
+ },
426
+ ],
427
+ });
428
+ }
429
+
430
+ export function createInstalledFlowHost(options = {}) {
431
+ const registry = options.registry ?? new MethodRegistry();
432
+ const sinkEvents = [];
433
+ const userOnSinkOutput =
434
+ options.onSinkOutput ?? options.runtimeOptions?.onSinkOutput ?? null;
435
+ const runtime =
436
+ options.runtime ??
437
+ new FlowRuntime({
438
+ registry,
439
+ ...(options.runtimeOptions ?? {}),
440
+ onSinkOutput(event) {
441
+ const sinkEvent = {
442
+ index: sinkEvents.length,
443
+ ...event,
444
+ };
445
+ sinkEvents.push(sinkEvent);
446
+ if (typeof userOnSinkOutput === "function") {
447
+ userOnSinkOutput(sinkEvent);
448
+ }
449
+ },
450
+ });
451
+
452
+ let started = false;
453
+ let discoveredPackages = [];
454
+ let loadedPackages = [];
455
+ let program =
456
+ options.program !== undefined && options.program !== null
457
+ ? normalizeProgram(options.program)
458
+ : null;
459
+
460
+ function dedupePackages(pluginPackages) {
461
+ const packagesByPluginId = new Map();
462
+ for (const pluginPackage of pluginPackages) {
463
+ const normalizedPackage = normalizeInstalledPluginPackage(pluginPackage);
464
+ const key =
465
+ normalizedPackage.pluginId ||
466
+ normalizedPackage.manifest?.pluginId ||
467
+ normalizedPackage.packageId;
468
+ packagesByPluginId.set(key, normalizedPackage);
469
+ }
470
+ return Array.from(packagesByPluginId.values());
471
+ }
472
+
473
+ function collectRequiredPluginIds(programValue = program) {
474
+ if (!programValue) {
475
+ return null;
476
+ }
477
+ const requiredPluginIds = new Set(
478
+ normalizeStringArray(programValue.requiredPlugins),
479
+ );
480
+ for (const node of programValue.nodes ?? []) {
481
+ if (node.pluginId) {
482
+ requiredPluginIds.add(node.pluginId);
483
+ }
484
+ }
485
+ return requiredPluginIds;
486
+ }
487
+
488
+ function isLoadablePluginPackage(pluginPackage) {
489
+ return Boolean(
490
+ pluginPackage.handlers ||
491
+ pluginPackage.createHandlers ||
492
+ pluginPackage.module ||
493
+ pluginPackage.modulePath,
494
+ );
495
+ }
496
+
497
+ function buildStartupSummary() {
498
+ return {
499
+ started,
500
+ programId: runtime.getProgram()?.programId ?? program?.programId ?? null,
501
+ discoveredPackages: discoveredPackages.map((item) => item.pluginId),
502
+ registeredPluginIds: registry.listPlugins().map((item) => item.pluginId),
503
+ };
504
+ }
505
+
506
+ async function resolveSelectedPluginPackages(refreshOptions = {}) {
507
+ const nextProgram =
508
+ refreshOptions.program !== undefined && refreshOptions.program !== null
509
+ ? normalizeProgram(refreshOptions.program)
510
+ : program;
511
+ const explicitPackages = Array.isArray(refreshOptions.pluginPackages)
512
+ ? refreshOptions.pluginPackages
513
+ : Array.isArray(options.pluginPackages)
514
+ ? options.pluginPackages
515
+ : [];
516
+ const shouldDiscover =
517
+ refreshOptions.discover ?? options.discover ?? true;
518
+ const nextDiscoveredPackages =
519
+ shouldDiscover === false
520
+ ? []
521
+ : await discoverInstalledPluginPackages({
522
+ rootDirectories:
523
+ refreshOptions.pluginRootDirectories ??
524
+ refreshOptions.rootDirectories ??
525
+ options.pluginRootDirectories ??
526
+ options.rootDirectories ??
527
+ [],
528
+ moduleCandidates:
529
+ refreshOptions.moduleCandidates ?? options.moduleCandidates,
530
+ });
531
+ const allPackages = dedupePackages([
532
+ ...nextDiscoveredPackages,
533
+ ...explicitPackages,
534
+ ]);
535
+ const requiredPluginIds = collectRequiredPluginIds(nextProgram);
536
+ const selectedPackages = allPackages.filter((pluginPackage) => {
537
+ if (requiredPluginIds && requiredPluginIds.size > 0) {
538
+ return requiredPluginIds.has(pluginPackage.pluginId);
539
+ }
540
+ return isLoadablePluginPackage(pluginPackage);
541
+ });
542
+
543
+ return {
544
+ nextProgram,
545
+ nextDiscoveredPackages,
546
+ selectedPackages,
547
+ };
548
+ }
549
+
550
+ async function refreshPlugins(refreshOptions = {}) {
551
+ const {
552
+ nextProgram,
553
+ nextDiscoveredPackages,
554
+ selectedPackages,
555
+ } = await resolveSelectedPluginPackages(refreshOptions);
556
+ const importModule = refreshOptions.importModule ?? options.importModule;
557
+ const context =
558
+ refreshOptions.context !== undefined
559
+ ? refreshOptions.context
560
+ : options.context ?? null;
561
+ const nextLoadedPackages = [];
562
+
563
+ for (const pluginPackage of selectedPackages) {
564
+ nextLoadedPackages.push(
565
+ await loadInstalledPluginPackage(pluginPackage, {
566
+ importModule,
567
+ context,
568
+ }),
569
+ );
570
+ }
571
+
572
+ const managedPluginIds = new Set(
573
+ loadedPackages.map(
574
+ (item) => item.manifest?.pluginId ?? item.pluginPackage?.pluginId,
575
+ ),
576
+ );
577
+ for (const loaded of nextLoadedPackages) {
578
+ const pluginId = loaded.manifest.pluginId;
579
+ if (!managedPluginIds.has(pluginId) && registry.getPlugin(pluginId)) {
580
+ throw new Error(
581
+ `Installed flow host cannot refresh plugin "${pluginId}" because the registry already contains an externally managed plugin with the same id.`,
582
+ );
583
+ }
584
+ }
585
+
586
+ const validationRegistry = new MethodRegistry();
587
+ for (const loaded of nextLoadedPackages) {
588
+ validationRegistry.registerPlugin({
589
+ manifest: loaded.manifest,
590
+ handlers: loaded.handlers,
591
+ plugin: loaded.module ?? loaded.pluginPackage,
592
+ });
593
+ }
594
+
595
+ for (const pluginId of managedPluginIds) {
596
+ registry.unregisterPlugin(pluginId);
597
+ }
598
+ for (const loaded of nextLoadedPackages) {
599
+ registry.registerPlugin({
600
+ manifest: loaded.manifest,
601
+ handlers: loaded.handlers,
602
+ plugin: loaded.module ?? loaded.pluginPackage,
603
+ });
604
+ }
605
+
606
+ discoveredPackages = nextDiscoveredPackages;
607
+ loadedPackages = nextLoadedPackages;
608
+ program = nextProgram;
609
+ if (program) {
610
+ runtime.loadProgram(program);
611
+ }
612
+ if (refreshOptions.clearSinkOutputs === true) {
613
+ sinkEvents.splice(0, sinkEvents.length);
614
+ }
615
+ started = true;
616
+
617
+ return buildStartupSummary();
618
+ }
619
+
620
+ async function start() {
621
+ if (!started) {
622
+ await refreshPlugins();
623
+ }
624
+ return buildStartupSummary();
625
+ }
626
+
627
+ return {
628
+ registry,
629
+ runtime,
630
+ getProgram() {
631
+ return runtime.getProgram() ?? program;
632
+ },
633
+ getLoadedPackages() {
634
+ return loadedPackages.map((item) => item.pluginPackage);
635
+ },
636
+ getSinkEventCount() {
637
+ return sinkEvents.length;
638
+ },
639
+ getSinkOutputsSince(index = 0) {
640
+ return sinkEvents.slice(Math.max(0, Number(index) || 0));
641
+ },
642
+ clearSinkOutputs() {
643
+ sinkEvents.splice(0, sinkEvents.length);
644
+ },
645
+ async start() {
646
+ return start();
647
+ },
648
+ async refreshPlugins(refreshOptions = {}) {
649
+ return refreshPlugins(refreshOptions);
650
+ },
651
+ loadProgram(nextProgram) {
652
+ program = normalizeProgram(nextProgram);
653
+ if (started) {
654
+ return runtime.loadProgram(program);
655
+ }
656
+ return program;
657
+ },
658
+ enqueueTriggerFrames(triggerId, frames) {
659
+ return runtime.enqueueTriggerFrames(triggerId, frames);
660
+ },
661
+ enqueueNodeFrames(nodeId, portId, frames, backpressurePolicy, queueDepth) {
662
+ return runtime.enqueueNodeFrames(
663
+ nodeId,
664
+ portId,
665
+ frames,
666
+ backpressurePolicy,
667
+ queueDepth,
668
+ );
669
+ },
670
+ drain(options) {
671
+ return runtime.drain(options);
672
+ },
673
+ isIdle() {
674
+ return runtime.isIdle();
675
+ },
676
+ inspectQueues() {
677
+ return runtime.inspectQueues();
678
+ },
679
+ };
680
+ }
681
+
682
+ export function createInstalledFlowService(options = {}) {
683
+ const host = options.host ?? createInstalledFlowHost(options);
684
+ const timerHandles = new Map();
685
+ const setIntervalFn =
686
+ options.setIntervalFn ??
687
+ globalThis.setInterval?.bind(globalThis) ??
688
+ null;
689
+ const clearIntervalFn =
690
+ options.clearIntervalFn ??
691
+ globalThis.clearInterval?.bind(globalThis) ??
692
+ null;
693
+ const nowFn = options.nowFn ?? Date.now;
694
+ const onError = options.onError ?? null;
695
+ let started = false;
696
+
697
+ function getProgram() {
698
+ const program = host.getProgram();
699
+ if (!program) {
700
+ throw new Error("Installed flow service has no loaded program.");
701
+ }
702
+ return program;
703
+ }
704
+
705
+ function listTriggersByKind(kind) {
706
+ return getProgram().triggers.filter((trigger) => trigger.kind === kind);
707
+ }
708
+
709
+ function resolveHttpTrigger(request = {}) {
710
+ const program = getProgram();
711
+ const requestedTriggerId = normalizeString(request.triggerId, null);
712
+ const requestedPath = normalizeString(request.path, null);
713
+ const trigger = program.triggers.find((candidate) => {
714
+ if (candidate.kind !== TriggerKind.HTTP_REQUEST) {
715
+ return false;
716
+ }
717
+ if (requestedTriggerId) {
718
+ return candidate.triggerId === requestedTriggerId;
719
+ }
720
+ return normalizeString(candidate.source, null) === requestedPath;
721
+ });
722
+ if (!trigger) {
723
+ throw new Error(
724
+ `No HTTP trigger matches ${requestedTriggerId ?? requestedPath ?? "<unknown>"}.`,
725
+ );
726
+ }
727
+ return trigger;
728
+ }
729
+
730
+ async function dispatchTriggerFrames(triggerId, frames) {
731
+ await host.start();
732
+ const startIndex = host.getSinkEventCount();
733
+ host.enqueueTriggerFrames(triggerId, frames);
734
+ const drainResult = await host.drain(options.drainOptions);
735
+ return {
736
+ triggerId,
737
+ outputs: host.getSinkOutputsSince(startIndex),
738
+ ...drainResult,
739
+ };
740
+ }
741
+
742
+ async function dispatchTimerTrigger(triggerId, input = {}) {
743
+ const trigger = listTriggersByKind(TriggerKind.TIMER).find(
744
+ (candidate) => candidate.triggerId === triggerId,
745
+ );
746
+ if (!trigger) {
747
+ throw new Error(`Unknown timer trigger "${triggerId}".`);
748
+ }
749
+ return dispatchTriggerFrames(triggerId, [
750
+ buildTimerTriggerFrame(trigger, {
751
+ ...input,
752
+ firedAt: input.firedAt ?? nowFn(),
753
+ }),
754
+ ]);
755
+ }
756
+
757
+ async function handleHttpRequest(request = {}) {
758
+ const trigger = resolveHttpTrigger(request);
759
+ const response = await dispatchTriggerFrames(trigger.triggerId, [
760
+ buildHttpRequestTriggerFrame(trigger, request),
761
+ ]);
762
+ return {
763
+ triggerId: trigger.triggerId,
764
+ route: trigger.source ?? null,
765
+ ...response,
766
+ };
767
+ }
768
+
769
+ function listTimerTriggers() {
770
+ return listTriggersByKind(TriggerKind.TIMER).map((trigger) => ({
771
+ triggerId: trigger.triggerId,
772
+ source: trigger.source,
773
+ defaultIntervalMs: trigger.defaultIntervalMs,
774
+ description: trigger.description,
775
+ active: timerHandles.has(trigger.triggerId),
776
+ }));
777
+ }
778
+
779
+ function listHttpRoutes() {
780
+ return listTriggersByKind(TriggerKind.HTTP_REQUEST).map((trigger) => ({
781
+ triggerId: trigger.triggerId,
782
+ path: trigger.source ?? null,
783
+ description: trigger.description,
784
+ }));
785
+ }
786
+
787
+ function startTimerServices() {
788
+ if (setIntervalFn === null || clearIntervalFn === null) {
789
+ return;
790
+ }
791
+ for (const trigger of listTriggersByKind(TriggerKind.TIMER)) {
792
+ if (timerHandles.has(trigger.triggerId)) {
793
+ continue;
794
+ }
795
+ const intervalMs = Number(trigger.defaultIntervalMs ?? 0);
796
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
797
+ continue;
798
+ }
799
+ const normalizedIntervalMs = Math.max(1, Math.trunc(intervalMs));
800
+ const handle = setIntervalFn(() => {
801
+ void dispatchTimerTrigger(trigger.triggerId).catch((error) => {
802
+ if (typeof onError === "function") {
803
+ onError(error, {
804
+ source: "timer",
805
+ triggerId: trigger.triggerId,
806
+ });
807
+ }
808
+ });
809
+ }, normalizedIntervalMs);
810
+ timerHandles.set(trigger.triggerId, {
811
+ handle,
812
+ intervalMs: normalizedIntervalMs,
813
+ });
814
+ }
815
+ }
816
+
817
+ function stopTimerServices() {
818
+ if (clearIntervalFn === null) {
819
+ timerHandles.clear();
820
+ return;
821
+ }
822
+ for (const { handle } of timerHandles.values()) {
823
+ clearIntervalFn(handle);
824
+ }
825
+ timerHandles.clear();
826
+ }
827
+
828
+ return {
829
+ host,
830
+ async start() {
831
+ const startup = await host.start();
832
+ if (!started) {
833
+ if (options.autoStartTimers !== false) {
834
+ startTimerServices();
835
+ }
836
+ started = true;
837
+ }
838
+ return {
839
+ ...startup,
840
+ timerTriggers: listTimerTriggers(),
841
+ httpRoutes: listHttpRoutes(),
842
+ };
843
+ },
844
+ async refresh(refreshOptions = {}) {
845
+ const restartTimers = started && options.autoStartTimers !== false;
846
+ stopTimerServices();
847
+ const refreshResult = await host.refreshPlugins(refreshOptions);
848
+ if (restartTimers) {
849
+ startTimerServices();
850
+ }
851
+ return {
852
+ ...refreshResult,
853
+ timerTriggers: listTimerTriggers(),
854
+ httpRoutes: listHttpRoutes(),
855
+ };
856
+ },
857
+ stop() {
858
+ stopTimerServices();
859
+ started = false;
860
+ },
861
+ dispatchTriggerFrames,
862
+ dispatchTimerTrigger,
863
+ handleHttpRequest,
864
+ listTimerTriggers,
865
+ listHttpRoutes,
866
+ getServiceSummary() {
867
+ return {
868
+ started,
869
+ timerTriggers: listTimerTriggers(),
870
+ httpRoutes: listHttpRoutes(),
871
+ };
872
+ },
873
+ };
874
+ }
875
+
876
+ export default {
877
+ createInstalledFlowHost,
878
+ createInstalledFlowService,
879
+ createInstalledFlowHostedRuntimePlan,
880
+ discoverInstalledPluginPackages,
881
+ loadInstalledPluginPackage,
882
+ normalizeInstalledPluginPackage,
883
+ registerInstalledPluginPackage,
884
+ registerInstalledPluginPackages,
885
+ };