@rivetkit/effect 0.0.0-06-09-refactor-rivetkit-split-workflow-context-into-workflowcontext-workflowstepcontext.e0c9540

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.
Files changed (89) hide show
  1. package/dist/Action.d.ts +104 -0
  2. package/dist/Action.d.ts.map +1 -0
  3. package/dist/Action.js +50 -0
  4. package/dist/Action.js.map +1 -0
  5. package/dist/Actor.d.ts +133 -0
  6. package/dist/Actor.d.ts.map +1 -0
  7. package/dist/Actor.js +104 -0
  8. package/dist/Actor.js.map +1 -0
  9. package/dist/Client.d.ts +31 -0
  10. package/dist/Client.d.ts.map +1 -0
  11. package/dist/Client.js +98 -0
  12. package/dist/Client.js.map +1 -0
  13. package/dist/Logger.d.ts +29 -0
  14. package/dist/Logger.d.ts.map +1 -0
  15. package/dist/Logger.js +31 -0
  16. package/dist/Logger.js.map +1 -0
  17. package/dist/Registry.d.ts +72 -0
  18. package/dist/Registry.d.ts.map +1 -0
  19. package/dist/Registry.js +125 -0
  20. package/dist/Registry.js.map +1 -0
  21. package/dist/RivetError.d.ts +438 -0
  22. package/dist/RivetError.d.ts.map +1 -0
  23. package/dist/RivetError.js +873 -0
  24. package/dist/RivetError.js.map +1 -0
  25. package/dist/State.d.ts +123 -0
  26. package/dist/State.d.ts.map +1 -0
  27. package/dist/State.js +104 -0
  28. package/dist/State.js.map +1 -0
  29. package/dist/internal/ActionDispatcher.d.ts +14 -0
  30. package/dist/internal/ActionDispatcher.d.ts.map +1 -0
  31. package/dist/internal/ActionDispatcher.js +100 -0
  32. package/dist/internal/ActionDispatcher.js.map +1 -0
  33. package/dist/internal/ActionErrorEnvelope.d.ts +11 -0
  34. package/dist/internal/ActionErrorEnvelope.d.ts.map +1 -0
  35. package/dist/internal/ActionErrorEnvelope.js +14 -0
  36. package/dist/internal/ActionErrorEnvelope.js.map +1 -0
  37. package/dist/internal/ActorInstanceManager.d.ts +28 -0
  38. package/dist/internal/ActorInstanceManager.d.ts.map +1 -0
  39. package/dist/internal/ActorInstanceManager.js +51 -0
  40. package/dist/internal/ActorInstanceManager.js.map +1 -0
  41. package/dist/internal/ActorStateAdapter.d.ts +18 -0
  42. package/dist/internal/ActorStateAdapter.d.ts.map +1 -0
  43. package/dist/internal/ActorStateAdapter.js +29 -0
  44. package/dist/internal/ActorStateAdapter.js.map +1 -0
  45. package/dist/internal/StateOptions.d.ts +12 -0
  46. package/dist/internal/StateOptions.d.ts.map +1 -0
  47. package/dist/internal/StateOptions.js +2 -0
  48. package/dist/internal/StateOptions.js.map +1 -0
  49. package/dist/internal/logging.d.ts +23 -0
  50. package/dist/internal/logging.d.ts.map +1 -0
  51. package/dist/internal/logging.js +162 -0
  52. package/dist/internal/logging.js.map +1 -0
  53. package/dist/internal/tracing.d.ts +23 -0
  54. package/dist/internal/tracing.d.ts.map +1 -0
  55. package/dist/internal/tracing.js +30 -0
  56. package/dist/internal/tracing.js.map +1 -0
  57. package/dist/internal/utils.d.ts +7 -0
  58. package/dist/internal/utils.d.ts.map +1 -0
  59. package/dist/internal/utils.js +7 -0
  60. package/dist/internal/utils.js.map +1 -0
  61. package/dist/mod.d.ts +8 -0
  62. package/dist/mod.d.ts.map +1 -0
  63. package/dist/mod.js +8 -0
  64. package/dist/mod.js.map +1 -0
  65. package/package.json +46 -0
  66. package/src/Action.ts +231 -0
  67. package/src/Actor.test-d.ts +603 -0
  68. package/src/Actor.test.ts +206 -0
  69. package/src/Actor.ts +550 -0
  70. package/src/Client.test.ts +210 -0
  71. package/src/Client.ts +216 -0
  72. package/src/Logger.ts +43 -0
  73. package/src/Registry.test-d.ts +126 -0
  74. package/src/Registry.test.ts +411 -0
  75. package/src/Registry.ts +243 -0
  76. package/src/RivetError.test.ts +188 -0
  77. package/src/RivetError.ts +1044 -0
  78. package/src/State.test.ts +181 -0
  79. package/src/State.ts +224 -0
  80. package/src/internal/ActionDispatcher.ts +192 -0
  81. package/src/internal/ActionErrorEnvelope.ts +19 -0
  82. package/src/internal/ActorInstanceManager.ts +143 -0
  83. package/src/internal/ActorStateAdapter.ts +88 -0
  84. package/src/internal/StateOptions.ts +17 -0
  85. package/src/internal/logging.test.ts +288 -0
  86. package/src/internal/logging.ts +237 -0
  87. package/src/internal/tracing.ts +42 -0
  88. package/src/internal/utils.ts +12 -0
  89. package/src/mod.ts +7 -0
@@ -0,0 +1,181 @@
1
+ import { assert, describe, it } from "@effect/vitest";
2
+ import { State } from "@rivetkit/effect";
3
+ import { Effect, Exit, PubSub, Stream } from "effect";
4
+
5
+ // Helper: build a State backed by a plain mutable cell, with
6
+ // Effect-typed read/write closures. Mirrors how Registry wires
7
+ // `decodeUnknownEffect` / `encodeUnknownEffect` over `c.state`.
8
+ const makeCellState = <A>(initial: A) => {
9
+ const cell = { value: initial };
10
+ return State.make<A, never, never>(
11
+ () => Effect.sync(() => cell.value),
12
+ (v) =>
13
+ Effect.sync(() => {
14
+ cell.value = v;
15
+ }),
16
+ ).pipe(Effect.map((s) => ({ s, cell })));
17
+ };
18
+
19
+ describe("State", () => {
20
+ it.effect("get reflects the backing store", () =>
21
+ Effect.gen(function* () {
22
+ const { s, cell } = yield* makeCellState(42);
23
+ assert.strictEqual(yield* State.get(s), 42);
24
+
25
+ cell.value = 100;
26
+ assert.strictEqual(yield* State.get(s), 100);
27
+ }),
28
+ );
29
+
30
+ it.effect("set writes through to the backing store", () =>
31
+ Effect.gen(function* () {
32
+ const { s, cell } = yield* makeCellState(0);
33
+ yield* State.set(s, 7);
34
+ assert.strictEqual(cell.value, 7);
35
+ assert.strictEqual(yield* State.get(s), 7);
36
+ }),
37
+ );
38
+
39
+ it.effect("update applies f over read/write", () =>
40
+ Effect.gen(function* () {
41
+ const { s, cell } = yield* makeCellState(10);
42
+ yield* State.update(s, (n) => n + 5);
43
+ assert.strictEqual(cell.value, 15);
44
+ }),
45
+ );
46
+
47
+ it.effect("updateAndGet returns the new value and commits it", () =>
48
+ Effect.gen(function* () {
49
+ const { s, cell } = yield* makeCellState(10);
50
+ const next = yield* State.updateAndGet(s, (n) => n + 5);
51
+ assert.strictEqual(next, 15);
52
+ assert.strictEqual(cell.value, 15);
53
+ }),
54
+ );
55
+
56
+ it.effect("modify returns B and commits the new value", () =>
57
+ Effect.gen(function* () {
58
+ const { s, cell } = yield* makeCellState("a");
59
+ const b = yield* State.modify(
60
+ s,
61
+ (str) => [str.length, `${str}b`] as const,
62
+ );
63
+ assert.strictEqual(b, 1);
64
+ assert.strictEqual(cell.value, "ab");
65
+ }),
66
+ );
67
+
68
+ it.effect(
69
+ "update is atomic across concurrent fibers (no lost updates)",
70
+ () =>
71
+ Effect.gen(function* () {
72
+ const { s, cell } = yield* makeCellState(0);
73
+ yield* Effect.all(
74
+ Array.from({ length: 100 }, () =>
75
+ State.update(s, (n) => n + 1),
76
+ ),
77
+ { concurrency: "unbounded" },
78
+ );
79
+ assert.strictEqual(cell.value, 100);
80
+ }),
81
+ );
82
+
83
+ it.effect("changes replays the most recent published value", () =>
84
+ Effect.gen(function* () {
85
+ const { s } = yield* makeCellState(0);
86
+ const initial = yield* State.changes(s).pipe(
87
+ Stream.take(1),
88
+ Stream.runCollect,
89
+ );
90
+ assert.deepStrictEqual(initial, [0]);
91
+
92
+ State.publishUnsafe(s, 7);
93
+ const later = yield* State.changes(s).pipe(
94
+ Stream.take(1),
95
+ Stream.runCollect,
96
+ );
97
+ assert.deepStrictEqual(later, [7]);
98
+ }),
99
+ );
100
+
101
+ it.effect("publish pushes values to live subscribers", () =>
102
+ Effect.gen(function* () {
103
+ const { s } = yield* makeCellState(0);
104
+ yield* Effect.scoped(
105
+ Effect.gen(function* () {
106
+ const sub = yield* PubSub.subscribe(s.pubsub);
107
+ assert.strictEqual(yield* PubSub.take(sub), 0);
108
+
109
+ yield* State.publish(s, 1);
110
+ yield* State.publish(s, 2);
111
+ assert.strictEqual(yield* PubSub.take(sub), 1);
112
+ assert.strictEqual(yield* PubSub.take(sub), 2);
113
+ }),
114
+ );
115
+ }),
116
+ );
117
+
118
+ it.effect("set does NOT auto-publish — the runtime does", () =>
119
+ Effect.gen(function* () {
120
+ const { s } = yield* makeCellState(0);
121
+ yield* State.set(s, 99);
122
+ // replay should still hold the initial 0, not 99
123
+ const latest = yield* State.changes(s).pipe(
124
+ Stream.take(1),
125
+ Stream.runCollect,
126
+ );
127
+ assert.deepStrictEqual(latest, [0]);
128
+ }),
129
+ );
130
+
131
+ it.effect("isState discriminates", () =>
132
+ Effect.gen(function* () {
133
+ const { s } = yield* makeCellState(0);
134
+ assert.isTrue(State.isState(s));
135
+ assert.isFalse(State.isState({}));
136
+ assert.isFalse(State.isState(null));
137
+ assert.isFalse(State.isState(42));
138
+ }),
139
+ );
140
+
141
+ it.effect("supports .pipe()", () =>
142
+ Effect.gen(function* () {
143
+ const { s } = yield* makeCellState(0);
144
+ yield* s.pipe(State.set(5));
145
+ assert.strictEqual(yield* State.get(s), 5);
146
+
147
+ yield* s.pipe(State.update((n) => n * 2));
148
+ assert.strictEqual(yield* State.get(s), 10);
149
+ }),
150
+ );
151
+
152
+ it.effect("read failure propagates through get", () =>
153
+ Effect.gen(function* () {
154
+ const reads = { count: 0 };
155
+ // Construction reads once to seed the pubsub; subsequent reads
156
+ // fail. Mirrors a schema mismatch on persisted state.
157
+ const s = yield* State.make<number, "boom", never>(
158
+ () =>
159
+ Effect.suspend(() => {
160
+ reads.count++;
161
+ if (reads.count === 1) return Effect.succeed(0);
162
+ return Effect.fail("boom" as const);
163
+ }),
164
+ () => Effect.void,
165
+ );
166
+ const exit = yield* Effect.exit(State.get(s));
167
+ assert.isTrue(Exit.isFailure(exit));
168
+ }),
169
+ );
170
+
171
+ it.effect("write failure propagates through set", () =>
172
+ Effect.gen(function* () {
173
+ const s = yield* State.make<number, "boom", never>(
174
+ () => Effect.succeed(0),
175
+ () => Effect.fail("boom" as const),
176
+ );
177
+ const exit = yield* Effect.exit(State.set(s, 1));
178
+ assert.isTrue(Exit.isFailure(exit));
179
+ }),
180
+ );
181
+ });
package/src/State.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * `State` is a typed view over an actor's persisted state, plus a
3
+ * subscribable stream of every change.
4
+ *
5
+ * Unlike a `Ref`, `State` has no in-memory cell — the persisted store
6
+ * is the source of truth. Reads decode the live store on demand;
7
+ * writes encode and overwrite it. A `PubSub<A>` backs {@link changes}
8
+ * and is fed externally — the runtime publishes to it from rivetkit's
9
+ * `onStateChange` callback so subscribers see every committed change,
10
+ * including ones initiated outside the SDK.
11
+ *
12
+ * Read and write are Effect-typed so schemas with asynchronous
13
+ * transforms (or service requirements) are supported. `update` and
14
+ * `modify` serialize through a per-`State` semaphore so read/apply/
15
+ * write triples are atomic across fibers; `set` shares the same lock
16
+ * so all writes are linearized.
17
+ *
18
+ * The PubSub uses replay = 1, matching `SubscriptionRef`: a new
19
+ * subscriber immediately sees the most recent value.
20
+ */
21
+ import {
22
+ Effect,
23
+ Inspectable,
24
+ identity,
25
+ Pipeable,
26
+ Predicate,
27
+ PubSub,
28
+ Semaphore,
29
+ Stream,
30
+ type Types,
31
+ } from "effect";
32
+ import { dual } from "effect/Function";
33
+
34
+ const TypeId = "~@rivetkit/effect/State";
35
+
36
+ /**
37
+ * A view over a persisted state cell with a subscribable change stream.
38
+ *
39
+ * - `A` — the value type
40
+ * - `E` — the read/write closures' failure type (e.g. a schema's
41
+ * `SchemaError` when read/write decode/encode against a schema)
42
+ * - `R` — the read/write closures' service requirements
43
+ */
44
+ export interface State<A, E = never, R = never>
45
+ extends Variance<A, E, R>,
46
+ Pipeable.Pipeable,
47
+ Inspectable.Inspectable {
48
+ readonly read: () => Effect.Effect<A, E, R>;
49
+ readonly write: (value: A) => Effect.Effect<void, E, R>;
50
+ readonly pubsub: PubSub.PubSub<A>;
51
+ /**
52
+ * Serializes writes (`set`, `update`, `modify`) so the read/apply/
53
+ * write triple is atomic. The runtime may also use this semaphore
54
+ * to serialize its own decode-and-publish work from
55
+ * `onStateChange`, keeping the change stream's order consistent
56
+ * with the write order.
57
+ */
58
+ readonly semaphore: Semaphore.Semaphore;
59
+ }
60
+
61
+ export const isState = (u: unknown): u is State<unknown, unknown> =>
62
+ Predicate.hasProperty(u, TypeId);
63
+
64
+ export interface Variance<A, E, R> {
65
+ readonly [TypeId]: {
66
+ readonly _A: Types.Invariant<A>;
67
+ readonly _E: Types.Covariant<E>;
68
+ readonly _R: Types.Covariant<R>;
69
+ };
70
+ }
71
+
72
+ const Proto = {
73
+ ...Pipeable.Prototype,
74
+ ...Inspectable.BaseProto,
75
+ [TypeId]: { _A: identity, _E: identity, _R: identity },
76
+ toJSON(this: State<unknown, unknown, unknown>) {
77
+ return { _id: "State" };
78
+ },
79
+ };
80
+
81
+ /**
82
+ * Creates a `State` from `read` and `write` closures over the
83
+ * underlying store. The closures are responsible for any
84
+ * encoding/decoding; `State` itself is schema-agnostic.
85
+ *
86
+ * The current value (per `read()`) is published to the pubsub on
87
+ * construction so any subscription obtained later replays it.
88
+ *
89
+ * The PubSub is not explicitly shut down — it's reclaimed by GC when
90
+ * the `State` and any subscribers become unreachable.
91
+ */
92
+ export const make = Effect.fnUntraced(function* <A, E, R>(
93
+ read: () => Effect.Effect<A, E, R>,
94
+ write: (value: A) => Effect.Effect<void, E, R>,
95
+ ): Effect.fn.Return<State<A, E, R>, E, R> {
96
+ const pubsub = yield* PubSub.unbounded<A>({ replay: 1 });
97
+ const initial = yield* read();
98
+ PubSub.publishUnsafe(pubsub, initial);
99
+ const self = Object.create(Proto);
100
+ self.read = read;
101
+ self.write = write;
102
+ self.pubsub = pubsub;
103
+ self.semaphore = Semaphore.makeUnsafe(1);
104
+ return self;
105
+ });
106
+
107
+ /**
108
+ * Reads the current value.
109
+ */
110
+ export const get = <A, E, R>(self: State<A, E, R>): Effect.Effect<A, E, R> =>
111
+ self.read();
112
+
113
+ /**
114
+ * Replaces the value. Serialized with `update` / `modify` so writes
115
+ * happen in invocation order.
116
+ */
117
+ export const set: {
118
+ <A>(value: A): <E, R>(self: State<A, E, R>) => Effect.Effect<void, E, R>;
119
+ <A, E, R>(self: State<A, E, R>, value: A): Effect.Effect<void, E, R>;
120
+ } = dual(
121
+ 2,
122
+ <A, E, R>(self: State<A, E, R>, value: A): Effect.Effect<void, E, R> =>
123
+ Semaphore.withPermit(self.semaphore, self.write(value)),
124
+ );
125
+
126
+ /**
127
+ * Updates the value by applying `f` to the current value. The
128
+ * read/apply/write triple is atomic across fibers.
129
+ */
130
+ export const update: {
131
+ <A>(
132
+ f: (a: A) => A,
133
+ ): <E, R>(self: State<A, E, R>) => Effect.Effect<void, E, R>;
134
+ <A, E, R>(self: State<A, E, R>, f: (a: A) => A): Effect.Effect<void, E, R>;
135
+ } = dual(
136
+ 2,
137
+ <A, E, R>(
138
+ self: State<A, E, R>,
139
+ f: (a: A) => A,
140
+ ): Effect.Effect<void, E, R> =>
141
+ Semaphore.withPermit(
142
+ self.semaphore,
143
+ Effect.flatMap(self.read(), (a) => self.write(f(a))),
144
+ ),
145
+ );
146
+
147
+ /**
148
+ * Updates the value by applying `f` and returns the new value. The
149
+ * read/apply/write triple is atomic across fibers.
150
+ */
151
+ export const updateAndGet: {
152
+ <A>(f: (a: A) => A): <E, R>(self: State<A, E, R>) => Effect.Effect<A, E, R>;
153
+ <A, E, R>(self: State<A, E, R>, f: (a: A) => A): Effect.Effect<A, E, R>;
154
+ } = dual(
155
+ 2,
156
+ <A, E, R>(self: State<A, E, R>, f: (a: A) => A): Effect.Effect<A, E, R> =>
157
+ Semaphore.withPermit(
158
+ self.semaphore,
159
+ Effect.flatMap(self.read(), (a) => {
160
+ const next = f(a);
161
+ return Effect.as(self.write(next), next);
162
+ }),
163
+ ),
164
+ );
165
+
166
+ /**
167
+ * Atomically replaces the value with the second element of `f(prev)`
168
+ * and returns the first. The read/apply/write triple is atomic across
169
+ * fibers.
170
+ */
171
+ export const modify: {
172
+ <A, B>(
173
+ f: (a: A) => readonly [B, A],
174
+ ): <E, R>(self: State<A, E, R>) => Effect.Effect<B, E, R>;
175
+ <A, E, R, B>(
176
+ self: State<A, E, R>,
177
+ f: (a: A) => readonly [B, A],
178
+ ): Effect.Effect<B, E, R>;
179
+ } = dual(
180
+ 2,
181
+ <A, E, R, B>(
182
+ self: State<A, E, R>,
183
+ f: (a: A) => readonly [B, A],
184
+ ): Effect.Effect<B, E, R> =>
185
+ Semaphore.withPermit(
186
+ self.semaphore,
187
+ Effect.flatMap(self.read(), (a) => {
188
+ const [b, next] = f(a);
189
+ return Effect.as(self.write(next), b);
190
+ }),
191
+ ),
192
+ );
193
+
194
+ /**
195
+ * Stream of every value published to this `State`. New subscribers
196
+ * immediately see the most recent value (replay = 1), then every
197
+ * subsequent publish.
198
+ */
199
+ export const changes = <A, E, R>(self: State<A, E, R>): Stream.Stream<A> =>
200
+ Stream.fromPubSub(self.pubsub);
201
+
202
+ /**
203
+ * Publish a value to the change stream as an `Effect`. Does not
204
+ * modify the underlying store.
205
+ */
206
+ export const publish: {
207
+ <A>(value: A): <E, R>(self: State<A, E, R>) => Effect.Effect<boolean>;
208
+ <A, E, R>(self: State<A, E, R>, value: A): Effect.Effect<boolean>;
209
+ } = dual(
210
+ 2,
211
+ <A, E, R>(self: State<A, E, R>, value: A): Effect.Effect<boolean> =>
212
+ PubSub.publish(self.pubsub, value),
213
+ );
214
+
215
+ /**
216
+ * Synchronous variant of {@link publish}. Returns `true` when the
217
+ * publish succeeded, `false` if the pubsub is shut down. The runtime
218
+ * uses this from rivetkit's `onStateChange` callback to feed the
219
+ * change stream.
220
+ */
221
+ export const publishUnsafe = <A, E, R>(
222
+ self: State<A, E, R>,
223
+ value: A,
224
+ ): boolean => PubSub.publishUnsafe(self.pubsub, value);
@@ -0,0 +1,192 @@
1
+ import {
2
+ Cause,
3
+ Effect,
4
+ Exit,
5
+ type Fiber,
6
+ Option,
7
+ Record,
8
+ Schema,
9
+ Tracer,
10
+ } from "effect";
11
+ import * as Rivetkit from "rivetkit";
12
+ import type * as Action from "../Action.ts";
13
+ import type {
14
+ ActionHandlersFrom,
15
+ ActionRequest,
16
+ Actor,
17
+ } from "../Actor.ts";
18
+ import type * as Client from "../Client.ts";
19
+ import * as ActionErrorEnvelope from "./ActionErrorEnvelope.ts";
20
+ import { makeActorLogAnnotations } from "./logging.ts";
21
+ import { readTraceMeta, rpcSystem } from "./tracing.ts";
22
+ import { hasStringProperty } from "./utils.ts";
23
+
24
+ export type Instance<ActionHandlers> = {
25
+ readonly actionHandlers: ActionHandlers;
26
+ readonly runFork: <A, E>(
27
+ effect: Effect.Effect<A, E, any>,
28
+ options?: Effect.RunOptions,
29
+ ) => Fiber.Fiber<A, E>;
30
+ };
31
+
32
+ export const make = <
33
+ Name extends string,
34
+ Actions extends Action.AnyWithProps,
35
+ ActionHandlers extends ActionHandlersFrom<Actions>,
36
+ ActorDefinition extends Rivetkit.AnyActorDefinition,
37
+ >({
38
+ actor,
39
+ getInstance,
40
+ }: {
41
+ readonly actor: Actor<Name, Actions>;
42
+ readonly getInstance: (
43
+ actorId: string,
44
+ ) => Instance<ActionHandlers> | undefined;
45
+ }) =>
46
+ Record.fromIterableWith(actor.actions, (action) => {
47
+ const decodePayload = Schema.decodeUnknownEffect(
48
+ Schema.toCodecJson(action.payloadSchema),
49
+ );
50
+ const encodeSuccess = Schema.encodeEffect(
51
+ Schema.toCodecJson(action.successSchema),
52
+ );
53
+ const encodeError = Schema.encodeEffect(
54
+ Schema.toCodecJson(action.errorSchema),
55
+ );
56
+
57
+ return [
58
+ action._tag,
59
+ async (
60
+ c: Rivetkit.ActionContextOf<ActorDefinition>,
61
+ payload: Action.Payload<typeof action>,
62
+ meta?: Client.ActionMeta, // TODO: Find better type
63
+ ) => {
64
+ // Always wrap in a server-side span so the handler has a
65
+ // live `currentSpan` even when the caller didn't ship trace
66
+ // context (e.g., a non-Effect-SDK client). When trace context
67
+ // is present, reattach it as the parent so the server span
68
+ // joins the caller's trace.
69
+ const rpcMethod = `${actor.name}/${action._tag}`;
70
+ const traceMeta = readTraceMeta(meta);
71
+
72
+ const instance = getInstance(c.actorId);
73
+ if (!instance) {
74
+ if (c.abortSignal.aborted) throw makeActorAbortedError();
75
+ throw new Error("actor instance missing");
76
+ }
77
+
78
+ const actionEffect = Effect.gen(function* () {
79
+ // The handler map is keyed by the same action
80
+ // definitions being registered here, but
81
+ // TypeScript loses that relationship once the
82
+ // actions are widened into the RivetKit actions
83
+ // record.
84
+ const actionHandler = instance.actionHandlers[
85
+ action._tag as keyof ActionHandlers
86
+ ] as (
87
+ envelope: ActionRequest<typeof action>,
88
+ ) => Action.ResultFrom<typeof action, any>;
89
+ // Raw RivetKit clients call no-argument actions with an
90
+ // absent first argument. The Effect JSON Void codec expects
91
+ // null, so adapt only actions that declared no payload.
92
+ const payloadForDecode =
93
+ !action.hasPayload && payload === undefined
94
+ ? null
95
+ : payload;
96
+ const decodedPayload = yield* decodePayload(
97
+ payloadForDecode,
98
+ ).pipe(
99
+ Effect.mapError(() =>
100
+ new Rivetkit.RivetError(
101
+ "request",
102
+ "invalid",
103
+ `Invalid payload for action ${actor.name}/${action._tag}`,
104
+ ),
105
+ ),
106
+ );
107
+ // The payload was decoded with this action's schema,
108
+ // so this is the runtime boundary that restores the
109
+ // typed envelope expected by the user handler.
110
+ const actionRequest = {
111
+ _tag: action._tag,
112
+ action,
113
+ payload: decodedPayload,
114
+ } as ActionRequest<typeof action>;
115
+
116
+ const resultExit = yield* Effect.exit(
117
+ actionHandler(actionRequest),
118
+ );
119
+
120
+ if (Exit.isSuccess(resultExit)) {
121
+ return yield* encodeSuccess(resultExit.value).pipe(
122
+ Effect.orDie,
123
+ );
124
+ }
125
+
126
+ const expectedError = Exit.findErrorOption(resultExit);
127
+
128
+ if (Option.isSome(expectedError)) {
129
+ const encodedError = yield* encodeError(
130
+ expectedError.value,
131
+ ).pipe(Effect.orDie);
132
+
133
+ return yield* Effect.fail(
134
+ new Rivetkit.UserError(
135
+ hasStringProperty("message")(encodedError)
136
+ ? encodedError.message
137
+ : `${action._tag} failed`,
138
+ {
139
+ code: hasStringProperty("_tag")(
140
+ encodedError,
141
+ )
142
+ ? encodedError._tag
143
+ : undefined,
144
+ metadata:
145
+ ActionErrorEnvelope.make(encodedError),
146
+ },
147
+ ),
148
+ );
149
+ }
150
+
151
+ return yield* Effect.failCause(resultExit.cause);
152
+ }).pipe(
153
+ Effect.withSpan(rpcMethod, {
154
+ parent: traceMeta
155
+ ? Tracer.externalSpan(traceMeta)
156
+ : undefined,
157
+ kind: "server",
158
+ attributes: {
159
+ "rpc.system.name": rpcSystem,
160
+ "rpc.method": rpcMethod,
161
+ },
162
+ }),
163
+ Effect.annotateLogs(makeActorLogAnnotations(c)),
164
+ );
165
+ const fiber = instance.runFork(actionEffect, {
166
+ signal: c.abortSignal,
167
+ });
168
+ const exit = await new Promise<Exit.Exit<unknown, unknown>>(
169
+ (resolve) => fiber.addObserver(resolve),
170
+ );
171
+
172
+ if (Exit.isSuccess(exit)) return exit.value;
173
+ // Action fibers can be interrupted by a caller abort signal
174
+ // or by the actor instance scope closing during sleep, destroy,
175
+ // or shutdown. Surface those lifecycle exits as RivetKit's
176
+ // structured action-aborted error instead of an internal error.
177
+ if (Cause.hasInterruptsOnly(exit.cause)) {
178
+ throw makeActorAbortedError();
179
+ }
180
+ const expectedError = Exit.findErrorOption(exit);
181
+ if (Option.isSome(expectedError)) {
182
+ throw expectedError.value;
183
+ }
184
+ throw Cause.squash(exit.cause);
185
+ },
186
+ ];
187
+ });
188
+
189
+ const makeActorAbortedError = () =>
190
+ new Rivetkit.RivetError("actor", "aborted", "Actor aborted", {
191
+ public: true,
192
+ });
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const tag = "EffectActionError" as const;
4
+
5
+ export const schemaVersion = 1 as const;
6
+
7
+ export const ActionErrorEnvelope = Schema.Struct({
8
+ _tag: Schema.tag(tag),
9
+ version: Schema.Literal(schemaVersion),
10
+ error: Schema.Unknown,
11
+ });
12
+
13
+ export type ActionErrorEnvelope = typeof ActionErrorEnvelope.Type;
14
+
15
+ export const make = (error: unknown): ActionErrorEnvelope => ({
16
+ _tag: tag,
17
+ version: schemaVersion,
18
+ error,
19
+ });