@rotorsoft/act 0.9.0 → 0.11.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.
@@ -8,6 +8,7 @@ export * from "./act-builder.js";
8
8
  export * from "./act.js";
9
9
  export * from "./config.js";
10
10
  export * from "./ports.js";
11
+ export * from "./projection-builder.js";
11
12
  export * from "./slice-builder.js";
12
13
  export * from "./state-builder.js";
13
14
  export * from "./types/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAEtB;;;;GAIG;AACH,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAEtB;;;;GAIG;AACH,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC"}
@@ -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,14 @@ export type StateBuilder<S extends Schema, N extends string = string> = {
81
81
  };
82
82
  };
83
83
  };
84
+ /** Helper: a single-key record mapping a state name to its Zod schema. */
85
+ type StateEntry<K extends string = string, S extends Schema = Schema> = {
86
+ [P in K]: ZodType<S>;
87
+ };
88
+ /** Helper: a single-key record mapping an action name to its Zod schema. */
89
+ type ActionEntry<K extends string = string, AX extends Schema = Schema> = {
90
+ [P in K]: ZodType<AX>;
91
+ };
84
92
  /**
85
93
  * Builder interface for defining actions (commands) on a state.
86
94
  *
@@ -98,24 +106,27 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
98
106
  * Defines an action (command) that can be executed on this state.
99
107
  *
100
108
  * Actions represent intents to change state - they should be named in imperative form
101
- * (e.g., "createUser", "incrementCounter", "placeOrder"). Actions are validated against
109
+ * (e.g., "CreateUser", "IncrementCounter", "PlaceOrder"). Actions are validated against
102
110
  * their schema and must emit at least one event.
103
111
  *
112
+ * Pass a `{ ActionName: schema }` record — use shorthand `{ ActionName }`
113
+ * when the variable name matches the action name. The key becomes the
114
+ * action name, the value the Zod schema.
115
+ *
104
116
  * @template K - Action name (string literal type)
105
117
  * @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
118
+ * @param entry - Single-key record `{ ActionName: schema }`
108
119
  * @returns An object with `.given()` and `.emit()` for further configuration
109
120
  *
110
121
  * @example Simple action without invariants
111
122
  * ```typescript
112
- * .on("increment", z.object({ by: z.number() }))
123
+ * .on({ increment: z.object({ by: z.number() }) })
113
124
  * .emit((action) => ["Incremented", { amount: action.by }])
114
125
  * ```
115
126
  *
116
127
  * @example Action with business rules
117
128
  * ```typescript
118
- * .on("withdraw", z.object({ amount: z.number() }))
129
+ * .on({ withdraw: z.object({ amount: z.number() }) })
119
130
  * .given([
120
131
  * (_, snap) => snap.state.balance >= 0 || "Account closed",
121
132
  * (_, snap, action) => snap.state.balance >= action.amount || "Insufficient funds"
@@ -123,17 +134,14 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
123
134
  * .emit((action) => ["Withdrawn", { amount: action.amount }])
124
135
  * ```
125
136
  *
126
- * @example Action emitting multiple events
137
+ * @example Action with shorthand (variable name matches action name)
127
138
  * ```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
- * ])
139
+ * const OpenTicket = z.object({ title: z.string() });
140
+ * .on({ OpenTicket })
141
+ * .emit((action) => ["TicketOpened", { title: action.title }])
134
142
  * ```
135
143
  */
136
- on: <K extends string, AX extends Schema>(action: K, schema: ZodType<AX>) => {
144
+ on: <K extends string, AX extends Schema>(entry: ActionEntry<K, AX>) => {
137
145
  /**
138
146
  * Adds business rule invariants that must hold before the action can execute.
139
147
  *
@@ -261,11 +269,11 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
261
269
  *
262
270
  * @example
263
271
  * ```typescript
264
- * const Counter = state("Counter", schema)
272
+ * const Counter = state({ Counter: schema })
265
273
  * .init(() => ({ count: 0 }))
266
274
  * .emits({ Incremented: z.object({ amount: z.number() }) })
267
275
  * .patch({ Incremented: (event, state) => ({ count: state.count + event.data.amount }) })
268
- * .on("increment", z.object({ by: z.number() }))
276
+ * .on({ increment: z.object({ by: z.number() }) })
269
277
  * .emit((action) => ["Incremented", { amount: action.by }])
270
278
  * .build(); // Returns State<S, E, A, N>
271
279
  * ```
@@ -289,8 +297,7 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
289
297
  * 6. Snapshotting strategy via `.snap()`
290
298
  *
291
299
  * @template S - Zod schema type defining the shape of the state
292
- * @param name - Unique identifier for this state type (e.g., "Counter", "User", "Order")
293
- * @param state - Zod schema defining the structure of the state
300
+ * @param entry - Single-key record mapping state name to Zod schema (e.g., `{ Counter: z.object({ count: z.number() }) }`)
294
301
  * @returns A StateBuilder instance for fluent API configuration
295
302
  *
296
303
  * @example Basic counter state
@@ -298,7 +305,7 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
298
305
  * import { state } from "@rotorsoft/act";
299
306
  * import { z } from "zod";
300
307
  *
301
- * const Counter = state("Counter", z.object({ count: z.number() }))
308
+ * const Counter = state({ Counter: z.object({ count: z.number() }) })
302
309
  * .init(() => ({ count: 0 }))
303
310
  * .emits({
304
311
  * Incremented: z.object({ amount: z.number() })
@@ -306,18 +313,18 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
306
313
  * .patch({
307
314
  * Incremented: (event, state) => ({ count: state.count + event.data.amount })
308
315
  * })
309
- * .on("increment", z.object({ by: z.number() }))
316
+ * .on({ increment: z.object({ by: z.number() }) })
310
317
  * .emit((action) => ["Incremented", { amount: action.by }])
311
318
  * .build();
312
319
  * ```
313
320
  *
314
321
  * @example State with multiple events and invariants
315
322
  * ```typescript
316
- * const BankAccount = state("BankAccount", z.object({
323
+ * const BankAccount = state({ BankAccount: z.object({
317
324
  * balance: z.number(),
318
325
  * currency: z.string(),
319
326
  * status: z.enum(["open", "closed"])
320
- * }))
327
+ * }) })
321
328
  * .init(() => ({ balance: 0, currency: "USD", status: "open" }))
322
329
  * .emits({
323
330
  * Deposited: z.object({ amount: z.number() }),
@@ -329,19 +336,19 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
329
336
  * Withdrawn: (event, state) => ({ balance: state.balance - event.data.amount }),
330
337
  * Closed: () => ({ status: "closed", balance: 0 })
331
338
  * })
332
- * .on("deposit", z.object({ amount: z.number() }))
339
+ * .on({ deposit: z.object({ amount: z.number() }) })
333
340
  * .given([
334
341
  * (_, snap) => snap.state.status === "open" || "Account must be open"
335
342
  * ])
336
343
  * .emit((action) => ["Deposited", { amount: action.amount }])
337
- * .on("withdraw", z.object({ amount: z.number() }))
344
+ * .on({ withdraw: z.object({ amount: z.number() }) })
338
345
  * .given([
339
346
  * (_, snap) => snap.state.status === "open" || "Account must be open",
340
347
  * (_, snap, action) =>
341
348
  * snap.state.balance >= action.amount || "Insufficient funds"
342
349
  * ])
343
350
  * .emit((action) => ["Withdrawn", { amount: action.amount }])
344
- * .on("close", z.object({}))
351
+ * .on({ close: z.object({}) })
345
352
  * .given([
346
353
  * (_, snap) => snap.state.status === "open" || "Already closed",
347
354
  * (_, snap) => snap.state.balance === 0 || "Balance must be zero"
@@ -352,11 +359,11 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
352
359
  *
353
360
  * @example State with snapshotting
354
361
  * ```typescript
355
- * const User = state("User", z.object({
362
+ * const User = state({ User: z.object({
356
363
  * name: z.string(),
357
364
  * email: z.string(),
358
365
  * loginCount: z.number()
359
- * }))
366
+ * }) })
360
367
  * .init((data) => ({ ...data, loginCount: 0 }))
361
368
  * .emits({
362
369
  * UserCreated: z.object({ name: z.string(), email: z.string() }),
@@ -366,9 +373,9 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
366
373
  * UserCreated: (event) => event.data,
367
374
  * UserLoggedIn: (_, state) => ({ loginCount: state.loginCount + 1 })
368
375
  * })
369
- * .on("createUser", z.object({ name: z.string(), email: z.string() }))
376
+ * .on({ createUser: z.object({ name: z.string(), email: z.string() }) })
370
377
  * .emit((action) => ["UserCreated", action])
371
- * .on("login", z.object({}))
378
+ * .on({ login: z.object({}) })
372
379
  * .emit(() => ["UserLoggedIn", {}])
373
380
  * .snap((snap) => snap.patches >= 10) // Snapshot every 10 events
374
381
  * .build();
@@ -379,5 +386,6 @@ export type ActionBuilder<S extends Schema, E extends Schemas, A extends Schemas
379
386
  * @see {@link https://rotorsoft.github.io/act-root/docs/intro | Getting Started Guide}
380
387
  * @see {@link https://rotorsoft.github.io/act-root/docs/examples/calculator | Calculator Example}
381
388
  */
382
- export declare function state<N extends string, S extends Schema>(name: N, state: ZodType<S>): StateBuilder<S, N>;
389
+ export declare function state<N extends string, S extends Schema>(entry: StateEntry<N, S>): StateBuilder<S, N>;
390
+ export {};
383
391
  //# 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":"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"}
@@ -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);
@@ -83,7 +83,7 @@ import type { Patch, Schema } from "./types/index.js";
83
83
  * import { state } from "@rotorsoft/act";
84
84
  * import { z } from "zod";
85
85
  *
86
- * const Counter = state("Counter", z.object({ count: z.number() }))
86
+ * const Counter = state({ Counter: z.object({ count: z.number() }) })
87
87
  * .init(() => ({ count: 0 }))
88
88
  * .emits({ Incremented: z.object({ by: z.number() }) })
89
89
  * .patch({
@@ -166,10 +166,10 @@ export declare const patch: <S extends Schema>(original: Readonly<S>, patches: R
166
166
  * import { state } from "@rotorsoft/act";
167
167
  * import { z } from "zod";
168
168
  *
169
- * const Counter = state("Counter", z.object({ count: z.number() }))
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);
@@ -1546,7 +1631,11 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
1546
1631
  }
1547
1632
 
1548
1633
  // src/state-builder.ts
1549
- function state(name, state2) {
1634
+ function state(entry) {
1635
+ const keys = Object.keys(entry);
1636
+ if (keys.length !== 1) throw new Error("state() requires exactly one key");
1637
+ const name = keys[0];
1638
+ const stateSchema = entry[name];
1550
1639
  return {
1551
1640
  init(init) {
1552
1641
  return {
@@ -1556,7 +1645,7 @@ function state(name, state2) {
1556
1645
  return action_builder({
1557
1646
  events,
1558
1647
  actions: {},
1559
- state: state2,
1648
+ state: stateSchema,
1560
1649
  name,
1561
1650
  init,
1562
1651
  patch: patch2,
@@ -1571,7 +1660,11 @@ function state(name, state2) {
1571
1660
  }
1572
1661
  function action_builder(state2) {
1573
1662
  return {
1574
- on(action2, schema) {
1663
+ on(entry) {
1664
+ const keys = Object.keys(entry);
1665
+ if (keys.length !== 1) throw new Error(".on() requires exactly one key");
1666
+ const action2 = keys[0];
1667
+ const schema = entry[action2];
1575
1668
  if (action2 in state2.actions)
1576
1669
  throw new Error(`Duplicate action "${action2}"`);
1577
1670
  const actions = { ...state2.actions, [action2]: schema };
@@ -1625,10 +1718,12 @@ function action_builder(state2) {
1625
1718
  dispose,
1626
1719
  disposeAndExit,
1627
1720
  extend,
1721
+ isProjection,
1628
1722
  isSlice,
1629
1723
  logger,
1630
1724
  patch,
1631
1725
  port,
1726
+ projection,
1632
1727
  sleep,
1633
1728
  slice,
1634
1729
  state,