waygraph 0.15.33 → 0.15.35

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.
@@ -45,6 +45,11 @@ export function branch(block, routes) {
45
45
  },
46
46
  routes: routesAsData,
47
47
  });
48
+ // Without this, branch()'s output carried no __waygraphKind/__waygraphSalt at all (defineBlock
49
+ // never stamps them - factories do, after defineBlock returns), so a branched Block could never
50
+ // pass MapBuilder's assertMapKind/assertMapSalt checks. Real bug: branching was unusable from the
51
+ // Waygraph Map, only from a raw runGraph() call.
52
+ copyWaygraphRuntime(block, routed);
48
53
  return routed;
49
54
  }
50
55
  /**
@@ -269,6 +269,43 @@ export declare class MapBuilder<Out extends Checkpoint<string>> {
269
269
  method<NextOut extends Checkpoint<string>>(block: (MethodBlock<Out, NextOut> | EffectBlock<Out, NextOut>) & {
270
270
  name: string;
271
271
  }): MapBuilder<NextOut>;
272
+ /**
273
+ * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
274
+ * resolved Checkpoint's tag - same page, same context, same mem. `routes` must cover every tag
275
+ * of `Out` (a missing one is a compile error, matching {@link branch}'s own exhaustiveness);
276
+ * `null` means that tag is terminal (the branched Flow just returns that Checkpoint).
277
+ *
278
+ * Each non-null route is a function, not a ready-made `Flow` - a fresh Flow always starts at the
279
+ * special `S` (start) Checkpoint, but a branch's blocks start from the TAG it routes on (e.g.
280
+ * `LoggedIn`), so the function is handed a brand-new `MapBuilder` already seeded at that exact
281
+ * Checkpoint to chain `.method()/.gotoPage()/.assert()/.end()` (or another `.branch()`) off of.
282
+ *
283
+ * Real gap this fixes: the plain `branch()` helper's `.next` routing is only ever consulted by
284
+ * `runGraph`'s own graph walk - `defineFlow`/`.end()` compose steps pairwise via `connect()`,
285
+ * which never looks at `.next` at all, so a Map-built Flow had no way to branch. This method
286
+ * branches at the Flow level instead: it finalizes the prefix, runs it, and picks the next Flow
287
+ * itself once the real tag is known - no engine-level routing involved.
288
+ *
289
+ * Only the `run(context, mem[, options])` signature is supported - the page must survive across
290
+ * the branch, and the bare `run(mem, config)` convenience form always closes its own browser
291
+ * before a tag is even known, so it throws instead of silently reopening a fresh (state-losing) page.
292
+ *
293
+ * The route's first Block still sees `input.__state === "__start__"` in `act()`, not the tag that
294
+ * routed there - `runGraph` seeds every Flow's entry Block that way unconditionally, the same as
295
+ * any standalone `defineFlow([start, block, end])` already does. Read `page`/`mem`, not `input`,
296
+ * in `act()` - every real Block in this codebase already follows that convention.
297
+ *
298
+ * @example
299
+ * map({ homeOrigin }).gotoPage(NavCartBlock).branch({
300
+ * ItemInCart: (m) => m.method(RemoveFromCartBlock).end(),
301
+ * LoggedIn: (m) => m.method(AddToCartBlock).end(),
302
+ * });
303
+ */
304
+ branch<Routes extends {
305
+ [K in Out["__state"]]: ((m: MapBuilder<Extract<Out, Checkpoint<K>>>) => Flow<any>) | null;
306
+ }>(routes: Routes): Flow<{
307
+ [K in keyof Routes & string]: Routes[K] extends (m: any) => Flow<infer R> ? R : Extract<Out, Checkpoint<K>>;
308
+ }[keyof Routes & string]>;
272
309
  /**
273
310
  * Finalizes this chain into a real, runnable {@link Flow} - same object
274
311
  * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
@@ -1,5 +1,6 @@
1
1
  import { end, start } from "./run-graph.js";
2
2
  import { buildFlow } from "./flow.js";
3
+ import { MemPage } from "../mem-page.js";
3
4
  import { fastForwardComposeBlock } from "./compose.js";
4
5
  /**
5
6
  * Holds engine-level configuration shared across every flow defined from it -
@@ -104,6 +105,59 @@ function assertMapOrigin(block, homeOrigin, expect, method) {
104
105
  `home origin (${wantOrigin}) - use .gotoPage() for an internal destination.`);
105
106
  }
106
107
  }
108
+ /**
109
+ * Builds the `Flow` object `MapBuilder.branch()` returns: run `before`, then dispatch on the
110
+ * resolved tag into whichever `routes` entry matches (same page/context/mem), or return that
111
+ * Checkpoint as-is when its route is `null`. Factored out so `withBlockVerify`/`modBlockVerify`
112
+ * can patch `before` and rebuild the same branched shape, exactly like a plain Flow's own.
113
+ */
114
+ function buildBranchedFlow(before, routes) {
115
+ const run = (async (contextOrMem, memOrConfig, options) => {
116
+ if (contextOrMem instanceof MemPage) {
117
+ throw new Error("Waygraph map: a .branch()-ed Flow can't run via run(mem, config) - that convenience form " +
118
+ "always closes its own browser/page before a branch's tag is even known. Call " +
119
+ "run(context, mem[, options]) instead, keeping the same page across the branch.");
120
+ }
121
+ const context = contextOrMem;
122
+ const mem = memOrConfig;
123
+ const gotOwnPage = options?.page === undefined;
124
+ const page = options?.page ?? (await context.newPage());
125
+ const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
126
+ const first = (await before.run(context, mem, { ...options, page, closeOnFinish: false }));
127
+ const branchFlow = routes[first.result.__state];
128
+ if (!branchFlow) {
129
+ if (options?.closeOnFinish === false)
130
+ return { result: first.result, page: first.page };
131
+ if (closeOnFinish)
132
+ await first.page.close();
133
+ return first.result;
134
+ }
135
+ return branchFlow.run(context, mem, { ...options, page: first.page, closeOnFinish });
136
+ });
137
+ return {
138
+ resetSession: before.resetSession,
139
+ ...(before.title !== undefined ? { title: before.title } : {}),
140
+ ...(before.expectedFailureReason !== undefined ? { expectedFailureReason: before.expectedFailureReason } : {}),
141
+ ...(before.highlightFixtures !== undefined ? { highlightFixtures: before.highlightFixtures } : {}),
142
+ ...(before.demoPace !== undefined ? { demoPace: before.demoPace } : {}),
143
+ ...(before.highlightStyle !== undefined ? { highlightStyle: before.highlightStyle } : {}),
144
+ ...(before.memStub !== undefined ? { memStub: before.memStub } : {}),
145
+ // Static introspection (`waygraph graph`/list): the fixed prefix, plus every branch's own
146
+ // Blocks prefixed by the tag that leads to them - can't know at analysis time which one a real
147
+ // run takes, so this shows all of them rather than none.
148
+ blocks: () => [
149
+ ...before.blocks(),
150
+ ...Object.entries(routes).flatMap(([tag, f]) => f ? f.blocks().map((b) => ({ ...b, name: `${tag} -> ${b.name}` })) : []),
151
+ ],
152
+ run,
153
+ withBlockVerify(block, verify) {
154
+ return buildBranchedFlow(before.withBlockVerify(block, verify), routes);
155
+ },
156
+ modBlockVerify(block, nameOrIndex, newCheck) {
157
+ return buildBranchedFlow(before.modBlockVerify(block, nameOrIndex, newCheck), routes);
158
+ },
159
+ };
160
+ }
107
161
  /**
108
162
  * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
109
163
  * comment for why this exists. Each step method is scoped to exactly the
@@ -226,6 +280,50 @@ export class MapBuilder {
226
280
  assertMapSalt(block, ["method", "effect"], "method");
227
281
  return this.appendStep(block);
228
282
  }
283
+ /**
284
+ * Branch: run everything added so far, then continue into whichever `routes[tag]` matches the
285
+ * resolved Checkpoint's tag - same page, same context, same mem. `routes` must cover every tag
286
+ * of `Out` (a missing one is a compile error, matching {@link branch}'s own exhaustiveness);
287
+ * `null` means that tag is terminal (the branched Flow just returns that Checkpoint).
288
+ *
289
+ * Each non-null route is a function, not a ready-made `Flow` - a fresh Flow always starts at the
290
+ * special `S` (start) Checkpoint, but a branch's blocks start from the TAG it routes on (e.g.
291
+ * `LoggedIn`), so the function is handed a brand-new `MapBuilder` already seeded at that exact
292
+ * Checkpoint to chain `.method()/.gotoPage()/.assert()/.end()` (or another `.branch()`) off of.
293
+ *
294
+ * Real gap this fixes: the plain `branch()` helper's `.next` routing is only ever consulted by
295
+ * `runGraph`'s own graph walk - `defineFlow`/`.end()` compose steps pairwise via `connect()`,
296
+ * which never looks at `.next` at all, so a Map-built Flow had no way to branch. This method
297
+ * branches at the Flow level instead: it finalizes the prefix, runs it, and picks the next Flow
298
+ * itself once the real tag is known - no engine-level routing involved.
299
+ *
300
+ * Only the `run(context, mem[, options])` signature is supported - the page must survive across
301
+ * the branch, and the bare `run(mem, config)` convenience form always closes its own browser
302
+ * before a tag is even known, so it throws instead of silently reopening a fresh (state-losing) page.
303
+ *
304
+ * The route's first Block still sees `input.__state === "__start__"` in `act()`, not the tag that
305
+ * routed there - `runGraph` seeds every Flow's entry Block that way unconditionally, the same as
306
+ * any standalone `defineFlow([start, block, end])` already does. Read `page`/`mem`, not `input`,
307
+ * in `act()` - every real Block in this codebase already follows that convention.
308
+ *
309
+ * @example
310
+ * map({ homeOrigin }).gotoPage(NavCartBlock).branch({
311
+ * ItemInCart: (m) => m.method(RemoveFromCartBlock).end(),
312
+ * LoggedIn: (m) => m.method(AddToCartBlock).end(),
313
+ * });
314
+ */
315
+ branch(routes) {
316
+ if (this.steps.length === 0) {
317
+ throw new Error("Waygraph map: .branch() needs at least one prior step - nothing to branch from. " +
318
+ "Add .gotoPage()/.gotoExternal()/.method()/.assert() first.");
319
+ }
320
+ const before = this.end();
321
+ const resolvedRoutes = {};
322
+ for (const [tag, fn] of Object.entries(routes)) {
323
+ resolvedRoutes[tag] = fn ? fn(new MapBuilder(this.engine, [], this.homeOrigin, null)) : null;
324
+ }
325
+ return buildBranchedFlow(before, resolvedRoutes);
326
+ }
229
327
  /**
230
328
  * Finalizes this chain into a real, runnable {@link Flow} - same object
231
329
  * `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.33",
3
+ "version": "0.15.35",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",