effect-actors-bun 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dallen Pyrah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,467 @@
1
+ # effect-actors
2
+
3
+ Durable, typed actors that run inside your own Effect application. There is no separate control plane: you define actors in code, compose them into `Layer`s, and run them in your processes on top of `effect/unstable/cluster`.
4
+
5
+ Two actor kinds:
6
+
7
+ - **State actors** own framework-managed state. A command turns a pure reducer result into one SQL transaction that commits the next state, an immutable receipt, emitted events, timer changes, and outbox intents — before the reply reaches the client.
8
+ - **Hosted actors** supervise application-supplied authority inside an actor scope. Your service keeps its own durability; the framework provides addressing, lifecycle, and a typed client around it.
9
+
10
+ ## Status
11
+
12
+ The three packages release together at `0.0.1` and require the Effect `4.0.0-rc.112` family pinned exactly, including `@effect/platform-node-shared`. Bun is pinned to `1.4.0`; the Node package needs Node `>= 24`.
13
+
14
+ Packages are published only by the tagged release workflow on GitHub Actions — see [docs/release.md](docs/release.md).
15
+
16
+ ## Packages
17
+
18
+ | Package | Contents |
19
+ | -------------------- | -------------------------------------------------------------------------------------- |
20
+ | `effect-actors` | Actor definitions, typed clients, runtime services, SQL store, hosted actors, gateway. |
21
+ | `effect-actors-bun` | Bun + SQLite single runner; Bun + PostgreSQL cluster and client-only compositions. |
22
+ | `effect-actors-node` | Node + PostgreSQL single runner, cluster, and client-only compositions. |
23
+
24
+ ## Install
25
+
26
+ From source, until the first tag publishes:
27
+
28
+ ```bash
29
+ git clone https://github.com/dallenpyrah/effect-actors.git
30
+ cd effect-actors
31
+ bun install --frozen-lockfile
32
+ bun run build
33
+ ```
34
+
35
+ After publication, pin the whole Effect family exactly — a mixed prerelease graph is not supported:
36
+
37
+ ```bash
38
+ # Bun + SQLite
39
+ bun add --exact effect@4.0.0-rc.112 @effect/platform-bun@4.0.0-rc.112 \
40
+ effect-actors@0.0.1 effect-actors-bun@0.0.1
41
+
42
+ # Node + PostgreSQL
43
+ npm install --save-exact effect@4.0.0-rc.112 @effect/platform-node@4.0.0-rc.112 \
44
+ effect-actors@0.0.1 effect-actors-node@0.0.1
45
+ ```
46
+
47
+ ## Quickstart
48
+
49
+ A counter actor on Bun + SQLite. This program is complete and runnable; the CI suite typechecks and executes every TypeScript block on this page against the packed packages.
50
+
51
+ ```ts
52
+ import { BunRuntime } from "@effect/platform-bun"
53
+ import { Effect, Layer, Result, Schema, Stream } from "effect"
54
+ import { Actor } from "effect-actors"
55
+ import { SqliteSingle } from "effect-actors-bun"
56
+
57
+ // Domain errors are schema-coded so they round-trip through receipts and the gateway.
58
+ class NonPositiveIncrement extends Schema.Error<NonPositiveIncrement>("NonPositiveIncrement")({
59
+ _tag: Schema.tag("NonPositiveIncrement"),
60
+ by: Schema.Int,
61
+ }) {}
62
+
63
+ const Counter = Actor.make("Counter", {
64
+ protocolVersion: 1,
65
+ state: {
66
+ version: 1,
67
+ schema: Schema.Struct({ count: Schema.Int }),
68
+ initial: () => ({ count: 0 }),
69
+ migrations: [],
70
+ },
71
+ events: Schema.Struct({ _tag: Schema.Literal("Incremented"), count: Schema.Int }),
72
+ })
73
+ // Reducers are synchronous and pure. A transition is data: next state, reply
74
+ // value, events, timers, outbox calls — committed in one transaction.
75
+ .command("increment", {
76
+ payload: Schema.Struct({ by: Schema.Int }),
77
+ success: Schema.Struct({ count: Schema.Int }),
78
+ error: NonPositiveIncrement,
79
+ reduce: (state, { by }) => {
80
+ if (by <= 0) return Result.fail(new NonPositiveIncrement({ by }))
81
+ const count = state.count + by
82
+ return Result.succeed({
83
+ state: { count },
84
+ value: { count },
85
+ events: [{ _tag: "Incremented" as const, count }],
86
+ })
87
+ },
88
+ })
89
+ .query("read", {
90
+ payload: Schema.Null,
91
+ success: Schema.Struct({ count: Schema.Int }),
92
+ error: Schema.Never,
93
+ read: (state) => Result.succeed({ count: state.count }),
94
+ })
95
+
96
+ const database = { filename: "./counter.sqlite" }
97
+ const store = { tablePrefix: "actors" }
98
+
99
+ // `Counter.toLayer()` registers the actor; `SqliteSingle.layer` provides the
100
+ // runtime, SQL store, cluster runner, and typed `Client` service.
101
+ const MainLive = Counter.toLayer().pipe(
102
+ Layer.provideMerge(
103
+ SqliteSingle.layer({
104
+ database,
105
+ store,
106
+ runtime: {
107
+ namespace: "demo",
108
+ maxCommandBytes: 16_384,
109
+ maxStateBytes: 16_384,
110
+ maxEventBytes: 4_096,
111
+ maxEventsPerTurn: 16,
112
+ maxOutboxCallsPerTurn: 16,
113
+ maxTimerChangesPerTurn: 16,
114
+ maxTurnDuration: "5 seconds",
115
+ actorLeaseDuration: "30 seconds",
116
+ actorLeaseRenewEvery: "10 seconds",
117
+ },
118
+ }),
119
+ ),
120
+ )
121
+
122
+ const program = Effect.gen(function* () {
123
+ const counterFor = yield* Counter.client
124
+ const counter = counterFor({ tenant: "acme", key: ["billing"] })
125
+
126
+ const before = yield* counter.queries.read(null)
127
+
128
+ // `commandId` is the idempotency identity for this actor address — derive it
129
+ // from the caller's request so a retry replays the receipt instead of running again.
130
+ const command = { commandId: "req-0001", expectedRevision: before.revision }
131
+ const first = yield* counter.commands.increment({ by: 1 }, command)
132
+ const replay = yield* counter.commands.increment({ by: 1 }, command)
133
+ yield* Effect.logInfo("duplicate command replayed", first.revision === replay.revision)
134
+
135
+ // A query returns the projection plus an event cursor; subscribing after that
136
+ // cursor is a gap-free handoff from read model to stream.
137
+ yield* counter.events({ after: before.cursor }).pipe(
138
+ Stream.take(1),
139
+ Stream.runForEach((event) => Effect.logInfo("event committed", event)),
140
+ )
141
+
142
+ return yield* counter.queries.read(null)
143
+ })
144
+
145
+ // The store layer validates the framework schema when it is built, so
146
+ // `migrate` must complete before `Effect.provide` constructs `MainLive`.
147
+ // In production the migration is a separate deploy step, not part of startup.
148
+ BunRuntime.runMain(
149
+ SqliteSingle.migrate({ database, store }).pipe(
150
+ Effect.andThen(program.pipe(Effect.provide(MainLive))),
151
+ ),
152
+ )
153
+ ```
154
+
155
+ For a long-lived runner — a process that hosts actors until it is stopped — merge each actor layer (and `ActorRuntime.layerDispatchers` when reducers use timers or outbox) over the platform layer, then hand it to `Layer.launch`:
156
+
157
+ `BunRuntime.runMain(Layer.launch(RunnerLive))` or `NodeRuntime.runMain(Layer.launch(RunnerLive))` keep the process alive and run finalizers on SIGINT/SIGTERM. `SqliteSingle` starts no runner listener, so it is strictly one actor-owning process.
158
+
159
+ Migrations stay a separate release step. `migrate` is a plain `Effect`, so the migration entrypoint is small:
160
+
161
+ ```ts
162
+ import { BunRuntime } from "@effect/platform-bun"
163
+ import { SqliteSingle } from "effect-actors-bun"
164
+
165
+ BunRuntime.runMain(
166
+ SqliteSingle.migrate({
167
+ database: { filename: "./counter.sqlite" },
168
+ store: { tablePrefix: "actors" },
169
+ }),
170
+ )
171
+ ```
172
+
173
+ The runnable, CI-verified version of this walkthrough lives in [examples/state-counter](examples/state-counter/sqlite.ts); `bun run pack && bun run examples:check` exercises all packaged examples end to end.
174
+
175
+ ## What a command commits
176
+
177
+ Commands serialize per actor address. On a successful reducer result the store writes — in one transaction — the next state and revision, the success receipt, events with cursor advancement, timer changes, and outbox intents. A typed domain failure writes a failure receipt and nothing else; an `expectedRevision` mismatch writes a `RevisionConflict` receipt without running the reducer.
178
+
179
+ | Retry | Result |
180
+ | ------------------------------------------- | ----------------------------------------------------------- |
181
+ | Same `commandId`, identical canonical input | Stored receipt returned; the reducer does not run again |
182
+ | Same `commandId`, changed input | `CommandConflict` — send a corrected request under a new ID |
183
+ | New `commandId`, stale `expectedRevision` | `RevisionConflict` failure receipt; state unchanged |
184
+
185
+ Retention is an operator policy: resuming an event stream below the retained floor fails with `CursorExpired`, and the client recovers from a fresh query projection. Outbox delivery is at-least-once — give target commands deterministic command IDs and idempotent handlers. The full contract, including address and identifier limits, is in [docs/architecture/guarantees.md](docs/architecture/guarantees.md) and [docs/guides/state-actors.md](docs/guides/state-actors.md).
186
+
187
+ ## Timers and outbox
188
+
189
+ A reducer can attach `timers` and `outbox` intents to its transition; they are durable with the commit but executed later by a dispatcher:
190
+
191
+ ```ts
192
+ import { BunRuntime } from "@effect/platform-bun"
193
+ import { Effect, Layer, Result, Schema } from "effect"
194
+ import { Actor, ActorRuntime } from "effect-actors"
195
+ import { SqliteSingle } from "effect-actors-bun"
196
+
197
+ const Reminder = Actor.make("Reminder", {
198
+ protocolVersion: 1,
199
+ state: {
200
+ version: 1,
201
+ schema: Schema.Struct({ note: Schema.String, fired: Schema.Boolean }),
202
+ initial: () => ({ note: "", fired: false }),
203
+ migrations: [],
204
+ },
205
+ events: Schema.Struct({ _tag: Schema.Literal("Fired"), note: Schema.String }),
206
+ })
207
+ .command("schedule", {
208
+ payload: Schema.Struct({ note: Schema.String }),
209
+ success: Schema.Null,
210
+ error: Schema.Never,
211
+ reduce: (state, { note }, context) =>
212
+ Result.succeed({
213
+ state: { note, fired: false },
214
+ value: null,
215
+ timers: [
216
+ {
217
+ _tag: "Set" as const,
218
+ timerId: "reminder",
219
+ dueAtMillis: context.nowMillis + 100,
220
+ command: "fire",
221
+ payload: { note },
222
+ },
223
+ ],
224
+ }),
225
+ })
226
+ .command("fire", {
227
+ payload: Schema.Struct({ note: Schema.String }),
228
+ success: Schema.Null,
229
+ error: Schema.Never,
230
+ reduce: (state, { note }) =>
231
+ state.note === note && !state.fired
232
+ ? Result.succeed({
233
+ state: { ...state, fired: true },
234
+ value: null,
235
+ events: [{ _tag: "Fired" as const, note }],
236
+ })
237
+ : Result.succeed({ state, value: null }),
238
+ })
239
+ .query("read", {
240
+ payload: Schema.Null,
241
+ success: Schema.Struct({ note: Schema.String, fired: Schema.Boolean }),
242
+ error: Schema.Never,
243
+ read: (state) => Result.succeed(state),
244
+ })
245
+
246
+ const database = { filename: "./reminder.sqlite" }
247
+ const store = { tablePrefix: "reminders" }
248
+
249
+ const RunnerLive = Layer.mergeAll(
250
+ Reminder.toLayer(),
251
+ // Without a dispatcher in scope, timer and outbox intents stay committed but pending.
252
+ ActorRuntime.layerDispatchers({
253
+ pollInterval: "25 millis",
254
+ batchSize: 16,
255
+ concurrency: 4,
256
+ claimDuration: "1 second",
257
+ }),
258
+ ).pipe(
259
+ Layer.provideMerge(
260
+ SqliteSingle.layer({
261
+ database,
262
+ store,
263
+ runtime: {
264
+ namespace: "demo",
265
+ maxCommandBytes: 16_384,
266
+ maxStateBytes: 16_384,
267
+ maxEventBytes: 4_096,
268
+ maxEventsPerTurn: 16,
269
+ maxOutboxCallsPerTurn: 16,
270
+ maxTimerChangesPerTurn: 16,
271
+ maxTurnDuration: "5 seconds",
272
+ actorLeaseDuration: "30 seconds",
273
+ actorLeaseRenewEvery: "10 seconds",
274
+ },
275
+ }),
276
+ ),
277
+ )
278
+
279
+ const program = Effect.gen(function* () {
280
+ const reminder = (yield* Reminder.client)({ tenant: "acme", key: ["demo"] })
281
+ yield* reminder.commands.schedule({ note: "stretch" }, { commandId: "schedule-1" })
282
+ yield* Effect.sleep("500 millis")
283
+ const state = yield* reminder.queries.read(null)
284
+ yield* Effect.logInfo("dispatcher fired the committed timer", state.value)
285
+ })
286
+
287
+ BunRuntime.runMain(
288
+ SqliteSingle.migrate({ database, store }).pipe(
289
+ Effect.andThen(program.pipe(Effect.provide(RunnerLive))),
290
+ ),
291
+ )
292
+ ```
293
+
294
+ A timer `Set` on an existing `timerId` replaces it and bumps its generation, so a stale wake cannot run the replacement; `{ _tag: "Cancel", timerId }` removes it. The dispatcher polls and claims inside the surrounding scope — it is not a detached background promise, and a stopped deployment does not wake itself. Cross-actor delivery works the same way through `outbox` entries targeting another actor's command. See [docs/guides/timers-and-outbox.md](docs/guides/timers-and-outbox.md) and the runnable [timer-outbox example](examples/workflows/timer-outbox/sqlite.ts).
295
+
296
+ ## Hosted actors
297
+
298
+ `HostActor` wraps execution the framework cannot own — an engine, an integration, an authority with its own journal. `make(context)` runs on every activation and returns handlers plus a `failure` supervisor effect; `context.fork` runs scoped background work and `context.scope` takes finalizers.
299
+
300
+ ```ts
301
+ import { BunRuntime } from "@effect/platform-bun"
302
+ import { Effect, Layer, Ref, Schema, Scope } from "effect"
303
+ import { HostActor } from "effect-actors"
304
+ import { SqliteSingle } from "effect-actors-bun"
305
+
306
+ const Ticker = HostActor.make("Ticker", {
307
+ protocolVersion: 1,
308
+ actions: {
309
+ snapshot: HostActor.action({
310
+ payload: Schema.Null,
311
+ success: Schema.Struct({ ticks: Schema.Int }),
312
+ error: Schema.Never,
313
+ delivery: { _tag: "Transient" },
314
+ }),
315
+ },
316
+ })
317
+
318
+ const database = { filename: "./ticker.sqlite" }
319
+ const store = { tablePrefix: "ticker" }
320
+ const runtime = {
321
+ namespace: "demo",
322
+ maxCommandBytes: 16_384,
323
+ maxStateBytes: 16_384,
324
+ maxEventBytes: 4_096,
325
+ maxEventsPerTurn: 16,
326
+ maxOutboxCallsPerTurn: 16,
327
+ maxTimerChangesPerTurn: 16,
328
+ maxTurnDuration: "5 seconds",
329
+ actorLeaseDuration: "30 seconds",
330
+ actorLeaseRenewEvery: "10 seconds",
331
+ } as const
332
+
333
+ const TickerLive = Ticker.toLayer(
334
+ ({ fork, scope }) =>
335
+ Effect.gen(function* () {
336
+ const ticks = yield* Ref.make(0)
337
+ yield* Scope.addFinalizer(scope, Effect.logInfo("ticker activation closed"))
338
+ yield* fork(
339
+ Effect.sleep("50 millis").pipe(
340
+ Effect.andThen(Ref.update(ticks, (count) => count + 1)),
341
+ Effect.forever,
342
+ ),
343
+ )
344
+ return {
345
+ handlers: Ticker.of({
346
+ snapshot: () => Ref.get(ticks).pipe(Effect.map((ticks) => ({ ticks }))),
347
+ }),
348
+ failure: Effect.never,
349
+ }
350
+ }),
351
+ { maxIdleTime: "30 seconds", mailboxCapacity: 16, concurrency: 1 },
352
+ ).pipe(Layer.provideMerge(SqliteSingle.layer({ database, store, runtime })))
353
+
354
+ const program = Effect.gen(function* () {
355
+ const tickerFor = yield* Ticker.client
356
+ const ticker = tickerFor({ tenant: "acme", key: ["main"] })
357
+ const first = yield* ticker.actions.snapshot(null)
358
+ yield* Effect.sleep("300 millis")
359
+ const later = yield* ticker.actions.snapshot(null)
360
+ yield* Effect.logInfo("ticker advanced while hosted", { first, later })
361
+ })
362
+
363
+ BunRuntime.runMain(
364
+ SqliteSingle.migrate({ database, store }).pipe(
365
+ Effect.andThen(program.pipe(Effect.provide(TickerLive))),
366
+ ),
367
+ )
368
+ ```
369
+
370
+ Closures and fibers are not persisted — on reactivation `make` must rebuild pending work from your own durable authority. A retryable authority failure closes the scope and reactivates with backoff (500ms up to a 10s cap); a nonretryable one stops retries for that entity lifetime. Persisted delivery replays a request key your authority must store. [docs/guides/hosted-actors.md](docs/guides/hosted-actors.md) lists what the application owns before a persisted action can claim durable behavior; [examples/hosted-lifecycle](examples/hosted-lifecycle/sqlite.ts) verifies scope and finalizer semantics against real SQLite.
371
+
372
+ ## Public gateway
373
+
374
+ `Gateway.layer(options)` mounts HTTP, SSE, and WebSocket routes on an `HttpRouter` you own, exposing only the actor `protocol`s you list. Every request — commands, queries, receipt lookups, subscriptions, and their retries — goes through a `Gateway.Security` service you supply (`authenticate`, `authorize`, `admit`), including tenant scoping, origin checks, and bounded connection/event buffers.
375
+
376
+ A remote process gets the same typed client by swapping the `Client` implementation: provide `ActorClient.layerHttp({ namespace, baseUrl })` instead of a cluster composition and `Counter.client` talks to the gateway over HTTP.
377
+
378
+ Gateway integration is still being qualified against the platform compositions. Treat [docs/guides/gateway.md](docs/guides/gateway.md) as the contract and verify your deployment before exposing a route.
379
+
380
+ ## Deployment
381
+
382
+ | Topology | Composition | Actor ownership |
383
+ | --------------------- | --------------------------------------- | -------------------------------------------- |
384
+ | Bun + SQLite | `SqliteSingle` (`effect-actors-bun`) | One process, local file — no runner listener |
385
+ | Node + PostgreSQL | `PostgresSingle` (`effect-actors-node`) | One runner against shared PostgreSQL |
386
+ | Bun/Node + PostgreSQL | `PostgresCluster` | Replicas behind private runner RPC |
387
+ | Bun/Node + PostgreSQL | `PostgresClient` | None — clients and gateways only, no store |
388
+
389
+ The runtime `namespace` is also the Cluster shard group: replicas sharing a database must agree on namespace, `tablePrefix`, and actor definitions. `PostgresCluster` runners need a peer-reachable `advertisedAddress` and a bindable `listenAddress` (build both with `RunnerAddress.make(host, port)` from `effect/unstable/cluster`). A client-only process assigns no shard groups and cannot host actors or dispatchers — it is how you run a public gateway without actor ownership. Details in [docs/guides/deployment.md](docs/guides/deployment.md).
390
+
391
+ ## Testing
392
+
393
+ `Testing.layerMemory` provides the runtime, client, and an in-memory store — a full actor environment with no database:
394
+
395
+ ```ts
396
+ import { BunRuntime } from "@effect/platform-bun"
397
+ import { Effect, Layer, Result, Schema } from "effect"
398
+ import { Actor, Testing } from "effect-actors"
399
+
400
+ const Counter = Actor.make("Counter", {
401
+ protocolVersion: 1,
402
+ state: {
403
+ version: 1,
404
+ schema: Schema.Struct({ count: Schema.Int }),
405
+ initial: () => ({ count: 0 }),
406
+ migrations: [],
407
+ },
408
+ events: Schema.Never,
409
+ }).command("increment", {
410
+ payload: Schema.Struct({ by: Schema.Int }),
411
+ success: Schema.Struct({ count: Schema.Int }),
412
+ error: Schema.Never,
413
+ reduce: (state, { by }) => {
414
+ const count = state.count + by
415
+ return Result.succeed({ state: { count }, value: { count } })
416
+ },
417
+ })
418
+
419
+ const TestLive = Counter.toLayer().pipe(Layer.provideMerge(Testing.layerMemory))
420
+
421
+ const program = Effect.gen(function* () {
422
+ const counter = (yield* Counter.client)({ tenant: "acme", key: ["test"] })
423
+ const receipt = yield* counter.commands.increment({ by: 2 }, { commandId: "test-1" })
424
+ yield* Effect.logInfo("in-memory receipt", receipt)
425
+ })
426
+
427
+ BunRuntime.runMain(program.pipe(Effect.provide(TestLive)))
428
+ ```
429
+
430
+ ## What v1 does not do
431
+
432
+ - No exactly-once external side effects — outbox delivery is at-least-once
433
+ - No automatic global placement or live process migration
434
+ - No socket hibernation, code sandboxing, or zero-compute timer wakeups
435
+ - No cross-actor transactions — one atomic commit per actor address
436
+ - No managed backups; multi-runner evidence is limited to the tested failover cases
437
+ - Hosted actors do not add a second state store for your authority's data
438
+
439
+ ## Documentation
440
+
441
+ - [State actors](docs/guides/state-actors.md) — reducers, command identity, revisions, receipts, cursors
442
+ - [Timers and outbox](docs/guides/timers-and-outbox.md) — deferred and cross-actor work
443
+ - [Hosted actors](docs/guides/hosted-actors.md) — authority obligations and lifecycle
444
+ - [Gateway](docs/guides/gateway.md) — HTTP/SSE/WebSocket routes and the security contract
445
+ - [Deployment](docs/guides/deployment.md) — topologies and runner configuration
446
+ - [Operations](docs/guides/operations.md) — migrations, retention, recovery, diagnostics
447
+ - [Guarantees](docs/architecture/guarantees.md) — the contract and its boundaries
448
+ - [Examples](examples/README.md) — runnable, packaged, finite programs
449
+
450
+ ## Developing in this repo
451
+
452
+ ````bash
453
+ bun install --frozen-lockfile
454
+ bun run check # format, lint, typecheck
455
+ bun run test # package test suites
456
+ bun run pack # build distributable tarballs
457
+ bun run smoke:consumer # install tarballs into an isolated consumer
458
+ bun run examples:check # typecheck and run examples — and every ```ts block on this page
459
+ ````
460
+
461
+ PostgreSQL-backed tests need a disposable database; see [docs/guides/operations.md](docs/guides/operations.md#development-verification).
462
+
463
+ Releases publish only from the `v<version>` tag workflow on GitHub Actions, never from a workstation — [docs/release.md](docs/release.md).
464
+
465
+ ## License
466
+
467
+ [MIT](LICENSE) © 2026 Dallen Pyrah
@@ -0,0 +1,3 @@
1
+ export * as PostgresClient from "./postgres/client.js";
2
+ export * as PostgresCluster from "./postgres/cluster.js";
3
+ export * as SqliteSingle from "./sqlite/single.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * as PostgresClient from "./postgres/client.js";
2
+ export * as PostgresCluster from "./postgres/cluster.js";
3
+ export * as SqliteSingle from "./sqlite/single.js";
@@ -0,0 +1,11 @@
1
+ import * as PgClient from "@effect/sql-pg/PgClient";
2
+ import { Layer } from "effect";
3
+ import type * as ShardingConfig from "effect/unstable/cluster/ShardingConfig";
4
+ import { ActorRuntime } from "effect-actors";
5
+ export type ShardingOptions = Omit<Partial<ShardingConfig.ShardingConfig["Service"]>, "runnerAddress" | "runnerListenAddress" | "availableShardGroups" | "assignedShardGroups">;
6
+ export interface Options {
7
+ readonly database: PgClient.PgPoolConfig;
8
+ readonly runtime: ActorRuntime.Configuration;
9
+ readonly sharding?: ShardingOptions | undefined;
10
+ }
11
+ export declare const layer: (options: Options) => Layer.Layer<import("effect-actors/Client").Client | ActorRuntime.Runtime, import("effect/Config").ConfigError | import("effect-actors/Errors").RegistrationError | import("effect/unstable/sql/SqlError").SqlError, never>;
@@ -0,0 +1,20 @@
1
+ import { BunClusterHttp } from "@effect/platform-bun";
2
+ import * as PgClient from "@effect/sql-pg/PgClient";
3
+ import { Layer, Option } from "effect";
4
+ import { ActorRuntime } from "effect-actors";
5
+ export const layer = (options) => {
6
+ const database = PgClient.layer(options.database);
7
+ const cluster = BunClusterHttp.layer({
8
+ transport: "http",
9
+ clientOnly: true,
10
+ storage: "sql",
11
+ shardingConfig: {
12
+ ...options.sharding,
13
+ availableShardGroups: [options.runtime.namespace],
14
+ assignedShardGroups: [],
15
+ runnerAddress: Option.none(),
16
+ runnerListenAddress: Option.none(),
17
+ },
18
+ });
19
+ return ActorRuntime.layer(options.runtime).pipe(Layer.provide(cluster), Layer.provide(database));
20
+ };
@@ -0,0 +1,20 @@
1
+ import * as PgClient from "@effect/sql-pg/PgClient";
2
+ import { Effect, Layer } from "effect";
3
+ import type * as RunnerAddress from "effect/unstable/cluster/RunnerAddress";
4
+ import type * as ShardingConfig from "effect/unstable/cluster/ShardingConfig";
5
+ import { ActorRuntime, ActorStore } from "effect-actors";
6
+ export type ShardingOptions = Omit<Partial<ShardingConfig.ShardingConfig["Service"]>, "runnerAddress" | "runnerListenAddress" | "availableShardGroups" | "assignedShardGroups">;
7
+ export interface Options {
8
+ readonly database: PgClient.PgPoolConfig;
9
+ readonly store: ActorStore.SqlOptions;
10
+ readonly runtime: ActorRuntime.Configuration;
11
+ readonly advertisedAddress: RunnerAddress.RunnerAddress;
12
+ readonly listenAddress: RunnerAddress.RunnerAddress;
13
+ readonly sharding?: ShardingOptions | undefined;
14
+ }
15
+ export interface MigrationOptions {
16
+ readonly database: PgClient.PgPoolConfig;
17
+ readonly store: ActorStore.SqlOptions;
18
+ }
19
+ export declare const migrate: (options: MigrationOptions) => Effect.Effect<void, import("effect-actors/Errors").InvalidInput | import("effect/unstable/sql/SqlError").SqlError | import("effect-actors/Errors").StorageUnavailable | import("effect-actors/Errors").UnsupportedVersion, never>;
20
+ export declare const layer: (options: Options) => Layer.Layer<import("effect-actors/Client").Client | import("effect/unstable/cluster/MessageStorage").MessageStorage | import("effect/unstable/cluster/Runners").Runners | ActorRuntime.Runtime | import("effect/unstable/cluster/Sharding").Sharding | ActorStore.Store, import("effect/Config").ConfigError | import("effect-actors/Errors").RegistrationError | import("effect/unstable/http/HttpServerError").ServeError | import("effect/unstable/sql/SqlError").SqlError | import("effect-actors/Errors").StoreError, never>;
@@ -0,0 +1,21 @@
1
+ import { BunClusterHttp, BunCrypto } from "@effect/platform-bun";
2
+ import * as PgClient from "@effect/sql-pg/PgClient";
3
+ import { Effect, Layer, Option } from "effect";
4
+ import { ActorRuntime, ActorStore } from "effect-actors";
5
+ export const migrate = (options) => ActorStore.migrate(options.store).pipe(Effect.provide(PgClient.layer(options.database)));
6
+ export const layer = (options) => {
7
+ const database = PgClient.layer(options.database);
8
+ const cluster = BunClusterHttp.layer({
9
+ transport: "http",
10
+ storage: "sql",
11
+ shardingConfig: {
12
+ ...options.sharding,
13
+ availableShardGroups: [options.runtime.namespace],
14
+ assignedShardGroups: [options.runtime.namespace],
15
+ runnerAddress: Option.some(options.advertisedAddress),
16
+ runnerListenAddress: Option.some(options.listenAddress),
17
+ },
18
+ });
19
+ const infrastructure = Layer.mergeAll(cluster, ActorStore.layerSql(options.store)).pipe(Layer.provide([database, BunCrypto.layer]));
20
+ return ActorRuntime.layer(options.runtime).pipe(Layer.provideMerge(infrastructure));
21
+ };
@@ -0,0 +1,17 @@
1
+ import * as SqliteClient from "@effect/sql-sqlite-bun/SqliteClient";
2
+ import { Effect, Layer } from "effect";
3
+ import type * as ShardingConfig from "effect/unstable/cluster/ShardingConfig";
4
+ import { ActorRuntime, ActorStore } from "effect-actors";
5
+ export type ShardingOptions = Omit<Partial<ShardingConfig.ShardingConfig["Service"]>, "runnerAddress" | "runnerListenAddress" | "availableShardGroups" | "assignedShardGroups">;
6
+ export interface Options {
7
+ readonly database: SqliteClient.SqliteClientConfig;
8
+ readonly store: ActorStore.SqlOptions;
9
+ readonly runtime: ActorRuntime.Configuration;
10
+ readonly sharding?: ShardingOptions | undefined;
11
+ }
12
+ export interface MigrationOptions {
13
+ readonly database: SqliteClient.SqliteClientConfig;
14
+ readonly store: ActorStore.SqlOptions;
15
+ }
16
+ export declare const migrate: (options: MigrationOptions) => Effect.Effect<void, import("effect-actors/Errors").InvalidInput | import("effect-actors/Errors").StorageUnavailable | import("effect-actors/Errors").UnsupportedVersion, never>;
17
+ export declare const layer: (options: Options) => Layer.Layer<import("effect-actors/Client").Client | import("effect/unstable/cluster/MessageStorage").MessageStorage | import("effect/unstable/cluster/Runners").Runners | ActorRuntime.Runtime | import("effect/unstable/cluster/Sharding").Sharding | ActorStore.Store, import("effect/Config").ConfigError | import("effect-actors/Errors").RegistrationError | import("effect-actors/Errors").StoreError, never>;
@@ -0,0 +1,18 @@
1
+ import { BunCrypto } from "@effect/platform-bun";
2
+ import * as SqliteClient from "@effect/sql-sqlite-bun/SqliteClient";
3
+ import { Effect, Layer } from "effect";
4
+ import { SingleRunner } from "effect/unstable/cluster";
5
+ import { ActorRuntime, ActorStore } from "effect-actors";
6
+ export const migrate = (options) => ActorStore.migrate(options.store).pipe(Effect.provide(SqliteClient.layer(options.database)));
7
+ export const layer = (options) => {
8
+ const database = SqliteClient.layer(options.database);
9
+ const infrastructure = Layer.mergeAll(SingleRunner.layer({
10
+ runnerStorage: "memory",
11
+ shardingConfig: {
12
+ ...options.sharding,
13
+ availableShardGroups: [options.runtime.namespace],
14
+ assignedShardGroups: [options.runtime.namespace],
15
+ },
16
+ }), ActorStore.layerSql(options.store)).pipe(Layer.provide([database, BunCrypto.layer]));
17
+ return ActorRuntime.layer(options.runtime).pipe(Layer.provideMerge(infrastructure));
18
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "effect-actors-bun",
3
+ "version": "0.0.1",
4
+ "description": "Bun platform composition for effect-actors",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/dallenpyrah/effect-actors.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/dallenpyrah/effect-actors/issues"
27
+ },
28
+ "homepage": "https://github.com/dallenpyrah/effect-actors#readme",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "rm -rf dist && tsc --project tsconfig.build.json",
34
+ "lint": "prettier --check --cache --ignore-unknown --ignore-path ../../.prettierignore .",
35
+ "test": "bun test --timeout 30000",
36
+ "typecheck": "tsc --noEmit --project tsconfig.json"
37
+ },
38
+ "dependencies": {
39
+ "@effect/platform-bun": "4.0.0-rc.112",
40
+ "@effect/platform-node-shared": "4.0.0-rc.112",
41
+ "@effect/sql-pg": "4.0.0-rc.112",
42
+ "@effect/sql-sqlite-bun": "4.0.0-rc.112",
43
+ "effect-actors": "0.0.1"
44
+ },
45
+ "peerDependencies": {
46
+ "effect": "4.0.0-rc.112"
47
+ }
48
+ }