@pikku/core 0.12.146 → 0.12.148
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/contributors/contributors.types.d.ts +77 -0
- package/dist/contributors/contributors.types.js +1 -0
- package/dist/contributors/define-contributors.d.ts +7 -0
- package/dist/contributors/define-contributors.js +6 -0
- package/dist/contributors/index.d.ts +4 -0
- package/dist/contributors/index.js +2 -0
- package/dist/contributors/resolve-contributors.d.ts +51 -0
- package/dist/contributors/resolve-contributors.js +158 -0
- package/dist/function/function-runner.js +7 -0
- package/dist/function/functions.types.d.ts +8 -0
- package/dist/lifecycle.d.ts +19 -0
- package/dist/lifecycle.js +54 -0
- package/dist/types/state.types.d.ts +5 -4
- package/dist/wirings/addon/addon-reach.d.ts +10 -0
- package/dist/wirings/addon/addon-reach.js +29 -0
- package/dist/wirings/addon/addon-runner.js +11 -0
- package/dist/wirings/addon/define-plugin.d.ts +62 -0
- package/dist/wirings/addon/define-plugin.js +9 -0
- package/dist/wirings/addon/index.d.ts +2 -2
- package/dist/wirings/addon/index.js +1 -1
- package/dist/wirings/addon/wire-addon.d.ts +17 -7
- package/dist/wirings/agent/agent-rpc.js +6 -3
- package/dist/wirings/rpc/rpc-runner.d.ts +2 -3
- package/dist/wirings/rpc/rpc-runner.js +3 -22
- package/dist/wirings/workflow/graph/graph-runner.js +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +3 -4
- package/dist/wirings/workflow/workflow-compensation.js +2 -2
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +5 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +5 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +2 -2
- package/package.json +4 -2
- package/dist/wirings/addon/define-screens.d.ts +0 -34
- package/dist/wirings/addon/define-screens.js +0 -5
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a target gets a contributor's bindings from. `env` is plain
|
|
3
|
+
* environment variables, which every target has; `cloudflare` is Cloudflare
|
|
4
|
+
* worker bindings, which only the Cloudflare deploy provides.
|
|
5
|
+
*/
|
|
6
|
+
export type ContributorBindingSource = 'env' | 'cloudflare';
|
|
7
|
+
/** The database the generated app uses, as contributors see it at build time. */
|
|
8
|
+
export type ContributorDb = {
|
|
9
|
+
/** The SQL engine the app's database runs on. MySQL is not represented. */
|
|
10
|
+
engine: 'sqlite' | 'postgres';
|
|
11
|
+
/** Column coercion map from `pikku db generate`, keyed by table then column. */
|
|
12
|
+
coercionMap?: Record<string, Record<string, 'date' | 'bool' | 'json'>>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build-time facts about the app being generated. Handed to `when` while the
|
|
16
|
+
* generator selects contributors and, as plain data, to `run`.
|
|
17
|
+
*/
|
|
18
|
+
export interface ContributorPlatform {
|
|
19
|
+
/** Service names the app (dev) or the deploy unit requires. */
|
|
20
|
+
serviceNames: string[];
|
|
21
|
+
/** True when the app uses the queue, so a contributor can provide or skip a queue service. */
|
|
22
|
+
needsQueue: boolean;
|
|
23
|
+
/** True when the app uses workflows, so a contributor can provide or skip workflow services. */
|
|
24
|
+
needsWorkflow: boolean;
|
|
25
|
+
/** True when the app uses AI agents, so a contributor can provide or skip agent services. */
|
|
26
|
+
needsAgent: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Baseline services the generated entry already holds before any
|
|
29
|
+
* contributor runs. They satisfy a contributor's `needs`, are read as
|
|
30
|
+
* `runtime.<name>`, and a contributor may provide the same key to replace
|
|
31
|
+
* them. Undefined means none.
|
|
32
|
+
*/
|
|
33
|
+
runtimeServices?: string[];
|
|
34
|
+
/** The app's database, when it uses SQLite or Postgres. Unset otherwise. */
|
|
35
|
+
db?: ContributorDb;
|
|
36
|
+
}
|
|
37
|
+
export interface Contributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
38
|
+
/**
|
|
39
|
+
* Service keys `run` sets on `services`. May be empty for a contributor that
|
|
40
|
+
* only decorates services another contributor provides.
|
|
41
|
+
*/
|
|
42
|
+
provides?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Service keys this contributor reads, from `runtime` or from `services`
|
|
45
|
+
* set by another contributor. A dotted key (`logSources.stage`) is met by a
|
|
46
|
+
* contributor providing it exactly or providing a prefix of it
|
|
47
|
+
* (`logSources`). Contributors that provide a need run first.
|
|
48
|
+
*/
|
|
49
|
+
needs?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* Binding sources the target must be able to supply for this contributor to
|
|
52
|
+
* apply. Defaults to `['env']`. A contributor the project opted in to whose
|
|
53
|
+
* `requires` the target cannot meet is a build error.
|
|
54
|
+
*/
|
|
55
|
+
requires?: ContributorBindingSource[];
|
|
56
|
+
/** Build-time only. A contributor returning false is left out entirely. */
|
|
57
|
+
when?(platform: TPlatform): boolean;
|
|
58
|
+
/** Constructs services. Runtime `env` guards belong here, not in `when`. */
|
|
59
|
+
run(services: TServices, env: Record<string, string | undefined>, runtime: Record<string, any>, platform: TPlatform): void | Promise<void>;
|
|
60
|
+
/** Runtime setup, called where the server lifecycle runs. */
|
|
61
|
+
onServerStart?(services: TServices): void | Promise<void>;
|
|
62
|
+
/** Teardown, called in reverse `needs` order where the server lifecycle runs. */
|
|
63
|
+
onServerStop?(services: TServices): void | Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** The per-target forms of one named contributor: a `dev` variant and a `deploy` variant. */
|
|
66
|
+
export interface ContributorVariants<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
67
|
+
/** The contributor `pikku dev` applies. Omit it to leave this contributor out of dev. */
|
|
68
|
+
dev?: Contributor<TServices, TPlatform>;
|
|
69
|
+
/** The contributor a deploy build applies. Omit it to leave this contributor out of deploys. */
|
|
70
|
+
deploy?: Contributor<TServices, TPlatform>;
|
|
71
|
+
}
|
|
72
|
+
/** The build a contributor is selected for: `pikku dev`, or a deploy. */
|
|
73
|
+
export type ContributorTarget = 'dev' | 'deploy';
|
|
74
|
+
export interface NamedContributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
|
|
75
|
+
name: string;
|
|
76
|
+
contributor: Contributor<TServices, TPlatform>;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContributorPlatform, ContributorVariants } from './contributors.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Registers an addon's contributors by name. A project opts in to a name with
|
|
4
|
+
* `wireAddon({ contributors: ['name'] })`; only the selected variant for the
|
|
5
|
+
* build target is bundled.
|
|
6
|
+
*/
|
|
7
|
+
export declare const defineContributors: <const T extends Record<string, ContributorVariants<any, ContributorPlatform>>>(contributors: T) => T;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineContributors } from './define-contributors.js';
|
|
2
|
+
export { ContributorDependencyError, DEFAULT_CONTRIBUTOR_BINDING_SOURCES, orderContributors, resolveContributors, selectContributors, } from './resolve-contributors.js';
|
|
3
|
+
export type { SkippedContributor } from './resolve-contributors.js';
|
|
4
|
+
export type { Contributor, ContributorBindingSource, ContributorDb, ContributorPlatform, ContributorTarget, ContributorVariants, NamedContributor, } from './contributors.types.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ContributorBindingSource, ContributorPlatform, NamedContributor } from './contributors.types.js';
|
|
2
|
+
export declare class ContributorDependencyError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const DEFAULT_CONTRIBUTOR_BINDING_SOURCES: readonly ContributorBindingSource[];
|
|
6
|
+
export interface SkippedContributor {
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* `when` returned false; the target cannot supply its `requires` (checked
|
|
10
|
+
* only once `when` passed, so this one is a contributor the build wanted);
|
|
11
|
+
* or it was `optional` and nothing meets a need of it.
|
|
12
|
+
*/
|
|
13
|
+
reason: 'when' | 'requires' | 'needs';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Drops contributors whose `when` is false or whose `requires` the target
|
|
17
|
+
* cannot supply. The clash and ordering checks run on what is left, so an
|
|
18
|
+
* exclusive pair of `when`s never clashes.
|
|
19
|
+
*/
|
|
20
|
+
export declare const selectContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
|
|
21
|
+
platform: ContributorPlatform;
|
|
22
|
+
supported?: readonly ContributorBindingSource[];
|
|
23
|
+
}) => {
|
|
24
|
+
selected: T[];
|
|
25
|
+
skipped: SkippedContributor[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Orders contributors so each runs after every contributor providing one of
|
|
29
|
+
* its `needs`. Contributors without needs keep their input order.
|
|
30
|
+
*
|
|
31
|
+
* Throws a `ContributorDependencyError` for a repeated name, a need nothing
|
|
32
|
+
* provides, a key two contributors both provide, or a cycle. A key listed in
|
|
33
|
+
* `options.runtime` is a baseline service: it meets a need and a contributor
|
|
34
|
+
* may provide it too, replacing it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const orderContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options?: {
|
|
37
|
+
runtime?: readonly string[];
|
|
38
|
+
}) => T[];
|
|
39
|
+
/**
|
|
40
|
+
* Filters by `when`/`requires`, then checks clashes and orders what is left.
|
|
41
|
+
* Names in `optional` are dropped, rather than failing the build, when a need
|
|
42
|
+
* of theirs is met by nothing.
|
|
43
|
+
*/
|
|
44
|
+
export declare const resolveContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
|
|
45
|
+
platform: ContributorPlatform;
|
|
46
|
+
supported?: readonly ContributorBindingSource[];
|
|
47
|
+
optional?: ReadonlySet<string>;
|
|
48
|
+
}) => {
|
|
49
|
+
ordered: T[];
|
|
50
|
+
skipped: SkippedContributor[];
|
|
51
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export class ContributorDependencyError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ContributorDependencyError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export const DEFAULT_CONTRIBUTOR_BINDING_SOURCES = ['env'];
|
|
8
|
+
const satisfies = (provided, need) => provided === need || need.startsWith(`${provided}.`);
|
|
9
|
+
/**
|
|
10
|
+
* Drops contributors whose `when` is false or whose `requires` the target
|
|
11
|
+
* cannot supply. The clash and ordering checks run on what is left, so an
|
|
12
|
+
* exclusive pair of `when`s never clashes.
|
|
13
|
+
*/
|
|
14
|
+
export const selectContributors = (contributors, options) => {
|
|
15
|
+
const supported = options.supported ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
|
|
16
|
+
const selected = [];
|
|
17
|
+
const skipped = [];
|
|
18
|
+
for (const entry of contributors) {
|
|
19
|
+
const requires = entry.contributor.requires ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
|
|
20
|
+
// `when` first: a contributor that is not meant for this build is never
|
|
21
|
+
// reported as one the target failed to support.
|
|
22
|
+
if (entry.contributor.when?.(options.platform) === false) {
|
|
23
|
+
skipped.push({ name: entry.name, reason: 'when' });
|
|
24
|
+
}
|
|
25
|
+
else if (!requires.every((source) => supported.includes(source))) {
|
|
26
|
+
skipped.push({ name: entry.name, reason: 'requires' });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
selected.push(entry);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { selected, skipped };
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Orders contributors so each runs after every contributor providing one of
|
|
36
|
+
* its `needs`. Contributors without needs keep their input order.
|
|
37
|
+
*
|
|
38
|
+
* Throws a `ContributorDependencyError` for a repeated name, a need nothing
|
|
39
|
+
* provides, a key two contributors both provide, or a cycle. A key listed in
|
|
40
|
+
* `options.runtime` is a baseline service: it meets a need and a contributor
|
|
41
|
+
* may provide it too, replacing it.
|
|
42
|
+
*/
|
|
43
|
+
export const orderContributors = (contributors, options = {}) => {
|
|
44
|
+
const runtime = options.runtime ?? [];
|
|
45
|
+
const names = new Set();
|
|
46
|
+
for (const { name } of contributors) {
|
|
47
|
+
if (names.has(name)) {
|
|
48
|
+
throw new ContributorDependencyError(`Contributor '${name}' is selected more than once.`);
|
|
49
|
+
}
|
|
50
|
+
names.add(name);
|
|
51
|
+
}
|
|
52
|
+
const owners = new Map();
|
|
53
|
+
for (const entry of contributors) {
|
|
54
|
+
for (const key of entry.contributor.provides ?? []) {
|
|
55
|
+
const owner = owners.get(key);
|
|
56
|
+
if (owner && owner !== entry) {
|
|
57
|
+
throw new ContributorDependencyError(`Contributors '${owner.name}' and '${entry.name}' both provide '${key}'. Each service key has one provider.`);
|
|
58
|
+
}
|
|
59
|
+
owners.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const dependencies = new Map();
|
|
63
|
+
for (const entry of contributors) {
|
|
64
|
+
const deps = new Set();
|
|
65
|
+
// A contributor that provides what it needs (`provides: ['x'], needs: ['x']`,
|
|
66
|
+
// even for a baseline x) depends on itself, which is a cycle; a decorator
|
|
67
|
+
// declares empty `provides` and reads x from `services`/`runtime` instead.
|
|
68
|
+
for (const need of entry.contributor.needs ?? []) {
|
|
69
|
+
const providers = contributors.filter((other) => (other.contributor.provides ?? []).some((key) => satisfies(key, need)));
|
|
70
|
+
const fromRuntime = runtime.some((key) => satisfies(key, need));
|
|
71
|
+
if (providers.length === 0 && !fromRuntime) {
|
|
72
|
+
const near = contributors
|
|
73
|
+
.filter((other) => (other.contributor.provides ?? []).some((key) => key.startsWith(`${need}.`)))
|
|
74
|
+
.map((other) => other.name);
|
|
75
|
+
const hint = near.length > 0
|
|
76
|
+
? ` ${near.join(', ')} provide${near.length === 1 ? 's' : ''} keys under it, but not '${need}' itself.`
|
|
77
|
+
: runtime.length > 0
|
|
78
|
+
? ` No contributor provides it, and the runtime only has: ${runtime.join(', ')}.`
|
|
79
|
+
: ` No contributor provides it, and this runtime provides none.`;
|
|
80
|
+
throw new ContributorDependencyError(`Contributor '${entry.name}' needs '${need}', which nothing provides.${hint}`);
|
|
81
|
+
}
|
|
82
|
+
for (const provider of providers)
|
|
83
|
+
deps.add(provider);
|
|
84
|
+
}
|
|
85
|
+
dependencies.set(entry, [...deps]);
|
|
86
|
+
}
|
|
87
|
+
const ordered = [];
|
|
88
|
+
const placed = new Set();
|
|
89
|
+
while (ordered.length < contributors.length) {
|
|
90
|
+
const next = contributors.find((entry) => !placed.has(entry) &&
|
|
91
|
+
dependencies.get(entry).every((dep) => placed.has(dep)));
|
|
92
|
+
if (!next) {
|
|
93
|
+
throw new ContributorDependencyError(`Contributors depend on each other in a cycle: ${findCycle(contributors.filter((c) => !placed.has(c)), dependencies)
|
|
94
|
+
.map((c) => c.name)
|
|
95
|
+
.join(' -> ')}`);
|
|
96
|
+
}
|
|
97
|
+
placed.add(next);
|
|
98
|
+
ordered.push(next);
|
|
99
|
+
}
|
|
100
|
+
return ordered;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Drops, repeatedly, the `optional` contributors whose `needs` nothing left
|
|
104
|
+
* provides, so one that only decorates a service (a wrapper around
|
|
105
|
+
* `services.kysely`) applies when the service exists and is not an error when
|
|
106
|
+
* it does not.
|
|
107
|
+
*/
|
|
108
|
+
const dropUnmetOptional = (selected, optional, runtime) => {
|
|
109
|
+
let kept = [...selected];
|
|
110
|
+
const dropped = [];
|
|
111
|
+
for (;;) {
|
|
112
|
+
const unmet = kept.find((entry) => optional.has(entry.name) &&
|
|
113
|
+
(entry.contributor.needs ?? []).some((need) => !runtime.some((key) => satisfies(key, need)) &&
|
|
114
|
+
!kept.some((other) => other !== entry &&
|
|
115
|
+
(other.contributor.provides ?? []).some((key) => satisfies(key, need)))));
|
|
116
|
+
if (!unmet)
|
|
117
|
+
return { kept, dropped };
|
|
118
|
+
kept = kept.filter((entry) => entry !== unmet);
|
|
119
|
+
dropped.push({ name: unmet.name, reason: 'needs' });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Filters by `when`/`requires`, then checks clashes and orders what is left.
|
|
124
|
+
* Names in `optional` are dropped, rather than failing the build, when a need
|
|
125
|
+
* of theirs is met by nothing.
|
|
126
|
+
*/
|
|
127
|
+
export const resolveContributors = (contributors, options) => {
|
|
128
|
+
const { selected, skipped } = selectContributors(contributors, options);
|
|
129
|
+
const { kept, dropped } = dropUnmetOptional(selected, options.optional ?? new Set(), options.platform.runtimeServices ?? []);
|
|
130
|
+
return {
|
|
131
|
+
ordered: orderContributors(kept, {
|
|
132
|
+
runtime: options.platform.runtimeServices,
|
|
133
|
+
}),
|
|
134
|
+
skipped: [...skipped, ...dropped],
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const findCycle = (remaining, dependencies) => {
|
|
138
|
+
const pending = new Set(remaining);
|
|
139
|
+
const visit = (node, path) => {
|
|
140
|
+
const at = path.indexOf(node);
|
|
141
|
+
if (at >= 0)
|
|
142
|
+
return [...path.slice(at), node];
|
|
143
|
+
for (const dep of dependencies.get(node) ?? []) {
|
|
144
|
+
if (!pending.has(dep))
|
|
145
|
+
continue;
|
|
146
|
+
const cycle = visit(dep, [...path, node]);
|
|
147
|
+
if (cycle)
|
|
148
|
+
return cycle;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
};
|
|
152
|
+
for (const node of remaining) {
|
|
153
|
+
const cycle = visit(node, []);
|
|
154
|
+
if (cycle)
|
|
155
|
+
return cycle;
|
|
156
|
+
}
|
|
157
|
+
return remaining;
|
|
158
|
+
};
|
|
@@ -62,6 +62,13 @@ async function resolveSession(wire, singletonServices, sessionService) {
|
|
|
62
62
|
* @param packageName - The addon package it belongs to; `null` for the app.
|
|
63
63
|
*/
|
|
64
64
|
export const addFunction = (funcName, funcConfig, packageName = null) => {
|
|
65
|
+
if (funcConfig.disabled) {
|
|
66
|
+
throw new Error(`Function '${funcName}' is disabled` +
|
|
67
|
+
(typeof funcConfig.disabled === 'string'
|
|
68
|
+
? ` (${funcConfig.disabled})`
|
|
69
|
+
: '') +
|
|
70
|
+
' and cannot be registered.');
|
|
71
|
+
}
|
|
65
72
|
pikkuState(packageName, 'function', 'functions').set(funcName, funcConfig);
|
|
66
73
|
};
|
|
67
74
|
export const getFunctionNames = (packageName = null) => {
|
|
@@ -128,6 +128,14 @@ export type CorePikkuFunctionConfig<PikkuFunction extends CorePikkuFunction<any,
|
|
|
128
128
|
version?: number;
|
|
129
129
|
/** Filters this function in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
130
130
|
tags?: string[];
|
|
131
|
+
/**
|
|
132
|
+
* Switches the function off: `true`, or a string saying why. The build
|
|
133
|
+
* leaves it out of every map, client and wiring, and refuses (PKU562)
|
|
134
|
+
* anything that still references it, so marking a function believed dead
|
|
135
|
+
* proves it. The source keeps type-checking; deleting the flag turns it
|
|
136
|
+
* back on.
|
|
137
|
+
*/
|
|
138
|
+
disabled?: true | string;
|
|
131
139
|
/** Makes the function callable from outside as `POST /rpc/<name>`. Without a session requirement, a permission or an addon gate, that means callable by anyone. */
|
|
132
140
|
expose?: boolean;
|
|
133
141
|
/**
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -15,3 +15,22 @@ export declare const pikkuServerLifecycle: <SS extends CoreSingletonServices = C
|
|
|
15
15
|
* @returns A function that removes the handlers.
|
|
16
16
|
*/
|
|
17
17
|
export declare const onShutdownSignals: (run: (signal: NodeJS.Signals) => void | Promise<void>, signals?: NodeJS.Signals[]) => (() => void);
|
|
18
|
+
/** A contributor's `onServerStart` / `onServerStop`, as the generated platform-services file lists it. */
|
|
19
|
+
export interface ContributorServerHook {
|
|
20
|
+
name: string;
|
|
21
|
+
run: (services: any) => void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Composes the server lifecycle around the app's own: contributor start hooks
|
|
25
|
+
* run first, in order, then the app's `beforeStart`; the app's `afterStart` and
|
|
26
|
+
* `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
|
|
27
|
+
* (already in reverse run order, as the generated file lists them).
|
|
28
|
+
*
|
|
29
|
+
* A start hook that throws aborts startup with an error naming the contributor.
|
|
30
|
+
* Every stop hook is attempted even if an earlier one (or the app's
|
|
31
|
+
* `afterStop`) threw; the failures are then thrown together, each named.
|
|
32
|
+
*/
|
|
33
|
+
export declare const composeServerLifecycle: <SS extends CoreSingletonServices = CoreSingletonServices>(app: ServerLifecycle<SS> | undefined, hooks: {
|
|
34
|
+
start?: readonly ContributorServerHook[];
|
|
35
|
+
stop?: readonly ContributorServerHook[];
|
|
36
|
+
}) => ServerLifecycle<SS>;
|
package/dist/lifecycle.js
CHANGED
|
@@ -67,3 +67,57 @@ export const onShutdownSignals = (run, signals = ['SIGINT', 'SIGTERM']) => {
|
|
|
67
67
|
proc.off(signal, handler);
|
|
68
68
|
};
|
|
69
69
|
};
|
|
70
|
+
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
71
|
+
/**
|
|
72
|
+
* Composes the server lifecycle around the app's own: contributor start hooks
|
|
73
|
+
* run first, in order, then the app's `beforeStart`; the app's `afterStart` and
|
|
74
|
+
* `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
|
|
75
|
+
* (already in reverse run order, as the generated file lists them).
|
|
76
|
+
*
|
|
77
|
+
* A start hook that throws aborts startup with an error naming the contributor.
|
|
78
|
+
* Every stop hook is attempted even if an earlier one (or the app's
|
|
79
|
+
* `afterStop`) threw; the failures are then thrown together, each named.
|
|
80
|
+
*/
|
|
81
|
+
export const composeServerLifecycle = (app, hooks) => ({
|
|
82
|
+
beforeStart: async (services) => {
|
|
83
|
+
for (const hook of hooks.start ?? []) {
|
|
84
|
+
try {
|
|
85
|
+
await hook.run(services);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`Contributor '${hook.name}' failed in onServerStart: ${describeError(error)}`, { cause: error });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await app?.beforeStart?.(services);
|
|
92
|
+
},
|
|
93
|
+
afterStart: (services) => app?.afterStart?.(services),
|
|
94
|
+
beforeStop: (services) => app?.beforeStop?.(services),
|
|
95
|
+
afterStop: async (services) => {
|
|
96
|
+
const failures = [];
|
|
97
|
+
const errors = [];
|
|
98
|
+
let hookFailed = false;
|
|
99
|
+
try {
|
|
100
|
+
await app?.afterStop?.(services);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
failures.push(`the app's afterStop: ${describeError(error)}`);
|
|
104
|
+
errors.push(error);
|
|
105
|
+
}
|
|
106
|
+
for (const hook of hooks.stop ?? []) {
|
|
107
|
+
try {
|
|
108
|
+
await hook.run(services);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
hookFailed = true;
|
|
112
|
+
failures.push(`contributor '${hook.name}' onServerStop: ${describeError(error)}`);
|
|
113
|
+
errors.push(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (errors.length === 1 && failures.length === 1 && !hookFailed) {
|
|
117
|
+
throw errors[0];
|
|
118
|
+
}
|
|
119
|
+
if (errors.length > 0) {
|
|
120
|
+
throw new AggregateError(errors, `Server stop failed: ${failures.join('; ')}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -15,7 +15,7 @@ import type { CoreScheduledTask, ScheduledTasksMeta } from '../wirings/scheduler
|
|
|
15
15
|
import type { CoreWorkflow, WorkflowsRuntimeMeta } from '../wirings/workflow/workflow.types.js';
|
|
16
16
|
import type { CoreFeature } from '../wirings/workflow/scenario.types.js';
|
|
17
17
|
import type { CoreTrigger, CoreTriggerSource, TriggerMeta, TriggerSourceMeta } from '../wirings/trigger/trigger.types.js';
|
|
18
|
-
import type { CreateConfig, CreateSingletonServices, CreateWireServices,
|
|
18
|
+
import type { CreateConfig, CreateSingletonServices, CreateWireServices, CoreSingletonServices } from './core.types.js';
|
|
19
19
|
import type { FunctionsMeta } from '../function/function-meta.types.js';
|
|
20
20
|
import type { CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../middleware/middleware.types.js';
|
|
21
21
|
import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
|
|
@@ -168,10 +168,11 @@ export interface PikkuPackageState {
|
|
|
168
168
|
permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>;
|
|
169
169
|
};
|
|
170
170
|
package: {
|
|
171
|
+
/** Every package's own factories, each typed where it is defined. */
|
|
171
172
|
factories: {
|
|
172
|
-
createConfig?: CreateConfig<
|
|
173
|
-
createSingletonServices?: CreateSingletonServices<
|
|
174
|
-
createWireServices?: CreateWireServices<
|
|
173
|
+
createConfig?: CreateConfig<any>;
|
|
174
|
+
createSingletonServices?: CreateSingletonServices<any, any>;
|
|
175
|
+
createWireServices?: CreateWireServices<any, any, any>;
|
|
175
176
|
} | null;
|
|
176
177
|
singletonServices: CoreSingletonServices | null;
|
|
177
178
|
/** The pikkuBetterAuth factory, self-registered when auth.ts is evaluated.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { PikkuError } from '../../errors/error-handler.js';
|
|
1
2
|
import type { PikkuRawWire } from '../../types/core.types.js';
|
|
2
3
|
export declare const addonMayReach: (caller: string, funcName: string) => boolean;
|
|
3
4
|
export declare const addonCallerOf: (wire: PikkuRawWire) => string | undefined;
|
|
5
|
+
export declare class AddonNotUsedError extends PikkuError {
|
|
6
|
+
constructor(caller: string, target: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The name an addon's call resolves to on this host, through its `uses`.
|
|
10
|
+
*
|
|
11
|
+
* knowledge: decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md
|
|
12
|
+
*/
|
|
13
|
+
export declare const mapAddonCall: (caller: string | undefined, funcName: string) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ForbiddenError } from '../../errors/errors.js';
|
|
2
|
+
import { PikkuError, addError } from '../../errors/error-handler.js';
|
|
2
3
|
import { ADDON_HEADER } from '../../middleware/addon-scope-cap.js';
|
|
3
4
|
import { pikkuState } from '../../pikku-state.js';
|
|
4
5
|
export const addonMayReach = (caller, funcName) => {
|
|
@@ -19,3 +20,31 @@ export const addonCallerOf = (wire) => {
|
|
|
19
20
|
}
|
|
20
21
|
return name;
|
|
21
22
|
};
|
|
23
|
+
export class AddonNotUsedError extends PikkuError {
|
|
24
|
+
constructor(caller, target) {
|
|
25
|
+
super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
addError(AddonNotUsedError, {
|
|
29
|
+
status: 403,
|
|
30
|
+
message: 'Addon called another addon it does not use.',
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* The name an addon's call resolves to on this host, through its `uses`.
|
|
34
|
+
*
|
|
35
|
+
* knowledge: decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md
|
|
36
|
+
*/
|
|
37
|
+
export const mapAddonCall = (caller, funcName) => {
|
|
38
|
+
if (!caller)
|
|
39
|
+
return funcName;
|
|
40
|
+
const colon = funcName.indexOf(':');
|
|
41
|
+
if (colon === -1)
|
|
42
|
+
return funcName;
|
|
43
|
+
const prefix = funcName.slice(0, colon);
|
|
44
|
+
if (prefix === caller)
|
|
45
|
+
return funcName;
|
|
46
|
+
const mapped = pikkuState(null, 'addons', 'packages').get(caller)?.uses?.[prefix];
|
|
47
|
+
if (mapped)
|
|
48
|
+
return `${mapped}${funcName.slice(colon)}`;
|
|
49
|
+
throw new AddonNotUsedError(caller, funcName);
|
|
50
|
+
};
|
|
@@ -105,6 +105,13 @@ const scopeWebhookService = (service, namespace) => {
|
|
|
105
105
|
scopedWebhookServices.add(scoped);
|
|
106
106
|
return scoped;
|
|
107
107
|
};
|
|
108
|
+
const authFromAppServices = () => {
|
|
109
|
+
const auth = pikkuState(null, 'package', 'singletonServices')?.auth;
|
|
110
|
+
if (typeof auth !== 'function') {
|
|
111
|
+
throw new Error('auth was called before the app had finished creating its singleton services');
|
|
112
|
+
}
|
|
113
|
+
return auth();
|
|
114
|
+
};
|
|
108
115
|
export const getOrCreatePackageSingletonServices = async (packageName, parentServices, addonInstance) => {
|
|
109
116
|
addonInstance ??= addonInstanceForNamespace(findAddonNamespaceForPackage(packageName) ?? undefined, packageName);
|
|
110
117
|
const cacheKey = addonInstance?.namespace ?? packageName;
|
|
@@ -114,6 +121,10 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
|
|
|
114
121
|
}
|
|
115
122
|
const factories = pikkuState(packageName, 'package', 'factories');
|
|
116
123
|
let existingServices = parentServices;
|
|
124
|
+
if (typeof existingServices.auth !== 'function' &&
|
|
125
|
+
pikkuState(null, 'package', 'authFactory')) {
|
|
126
|
+
existingServices = { ...existingServices, auth: authFromAppServices };
|
|
127
|
+
}
|
|
117
128
|
if (addonInstance?.secretOverrides && parentServices.secrets) {
|
|
118
129
|
existingServices = {
|
|
119
130
|
...existingServices,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an addon screen shows in Studio's navigation. Leave it out and the
|
|
3
|
+
* screen is routed but not listed.
|
|
4
|
+
*/
|
|
5
|
+
export type StudioNav = {
|
|
6
|
+
area: 'rail' | 'team' | 'project' | 'account';
|
|
7
|
+
/** The icon's name. */
|
|
8
|
+
icon: string;
|
|
9
|
+
order: number;
|
|
10
|
+
/** Names a function of the addon that returns a count shown on the entry. */
|
|
11
|
+
badge?: string;
|
|
12
|
+
};
|
|
13
|
+
type StudioScreenBase = {
|
|
14
|
+
/** The route path, under the prefix its `scope` gives it. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** A message key in the addon's catalog, `messages/{locale}.json`. It is also the nav label. */
|
|
17
|
+
title: string;
|
|
18
|
+
nav?: StudioNav;
|
|
19
|
+
/** Takes over the core screen at the same path. Only first-party addons may use it. */
|
|
20
|
+
replaces?: boolean;
|
|
21
|
+
/** `'bare'` renders the screen without the shell. */
|
|
22
|
+
layout?: 'bare';
|
|
23
|
+
/** Hides the screen and its nav for viewers without access. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
|
|
24
|
+
scopes?: string[];
|
|
25
|
+
} & ({
|
|
26
|
+
/** `'studio'` gives `/<path>`, `'team'` gives `/<team>/<path>`. */
|
|
27
|
+
scope: 'studio' | 'team';
|
|
28
|
+
needsApp?: never;
|
|
29
|
+
} | {
|
|
30
|
+
/** `/<team>/<project>[/<target>]/<path>`. */
|
|
31
|
+
scope: 'project';
|
|
32
|
+
/** The screen needs the project's app running. Defaults to `false`. */
|
|
33
|
+
needsApp?: boolean;
|
|
34
|
+
});
|
|
35
|
+
/** A screen rendered by the host from the addon's source. */
|
|
36
|
+
export type StudioComponentScreen = StudioScreenBase & {
|
|
37
|
+
component: () => Promise<{
|
|
38
|
+
default: unknown;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
/** A static program the host serves in a sandboxed iframe. */
|
|
42
|
+
export type StudioAppScreen = StudioScreenBase & {
|
|
43
|
+
/** Directory of the built program, relative to the addon package. */
|
|
44
|
+
app: string;
|
|
45
|
+
};
|
|
46
|
+
/** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
|
|
47
|
+
export type StudioScreen = StudioComponentScreen | StudioAppScreen;
|
|
48
|
+
/** The manifest passed to `definePlugin`: the addon's title, a message key, and its screens. */
|
|
49
|
+
export type StudioManifest = {
|
|
50
|
+
title: string;
|
|
51
|
+
screens: StudioScreen[];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Declares the screens and nav an addon brings to Pikku Studio. Read
|
|
55
|
+
* statically by `pikku all`, so every field must be a literal. Titles are
|
|
56
|
+
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
57
|
+
* missing from `messages/en.json` fails the build.
|
|
58
|
+
*/
|
|
59
|
+
export declare const definePlugin: (manifest: StudioManifest) => StudioManifest;
|
|
60
|
+
/** @deprecated Renamed to `definePlugin`. */
|
|
61
|
+
export declare const pikkuStudio: typeof definePlugin;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares the screens and nav an addon brings to Pikku Studio. Read
|
|
3
|
+
* statically by `pikku all`, so every field must be a literal. Titles are
|
|
4
|
+
* message keys in the addon's own catalog, `messages/{locale}.json`: a key
|
|
5
|
+
* missing from `messages/en.json` fails the build.
|
|
6
|
+
*/
|
|
7
|
+
export const definePlugin = (manifest) => manifest;
|
|
8
|
+
/** @deprecated Renamed to `definePlugin`. */
|
|
9
|
+
export const pikkuStudio = definePlugin;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
2
|
export type { WireAddonConfig } from './wire-addon.js';
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
3
|
+
export { definePlugin, pikkuStudio } from './define-plugin.js';
|
|
4
|
+
export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './define-plugin.js';
|
|
5
5
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
6
6
|
export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wire-remote-addon.js';
|
|
7
7
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { wireAddon } from './wire-addon.js';
|
|
2
|
-
export {
|
|
2
|
+
export { definePlugin, pikkuStudio } from './define-plugin.js';
|
|
3
3
|
export { wireRemoteAddon } from './wire-remote-addon.js';
|
|
4
4
|
export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
|
|
@@ -46,18 +46,22 @@ export type WireAddonConfig = {
|
|
|
46
46
|
/** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
|
|
47
47
|
tags?: string[];
|
|
48
48
|
/**
|
|
49
|
-
* Mounts the screens the addon ships (see `
|
|
50
|
-
*
|
|
51
|
-
* must declare at least one, or `pikku all` rejects the wiring.
|
|
49
|
+
* Mounts the screens the addon ships (see `definePlugin`) in the host, each at
|
|
50
|
+
* the path its `scope` gives it. Without it the addon's screens are ignored.
|
|
51
|
+
* The package must declare at least one, or `pikku all` rejects the wiring.
|
|
52
52
|
*/
|
|
53
53
|
ui?: boolean;
|
|
54
54
|
/** Required of every function in the addon, on top of the function's own. */
|
|
55
55
|
scopes?: string[];
|
|
56
56
|
/**
|
|
57
|
-
* The other addons this one may call, as `{ '<
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* The other addons this one may call, as `{ '<alias>': '<name>' }`: the name
|
|
58
|
+
* the addon calls another by, and the name that addon is wired under in this
|
|
59
|
+
* app — by `wireAddon` or `wireRemoteAddon`, either serves. The alias is the
|
|
60
|
+
* one the addon's package.json declares under `pikku.uses`
|
|
61
|
+
* (`{ "stripe": "@acme/addon-stripe@^1.0.0" }`), or, for an addon that lists
|
|
62
|
+
* plain names, the package name itself. The addon calls
|
|
63
|
+
* `'<alias>:<function>'`; anything it did not declare, or this app did not
|
|
64
|
+
* map, is refused, whatever else the app has wired.
|
|
61
65
|
*/
|
|
62
66
|
uses?: Record<string, string>;
|
|
63
67
|
/** Points a secret the addon reads at a different key in this deployment, so two instances can hold different credentials. */
|
|
@@ -96,6 +100,12 @@ export type WireAddonConfig = {
|
|
|
96
100
|
contentGrants?: Record<string, 'read' | 'write'>;
|
|
97
101
|
/** Hands over the whole `ContentService` unscoped. The value is the reason, recorded in the deploy manifest. */
|
|
98
102
|
globalContent?: string;
|
|
103
|
+
/**
|
|
104
|
+
* The contributors this project takes from the addon (those it registers with
|
|
105
|
+
* `defineContributors`), by name. None unless listed: an addon never injects
|
|
106
|
+
* services into a project that did not ask for them.
|
|
107
|
+
*/
|
|
108
|
+
contributors?: string[];
|
|
99
109
|
};
|
|
100
110
|
/**
|
|
101
111
|
* Installs an addon into this project: its functions, wirings and scopes become
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pikkuState } from '../../pikku-state.js';
|
|
2
|
+
import { mapAddonCall } from '../addon/addon-reach.js';
|
|
2
3
|
import { runAgent, resumeAgentSync } from './agent-runner.js';
|
|
3
4
|
import { streamAgent, resumeAgent, interruptAgent } from './agent-stream.js';
|
|
4
5
|
import { wrapChannelWithAGUI } from './agent-agui.js';
|
|
@@ -31,10 +32,10 @@ export const createAgentRPC = (wire, options) => {
|
|
|
31
32
|
}),
|
|
32
33
|
});
|
|
33
34
|
return {
|
|
34
|
-
run: async (agentName, input) => asRunResult(await runAgent(agentName, input, params())),
|
|
35
|
+
run: async (agentName, input) => asRunResult(await runAgent(mapAddonCall(wire.addonNamespace, agentName), input, params())),
|
|
35
36
|
stream: async (agentName, input, streamOptions) => {
|
|
36
37
|
let currentRunId;
|
|
37
|
-
await streamAgent(agentName, input, wrapChannelWithAGUI(streamingChannel(), {
|
|
38
|
+
await streamAgent(mapAddonCall(wire.addonNamespace, agentName), input, wrapChannelWithAGUI(streamingChannel(), {
|
|
38
39
|
threadId: input.threadId,
|
|
39
40
|
getRunId: () => currentRunId,
|
|
40
41
|
}), params(), undefined, {
|
|
@@ -49,7 +50,9 @@ export const createAgentRPC = (wire, options) => {
|
|
|
49
50
|
await resumeAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
|
|
50
51
|
},
|
|
51
52
|
interrupt: async (runId, reason) => interruptAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
|
|
52
|
-
approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName
|
|
53
|
+
approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: mapAddonCall(wire.addonNamespace, expectedAgentName))),
|
|
53
56
|
};
|
|
54
57
|
};
|
|
55
58
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AddonNotUsedError } from '../addon/addon-reach.js';
|
|
1
2
|
import type { CoreServices, PikkuRawWire } from '../../types/core.types.js';
|
|
2
3
|
import type { SessionService } from '../../services/user-session-service.js';
|
|
3
4
|
import type { CoreUserSession } from '../../types/core.types.js';
|
|
@@ -29,9 +30,7 @@ export declare class RemoteAddonFailedError extends PikkuError {
|
|
|
29
30
|
export declare class RemoteAddonTimeoutError extends PikkuError {
|
|
30
31
|
constructor(namespace: string, fnName: string, timeoutMs: number);
|
|
31
32
|
}
|
|
32
|
-
export
|
|
33
|
-
constructor(caller: string, target: string);
|
|
34
|
-
}
|
|
33
|
+
export { AddonNotUsedError };
|
|
35
34
|
export declare const resolveNamespace: (namespacedFunction: string) => ResolvedFunction | null;
|
|
36
35
|
export declare class ContextAwareRPCService {
|
|
37
36
|
private services;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addonCallerOf, addonMayReach } from '../addon/addon-reach.js';
|
|
1
|
+
import { AddonNotUsedError, addonCallerOf, addonMayReach, mapAddonCall, } from '../addon/addon-reach.js';
|
|
2
2
|
import { forwardStepName, isCompensationStepName, } from '../../compensation-name.js';
|
|
3
3
|
import { addonInstanceForNamespace } from '../addon/addon-runner.js';
|
|
4
4
|
import { isAddonFunctionExposed } from '../addon/wire-addon.js';
|
|
@@ -166,15 +166,7 @@ const remoteAddonResponseError = async (res, namespace, fnName, logger) => {
|
|
|
166
166
|
logger.error(`Remote addon '${namespace}:${fnName}' answered ${res.status}: ${body.slice(0, 1000)}`);
|
|
167
167
|
return new RemoteAddonFailedError(namespace, fnName);
|
|
168
168
|
};
|
|
169
|
-
export
|
|
170
|
-
constructor(caller, target) {
|
|
171
|
-
super(`Addon '${caller}' called '${target}' but does not list it in its wireAddon 'uses'.`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
addError(AddonNotUsedError, {
|
|
175
|
-
status: 403,
|
|
176
|
-
message: 'Addon called another addon it does not use.',
|
|
177
|
-
});
|
|
169
|
+
export { AddonNotUsedError };
|
|
178
170
|
export const resolveNamespace = (namespacedFunction) => {
|
|
179
171
|
const colonIndex = namespacedFunction.indexOf(':');
|
|
180
172
|
if (colonIndex === -1) {
|
|
@@ -367,18 +359,7 @@ export class ContextAwareRPCService {
|
|
|
367
359
|
});
|
|
368
360
|
}
|
|
369
361
|
mapUsedAddon(funcName) {
|
|
370
|
-
|
|
371
|
-
if (!caller)
|
|
372
|
-
return funcName;
|
|
373
|
-
const colon = funcName.indexOf(':');
|
|
374
|
-
const prefix = funcName.slice(0, colon);
|
|
375
|
-
if (prefix === caller)
|
|
376
|
-
return funcName;
|
|
377
|
-
const uses = pikkuState(null, 'addons', 'packages').get(caller)?.uses;
|
|
378
|
-
const mapped = uses?.[prefix];
|
|
379
|
-
if (mapped)
|
|
380
|
-
return `${mapped}${funcName.slice(colon)}`;
|
|
381
|
-
throw new AddonNotUsedError(caller, funcName);
|
|
362
|
+
return mapAddonCall(this.wire.addonNamespace, funcName);
|
|
382
363
|
}
|
|
383
364
|
/**
|
|
384
365
|
* Resolution half of an addon RPC call: never executes anything, so an
|
|
@@ -737,7 +737,7 @@ async function invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, no
|
|
|
737
737
|
export async function executeGraphStep(workflowService, rpcService, runId, stepId, nodeId, rpcName, data, graphName) {
|
|
738
738
|
try {
|
|
739
739
|
let result;
|
|
740
|
-
const subWorkflowMeta =
|
|
740
|
+
const subWorkflowMeta = getWorkflowMeta(rpcName);
|
|
741
741
|
const agentMeta = subWorkflowMeta
|
|
742
742
|
? undefined
|
|
743
743
|
: pikkuState(null, 'agent', 'agentsMeta')[rpcName];
|
|
@@ -801,7 +801,7 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
801
801
|
await workflowService.setStepRunning(stepState.stepId);
|
|
802
802
|
try {
|
|
803
803
|
let result;
|
|
804
|
-
const subWorkflowMeta =
|
|
804
|
+
const subWorkflowMeta = getWorkflowMeta(rpcName);
|
|
805
805
|
const agentMeta = subWorkflowMeta
|
|
806
806
|
? undefined
|
|
807
807
|
: pikkuState(null, 'agent', 'agentsMeta')[rpcName];
|
|
@@ -691,8 +691,7 @@ export class PikkuWorkflowService {
|
|
|
691
691
|
if (!run) {
|
|
692
692
|
throw new Error(`Workflow run not found: ${runId}`);
|
|
693
693
|
}
|
|
694
|
-
const
|
|
695
|
-
const workflowMeta = meta[run.workflow];
|
|
694
|
+
const workflowMeta = resolveWorkflowMeta(run.workflow)?.meta;
|
|
696
695
|
const isGraphWorkflow = workflowMeta?.source === 'graph';
|
|
697
696
|
let graphNodeId;
|
|
698
697
|
if (isGraphWorkflow && workflowMeta?.nodes) {
|
|
@@ -709,7 +708,7 @@ export class PikkuWorkflowService {
|
|
|
709
708
|
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, workflowMeta.nodes[graphNodeId].rpcName, data, run.workflow);
|
|
710
709
|
}
|
|
711
710
|
else {
|
|
712
|
-
if (
|
|
711
|
+
if (resolveWorkflowMeta(rpcName)) {
|
|
713
712
|
result = await startQueuedChildStep({
|
|
714
713
|
startWorkflow: (...args) => this.startWorkflow(...args),
|
|
715
714
|
setStepChildRunId: (...args) => this.setStepChildRunId(...args),
|
|
@@ -860,7 +859,7 @@ export class PikkuWorkflowService {
|
|
|
860
859
|
if (resolvedStepOptions.virtualUser) {
|
|
861
860
|
return resolvedStepOptions.virtualUser.invoke(rpcName, data);
|
|
862
861
|
}
|
|
863
|
-
if (
|
|
862
|
+
if (resolveWorkflowMeta(rpcName)) {
|
|
864
863
|
return runInlineChildStep({
|
|
865
864
|
startWorkflow: (...args) => this.startWorkflow(...args),
|
|
866
865
|
setStepChildRunId: (...args) => this.setStepChildRunId(...args),
|
|
@@ -2,6 +2,7 @@ import { DEFAULT_STEP_RETRIES, NO_COMPENSATE_STATE_PREFIX, WORKFLOW_TERMINAL_STA
|
|
|
2
2
|
import { pikkuState } from '../../pikku-state.js';
|
|
3
3
|
import { resolveNamespace } from '../rpc/rpc-runner.js';
|
|
4
4
|
import { runningStepLease } from './workflow-step-lease.js';
|
|
5
|
+
import { isWorkflowName } from './workflow-meta-resolver.js';
|
|
5
6
|
import { planUnwind } from './workflow-unwind-plan.js';
|
|
6
7
|
const ENTERABLE = new Set([
|
|
7
8
|
'running',
|
|
@@ -177,8 +178,7 @@ export const functionHasCompensation = (rpcName) => {
|
|
|
177
178
|
?.compensate === true
|
|
178
179
|
: false;
|
|
179
180
|
};
|
|
180
|
-
export const isChildWorkflowRecord = (record) => !!record.rpcName &&
|
|
181
|
-
pikkuState(null, 'workflows', 'meta')[record.rpcName] !== undefined;
|
|
181
|
+
export const isChildWorkflowRecord = (record) => !!record.rpcName && isWorkflowName(record.rpcName);
|
|
182
182
|
export const recordHasCompensation = (record) => isChildWorkflowRecord(record)
|
|
183
183
|
? !!record.childRunId
|
|
184
184
|
: !!record.rpcName && functionHasCompensation(record.rpcName);
|
|
@@ -9,3 +9,8 @@ export type ResolvedWorkflowMeta = {
|
|
|
9
9
|
* `namespace:name` form — against the addon package that claims the namespace.
|
|
10
10
|
*/
|
|
11
11
|
export declare const resolveWorkflowMeta: (name: string) => ResolvedWorkflowMeta | null;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a step's rpcName names a workflow — the app's, or an addon's by
|
|
14
|
+
* `namespace:name` — and so starts a child run rather than an RPC.
|
|
15
|
+
*/
|
|
16
|
+
export declare const isWorkflowName: (name: string) => boolean;
|
|
@@ -29,3 +29,8 @@ export const resolveWorkflowMeta = (name) => {
|
|
|
29
29
|
resolvedName: localName,
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Whether a step's rpcName names a workflow — the app's, or an addon's by
|
|
34
|
+
* `namespace:name` — and so starts a child run rather than an RPC.
|
|
35
|
+
*/
|
|
36
|
+
export const isWorkflowName = (name) => resolveWorkflowMeta(name) !== null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSingletonServices, pikkuState } from '../../pikku-state.js';
|
|
2
2
|
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
3
3
|
import { DEFAULT_STEP_RETRIES, forwardStepName } from './workflow-constants.js';
|
|
4
|
+
import { isWorkflowName } from './workflow-meta-resolver.js';
|
|
4
5
|
const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
5
6
|
export const resolveWorkflowConfig = () => {
|
|
6
7
|
const workflow = getSingletonServices().config?.workflow;
|
|
@@ -54,8 +55,7 @@ export const stepDispatchTarget = async (rpcName, stepName, parentIsInline) => {
|
|
|
54
55
|
}
|
|
55
56
|
return 'queue';
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
if (!isWorkflow || !hasQueue) {
|
|
58
|
+
if (!isWorkflowName(rpcName) || !hasQueue) {
|
|
59
59
|
return 'inline';
|
|
60
60
|
}
|
|
61
61
|
return (await parentIsInline()) ? 'inline' : 'queue';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.148",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"sideEffects": [
|
|
26
26
|
"./dist/errors/errors.js",
|
|
27
|
+
"./dist/wirings/addon/addon-reach.js",
|
|
27
28
|
"./dist/wirings/addon/remote-addon-auth.js",
|
|
28
29
|
"./dist/wirings/agent/agent-rpc.js",
|
|
29
30
|
"./dist/wirings/rpc/rpc-runner.js",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"./state": "./dist/pikku-state.js",
|
|
93
94
|
"./classification": "./dist/classification/index.js",
|
|
94
95
|
"./utils": "./dist/utils.js",
|
|
96
|
+
"./contributors": "./dist/contributors/index.js",
|
|
95
97
|
"./lifecycle": "./dist/lifecycle.js",
|
|
96
98
|
"./schema": "./dist/schema.js",
|
|
97
99
|
"./testing": "./dist/testing/index.js",
|
|
@@ -118,7 +120,7 @@
|
|
|
118
120
|
},
|
|
119
121
|
"devDependencies": {
|
|
120
122
|
"@ag-ui/core": "0.0.59",
|
|
121
|
-
"@pikku/deploy": "^0.12.
|
|
123
|
+
"@pikku/deploy": "^0.12.18",
|
|
122
124
|
"@types/node": "^26.6.5",
|
|
123
125
|
"esbuild": "~0.28.2",
|
|
124
126
|
"tsx": "^4.23.15"
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
type AddonScreenBase = {
|
|
2
|
-
/** Where the screen mounts under the addon, e.g. `/` or `/:id`. */
|
|
3
|
-
path: string;
|
|
4
|
-
title: string;
|
|
5
|
-
/** Lists the screen in the host's navigation. */
|
|
6
|
-
nav?: boolean;
|
|
7
|
-
/** Required of the viewer to see the screen. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
|
|
8
|
-
scopes?: string[];
|
|
9
|
-
};
|
|
10
|
-
/** A screen rendered by the host from the addon's source. */
|
|
11
|
-
export type AddonComponentScreen = AddonScreenBase & {
|
|
12
|
-
component: () => Promise<{
|
|
13
|
-
default: unknown;
|
|
14
|
-
}>;
|
|
15
|
-
};
|
|
16
|
-
/** A static program the host serves in a sandboxed iframe. */
|
|
17
|
-
export type AddonAppScreen = AddonScreenBase & {
|
|
18
|
-
/** Directory of the built program, relative to the addon package. */
|
|
19
|
-
app: string;
|
|
20
|
-
};
|
|
21
|
-
/** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
|
|
22
|
-
export type AddonScreen = AddonComponentScreen | AddonAppScreen;
|
|
23
|
-
/** The manifest passed to `defineScreens`: the addon's title, optional icon and its screens. */
|
|
24
|
-
export type ScreensManifest = {
|
|
25
|
-
title: string;
|
|
26
|
-
icon?: string;
|
|
27
|
-
screens: AddonScreen[];
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Declares the screens an addon ships, which the host mounts when it is wired
|
|
31
|
-
* with `ui: true`. Read statically by `pikku all`, so `path`, `title`, `nav`, `scopes` and `app` must be literals.
|
|
32
|
-
*/
|
|
33
|
-
export declare const defineScreens: (manifest: ScreensManifest) => ScreensManifest;
|
|
34
|
-
export {};
|