@ripplo/testing 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,6 +77,17 @@ export const preconditions = { authLoggedIn, dataProject };
77
77
 
78
78
  `precondition(name)` → `.description()` → `.requires()` (optional) → `.contract<T>()`. Each field in `T` must be a primitive — `string`, `number`, or `boolean` (run-scoped value).
79
79
 
80
+ #### Preconditions are create-only
81
+
82
+ Precondition setups must be **additive**: insert new rows, don't `update` or `delete` existing ones. Concurrent runs share a database, and a `WHERE` clause that looks scoped to the current run can match rows that belong to another run in flight. Even when writes are reliably scoped, mutating a row another precondition produced creates ordering dependencies that break under composition.
83
+
84
+ If a test needs a non-default state, the precondition that **creates** the underlying row should accept that state as input — don't seed a default and mutate it from a downstream precondition.
85
+
86
+ Allowed exceptions:
87
+
88
+ - `upsert` on a row whose primary key makes it a 1:1 settings record for the current run (e.g. a per-`(userId, resourceId)` view). Treat it as create-with-default.
89
+ - Teardown may delete — but only data the precondition itself created.
90
+
80
91
  ### Observers
81
92
 
82
93
  ```typescript
@@ -158,6 +169,7 @@ import {
158
169
  navigate,
159
170
  scrollIntoView,
160
171
  drag,
172
+ fixture,
161
173
  upload,
162
174
  handleDialog,
163
175
  clipboard,
@@ -171,10 +183,22 @@ fill(role("textbox", "Email"), "test@x.com"); // clear + type
171
183
  select(role("combobox", "Role"), "admin");
172
184
  check(role("checkbox", "Terms"));
173
185
  press("Enter");
174
- upload(testId("file-input"), "./test.png");
186
+ upload(testId("file-input"), fixture("logo.png"));
175
187
  drag(role("row", "Item 1"), role("row", "Item 2"));
176
188
  ```
177
189
 
190
+ ### Upload fixtures
191
+
192
+ `upload()` requires a `fixture()` reference. Fixture files live in `.ripplo/fixtures/` (committed). Limits: 10 MB per file, 50 MB total.
193
+
194
+ ```ts
195
+ // .ripplo/fixtures/logo.png exists
196
+ upload(testId("logo-input"), fixture("logo.png"));
197
+
198
+ // Multiple files
199
+ upload(testId("attachments"), [fixture("a.pdf"), fixture("b.pdf")]);
200
+ ```
201
+
178
202
  ### Assertions
179
203
 
180
204
  ```typescript
@@ -265,12 +289,21 @@ import { prisma } from "../lib/prisma.js";
265
289
  export const engine = createEngine(ripplo, {
266
290
  preconditions: {
267
291
  authLoggedIn: {
268
- setup: async (ctx) => {
269
- // create user, set cookies via ctx.setCookie()
270
- return { userId: ctx.uniqueId("user") };
292
+ // setup receives a *batch* of items — one per concurrent run that requires this precondition.
293
+ // Issue one bulk write (createMany) and return one result per item, in input order.
294
+ setup: async (items) => {
295
+ const seeds = items.map(({ ctx }) => ({
296
+ id: ctx.uniqueId("user"),
297
+ email: ctx.uniqueEmail(),
298
+ }));
299
+ await prisma.user.createMany({ data: seeds });
300
+ // each item.ctx.setCookie(...) flows back to that run's browser
301
+ return seeds.map(({ id }) => ({ userId: id }));
271
302
  },
272
- teardown: async (ctx) => {
273
- // clean up using ctx.data.userId
303
+ teardown: async (items) => {
304
+ await prisma.user.deleteMany({
305
+ where: { id: { in: items.map((it) => it.ctx.data.userId) } },
306
+ });
274
307
  },
275
308
  },
276
309
  dataProject: notImplemented("awaiting prisma seed helper"), // stub for planning
@@ -289,15 +322,17 @@ export const engine = createEngine(ripplo, {
289
322
  });
290
323
  ```
291
324
 
325
+ `setup` and `teardown` are **batched**: the runtime collects all concurrent runs that need a precondition within a 200ms window and calls the impl once with the full batch. Use `createMany` / `deleteMany` over per-item `create` / `delete` to keep DB load proportional to wall-clock time, not run count. The result array length must equal the input array length and order must be preserved (the engine zips by index back to runs).
326
+
292
327
  ### Setup context
293
328
 
294
- `ctx` passed to each precondition `setup`:
329
+ `item.ctx` available inside each batched setup item:
295
330
 
296
331
  - `ctx.runId` — unique 12-char id for this run
297
332
  - `ctx.fixed<T extends string | number | boolean>(value: T)` — static test value (any primitive)
298
333
  - `ctx.uniqueId(prefix)` — `ripplo-test-<prefix>-<runId>`
299
334
  - `ctx.uniqueEmail()` — `ripplo-test-<runId>@test.ripplo.ai`
300
- - `ctx.setCookie(name, value, options?)` — forwarded to the test browser as `Set-Cookie`
335
+ - `ctx.setCookie(name, value, options?)` — applied to the run's browser context before the test starts
301
336
 
302
337
  Helpers return plain primitives — interpolate, JSON.stringify, or pass through observer params directly. The return type is type-branded, so a hardcoded literal in a `setup` return fails at compile time.
303
338
 
@@ -434,10 +469,14 @@ const app = new Elysia().group("/ripplo", (g) => g.use(createElysiaHandler({ ena
434
469
  For unsupported frameworks. The adapters above are thin wrappers over this API.
435
470
 
436
471
  ```ts
437
- import { buildSetCookieHeader, serializeCookie, verifyWebhookSignature } from "@ripplo/testing";
472
+ import {
473
+ readAdapterWebhookSecret,
474
+ toBatchRunResults,
475
+ verifyWebhookSignature,
476
+ } from "@ripplo/testing";
438
477
  import { engine } from "./test/engine.js";
439
478
 
440
- const webhookSecret = engine.getConfig().webhookSecret;
479
+ const webhookSecret = readAdapterWebhookSecret();
441
480
 
442
481
  async function executePreconditions(req: Request): Promise<Response> {
443
482
  const body = await req.text();
@@ -454,27 +493,31 @@ async function executePreconditions(req: Request): Promise<Response> {
454
493
  return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 401 });
455
494
  }
456
495
 
457
- const { preconditions } = JSON.parse(body);
496
+ // Request body is a batch: { batch: [{ runId, preconditions: [...names] }, ...] }
497
+ const { batch } = JSON.parse(body);
458
498
  const appUrl = `${req.headers.get("x-forwarded-proto") ?? "http"}://${req.headers.get("host")}`;
459
- const result = await engine.executePreconditions(preconditions, { appUrl });
460
-
461
- const headers = new Headers({ "content-type": "application/json" });
462
- result.cookies.forEach((c) =>
463
- headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(c))),
499
+ const results = await engine.executePreconditions(
500
+ batch.map((b) => ({ runId: b.runId, names: b.preconditions })),
501
+ { appUrl },
464
502
  );
465
- return new Response(JSON.stringify(result), { headers });
503
+
504
+ // Response body: { results: [{ runId, ok, cookies, data, executed } | { runId, ok: false, error }] }
505
+ return new Response(JSON.stringify({ results: toBatchRunResults(results) }), {
506
+ headers: { "content-type": "application/json" },
507
+ });
466
508
  }
467
509
  // teardown-preconditions and execute-observer follow the same verify-then-dispatch pattern.
510
+ // teardown's request shape is { batch: [{ runId, preconditions, data }, ...] }, response { results: [{ runId, ok, error? }] }.
468
511
  ```
469
512
 
470
- You're responsible for: webhook verification (always before invoking the engine), routing the three endpoints, forwarding `result.cookies` to the test browser as `Set-Cookie` headers, and reading the raw body for signature verification before `JSON.parse`.
513
+ You're responsible for: webhook verification (always before invoking the engine), routing the three endpoints, calling `toBatchRunResults` / `toTeardownResults` to shape the response body, and reading the raw body for signature verification before `JSON.parse`. Cookies travel inside the JSON response body — never as `Set-Cookie` headers; the runtime parses them out and applies them to the test browser context.
471
514
 
472
515
  ## Security & parallelism
473
516
 
474
517
  - All requests signed via Standard Webhooks (HMAC-SHA256). Headers: `webhook-id`, `webhook-timestamp`, `webhook-signature`. **Always verify before executing.**
475
518
  - `ENABLE_RIPPLO_TESTING` gates every adapter. Never expose in production.
476
519
  - Use `ctx.uniqueId(prefix)` / `ctx.uniqueEmail()` so parallel runs don't collide.
477
- - Return created entity IDs in the data contract; teardown deletes only that run's data using the captured `runId`. Never bulk-delete.
520
+ - Return created entity IDs in the data contract; teardown deletes only data this run created. Bulk operations are fine — and encouraged — as long as the `WHERE` is scoped to the batch's own ids (`deleteMany({ where: { id: { in: items.map((it) => it.ctx.data.userId) } } })`). Never write a `WHERE` that could match another run's rows.
478
521
 
479
522
  ## Lockfile
480
523
 
package/dist/actions.d.ts CHANGED
@@ -3,6 +3,14 @@ import { U as UnlabeledStep } from './step-De52hTLd.js';
3
3
  import { CheckLocator, InputLocator, AnyLocator, SelectLocator } from './locators.js';
4
4
  import '@ripplo/spec';
5
5
 
6
+ declare class Fixture {
7
+ readonly kind: "fixture";
8
+ readonly name: string;
9
+ constructor(name: string);
10
+ }
11
+ declare function fixture(name: string): Fixture;
12
+ declare function isFixture(value: unknown): value is Fixture;
13
+
6
14
  type StringOrVariable = string | Variable<string>;
7
15
  declare function navigate(url: string): UnlabeledStep<{
8
16
  type: "goto";
@@ -87,7 +95,7 @@ declare function press(key: string): UnlabeledStep<{
87
95
  key: string;
88
96
  type: "press";
89
97
  }>;
90
- declare function upload(locator: AnyLocator, path: string, options?: StepOptions): UnlabeledStep<{
98
+ declare function upload(locator: AnyLocator, files: Fixture | ReadonlyArray<Fixture>, options?: StepOptions): UnlabeledStep<{
91
99
  files: string[];
92
100
  locator: {
93
101
  by: "testId";
@@ -247,4 +255,4 @@ declare function setViewport({ height, width }: SetViewportOptions): UnlabeledSt
247
255
  width: number;
248
256
  }>;
249
257
 
250
- export { check, clear, click, clipboard, dblclick, drag, fill, focus, handleDialog, hover, navigate, press, rightClick, scrollIntoView, select, setPermission, setViewport, typeText, uncheck, upload };
258
+ export { Fixture, check, clear, click, clipboard, dblclick, drag, fill, fixture, focus, handleDialog, hover, isFixture, navigate, press, rightClick, scrollIntoView, select, setPermission, setViewport, typeText, uncheck, upload };
package/dist/actions.js CHANGED
@@ -9,6 +9,24 @@ import {
9
9
  } from "./chunk-MGATMMCZ.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
+ // src/steps/fixture.ts
13
+ var Fixture = class {
14
+ kind = "fixture";
15
+ name;
16
+ constructor(name) {
17
+ if (name.length === 0) {
18
+ throw new Error("fixture(name) requires a non-empty name");
19
+ }
20
+ this.name = name;
21
+ }
22
+ };
23
+ function fixture(name) {
24
+ return new Fixture(name);
25
+ }
26
+ function isFixture(value) {
27
+ return value instanceof Fixture;
28
+ }
29
+
12
30
  // src/steps/actions.ts
13
31
  function navigate(url) {
14
32
  return createStep({ type: "goto", url: { type: "static", value: url } });
@@ -46,9 +64,10 @@ function hover(locator) {
46
64
  function press(key) {
47
65
  return createStep({ key, type: "press" });
48
66
  }
49
- function upload(locator, path, options) {
67
+ function upload(locator, files, options) {
68
+ const list = files instanceof Fixture ? [files] : [...files];
50
69
  return createStep({
51
- files: [path],
70
+ files: list.map((f) => f.name),
52
71
  locator: toSpecLocator(locator),
53
72
  type: "upload",
54
73
  uiOnly: options?.uiOnly
@@ -101,6 +120,7 @@ function setViewport({ height, width }) {
101
120
  return createStep({ height, type: "setViewport", width });
102
121
  }
103
122
  export {
123
+ Fixture,
104
124
  check,
105
125
  clear,
106
126
  click,
@@ -108,9 +128,11 @@ export {
108
128
  dblclick,
109
129
  drag,
110
130
  fill,
131
+ fixture,
111
132
  focus,
112
133
  handleDialog,
113
134
  hover,
135
+ isFixture,
114
136
  navigate,
115
137
  press,
116
138
  rightClick,
package/dist/assert.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { O as ObserverHandle, a as ObserverInput, P as Primitive, b as ObserverBudgetTier } from './types-16SB7zjP.js';
1
+ import { O as ObserverHandle, a as ObserverInput, P as Primitive, b as ObserverBudgetTier } from './types-BzZrl65Z.js';
2
2
  import { Variable, StaticStringRef, VariableRef } from './control.js';
3
3
  import { U as UnlabeledStep } from './step-De52hTLd.js';
4
4
  import { CheckLocator, AnyLocator } from './locators.js';
@@ -1,4 +1,4 @@
1
- import { P as Primitive, O as ObserverHandle, h as Precondition, m as PreconditionData, j as TestDefinition, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, k as TestValue, T as TeardownContext } from './types-16SB7zjP.js';
1
+ import { P as Primitive, O as ObserverHandle, h as Precondition, m as PreconditionData, j as TestDefinition, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, e as ObserverContext, g as ObserverOutcome, S as SetupContext, k as TestValue, T as TeardownContext } from './types-BzZrl65Z.js';
2
2
  import { ObserverBudget } from '@ripplo/spec';
3
3
  import { S as Step } from './step-De52hTLd.js';
4
4
 
@@ -90,9 +90,16 @@ interface TestNeedsCoverage {
90
90
  type PreconditionSetupResult<TData extends Record<string, Primitive>> = {
91
91
  readonly [K in keyof TData]: TestValue<TData[K]>;
92
92
  };
93
+ interface PreconditionSetupItem<TDeps extends Record<string, Record<string, Primitive>>> {
94
+ readonly ctx: SetupContext;
95
+ readonly deps: TDeps;
96
+ }
97
+ interface PreconditionTeardownItem<TData extends Record<string, Primitive>> {
98
+ readonly ctx: TeardownContext<TData>;
99
+ }
93
100
  interface PreconditionImpl<TData extends Record<string, Primitive>, TDeps extends Record<string, Record<string, Primitive>>> {
94
- readonly setup: (ctx: SetupContext, deps: TDeps) => Promise<PreconditionSetupResult<TData>>;
95
- readonly teardown: (ctx: TeardownContext<TData>) => Promise<void>;
101
+ readonly setup: (items: ReadonlyArray<PreconditionSetupItem<TDeps>>) => Promise<ReadonlyArray<PreconditionSetupResult<TData>>>;
102
+ readonly teardown: (items: ReadonlyArray<PreconditionTeardownItem<TData>>) => Promise<void>;
96
103
  }
97
104
  type ObserverImplFn<TInput extends Record<string, Primitive>> = (ctx: ObserverContext, params: TInput) => Promise<ObserverOutcome>;
98
105
  interface TestOptions {
@@ -10,26 +10,28 @@ function readAdapterWebhookSecret() {
10
10
  }
11
11
  return value;
12
12
  }
13
+ var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
14
+ var dataSchema = z.record(z.string(), z.record(z.string(), primitiveSchema));
13
15
  var batchRequestSchema = z.object({
14
- preconditions: z.array(z.string().min(1))
16
+ batch: z.array(
17
+ z.object({
18
+ preconditions: z.array(z.string().min(1)),
19
+ runId: z.string().min(1)
20
+ })
21
+ ).min(1)
15
22
  });
16
23
  var teardownRequestSchema = z.object({
17
- data: z.record(z.string(), z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))),
18
- preconditions: z.array(z.string().min(1))
24
+ batch: z.array(
25
+ z.object({
26
+ data: dataSchema,
27
+ preconditions: z.array(z.string().min(1)),
28
+ runId: z.string().min(1)
29
+ })
30
+ ).min(1)
19
31
  });
20
32
  var observerRequestSchema = z.object({
21
33
  observer: z.string().min(1).max(200),
22
- params: z.record(z.string().max(200), z.union([z.string(), z.number(), z.boolean()]))
23
- });
24
- var observerOutcomeSchema = z.discriminatedUnion("kind", [
25
- z.object({ kind: z.literal("pass") }),
26
- z.object({ kind: z.literal("retry"), reason: z.string() }),
27
- z.object({ kind: z.literal("fail"), reason: z.string() })
28
- ]);
29
- var observerResponseSchema = z.object({
30
- error: z.string().optional(),
31
- outcome: observerOutcomeSchema.optional(),
32
- success: z.boolean()
34
+ params: z.record(z.string().max(200), primitiveSchema)
33
35
  });
34
36
  function verifyWebhookSignature(payload, headers, secret) {
35
37
  try {
@@ -50,7 +52,7 @@ function verifyWebhookSignature(payload, headers, secret) {
50
52
  function serializeCookie(cookie) {
51
53
  return {
52
54
  domain: cookie.options?.domain,
53
- expires: cookie.options?.expires == null ? void 0 : new Date(cookie.options.expires * 1e3),
55
+ expires: cookie.options?.expires,
54
56
  httpOnly: cookie.options?.httpOnly,
55
57
  name: cookie.name,
56
58
  path: cookie.options?.path,
@@ -59,28 +61,19 @@ function serializeCookie(cookie) {
59
61
  value: cookie.value
60
62
  };
61
63
  }
62
- function buildSetCookieHeader(cookie) {
63
- const parts = [`${cookie.name}=${cookie.value}`];
64
- if (cookie.domain != null) {
65
- parts.push(`Domain=${cookie.domain}`);
66
- }
67
- if (cookie.path != null) {
68
- parts.push(`Path=${cookie.path}`);
69
- }
70
- if (cookie.expires != null) {
71
- parts.push(`Expires=${cookie.expires.toUTCString()}`);
72
- }
73
- if (cookie.httpOnly === true) {
74
- parts.push("HttpOnly");
75
- }
76
- if (cookie.secure === true) {
77
- parts.push("Secure");
78
- }
79
- if (cookie.sameSite != null) {
80
- const capitalized = cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1);
81
- parts.push(`SameSite=${capitalized}`);
82
- }
83
- return parts.join("; ");
64
+ function toBatchRunResults(results) {
65
+ return results.map(
66
+ (r) => r.success ? {
67
+ cookies: r.cookies.map((c) => serializeCookie(c)),
68
+ data: r.data,
69
+ executed: [...r.executed],
70
+ ok: true,
71
+ runId: r.runId
72
+ } : { error: r.error ?? "unknown error", ok: false, runId: r.runId }
73
+ );
74
+ }
75
+ function toTeardownResults(results) {
76
+ return results.map((r) => ({ error: r.error, ok: r.success, runId: r.runId }));
84
77
  }
85
78
 
86
79
  export {
@@ -90,5 +83,6 @@ export {
90
83
  observerRequestSchema,
91
84
  verifyWebhookSignature,
92
85
  serializeCookie,
93
- buildSetCookieHeader
86
+ toBatchRunResults,
87
+ toTeardownResults
94
88
  };
@@ -25,7 +25,7 @@ function compile(ripplo) {
25
25
  };
26
26
  });
27
27
  const tests = testDefs.map((def) => compileTest(def, preconditionDefs));
28
- return { observers, preconditions, tests };
28
+ return { fixtures: {}, observers, preconditions, tests };
29
29
  }
30
30
  function validateUniqueIds(defs) {
31
31
  const seen = /* @__PURE__ */ new Map();
@@ -1,9 +1,14 @@
1
1
  import { Observer, Precondition, WorkflowSpec } from '@ripplo/spec';
2
- import { d as RipploBuilder } from './builder-BMjy83Iy.js';
3
- import './types-16SB7zjP.js';
2
+ import { d as RipploBuilder } from './builder-DiVz3t1D.js';
3
+ import './types-BzZrl65Z.js';
4
4
  import './step-De52hTLd.js';
5
5
 
6
+ interface CompiledFixture {
7
+ readonly sha256: string;
8
+ readonly size: number;
9
+ }
6
10
  interface CompileResult {
11
+ readonly fixtures: Record<string, CompiledFixture>;
7
12
  readonly observers: Record<string, Observer>;
8
13
  readonly preconditions: Record<string, Precondition>;
9
14
  readonly tests: ReadonlyArray<CompiledTest>;
@@ -24,4 +29,4 @@ interface CompiledTest {
24
29
  }
25
30
  declare function compile(ripplo: RipploBuilder): CompileResult;
26
31
 
27
- export { type CompileResult, type CompiledTest, compile };
32
+ export { type CompileResult, type CompiledFixture, type CompiledTest, compile };
package/dist/compiler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compile
3
- } from "./chunk-DL3HLCD7.js";
3
+ } from "./chunk-YFOTJIVF.js";
4
4
  import "./chunk-MGATMMCZ.js";
5
5
  import "./chunk-4MGIQFAJ.js";
6
6
  export {
package/dist/elysia.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7
 
package/dist/elysia.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  batchRequestSchema,
3
- buildSetCookieHeader,
4
3
  observerRequestSchema,
5
4
  readAdapterWebhookSecret,
6
- serializeCookie,
7
5
  teardownRequestSchema,
6
+ toBatchRunResults,
7
+ toTeardownResults,
8
8
  verifyWebhookSignature
9
- } from "./chunk-V6LMXKGL.js";
9
+ } from "./chunk-XO36IU66.js";
10
10
  import "./chunk-4MGIQFAJ.js";
11
11
 
12
12
  // src/adapters/elysia.ts
@@ -27,29 +27,17 @@ function build({ enabled, engine }) {
27
27
  }
28
28
  const parsed = parseWith(gate.body, batchRequestSchema);
29
29
  if (parsed == null) {
30
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
30
+ return jsonResponse({ error: "Invalid request body" }, 400);
31
31
  }
32
32
  const host = request.headers.get("host");
33
33
  if (host == null || host.length === 0) {
34
- return jsonResponse({ error: "Missing host header", success: false }, 400);
34
+ return jsonResponse({ error: "Missing host header" }, 400);
35
35
  }
36
36
  const proto = request.headers.get("x-forwarded-proto") ?? "http";
37
37
  const appUrl = `${proto}://${host}`;
38
- const result = await engine.executePreconditions(parsed.preconditions, { appUrl });
39
- const headers = new Headers({ "content-type": "application/json" });
40
- result.cookies.forEach((cookie) => {
41
- headers.append("Set-Cookie", buildSetCookieHeader(serializeCookie(cookie)));
42
- });
43
- return new Response(
44
- JSON.stringify({
45
- data: result.data,
46
- error: result.error,
47
- executed: result.executed,
48
- runId: result.runId,
49
- success: result.success
50
- }),
51
- { headers, status: 200 }
52
- );
38
+ const items = parsed.batch.map((b) => ({ names: b.preconditions, runId: b.runId }));
39
+ const results = await engine.executePreconditions(items, { appUrl });
40
+ return jsonResponse({ results: toBatchRunResults(results) }, 200);
53
41
  }).put("/execute-observer", async ({ request }) => {
54
42
  const gate = await verifyAndReadBody(request, webhookSecret);
55
43
  if ("response" in gate) {
@@ -71,10 +59,15 @@ function build({ enabled, engine }) {
71
59
  }
72
60
  const parsed = parseWith(gate.body, teardownRequestSchema);
73
61
  if (parsed == null) {
74
- return jsonResponse({ error: "Invalid request body", success: false }, 400);
62
+ return jsonResponse({ error: "Invalid request body" }, 400);
75
63
  }
76
- await engine.teardown(parsed.preconditions, parsed.data);
77
- return jsonResponse({ success: true }, 200);
64
+ const items = parsed.batch.map((b) => ({
65
+ data: b.data,
66
+ names: b.preconditions,
67
+ runId: b.runId
68
+ }));
69
+ const results = await engine.teardown(items);
70
+ return jsonResponse({ results: toTeardownResults(results) }, 200);
78
71
  });
79
72
  }
80
73
  async function verifyAndReadBody(req, webhookSecret) {
@@ -1,7 +1,7 @@
1
- import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-BMjy83Iy.js';
2
- import { P as Primitive, g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, h as Precondition } from './types-16SB7zjP.js';
1
+ import { c as PreconditionRegistry, a as ObserverRegistry, O as ObserverImplFn, P as PreconditionImpl, e as RipploInstance } from './builder-DiVz3t1D.js';
2
+ import { P as Primitive, g as ObserverOutcome, C as CookieEntry, f as ObserverDefinition, l as PreconditionDefinition, U as UnimplementedItems, O as ObserverHandle, h as Precondition } from './types-BzZrl65Z.js';
3
3
 
4
- interface EngineResult {
4
+ interface EngineRunResult {
5
5
  readonly cookies: ReadonlyArray<CookieEntry>;
6
6
  readonly data: Record<string, Record<string, Primitive>>;
7
7
  readonly error: string | undefined;
@@ -12,6 +12,20 @@ interface EngineResult {
12
12
  interface ExecuteBatchOptions {
13
13
  readonly appUrl: string | undefined;
14
14
  }
15
+ interface ExecuteBatchItem {
16
+ readonly names: ReadonlyArray<string>;
17
+ readonly runId: string;
18
+ }
19
+ interface TeardownBatchItem {
20
+ readonly data: Record<string, Record<string, Primitive>>;
21
+ readonly names: ReadonlyArray<string>;
22
+ readonly runId: string;
23
+ }
24
+ interface TeardownRunResult {
25
+ readonly error: string | undefined;
26
+ readonly runId: string;
27
+ readonly success: boolean;
28
+ }
15
29
  interface ObserverExecutionResult {
16
30
  readonly error: string | undefined;
17
31
  readonly outcome: ObserverOutcome | undefined;
@@ -19,11 +33,11 @@ interface ObserverExecutionResult {
19
33
  }
20
34
  interface RipploEngine {
21
35
  readonly executeObserver: (name: string, params: Record<string, Primitive>) => Promise<ObserverExecutionResult>;
22
- readonly executePreconditions: (names: ReadonlyArray<string>, options?: ExecuteBatchOptions) => Promise<EngineResult>;
36
+ readonly executePreconditions: (items: ReadonlyArray<ExecuteBatchItem>, options?: ExecuteBatchOptions) => Promise<ReadonlyArray<EngineRunResult>>;
23
37
  readonly getObservers: () => ReadonlyArray<ObserverDefinition>;
24
38
  readonly getPreconditions: () => ReadonlyArray<PreconditionDefinition>;
25
39
  readonly getUnimplemented: () => UnimplementedItems;
26
- readonly teardown: (names: ReadonlyArray<string>, data: Record<string, Record<string, Primitive>>) => Promise<void>;
40
+ readonly teardown: (items: ReadonlyArray<TeardownBatchItem>) => Promise<ReadonlyArray<TeardownRunResult>>;
27
41
  }
28
42
  declare const NOT_IMPLEMENTED_BRAND: unique symbol;
29
43
  interface NotImplemented {
@@ -43,4 +57,4 @@ interface EngineImpls<P extends PreconditionRegistry, O extends ObserverRegistry
43
57
  }
44
58
  declare function createEngine<P extends PreconditionRegistry, O extends ObserverRegistry>(ripplo: RipploInstance<P, O>, impls: EngineImpls<P, O>): RipploEngine;
45
59
 
46
- export { type EngineImpls as E, type NotImplemented as N, type ObserverImplFnFor as O, type PreconditionImplFor as P, type RipploEngine as R, type EngineResult as a, type ExecuteBatchOptions as b, createEngine as c, notImplemented as n };
60
+ export { type EngineRunResult as E, type NotImplemented as N, type ObserverImplFnFor as O, type PreconditionImplFor as P, type RipploEngine as R, type TeardownRunResult as T, type EngineImpls as a, type ExecuteBatchItem as b, type ExecuteBatchOptions as c, type TeardownBatchItem as d, createEngine as e, notImplemented as n };
package/dist/express.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Router } from 'express';
2
- import { R as RipploEngine } from './engine-DMOkJdjd.js';
3
- import './builder-BMjy83Iy.js';
4
- import './types-16SB7zjP.js';
2
+ import { R as RipploEngine } from './engine-DVbF4E5A.js';
3
+ import './builder-DiVz3t1D.js';
4
+ import './types-BzZrl65Z.js';
5
5
  import './step-De52hTLd.js';
6
6
  import '@ripplo/spec';
7
7