@sanoma-ai/workflows 0.8.2 → 0.9.1
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 +6 -19
- package/dist/apply.js +8 -8
- package/dist/apply.js.map +1 -1
- package/dist/client.d.ts +1 -13
- package/dist/client.js +2 -22
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +7 -46
- package/dist/config.js +13 -39
- package/dist/config.js.map +1 -1
- package/dist/cucumber.d.ts +4 -2
- package/dist/cucumber.js +8 -3
- package/dist/cucumber.js.map +1 -1
- package/dist/db.d.ts +0 -1
- package/dist/db.js +2 -12
- package/dist/db.js.map +1 -1
- package/dist/fake.d.ts +1 -9
- package/dist/fake.js +2 -12
- package/dist/fake.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resource.d.ts +17 -0
- package/dist/resource.js +38 -0
- package/dist/resource.js.map +1 -1
- package/dist/run.d.ts +0 -2
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +10 -38
- package/dist/sandbox.js +36 -66
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +8 -18
- package/dist/scenario.js +60 -80
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.d.ts +1 -10
- package/dist/scenarios.js +2 -16
- package/dist/scenarios.js.map +1 -1
- package/dist/sites.d.ts +6 -0
- package/dist/sites.js +49 -10
- package/dist/sites.js.map +1 -1
- package/dist/source.d.ts +6 -0
- package/dist/source.js +13 -0
- package/dist/source.js.map +1 -1
- package/dist/steps.d.ts +3 -1
- package/dist/steps.js.map +1 -1
- package/dist/worker.d.ts +1 -7
- package/dist/worker.js +2 -3
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/apply.ts +16 -8
- package/src/client.ts +2 -28
- package/src/config.ts +14 -91
- package/src/cucumber.ts +16 -3
- package/src/db.ts +2 -13
- package/src/fake.ts +1 -13
- package/src/index.ts +0 -1
- package/src/resource.ts +43 -0
- package/src/run.ts +0 -2
- package/src/sandbox.ts +38 -94
- package/src/scenario.ts +73 -100
- package/src/scenarios.ts +3 -18
- package/src/sites.ts +67 -11
- package/src/source.ts +14 -0
- package/src/steps.ts +3 -1
- package/src/worker.ts +2 -9
- package/dist/demo.d.ts +0 -39
- package/dist/demo.js +0 -87
- package/dist/demo.js.map +0 -1
- package/src/demo.ts +0 -118
package/src/config.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { type CedarPolicySource, policyAt } from "./cedar.ts";
|
|
|
41
41
|
import type { Policy } from "./policy.ts";
|
|
42
42
|
import { type Resource, resourceTypesOf, withoutWriteOnly } from "./resource.ts";
|
|
43
43
|
import { APPLY_WORKFLOW, BUILT_IN_WORKFLOWS, CALL_WORKFLOW, CHANGE_WORKFLOW, DATA_VENDOR } from "./shared.ts";
|
|
44
|
-
import { type RemoteHalf, sourceDriver, sourceOps } from "./source.ts";
|
|
44
|
+
import { fakeSourceDriver, type RemoteHalf, SOURCE_RESOLVE, sourceDriver, sourceOps } from "./source.ts";
|
|
45
45
|
import { appsListDriver, appsListOps } from "./apps-list.ts";
|
|
46
46
|
import type { Steps } from "./steps.ts";
|
|
47
47
|
import { checkSchedule } from "./schedules.ts";
|
|
@@ -114,43 +114,6 @@ export interface SanomaConfig {
|
|
|
114
114
|
* that sets it and declares a wakeup of its own.
|
|
115
115
|
*/
|
|
116
116
|
wakeup?: true;
|
|
117
|
-
/** How this repo runs as a demo (`sanoma demo`, or `SANOMA_DEMO=1` on a host). */
|
|
118
|
-
demo?: DemoConfig;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** A demo's settings (`SanomaConfig.demo`). */
|
|
122
|
-
export interface DemoConfig {
|
|
123
|
-
/**
|
|
124
|
-
* The URL the banner's "Start with these apps" opens, such as the hosted wizard with the demo's
|
|
125
|
-
* apps chosen; without one, the banner has no such link.
|
|
126
|
-
*/
|
|
127
|
-
start?: string;
|
|
128
|
-
/**
|
|
129
|
-
* A session per visitor: each visitor gets a copy of the seeded database and a process of their
|
|
130
|
-
* own over it, so the moments wait for them alone and nothing they type reaches another. At most
|
|
131
|
-
* this many at once, or with `true` as many as the machine's memory and Postgres's connections
|
|
132
|
-
* hold; past it, visitors share one demo instance. Without it, every visitor shares that instance.
|
|
133
|
-
*/
|
|
134
|
-
sessions?: true | number;
|
|
135
|
-
/** Minutes a visitor's session lives unused before it is dropped. Default 30. */
|
|
136
|
-
idle?: number;
|
|
137
|
-
/** Minutes between seeds of the demo afresh (its shared instance, or the sessions' seed). Default 60. */
|
|
138
|
-
reseed?: number;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Refuses a `demo` whose numbers make no demo: `sessions` `true` or a whole number above 0, `idle` and `reseed` minutes above 0. */
|
|
142
|
-
function checkDemo(demo: DemoConfig | undefined): void {
|
|
143
|
-
const { sessions, idle, reseed } = demo ?? {};
|
|
144
|
-
if (sessions !== undefined && sessions !== true && !(Number.isInteger(sessions) && sessions > 0)) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
`The config's \`demo.sessions\` is \`true\` or how many at once, a whole number above 0, not ${JSON.stringify(sessions)}`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
for (const [key, minutes] of Object.entries({ idle, reseed })) {
|
|
150
|
-
if (minutes !== undefined && !(typeof minutes === "number" && minutes > 0)) {
|
|
151
|
-
throw new Error(`The config's \`demo.${key}\` is minutes, a number above 0, not ${JSON.stringify(minutes)}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
117
|
}
|
|
155
118
|
|
|
156
119
|
/** A config, checked, with everything a worker, client or app derives from it. */
|
|
@@ -180,18 +143,8 @@ export interface ResolvedConfig {
|
|
|
180
143
|
* `connectors` order: what `drivers` indexes and the credentials are read from (`credentialsOf`).
|
|
181
144
|
*/
|
|
182
145
|
driversByVendor: Map<string, Driver>;
|
|
183
|
-
/**
|
|
184
|
-
* The makers of the fakes the connectors attach, by vendor id in `connectors` order: each
|
|
185
|
-
* sandbox run makes its own. Resolved with `fakes: "instance"`, each gives its vendor's
|
|
186
|
-
* instance fake (`instanceFakes`).
|
|
187
|
-
*/
|
|
146
|
+
/** The makers of the fakes the connectors attach, by vendor id in `connectors` order: each sandbox run makes its own. */
|
|
188
147
|
fakes: Map<string, () => Fake<any, any>>;
|
|
189
|
-
/**
|
|
190
|
-
* Resolved with `fakes: "instance"` (a demo): each vendor's one fake, by vendor id, made once
|
|
191
|
-
* for this resolution. Each is its vendor's driver (`driversByVendor`), so live runs, sandbox
|
|
192
|
-
* runs and direct calls all call it, and the world's Givens seed it (`applyGivens`).
|
|
193
|
-
*/
|
|
194
|
-
instanceFakes?: ReadonlyMap<string, Fake<any, any>>;
|
|
195
148
|
/** The makers of the login halves the connectors attach, by vendor id in `connectors` order: the app makes one per public site that names it. */
|
|
196
149
|
logins: ReadonlyMap<string, NonNullable<ConnectorSetup["login"]>>;
|
|
197
150
|
/** The makers of the model halves the connectors attach, by vendor id in `connectors` order: the app makes each once for its model registry. */
|
|
@@ -288,17 +241,6 @@ export function rootOf(config: Pick<SanomaConfig, "root" | "file">): string | un
|
|
|
288
241
|
: undefined;
|
|
289
242
|
}
|
|
290
243
|
|
|
291
|
-
/** What `resolveConfig` is given besides the config. */
|
|
292
|
-
export interface ResolveOptions {
|
|
293
|
-
/**
|
|
294
|
-
* `"instance"`: run the whole instance on the connectors' fakes, as a demo does: each vendor's
|
|
295
|
-
* driver is one fake made for this resolution (`instanceFakes`), which every run and call
|
|
296
|
-
* shares. The collections stay on the config's database. Refused for a vendor whose connector
|
|
297
|
-
* attaches a driver and no fake, which would otherwise run live.
|
|
298
|
-
*/
|
|
299
|
-
fakes?: "instance";
|
|
300
|
-
}
|
|
301
|
-
|
|
302
244
|
/**
|
|
303
245
|
* Checks a config and derives what the runtime needs from it: the drivers and fakes from the
|
|
304
246
|
* connectors' setups (`withSetup`), the collections' operations, driver and fake, and from
|
|
@@ -309,7 +251,7 @@ export interface ResolveOptions {
|
|
|
309
251
|
* `root`. Each entry point resolves once: the worker and the client when they start, the app at
|
|
310
252
|
* `startApp`, the `sanoma` bin when it loads the config.
|
|
311
253
|
*/
|
|
312
|
-
export function resolveConfig(config: SanomaConfig
|
|
254
|
+
export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
313
255
|
if (!Array.isArray(config.connectors)) {
|
|
314
256
|
throw new Error("The config needs `connectors`: the connectors its workflows call, as their packages export them");
|
|
315
257
|
}
|
|
@@ -326,7 +268,6 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
326
268
|
if (config.policy !== undefined) {
|
|
327
269
|
throw new Error("The config's `policy` is gone: the policy is policies/*.cedar in the root; leave it out");
|
|
328
270
|
}
|
|
329
|
-
checkDemo(config.demo);
|
|
330
271
|
const collections = collectionsOf(config);
|
|
331
272
|
const ledger = ledgerOf(config);
|
|
332
273
|
const root = rootOf(config);
|
|
@@ -352,22 +293,19 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
352
293
|
]);
|
|
353
294
|
checkWakeupOps(types, ops);
|
|
354
295
|
checkDatabaseOps(types, ops);
|
|
355
|
-
const
|
|
356
|
-
const instanceFakes = options.fakes === "instance" ? instanceFakesOf(running, fakes) : undefined;
|
|
357
|
-
const driversByVendor = instanceFakes
|
|
358
|
-
? new Map([...instanceFakes].map(([vendor, fake]) => [vendor, fake.driver]))
|
|
359
|
-
: new Map([...makersOf(running, "driver")].map(([vendor, make]) => [vendor, make()]));
|
|
296
|
+
const driversByVendor = new Map([...makersOf(running, "driver")].map(([vendor, make]) => [vendor, make()]));
|
|
360
297
|
const remotes = new Map([...makersOf(config.connectors, "remote")].map(([vendor, make]) => [vendor, make()]));
|
|
361
298
|
const drivers = indexDrivers(
|
|
362
299
|
[
|
|
363
300
|
...driversByVendor.values(),
|
|
364
301
|
changeDriver(remotes),
|
|
365
|
-
|
|
302
|
+
appsListDriver,
|
|
366
303
|
...(hostTyped ? [sourceDriver] : []),
|
|
367
304
|
...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
|
|
368
305
|
],
|
|
369
306
|
ops,
|
|
370
307
|
);
|
|
308
|
+
const fakes = makersOf(running, "fake");
|
|
371
309
|
if (dataStore) fakes.set(DATA_VENDOR, () => collectionsFake(declared));
|
|
372
310
|
const names = new Map<string, WorkflowDefinition<any, any>>();
|
|
373
311
|
for (const wf of config.workflows ?? []) {
|
|
@@ -415,7 +353,6 @@ export function resolveConfig(config: SanomaConfig, options: ResolveOptions = {}
|
|
|
415
353
|
drivers,
|
|
416
354
|
driversByVendor,
|
|
417
355
|
fakes,
|
|
418
|
-
...(instanceFakes && { instanceFakes }),
|
|
419
356
|
logins: makersOf(config.connectors, "login"),
|
|
420
357
|
models: makersOf(config.connectors, "model"),
|
|
421
358
|
remotes,
|
|
@@ -459,10 +396,15 @@ function collectionsOf(config: SanomaConfig): Map<string, Collection> {
|
|
|
459
396
|
}
|
|
460
397
|
|
|
461
398
|
/**
|
|
462
|
-
* The built-in drivers
|
|
463
|
-
*
|
|
399
|
+
* The built-in drivers a sandbox run calls beside its fakes (`seedSandbox`), for the built-in
|
|
400
|
+
* operations among `ops` that no fake implements: `sanoma.apps.list`'s own, which needs no
|
|
401
|
+
* credentials and changes nothing (GitHub's public search), and `sanoma.source.resolve`'s fake
|
|
402
|
+
* (`fakeSourceDriver`), for a config with a host type.
|
|
464
403
|
*/
|
|
465
|
-
export const sandboxDrivers:
|
|
404
|
+
export const sandboxDrivers = (ops: ReadonlyMap<string, unknown>): Driver[] => [
|
|
405
|
+
appsListDriver,
|
|
406
|
+
...(ops.has(SOURCE_RESOLVE) ? [fakeSourceDriver] : []),
|
|
407
|
+
];
|
|
466
408
|
|
|
467
409
|
/**
|
|
468
410
|
* `sanoma.wakeup`, the runtime's own: the type's operations and `sanoma.wakeup.register` (effect
|
|
@@ -515,25 +457,6 @@ function makersOf<K extends keyof ConnectorSetup>(
|
|
|
515
457
|
return makers;
|
|
516
458
|
}
|
|
517
459
|
|
|
518
|
-
/**
|
|
519
|
-
* Each vendor's one fake, made now, for `resolveConfig`'s `fakes: "instance"`; `fakes` (the
|
|
520
|
-
* makers) then give these. Refuses a connector that attaches a driver and no fake.
|
|
521
|
-
*/
|
|
522
|
-
function instanceFakesOf(
|
|
523
|
-
connectors: readonly Connector<any, any>[],
|
|
524
|
-
fakes: Map<string, () => Fake<any, any>>,
|
|
525
|
-
): Map<string, Fake<any, any>> {
|
|
526
|
-
const live = [...makersOf(connectors, "driver").keys()].filter((vendor) => !fakes.has(vendor));
|
|
527
|
-
if (live.length) {
|
|
528
|
-
throw new Error(
|
|
529
|
-
`The instance runs on fakes, but the connector of ${live.join(", ")} attaches no fake, so it would run live; attach one (\`withSetup\`) or leave it out`,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
const made = new Map([...fakes].map(([vendor, make]) => [vendor, make()]));
|
|
533
|
-
for (const [vendor, fake] of made) fakes.set(vendor, () => fake);
|
|
534
|
-
return made;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
460
|
/**
|
|
538
461
|
* The `name` of `<root>/package.json`, kept whole (`@acme/ops`), when that file exists and names
|
|
539
462
|
* one; else "sanoma", as for a config with no `root`. Never a `package.json` above the root: a
|
package/src/cucumber.ts
CHANGED
|
@@ -173,7 +173,8 @@ const literal = (path: string) => path.replaceAll(/[*?[\]()+@]/g, "[$&]").replac
|
|
|
173
173
|
* examples row at that line. `makeWorld` makes each scenario's World, given the report so far
|
|
174
174
|
* (every file and pickle, read before the first scenario starts). A dry run matches the steps
|
|
175
175
|
* and runs none. A step's error that is a bug in code (`isBug`) is rethrown once the run is
|
|
176
|
-
* over, rather than reported as the step's failure.
|
|
176
|
+
* over, rather than reported as the step's failure. `timeoutMs` is how long a step may take
|
|
177
|
+
* (Cucumber's 5 s by default).
|
|
177
178
|
*/
|
|
178
179
|
export async function runFeatures(
|
|
179
180
|
library: ISupportCodeLibrary,
|
|
@@ -184,7 +185,15 @@ export async function runFeatures(
|
|
|
184
185
|
line,
|
|
185
186
|
dryRun = false,
|
|
186
187
|
isBug,
|
|
187
|
-
|
|
188
|
+
timeoutMs,
|
|
189
|
+
}: {
|
|
190
|
+
cwd: string;
|
|
191
|
+
files: string[];
|
|
192
|
+
line?: number;
|
|
193
|
+
dryRun?: boolean;
|
|
194
|
+
isBug: (err: unknown) => boolean;
|
|
195
|
+
timeoutMs?: number;
|
|
196
|
+
},
|
|
188
197
|
): Promise<Report> {
|
|
189
198
|
const report: Report = { query: new Query(), texts: new Map(), parseErrors: [] };
|
|
190
199
|
let bug: unknown;
|
|
@@ -200,7 +209,11 @@ export async function runFeatures(
|
|
|
200
209
|
}
|
|
201
210
|
const configuration: IRunOptions = {
|
|
202
211
|
sources: { defaultDialect: "en", paths: files.map(literal), names: [], tagExpression: "", order: "defined" },
|
|
203
|
-
support: {
|
|
212
|
+
support: {
|
|
213
|
+
...library,
|
|
214
|
+
World,
|
|
215
|
+
...(timeoutMs !== undefined && { defaultTimeout: timeoutMs }),
|
|
216
|
+
} as ISupportCodeLibrary,
|
|
204
217
|
runtime: {
|
|
205
218
|
dryRun,
|
|
206
219
|
failFast: false,
|
package/src/db.ts
CHANGED
|
@@ -81,20 +81,12 @@ const MADE_ALREADY = new Set(["23505", "42P04", "42P07", "42710"]);
|
|
|
81
81
|
/** True for the failure of a create that another process won: what was to be made is there. */
|
|
82
82
|
export const madeAlready = (err: unknown): boolean => MADE_ALREADY.has(pgCode(err));
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
* `SANOMA_POOL_SIZE`, read once: the most connections each Postgres pool of the process opens, its
|
|
86
|
-
* pg pools' and DBOS's system pool alike; unset, theirs (10 each). `sanoma demo`'s session host sets
|
|
87
|
-
* it low for each visitor's process, so a session costs a few connections, not a dozen.
|
|
88
|
-
*/
|
|
89
|
-
const askedPoolSize = Number(process.env.SANOMA_POOL_SIZE);
|
|
90
|
-
const POOL_SIZE = Number.isInteger(askedPoolSize) && askedPoolSize > 0 ? askedPoolSize : undefined;
|
|
91
|
-
|
|
92
84
|
/**
|
|
93
85
|
* A pool on the config's database for `what` (a word for the messages). An idle connection that
|
|
94
86
|
* drops (Postgres restarted) is replaced on the next query; unheard, it would end the process.
|
|
95
87
|
*/
|
|
96
88
|
export function openPool(databaseUrl: string, what: string): Pool {
|
|
97
|
-
const pool = new Pool({ connectionString: databaseUrl
|
|
89
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
98
90
|
pool.on("error", (err) => warn(`a ${what} connection failed: ${errorMessage(err)}`));
|
|
99
91
|
return pool;
|
|
100
92
|
}
|
|
@@ -107,10 +99,7 @@ export function openPool(databaseUrl: string, what: string): Pool {
|
|
|
107
99
|
* queries at once on one client, which pg deprecates. `prepareDatabase` sets the same 60 s guard
|
|
108
100
|
* on the database instead. Remove the switch when DBOS awaits its `SET`.
|
|
109
101
|
*/
|
|
110
|
-
export const DBOS_DATABASE_OPTIONS = {
|
|
111
|
-
systemDatabaseIdleTransactionTimeoutMs: 0,
|
|
112
|
-
...(POOL_SIZE !== undefined && { systemDatabasePoolSize: POOL_SIZE }),
|
|
113
|
-
} as const;
|
|
102
|
+
export const DBOS_DATABASE_OPTIONS = { systemDatabaseIdleTransactionTimeoutMs: 0 } as const;
|
|
114
103
|
|
|
115
104
|
/**
|
|
116
105
|
* Stamps the change runs `ids` still unfinished on another version with the application version
|
package/src/fake.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
DriverError,
|
|
8
8
|
type OpIdOf,
|
|
9
9
|
type Specs,
|
|
10
|
-
withSetup,
|
|
11
10
|
} from "./op.ts";
|
|
12
11
|
import { fail, type Instance, UNREGISTERED, type WakeupApi, wakeupCalls } from "./wakeup-driver.ts";
|
|
13
12
|
import { INSTANCE_TOKEN, instanceUrl } from "./wakeup-service.ts";
|
|
@@ -73,7 +72,7 @@ type Fault =
|
|
|
73
72
|
| { kind: "hold"; released: Promise<void> };
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
|
-
* Builds a fake vendor for a connector, for tests and
|
|
75
|
+
* Builds a fake vendor for a connector, for tests and sandbox runs. `ops` implements the operations
|
|
77
76
|
* against `state`, typed by the connector as `defineDriver` does. The fake adds what a real
|
|
78
77
|
* vendor does around them: a repeated idempotency key gets the first reply and changes
|
|
79
78
|
* nothing. A test can make the next call fail, lose its reply, hit a rate limit or wait.
|
|
@@ -159,17 +158,6 @@ export function defineFake<V extends string, S extends Specs, T extends Record<s
|
|
|
159
158
|
};
|
|
160
159
|
}
|
|
161
160
|
|
|
162
|
-
/**
|
|
163
|
-
* `connector` set up with this fake instead of the one its package attaches, for a test or a demo
|
|
164
|
-
* that seeds or reads the fake its runs call: live runs call its driver, and sandbox runs call it
|
|
165
|
-
* too, since its maker gives this very one. A driver swap alone is `withSetup(connector, { driver })`,
|
|
166
|
-
* which keeps the package's fake, so each sandbox run has one of its own. The operations are the
|
|
167
|
-
* same.
|
|
168
|
-
*/
|
|
169
|
-
export function withFake<C extends Connector<any, any>>(connector: C, fake: Fake<any, any>): C {
|
|
170
|
-
return withSetup(connector, { driver: () => fake.driver, fake: () => fake });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
161
|
// The built-in `sanoma.wakeup`'s fake: an in-memory wakeup service that calls the host's `/wake`.
|
|
174
162
|
|
|
175
163
|
/** An instance of the fake wakeup service, as it keeps one. */
|
package/src/index.ts
CHANGED
package/src/resource.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { checkDatabase, type DatabaseSpec } from "./database.ts";
|
|
3
|
+
import { isRunControlError } from "./errors.ts";
|
|
3
4
|
import { checkHost, type HostSpec } from "./host.ts";
|
|
4
5
|
import { type Connector, type OpSpec, RESOURCE, VENDOR } from "./op.ts";
|
|
5
6
|
import { isSanomaSecret, isSecret, type Secret } from "./secret.ts";
|
|
7
|
+
import { errorMessage } from "./shared.ts";
|
|
6
8
|
import { checkWakeup, type WakeupSpec } from "./wakeup.ts";
|
|
7
9
|
|
|
8
10
|
/** Which fields of a resource are flagged, by dotted path (`pages.html_url`; a list's items have no index). */
|
|
@@ -123,6 +125,47 @@ export interface AfterCreate {
|
|
|
123
125
|
until?: (output: any) => boolean;
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/** How long apply waits between calls of an `afterCreate` operation with `until`, and how many calls it makes. */
|
|
129
|
+
export const AFTER_CREATE_SECONDS = 5;
|
|
130
|
+
export const AFTER_CREATE_CALLS = 120;
|
|
131
|
+
/** How long an `afterCreate` operation may be polled, in all, in milliseconds. */
|
|
132
|
+
export const AFTER_CREATE_MS = AFTER_CREATE_SECONDS * AFTER_CREATE_CALLS * 1000;
|
|
133
|
+
|
|
134
|
+
/** Why an `afterCreate` operation with `until` did not pass: the time it was given, and its last call's error if that failed. */
|
|
135
|
+
export const afterCreateStuck = (op: string, failed: unknown): string =>
|
|
136
|
+
`${op} did not pass in ${AFTER_CREATE_MS / 60_000} minutes` +
|
|
137
|
+
(failed === undefined ? "" : `; its last call failed: ${errorMessage(failed)}`);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Calls one `afterCreate` operation for a new object as apply does: with `until`, again every
|
|
141
|
+
* few seconds until its output passes, a call that fails meanwhile being one that did not pass;
|
|
142
|
+
* without, once, a failure thrown. Returns why it did not pass, if it did not. Apply inlines the
|
|
143
|
+
* same loop in its `run`, which may not close over its `ctx`; an operation's own test calls this.
|
|
144
|
+
*/
|
|
145
|
+
export async function callAfterCreate(
|
|
146
|
+
hook: AfterCreate,
|
|
147
|
+
made: { id: string; export: string },
|
|
148
|
+
call: (op: string, input: unknown) => Promise<unknown>,
|
|
149
|
+
sleep: (seconds: number) => Promise<void>,
|
|
150
|
+
): Promise<string | undefined> {
|
|
151
|
+
const given = hook.input(made);
|
|
152
|
+
let output: unknown;
|
|
153
|
+
let failed: unknown;
|
|
154
|
+
for (let n = 0; n < AFTER_CREATE_CALLS && (n === 0 || (hook.until && !hook.until(output))); n++) {
|
|
155
|
+
if (n > 0) await sleep(AFTER_CREATE_SECONDS);
|
|
156
|
+
try {
|
|
157
|
+
output = await call(hook.op, given);
|
|
158
|
+
failed = undefined;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (isRunControlError(err) || !hook.until) throw err;
|
|
161
|
+
output = undefined;
|
|
162
|
+
failed = err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (hook.until && !hook.until(output)) return afterCreateStuck(hook.op, failed);
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
126
169
|
/**
|
|
127
170
|
* Where a declared resource keeps its brand, its type's `<vendor>.<type>`: a symbol, so a field
|
|
128
171
|
* holding a resource is told from data; a registered one (`Symbol.for`), so a copy of this
|
package/src/run.ts
CHANGED
|
@@ -46,8 +46,6 @@ export interface WorkerState {
|
|
|
46
46
|
drivers: Map<string, DriverFn>;
|
|
47
47
|
/** The makers of the config's fake vendors, by vendor id: a sandbox run makes its own of each when it is seeded. */
|
|
48
48
|
fakes: Map<string, () => Fake<any, any>>;
|
|
49
|
-
/** The instance's fakes, by vendor id, when it runs on them (`fakes: "instance"`): its sandbox runs seed and call these, not fakes of their own. */
|
|
50
|
-
instanceFakes?: ReadonlyMap<string, Fake<any, any>>;
|
|
51
49
|
/** Where sandbox runs' scenarios are, the resource types their steps may name, and the steps the connectors ship. */
|
|
52
50
|
scenarios?: URL;
|
|
53
51
|
/** The worker's `source`, when it was given one. */
|
package/src/sandbox.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { opIdsOf } from "./define.ts";
|
|
|
4
4
|
import { SanomaError } from "./errors.ts";
|
|
5
5
|
import type { Fake, ResourceFake } from "./fake.ts";
|
|
6
6
|
import { entry, type LedgerRecord, write } from "./ledger.ts";
|
|
7
|
-
import type {
|
|
7
|
+
import type { DriverFn } from "./op.ts";
|
|
8
8
|
import type { Run } from "./run.ts";
|
|
9
9
|
import { memorySecrets } from "./secret.ts";
|
|
10
10
|
import { type Given, loadScenarios, type Scenario } from "./scenario.ts";
|
|
@@ -58,7 +58,7 @@ export function seedResource(
|
|
|
58
58
|
scenario: string,
|
|
59
59
|
copy: ResourceFake,
|
|
60
60
|
given: Extract<Given, { resource: string }>,
|
|
61
|
-
):
|
|
61
|
+
): Extract<LedgerRecord, { type: "scenario.seeded" }>["seeds"][number] {
|
|
62
62
|
const { resource, type, name } = given;
|
|
63
63
|
try {
|
|
64
64
|
if ("gone" in given) {
|
|
@@ -77,81 +77,16 @@ export function seedResource(
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/** What a scenario's `Given` steps seeded, as its `scenario.seeded` record has it. */
|
|
81
|
-
export type Seeds = Extract<LedgerRecord, { type: "scenario.seeded" }>["seeds"];
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Where a scenario's `Given` steps go (`applyGivens`): a sandbox run's own fakes (`run`), or the
|
|
85
|
-
* instance a demo runs (`instance`), its collections in the config's database and every vendor
|
|
86
|
-
* on its one fake, which every run shares, so a fault, injected into a call some other run would
|
|
87
|
-
* make, is refused there.
|
|
88
|
-
*/
|
|
89
|
-
export interface GivenTarget {
|
|
90
|
-
kind: "run" | "instance";
|
|
91
|
-
/** What a `Given` operation is called through, by operation id. */
|
|
92
|
-
drivers: ReadonlyMap<string, DriverFn>;
|
|
93
|
-
/** The fakes, by vendor id: what a `Given` resource is put in, and a `run`'s faults injected into. */
|
|
94
|
-
fakes: ReadonlyMap<string, Fake<unknown>>;
|
|
95
|
-
/** The operations by id, for a fault's vendor. */
|
|
96
|
-
ops: ReadonlyMap<string, Op>;
|
|
97
|
-
/** The context of the call the `i`th `Given` makes. */
|
|
98
|
-
call(i: number, op: string): CallContext;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Applies the scenario's `Given` steps to the target: calls each operation through its drivers,
|
|
103
|
-
* puts or removes each resource in its vendor's fake, then, for a `run`, injects the faults.
|
|
104
|
-
* Checks first that every operation it seeds has a driver there, that every resource's vendor has
|
|
105
|
-
* a fake holding resources, and, for an `instance`, that it injects no fault. Returns what it
|
|
106
|
-
* seeded, in order.
|
|
107
|
-
*/
|
|
108
|
-
export async function applyGivens(scenario: Pick<Scenario, "name" | "given">, target: GivenTarget): Promise<Seeds> {
|
|
109
|
-
const { name, given } = scenario;
|
|
110
|
-
const fault = given.find((g) => "fault" in g);
|
|
111
|
-
if (fault && target.kind === "instance") {
|
|
112
|
-
throw new SanomaError(
|
|
113
|
-
"invalid_input",
|
|
114
|
-
`Scenario "${name}" injects a fault ("${fault.step}"), but its Givens go to the instance, whose fakes every run shares`,
|
|
115
|
-
{ scenario: name, step: fault.step },
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
const missing = [...new Set(given.flatMap((g) => ("op" in g && !target.drivers.has(g.op) ? [g.op] : [])))];
|
|
119
|
-
if (missing.length) {
|
|
120
|
-
throw new SanomaError(
|
|
121
|
-
"invalid_input",
|
|
122
|
-
`Scenario "${name}" seeds ${missing.join(", ")}, but no fake its connectors attach implements ${missing.length > 1 ? "them" : "it"}`,
|
|
123
|
-
{ scenario: name, missing },
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const plain = missingResourceFake(scenario, target.fakes);
|
|
127
|
-
if (plain) throw plain;
|
|
128
|
-
const seeded: Seeds = [];
|
|
129
|
-
for (const [i, g] of given.entries()) {
|
|
130
|
-
if ("fault" in g) continue;
|
|
131
|
-
if ("op" in g) {
|
|
132
|
-
const output = await target.drivers.get(g.op)!(g.input, target.call(i, g.op));
|
|
133
|
-
seeded.push({ op: g.op, input: g.input, output });
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
// Checked above: the vendor's fake holds resources.
|
|
137
|
-
seeded.push(seedResource(name, target.fakes.get(g.vendor) as Fake<unknown> & ResourceFake, g));
|
|
138
|
-
}
|
|
139
|
-
for (const g of given) {
|
|
140
|
-
if ("fault" in g) target.fakes.get(target.ops.get(g.op)!.vendor);
|
|
141
|
-
}
|
|
142
|
-
return seeded;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
80
|
/**
|
|
146
81
|
* Seeds a sandbox run of the scenario `name`, from the worker, in one step: reads the scenario,
|
|
147
|
-
* checks that it runs this workflow,
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
82
|
+
* checks that it runs this workflow, makes the run's own fakes (each vendor's maker, called once
|
|
83
|
+
* for the run), checks that every operation the workflow uses or the scenario seeds has one and
|
|
84
|
+
* that every resource it seeds has a fake holding resources, calls each `Given` operation
|
|
85
|
+
* through them, puts or removes each `Given` resource in them, injects the faults into them,
|
|
86
|
+
* then puts their functions on the run. The run then calls those only, so sandbox runs share no
|
|
87
|
+
* state when the makers make a new fake each. The step's checkpoint never stands in for them: a
|
|
88
|
+
* run whose seeding is replayed from it has none, and fails, since a sandbox run does not
|
|
89
|
+
* survive a worker restart.
|
|
155
90
|
*/
|
|
156
91
|
export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
157
92
|
const { state } = run;
|
|
@@ -168,19 +103,20 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
168
103
|
throw new SanomaError("invalid_input", `No scenario named "${name}"${why}`, { scenario: name });
|
|
169
104
|
}
|
|
170
105
|
if (scenario.workflow !== run.workflow) {
|
|
171
|
-
|
|
172
|
-
throw new SanomaError("invalid_input", `Scenario "${name}" ${runs}, not ${run.workflow}`, {
|
|
106
|
+
throw new SanomaError("invalid_input", `Scenario "${name}" runs ${scenario.workflow}, not ${run.workflow}`, {
|
|
173
107
|
scenario: name,
|
|
174
|
-
|
|
108
|
+
workflow: scenario.workflow,
|
|
175
109
|
});
|
|
176
110
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const drivers =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
111
|
+
const fakes = new Map([...state.fakes].map(([vendor, make]) => [vendor, make()]));
|
|
112
|
+
// The built-ins first, so a connector's fake that implements one of their operations wins.
|
|
113
|
+
const drivers = indexDrivers(
|
|
114
|
+
[...sandboxDrivers(state.ops), ...Array.from(fakes.values(), (f) => f.driver)],
|
|
115
|
+
state.ops,
|
|
116
|
+
);
|
|
117
|
+
const uses = opIdsOf(state.workflows.get(run.workflow)!);
|
|
118
|
+
const seeding = scenario.given.flatMap((g) => ("op" in g ? [g.op] : []));
|
|
119
|
+
const missing = [...new Set([...uses, ...seeding])].filter((id) => !drivers.has(id));
|
|
184
120
|
if (missing.length) {
|
|
185
121
|
throw new SanomaError(
|
|
186
122
|
"invalid_input",
|
|
@@ -188,20 +124,28 @@ export async function seedSandbox(run: Run, name: string): Promise<void> {
|
|
|
188
124
|
{ scenario: name, missing },
|
|
189
125
|
);
|
|
190
126
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
127
|
+
const plain = missingResourceFake(scenario, fakes);
|
|
128
|
+
if (plain) throw plain;
|
|
129
|
+
const seeded: Extract<LedgerRecord, { type: "scenario.seeded" }>["seeds"] = [];
|
|
130
|
+
for (const [i, given] of scenario.given.entries()) {
|
|
131
|
+
if ("fault" in given) continue;
|
|
132
|
+
if ("op" in given) {
|
|
133
|
+
const call = { idempotencyKey: `${run.id}:seed:${i}`, runId: run.id, opId: given.op, attempt: 1, secrets };
|
|
134
|
+
const output = await drivers.get(given.op)!(given.input, call);
|
|
135
|
+
seeded.push({ op: given.op, input: given.input, output });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Checked above: the vendor's fake holds resources.
|
|
139
|
+
seeded.push(seedResource(name, fakes.get(given.vendor) as Fake<unknown> & ResourceFake, given));
|
|
140
|
+
}
|
|
141
|
+
for (const given of scenario.given) {
|
|
142
|
+
if ("fault" in given) fakes.get(state.ops.get(given.op)!.vendor);
|
|
143
|
+
}
|
|
198
144
|
copies = drivers;
|
|
199
145
|
return { seeds: seeded };
|
|
200
146
|
},
|
|
201
147
|
{ name: "sandbox:seed" },
|
|
202
148
|
);
|
|
203
|
-
// Replayed on an instance that runs on fakes: they are the instance's, still there in this process.
|
|
204
|
-
if (!copies && state.instanceFakes) copies = state.drivers;
|
|
205
149
|
if (!copies) {
|
|
206
150
|
// Replayed from its checkpoint (after a worker restart, or in a fork), so the run has no fakes.
|
|
207
151
|
// The records written before the restart hold the seqs from here on: the failure goes after them.
|