Skip to content

Commit adeebea

Browse files
route one many
1 parent 113823c commit adeebea

49 files changed

Lines changed: 2839 additions & 437 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/route-one-many.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@walkeros/core': minor
3+
'@walkeros/collector': minor
4+
'@walkeros/cli': patch
5+
'@walkeros/mcp': patch
6+
---
7+
8+
Route grammar: rename `case` to `one` (first-match dispatch) and add `many`
9+
(all-match parallel fan-out, pre-collector only). `many` terminates the main
10+
chain and is rejected at post-collector positions (`destination.before`,
11+
`destination.next`); use multiple destinations for post-collector fan-out.
12+
`RouteCaseConfig` is renamed to `RouteOneConfig`; no aliases.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@walkeros/core': minor
3+
'@walkeros/collector': minor
4+
'@walkeros/mcp': minor
5+
'@walkeros/server-transformer-file': patch
6+
---
7+
8+
Add `Flow.Store.cache` for store-level caching: read-through + write-through
9+
wrapper with single-flight dedup, recursive composition via `cache.store`, and
10+
per-wrapper counters. `CacheRule` is now a discriminated union
11+
(`EventCacheRule | StoreCacheRule`); schema rejects inert fields in store
12+
contexts.
13+
14+
Built-in `__cache` upgraded with LRU, `maxEntries: 10000`, batched eviction, and
15+
active TTL sweep.
16+
17+
**Breaking:** `@walkeros/store-memory` is removed. Its logic is absorbed into
18+
`__cache`. Migration: drop the store declaration, or omit `cache.store` to use
19+
the built-in tier. `flow_validate` flags legacy references.

.claude-plugin/plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"skills/walkeros-understanding-development",
3232
"skills/walkeros-using-logger",
3333
"skills/walkeros-using-cli",
34+
"skills/walkeros-using-store-cache",
3435
"skills/walkeros-using-transformer-ga4",
3536
"skills/walkeros-create-destination",
3637
"skills/walkeros-create-source",

AGENT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Learn the concepts before coding:
2929
| [understanding-stores](skills/understanding-stores/SKILL.md) | Store interface, $store. wiring, lifecycle |
3030
| [using-logger](skills/using-logger/SKILL.md) | Logger access, DRY principles, when to log |
3131
| [using-step-examples](skills/using-step-examples/SKILL.md) | Step examples lifecycle, Three Type Zones, testing |
32+
| [using-store-cache](skills/walkeros-using-store-cache/SKILL.md) | Recipes for store-level cache, multi-tier composition |
3233
| [using-transformer-ga4](skills/walkeros-using-transformer-ga4/SKILL.md) | Wire `@walkeros/transformer-ga4`, override mappings, troubleshoot decoding |
3334

3435
## Creating Things

packages/cli/src/commands/bundle/bundler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function generateInlineCode(
108108
inline: Flow.Code,
109109
config: object,
110110
env?: object,
111-
chains?: { before?: Transformer.RouteSpec; next?: Transformer.RouteSpec },
111+
chains?: { before?: Transformer.Route; next?: Transformer.Route },
112112
isDestination?: boolean,
113113
): string {
114114
const pushFn = inline.push.replace('$code:', '');

packages/cli/src/commands/push/__tests__/simulate-isolation.test.ts

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,80 @@ import type {
44
Transformer,
55
WalkerOS,
66
} from '@walkeros/core';
7-
import { createIngest, createMockLogger } from '@walkeros/core';
7+
import { createIngest, createMockLogger, getNextSteps } from '@walkeros/core';
88
import {
99
destinationInit,
1010
destinationPush,
1111
transformerInit,
1212
transformerPush,
1313
runTransformerChain,
14-
walkChain,
15-
extractTransformerNextMap,
1614
} from '@walkeros/collector';
17-
import { compileNext, resolveNext } from '@walkeros/core';
1815

1916
/**
20-
* Resolve a `RouteSpec` to the static form expected by `walkChain` in these
21-
* fixture tests. Conditional shapes (`case` / `gate`) resolve through
22-
* `resolveNext` with an empty context, surfacing the result the engine would
23-
* compute for a request with no ingest or event.
17+
* Local mirror of the collector's internal chain walker. The collector no
18+
* longer exports `walkChain` from its public surface (Task 5.1 hard cut), so
19+
* these fixture tests carry their own minimal walker for resolving before
20+
* chains to ordered transformer-id arrays.
2421
*/
25-
function staticChain(
26-
spec: Transformer.RouteSpec | undefined,
27-
): string | string[] | undefined {
28-
if (spec === undefined) return undefined;
29-
return resolveNext(compileNext(spec)) ?? undefined;
22+
function localWalkChain(
23+
startId: string | string[] | undefined,
24+
transformers: Transformer.Transformers,
25+
): string[] {
26+
if (!startId) return [];
27+
if (Array.isArray(startId)) return startId;
28+
29+
const chain: string[] = [];
30+
const visited = new Set<string>();
31+
let current: string | undefined = startId;
32+
33+
while (current && transformers[current]) {
34+
if (visited.has(current)) break;
35+
visited.add(current);
36+
chain.push(current);
37+
38+
const next: Transformer.Route | undefined =
39+
transformers[current].config?.next;
40+
if (typeof next === 'string') {
41+
current = next;
42+
continue;
43+
}
44+
if (
45+
Array.isArray(next) &&
46+
next.every((entry) => typeof entry === 'string')
47+
) {
48+
for (const id of next) chain.push(id);
49+
break;
50+
}
51+
break;
52+
}
53+
54+
return chain;
55+
}
56+
57+
/**
58+
* Resolve a Route to an ordered chain of transformer ids. Uses the public
59+
* `getNextSteps` to compute entry points, then walks `.next` links.
60+
*/
61+
function resolveChain(
62+
spec: Transformer.Route | undefined,
63+
transformers: Transformer.Transformers,
64+
): string[] {
65+
if (spec === undefined) return [];
66+
if (typeof spec === 'string') return localWalkChain(spec, transformers);
67+
if (Array.isArray(spec) && spec.every((entry) => typeof entry === 'string')) {
68+
return localWalkChain(spec, transformers);
69+
}
70+
const ids = getNextSteps(spec);
71+
if (ids.length === 0) return [];
72+
if (ids.length === 1) return localWalkChain(ids[0], transformers);
73+
return ids;
3074
}
3175

3276
/**
3377
* Tests for before-chain execution in transformer simulation.
3478
*
35-
* Validates the pattern: resolve before chain -> run via runTransformerChain
36-
* -> then call transformerPush on the main transformer.
79+
* Validates the pattern: resolve before chain (via `resolveChain`) -> run via
80+
* runTransformerChain -> then call transformerPush on the main transformer.
3781
*
3882
* This mirrors the logic in simulateTransformer without requiring
3983
* a real ESM bundle.
@@ -112,10 +156,7 @@ describe('transformer simulation isolation — before chain', () => {
112156

113157
// Step 1: Resolve before chain
114158
const before = transformer.config.before;
115-
const beforeChainIds = walkChain(
116-
staticChain(before),
117-
extractTransformerNextMap(transformers),
118-
);
159+
const beforeChainIds = resolveChain(before, transformers);
119160
expect(beforeChainIds).toEqual(['enrich']);
120161

121162
// Step 2: Run before chain
@@ -242,10 +283,7 @@ describe('transformer simulation isolation — before chain', () => {
242283

243284
// Resolve before chain
244285
const before = transformer.config.before;
245-
const beforeChainIds = walkChain(
246-
staticChain(before),
247-
extractTransformerNextMap(transformers),
248-
);
286+
const beforeChainIds = resolveChain(before, transformers);
249287
expect(beforeChainIds).toEqual(['gate']);
250288

251289
// Run before chain — gate drops the event
@@ -313,10 +351,7 @@ describe('transformer simulation isolation — before chain', () => {
313351

314352
// Resolve before chain — should follow validate -> enrich via next link
315353
const before = transformer.config.before;
316-
const beforeChainIds = walkChain(
317-
staticChain(before),
318-
extractTransformerNextMap(transformers),
319-
);
354+
const beforeChainIds = resolveChain(before, transformers);
320355
expect(beforeChainIds).toEqual(['validate', 'enrich']);
321356

322357
// Run before chain
@@ -488,10 +523,7 @@ describe('destination simulation with before chain', () => {
488523
const before = destination.config.before;
489524
let processedEvent: WalkerOS.Event = inputEvent;
490525
if (before && collector.transformers) {
491-
const beforeChainIds = walkChain(
492-
staticChain(before),
493-
extractTransformerNextMap(collector.transformers),
494-
);
526+
const beforeChainIds = resolveChain(before, collector.transformers);
495527
expect(beforeChainIds).toEqual(['enrich']);
496528

497529
const beforeResult = await runTransformerChain(
@@ -564,10 +596,7 @@ describe('destination simulation with before chain', () => {
564596

565597
// Resolve and run before chain
566598
const before = destination.config.before;
567-
const beforeChainIds = walkChain(
568-
staticChain(before),
569-
extractTransformerNextMap(collector.transformers!),
570-
);
599+
const beforeChainIds = resolveChain(before, collector.transformers!);
571600
expect(beforeChainIds).toEqual(['gate']);
572601

573602
const beforeResult = await runTransformerChain(

packages/cli/src/commands/push/index.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import fs from 'fs-extra';
33
import {
44
createIngest,
55
getPlatform,
6-
compileNext,
7-
resolveNext,
6+
getNextSteps,
87
buildCacheContext,
98
} from '@walkeros/core';
109
import {
1110
transformerInit,
1211
transformerPush,
1312
runTransformerChain,
14-
walkChain,
15-
extractTransformerNextMap,
1613
wrapEnv,
1714
} from '@walkeros/collector';
1815
import { createCLILogger } from '../../core/cli-logger.js';
@@ -46,24 +43,66 @@ function isRecord(value: unknown): value is Record<string, unknown> {
4643
return value !== null && typeof value === 'object';
4744
}
4845

46+
function isString(value: unknown): value is string {
47+
return typeof value === 'string';
48+
}
49+
50+
/**
51+
* Walk a transformer chain via static `.next` links starting at `startId`.
52+
* Mirrors the collector's internal `walkChain` for the case where the
53+
* simulator already knows the entry-point id and the underlying chain is
54+
* static. Conditional `.next` shapes terminate the walk at this hop.
55+
*/
56+
function walkStaticChain(
57+
startId: string,
58+
transformers: import('@walkeros/core').Transformer.Transformers,
59+
): string[] {
60+
const chain: string[] = [];
61+
const visited = new Set<string>();
62+
let current: string | undefined = startId;
63+
64+
while (current && transformers[current]) {
65+
if (visited.has(current)) break;
66+
visited.add(current);
67+
chain.push(current);
68+
69+
const next: import('@walkeros/core').Transformer.Route | undefined =
70+
transformers[current].config?.next;
71+
if (typeof next === 'string') {
72+
current = next;
73+
continue;
74+
}
75+
if (Array.isArray(next) && next.every(isString)) {
76+
chain.push(...next);
77+
break;
78+
}
79+
// Conditional / undefined → terminate walk.
80+
break;
81+
}
82+
83+
return chain;
84+
}
85+
4986
/**
5087
* Resolve a before chain config to an ordered array of transformer IDs.
51-
* All Route shapes (string, sequence, RouteConfig) go through compileNext +
52-
* resolveNext; the engine handles narrowing.
88+
* Uses `getNextSteps` for the entry points and follows static `.next`
89+
* links via `walkStaticChain`.
5390
*/
5491
function resolveBeforeChain(
55-
before: import('@walkeros/core').Transformer.RouteSpec | undefined,
92+
before: import('@walkeros/core').Transformer.Route | undefined,
5693
transformers: import('@walkeros/core').Transformer.Transformers,
5794
ingest?: import('@walkeros/core').Ingest,
5895
event?: WalkerOS.DeepPartialEvent,
5996
): string[] {
6097
if (!before) return [];
61-
const resolved = resolveNext(
62-
compileNext(before),
63-
buildCacheContext(ingest, event),
64-
);
65-
if (!resolved) return [];
66-
return walkChain(resolved, extractTransformerNextMap(transformers));
98+
// Explicit string[] chain — use as-is.
99+
if (Array.isArray(before) && before.every(isString)) {
100+
return before;
101+
}
102+
const ids = getNextSteps(before, buildCacheContext(ingest, event));
103+
if (ids.length === 0) return [];
104+
if (ids.length === 1) return walkStaticChain(ids[0], transformers);
105+
return ids;
67106
}
68107

69108
/**

0 commit comments

Comments
 (0)