@rotorsoft/act 0.9.0 → 0.10.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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @module projection-builder
3
+ * @category Builders
4
+ *
5
+ * Fluent builder for composing projection handlers — read-model updaters
6
+ * that react to events and update external state (databases, caches, etc.).
7
+ *
8
+ * Projections differ from slices: they don't contain states, don't dispatch
9
+ * actions, and are pure side-effect handlers routed to a named stream.
10
+ */
11
+ import type { ZodType } from "zod";
12
+ import type { Committed, EventRegister, ReactionResolver, Schema, Schemas } from "./types/index.js";
13
+ /**
14
+ * A self-contained projection grouping read-model update handlers.
15
+ * Projections are composed into an Act orchestrator via `act().with(projection)`.
16
+ *
17
+ * @template E - Event schemas handled by this projection
18
+ */
19
+ export type Projection<E extends Schemas> = {
20
+ readonly _tag: "Projection";
21
+ readonly events: EventRegister<E>;
22
+ };
23
+ /**
24
+ * Type guard for distinguishing Projection from State and Slice objects.
25
+ */
26
+ export declare function isProjection(x: any): x is Projection<any>;
27
+ /** Helper: a single-key record mapping an event name to its Zod schema. */
28
+ type EventEntry<K extends string = string, D extends Schema = Schema> = {
29
+ [P in K]: ZodType<D>;
30
+ };
31
+ /** Infer the handler-result type after registering one event. */
32
+ type DoResult<E extends Schemas, K extends string, D extends Schema> = ProjectionBuilder<E & {
33
+ [P in K]: D;
34
+ }> & {
35
+ to: (resolver: ReactionResolver<E & {
36
+ [P in K]: D;
37
+ }, K> | string) => ProjectionBuilder<E & {
38
+ [P in K]: D;
39
+ }>;
40
+ void: () => ProjectionBuilder<E & {
41
+ [P in K]: D;
42
+ }>;
43
+ };
44
+ /**
45
+ * Fluent builder interface for composing projections.
46
+ *
47
+ * Provides a chainable API for registering event handlers that update
48
+ * read models. Unlike slices, projections have no `.with()` for states
49
+ * and handlers do not receive a `Dispatcher`.
50
+ *
51
+ * When a default target is provided via `projection("target")`, all
52
+ * handlers inherit that resolver. Per-handler `.to()` or `.void()` can
53
+ * still override it.
54
+ *
55
+ * @template E - Event schemas
56
+ */
57
+ export type ProjectionBuilder<E extends Schemas> = {
58
+ /**
59
+ * Begins defining a projection handler for a specific event.
60
+ *
61
+ * Pass a `{ EventName: schema }` record — use shorthand `{ EventName }`
62
+ * when the variable name matches the event name. The key becomes the
63
+ * event name, the value the Zod schema.
64
+ */
65
+ on: <K extends string, D extends Schema>(entry: EventEntry<K, D>) => {
66
+ do: (handler: (event: Committed<E & {
67
+ [P in K]: D;
68
+ }, K>, stream: string) => Promise<void>) => DoResult<E, K, D>;
69
+ };
70
+ /**
71
+ * Builds and returns the Projection data structure.
72
+ */
73
+ build: () => Projection<E>;
74
+ /**
75
+ * The registered event schemas and their reaction maps.
76
+ */
77
+ readonly events: EventRegister<E>;
78
+ };
79
+ /**
80
+ * Creates a new projection builder for composing read-model update handlers.
81
+ *
82
+ * Projections enable separation of read-model concerns from command handling.
83
+ * Each `.on({ Event }).do(handler)` call registers a handler that updates
84
+ * a projection (database table, cache, etc.) in response to events.
85
+ *
86
+ * Pass a target stream name to `projection("target")` so every handler
87
+ * inherits that resolver automatically. Omit it and use per-handler
88
+ * `.to()` / `.void()` when handlers route to different streams.
89
+ *
90
+ * @param target - Optional default target stream for all handlers
91
+ *
92
+ * @example Default target (all handlers routed to "tickets")
93
+ * ```typescript
94
+ * const TicketProjection = projection("tickets")
95
+ * .on({ TicketOpened })
96
+ * .do(async ({ stream, data }) => {
97
+ * await db.insert(tickets).values({ id: stream, ...data });
98
+ * })
99
+ * .on({ TicketClosed })
100
+ * .do(async ({ stream, data }) => {
101
+ * await db.update(tickets).set(data).where(eq(tickets.id, stream));
102
+ * })
103
+ * .build();
104
+ * ```
105
+ *
106
+ * @example Per-handler routing
107
+ * ```typescript
108
+ * const MultiProjection = projection()
109
+ * .on({ OrderPlaced })
110
+ * .do(async (event) => { ... })
111
+ * .to("orders")
112
+ * .on({ PaymentReceived })
113
+ * .do(async (event) => { ... })
114
+ * .to("payments")
115
+ * .build();
116
+ * ```
117
+ *
118
+ * @see {@link ProjectionBuilder} for builder methods
119
+ * @see {@link Projection} for the output type
120
+ */
121
+ export declare function projection<E extends Schemas = {}>(target?: string, events?: EventRegister<E>): ProjectionBuilder<E>;
122
+ export {};
123
+ //# sourceMappingURL=projection-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection-builder.d.ts","sourceRoot":"","sources":["../../src/projection-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EAGb,gBAAgB,EAChB,MAAM,EACN,OAAO,EACR,MAAM,kBAAkB,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI;IAC1C,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;CACnC,CAAC;AAEF;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAEzD;AAED,2EAA2E;AAC3E,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;KACrE,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CACrB,CAAC;AAEF,iEAAiE;AACjE,KAAK,QAAQ,CACX,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,IACd,iBAAiB,CAAC,CAAC,GAAG;KAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAAE,CAAC,GAAG;IAC3C,EAAE,EAAE,CACF,QAAQ,EAAE,gBAAgB,CAAC,CAAC,GAAG;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,EAAE,CAAC,CAAC,GAAG,MAAM,KACxD,iBAAiB,CAAC,CAAC,GAAG;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,iBAAiB,CAAC,CAAC,GAAG;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC;KAAE,CAAC,CAAC;CACpD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,OAAO,IAAI;IACjD;;;;;;OAMG;IACH,EAAE,EAAE,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EACrC,KAAK,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,KACpB;QACH,EAAE,EAAE,CACF,OAAO,EAAE,CACP,KAAK,EAAE,SAAS,CAAC,CAAC,GAAG;aAAG,CAAC,IAAI,CAAC,GAAG,CAAC;SAAE,EAAE,CAAC,CAAC,EACxC,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,IAAI,CAAC,KACf,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;KACxB,CAAC;IACF;;OAEG;IACH,KAAK,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3B;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;CACnC,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,EAAE,EAC/C,MAAM,CAAC,EAAE,MAAM,EACf,MAAM,GAAE,aAAa,CAAC,CAAC,CAA0B,GAChD,iBAAiB,CAAC,CAAC,CAAC,CAyEtB"}
@@ -81,6 +81,10 @@ export type StateBuilder<S extends Schema, N extends string = string> = {
81
81
  };
82
82
  };
83
83
  };
84
+ /** Helper: a single-key record mapping an action name to its Zod schema. */
85
+ type ActionEntry<K extends string = string, AX extends Schema = Schema> = {
86
+ [P in K]: ZodType<AX>;
87
+ };
84
88
  /**
85
89
  * Builder interface for defining actions (commands) on a state.
86
90
  *
@@ -98,24 +102,27 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
98
102
  * Defines an action (command) that can be executed on this state.
99
103
  *
100
104
  * Actions represent intents to change state - they should be named in imperative form
101
- * (e.g., "createUser", "incrementCounter", "placeOrder"). Actions are validated against
105
+ * (e.g., "CreateUser", "IncrementCounter", "PlaceOrder"). Actions are validated against
102
106
  * their schema and must emit at least one event.
103
107
  *
108
+ * Pass a `{ ActionName: schema }` record — use shorthand `{ ActionName }`
109
+ * when the variable name matches the action name. The key becomes the
110
+ * action name, the value the Zod schema.
111
+ *
104
112
  * @template K - Action name (string literal type)
105
113
  * @template AX - Action payload schema type
106
- * @param action - The action name (should be unique within this state)
107
- * @param schema - Zod schema for the action payload
114
+ * @param entry - Single-key record `{ ActionName: schema }`
108
115
  * @returns An object with `.given()` and `.emit()` for further configuration
109
116
  *
110
117
  * @example Simple action without invariants
111
118
  * ```typescript
112
- * .on("increment", z.object({ by: z.number() }))
119
+ * .on({ increment: z.object({ by: z.number() }) })
113
120
  * .emit((action) => ["Incremented", { amount: action.by }])
114
121
  * ```
115
122
  *
116
123
  * @example Action with business rules
117
124
  * ```typescript
118
- * .on("withdraw", z.object({ amount: z.number() }))
125
+ * .on({ withdraw: z.object({ amount: z.number() }) })
119
126
  * .given([
120
127
  * (_, snap) => snap.state.balance >= 0 || "Account closed",
121
128
  * (_, snap, action) => snap.state.balance >= action.amount || "Insufficient funds"
@@ -123,17 +130,14 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
123
130
  * .emit((action) => ["Withdrawn", { amount: action.amount }])
124
131
  * ```
125
132
  *
126
- * @example Action emitting multiple events
133
+ * @example Action with shorthand (variable name matches action name)
127
134
  * ```typescript
128
- * .on("completeOrder", z.object({ orderId: z.string() }))
129
- * .emit((action) => [
130
- * ["OrderCompleted", { orderId: action.orderId }],
131
- * ["InventoryReserved", { orderId: action.orderId }],
132
- * ["PaymentProcessed", { orderId: action.orderId }]
133
- * ])
135
+ * const OpenTicket = z.object({ title: z.string() });
136
+ * .on({ OpenTicket })
137
+ * .emit((action) => ["TicketOpened", { title: action.title }])
134
138
  * ```
135
139
  */
136
- on: <K extends string, AX extends Schema>(action: K, schema: ZodType<AX>) => {
140
+ on: <K extends string, AX extends Schema>(entry: ActionEntry<K, AX>) => {
137
141
  /**
138
142
  * Adds business rule invariants that must hold before the action can execute.
139
143
  *
@@ -265,7 +269,7 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
265
269
  * .init(() => ({ count: 0 }))
266
270
  * .emits({ Incremented: z.object({ amount: z.number() }) })
267
271
  * .patch({ Incremented: (event, state) => ({ count: state.count + event.data.amount }) })
268
- * .on("increment", z.object({ by: z.number() }))
272
+ * .on({ increment: z.object({ by: z.number() }) })
269
273
  * .emit((action) => ["Incremented", { amount: action.by }])
270
274
  * .build(); // Returns State<S, E, A, N>
271
275
  * ```
@@ -306,7 +310,7 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
306
310
  * .patch({
307
311
  * Incremented: (event, state) => ({ count: state.count + event.data.amount })
308
312
  * })
309
- * .on("increment", z.object({ by: z.number() }))
313
+ * .on({ increment: z.object({ by: z.number() }) })
310
314
  * .emit((action) => ["Incremented", { amount: action.by }])
311
315
  * .build();
312
316
  * ```
@@ -329,19 +333,19 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
329
333
  * Withdrawn: (event, state) => ({ balance: state.balance - event.data.amount }),
330
334
  * Closed: () => ({ status: "closed", balance: 0 })
331
335
  * })
332
- * .on("deposit", z.object({ amount: z.number() }))
336
+ * .on({ deposit: z.object({ amount: z.number() }) })
333
337
  * .given([
334
338
  * (_, snap) => snap.state.status === "open" || "Account must be open"
335
339
  * ])
336
340
  * .emit((action) => ["Deposited", { amount: action.amount }])
337
- * .on("withdraw", z.object({ amount: z.number() }))
341
+ * .on({ withdraw: z.object({ amount: z.number() }) })
338
342
  * .given([
339
343
  * (_, snap) => snap.state.status === "open" || "Account must be open",
340
344
  * (_, snap, action) =>
341
345
  * snap.state.balance >= action.amount || "Insufficient funds"
342
346
  * ])
343
347
  * .emit((action) => ["Withdrawn", { amount: action.amount }])
344
- * .on("close", z.object({}))
348
+ * .on({ close: z.object({}) })
345
349
  * .given([
346
350
  * (_, snap) => snap.state.status === "open" || "Already closed",
347
351
  * (_, snap) => snap.state.balance === 0 || "Balance must be zero"
@@ -366,9 +370,9 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
366
370
  * UserCreated: (event) => event.data,
367
371
  * UserLoggedIn: (_, state) => ({ loginCount: state.loginCount + 1 })
368
372
  * })
369
- * .on("createUser", z.object({ name: z.string(), email: z.string() }))
373
+ * .on({ createUser: z.object({ name: z.string(), email: z.string() }) })
370
374
  * .emit((action) => ["UserCreated", action])
371
- * .on("login", z.object({}))
375
+ * .on({ login: z.object({}) })
372
376
  * .emit(() => ["UserLoggedIn", {}])
373
377
  * .snap((snap) => snap.patches >= 10) // Snapshot every 10 events
374
378
  * .build();
@@ -380,4 +384,5 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
380
384
  * @see {@link https://rotorsoft.github.io/act-root/docs/examples/calculator | Calculator Example}
381
385
  */
382
386
  export declare function state<N extends string, S extends Schema>(name: N, state: ZodType<S>): StateBuilder<S, N>;
387
+ export {};
383
388
  //# sourceMappingURL=state-builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"state-builder.d.ts","sourceRoot":"","sources":["../../src/state-builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state-builder.d.ts","sourceRoot":"","sources":["../../src/state-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9B,OAAO,EACL,aAAa,EAGb,SAAS,EACT,aAAa,EACb,MAAM,EACN,OAAO,EACP,QAAQ,EACR,KAAK,EACL,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IACtE;;;;;;;;;;;;;;;;;;OAkBG;IACH,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,KAAK;QACjC;;;;;;;;;;;;;;;;;;WAkBG;QACH,KAAK,EAAE,CAAC,CAAC,SAAS,OAAO,EACvB,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,KAChB;YACH;;;;;;;;;;;;;;;;;;eAkBG;YACH,KAAK,EAAE,CACL,KAAK,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,KAEvB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;SACjC,CAAC;KACH,CAAC;CACH,CAAC;AAEF,4EAA4E;AAC5E,KAAK,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,EAAE,SAAS,MAAM,GAAG,MAAM,IAAI;KACvE,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,CACvB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,OAAO,EACjB,CAAC,SAAS,MAAM,GAAG,MAAM,IACvB;IACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,EAAE,EAAE,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,SAAS,MAAM,EACtC,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,EAAE,CAAC,KACtB;QACH;;;;;;;;;;;;;;;;;WAiBG;QACH,KAAK,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK;YAChC;;;;;;;;;;;;;;;;;eAiBG;YACH,IAAI,EAAE,CACJ,OAAO,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE;iBAAG,CAAC,IAAI,CAAC,GAAG,EAAE;aAAE,EAAE,CAAC,CAAC,KAC9C,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG;iBAAG,CAAC,IAAI,CAAC,GAAG,EAAE;aAAE,EAAE,CAAC,CAAC,CAAC;SACnD,CAAC;QACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAmCG;QACH,IAAI,EAAE,CACJ,OAAO,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE;aAAG,CAAC,IAAI,CAAC,GAAG,EAAE;SAAE,EAAE,CAAC,CAAC,KAC9C,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG;aAAG,CAAC,IAAI,CAAC,GAAG,EAAE;SAAE,EAAE,CAAC,CAAC,CAAC;KACnD,CAAC;IACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,IAAI,EAAE,CACJ,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,KACxC,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EACtD,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,GAChB,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAuBpB"}
@@ -187,7 +187,7 @@ export type Committed<E extends Schemas, K extends keyof E> = Message<E, K> & Co
187
187
  *
188
188
  * @example Using snapshot in action handler
189
189
  * ```typescript
190
- * .on("increment", z.object({ by: z.number() }))
190
+ * .on({ increment: z.object({ by: z.number() }) })
191
191
  * .emit((action, snapshot) => {
192
192
  * console.log("Current count:", snapshot.state.count);
193
193
  * console.log("Events applied:", snapshot.patches);
@@ -169,7 +169,7 @@ export declare const patch: <S extends Schema>(original: Readonly<S>, patches: R
169
169
  * const Counter = state("Counter", z.object({ count: z.number() }))
170
170
  * .init(() => ({ count: 0 }))
171
171
  * .emits({ Incremented: z.object({ by: z.number().positive() }) })
172
- * .on("increment", z.object({ by: z.number() }))
172
+ * .on({ increment: z.object({ by: z.number() }) })
173
173
  * .emit((action) => {
174
174
  * // validate() is called automatically before this runs
175
175
  * // action.by is guaranteed to be a number
package/dist/index.cjs CHANGED
@@ -53,10 +53,12 @@ __export(index_exports, {
53
53
  dispose: () => dispose,
54
54
  disposeAndExit: () => disposeAndExit,
55
55
  extend: () => extend,
56
+ isProjection: () => isProjection,
56
57
  isSlice: () => isSlice,
57
58
  logger: () => logger,
58
59
  patch: () => patch,
59
60
  port: () => port,
61
+ projection: () => projection,
60
62
  sleep: () => sleep,
61
63
  slice: () => slice,
62
64
  state: () => state,
@@ -1428,6 +1430,70 @@ var _this_ = ({ stream }) => ({
1428
1430
  });
1429
1431
  var _void_ = () => void 0;
1430
1432
 
1433
+ // src/projection-builder.ts
1434
+ function isProjection(x) {
1435
+ return x != null && x._tag === "Projection";
1436
+ }
1437
+ function projection(target, events = {}) {
1438
+ const defaultResolver = target ? { target } : void 0;
1439
+ const builder = {
1440
+ on: (entry) => {
1441
+ const keys = Object.keys(entry);
1442
+ if (keys.length !== 1) throw new Error(".on() requires exactly one key");
1443
+ const event = keys[0];
1444
+ const schema = entry[event];
1445
+ if (!(event in events)) {
1446
+ events[event] = {
1447
+ schema,
1448
+ reactions: /* @__PURE__ */ new Map()
1449
+ };
1450
+ }
1451
+ return {
1452
+ do: (handler) => {
1453
+ const reaction = {
1454
+ handler,
1455
+ resolver: defaultResolver ?? _this_,
1456
+ options: {
1457
+ blockOnError: true,
1458
+ maxRetries: 3
1459
+ }
1460
+ };
1461
+ const register = events[event];
1462
+ const name = handler.name || `${event}_${register.reactions.size}`;
1463
+ register.reactions.set(name, reaction);
1464
+ const nextBuilder = projection(
1465
+ target,
1466
+ events
1467
+ );
1468
+ return {
1469
+ ...nextBuilder,
1470
+ to(resolver) {
1471
+ register.reactions.set(name, {
1472
+ ...reaction,
1473
+ resolver: typeof resolver === "string" ? { target: resolver } : resolver
1474
+ });
1475
+ return nextBuilder;
1476
+ },
1477
+ void() {
1478
+ register.reactions.set(name, {
1479
+ ...reaction,
1480
+ resolver: _void_
1481
+ });
1482
+ return nextBuilder;
1483
+ }
1484
+ };
1485
+ }
1486
+ };
1487
+ },
1488
+ build: () => ({
1489
+ _tag: "Projection",
1490
+ events
1491
+ }),
1492
+ events
1493
+ };
1494
+ return builder;
1495
+ }
1496
+
1431
1497
  // src/slice-builder.ts
1432
1498
  function isSlice(x) {
1433
1499
  return x != null && x._tag === "Slice";
@@ -1486,6 +1552,25 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1486
1552
  }) {
1487
1553
  const builder = {
1488
1554
  with: ((input) => {
1555
+ if (isProjection(input)) {
1556
+ for (const eventName of Object.keys(input.events)) {
1557
+ const projRegister = input.events[eventName];
1558
+ const existing = registry.events[eventName];
1559
+ if (!existing) {
1560
+ registry.events[eventName] = {
1561
+ schema: projRegister.schema,
1562
+ reactions: new Map(projRegister.reactions)
1563
+ };
1564
+ } else {
1565
+ for (const [name, reaction] of projRegister.reactions) {
1566
+ let key = name;
1567
+ while (existing.reactions.has(key)) key = `${key}_p`;
1568
+ existing.reactions.set(key, reaction);
1569
+ }
1570
+ }
1571
+ }
1572
+ return act(states, registry);
1573
+ }
1489
1574
  if (isSlice(input)) {
1490
1575
  for (const s of input.states.values()) {
1491
1576
  registerState(s, states, registry.actions, registry.events);
@@ -1571,7 +1656,11 @@ function state(name, state2) {
1571
1656
  }
1572
1657
  function action_builder(state2) {
1573
1658
  return {
1574
- on(action2, schema) {
1659
+ on(entry) {
1660
+ const keys = Object.keys(entry);
1661
+ if (keys.length !== 1) throw new Error(".on() requires exactly one key");
1662
+ const action2 = keys[0];
1663
+ const schema = entry[action2];
1575
1664
  if (action2 in state2.actions)
1576
1665
  throw new Error(`Duplicate action "${action2}"`);
1577
1666
  const actions = { ...state2.actions, [action2]: schema };
@@ -1625,10 +1714,12 @@ function action_builder(state2) {
1625
1714
  dispose,
1626
1715
  disposeAndExit,
1627
1716
  extend,
1717
+ isProjection,
1628
1718
  isSlice,
1629
1719
  logger,
1630
1720
  patch,
1631
1721
  port,
1722
+ projection,
1632
1723
  sleep,
1633
1724
  slice,
1634
1725
  state,