effect-actors 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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/dist/actor/client.d.ts +11 -0
  4. package/dist/actor/client.js +68 -0
  5. package/dist/actor/definition.d.ts +77 -0
  6. package/dist/actor/definition.js +110 -0
  7. package/dist/actor/state/codec.d.ts +10 -0
  8. package/dist/actor/state/codec.js +117 -0
  9. package/dist/actor/state/compiled.d.ts +17 -0
  10. package/dist/actor/state/compiled.js +1 -0
  11. package/dist/actor/state/layer.d.ts +7 -0
  12. package/dist/actor/state/layer.js +167 -0
  13. package/dist/client/http/errors.d.ts +30 -0
  14. package/dist/client/http/errors.js +72 -0
  15. package/dist/client/http/events.d.ts +10 -0
  16. package/dist/client/http/events.js +30 -0
  17. package/dist/client/http/layer.d.ts +5 -0
  18. package/dist/client/http/layer.js +133 -0
  19. package/dist/client/http.d.ts +1 -0
  20. package/dist/client/http.js +1 -0
  21. package/dist/client/outcome.d.ts +4 -0
  22. package/dist/client/outcome.js +10 -0
  23. package/dist/client/service.d.ts +28 -0
  24. package/dist/client/service.js +4 -0
  25. package/dist/errors/index.d.ts +93 -0
  26. package/dist/errors/index.js +93 -0
  27. package/dist/gateway/access.d.ts +11 -0
  28. package/dist/gateway/access.js +38 -0
  29. package/dist/gateway/dispatch.d.ts +13 -0
  30. package/dist/gateway/dispatch.js +45 -0
  31. package/dist/gateway/fault.d.ts +32 -0
  32. package/dist/gateway/fault.js +85 -0
  33. package/dist/gateway/http/events.d.ts +8 -0
  34. package/dist/gateway/http/events.js +32 -0
  35. package/dist/gateway/http/response.d.ts +11 -0
  36. package/dist/gateway/http/response.js +57 -0
  37. package/dist/gateway/http/routes.d.ts +6 -0
  38. package/dist/gateway/http/routes.js +184 -0
  39. package/dist/gateway/http/validation.d.ts +19 -0
  40. package/dist/gateway/http/validation.js +92 -0
  41. package/dist/gateway/options.d.ts +31 -0
  42. package/dist/gateway/options.js +107 -0
  43. package/dist/gateway/security.d.ts +25 -0
  44. package/dist/gateway/security.js +4 -0
  45. package/dist/gateway/service.d.ts +113 -0
  46. package/dist/gateway/service.js +5 -0
  47. package/dist/gateway/websocket/operations.d.ts +20 -0
  48. package/dist/gateway/websocket/operations.js +96 -0
  49. package/dist/gateway/websocket.d.ts +8 -0
  50. package/dist/gateway/websocket.js +96 -0
  51. package/dist/gateway/wire.d.ts +172 -0
  52. package/dist/gateway/wire.js +102 -0
  53. package/dist/host/client.d.ts +9 -0
  54. package/dist/host/client.js +46 -0
  55. package/dist/host/definition.d.ts +71 -0
  56. package/dist/host/definition.js +68 -0
  57. package/dist/host/entity.d.ts +9 -0
  58. package/dist/host/entity.js +302 -0
  59. package/dist/index.d.ts +9 -0
  60. package/dist/index.js +9 -0
  61. package/dist/protocol/encoding.d.ts +29 -0
  62. package/dist/protocol/encoding.js +165 -0
  63. package/dist/protocol/index.d.ts +90 -0
  64. package/dist/protocol/index.js +1 -0
  65. package/dist/protocol/schemas.d.ts +49 -0
  66. package/dist/protocol/schemas.js +30 -0
  67. package/dist/runtime/cluster/address.d.ts +3 -0
  68. package/dist/runtime/cluster/address.js +21 -0
  69. package/dist/runtime/cluster/client.d.ts +9 -0
  70. package/dist/runtime/cluster/client.js +131 -0
  71. package/dist/runtime/cluster/protocol.d.ts +104 -0
  72. package/dist/runtime/cluster/protocol.js +69 -0
  73. package/dist/runtime/configuration.d.ts +4 -0
  74. package/dist/runtime/configuration.js +34 -0
  75. package/dist/runtime/dispatchers/layer.d.ts +6 -0
  76. package/dist/runtime/dispatchers/layer.js +53 -0
  77. package/dist/runtime/dispatchers/outbox.d.ts +4 -0
  78. package/dist/runtime/dispatchers/outbox.js +57 -0
  79. package/dist/runtime/layer.d.ts +6 -0
  80. package/dist/runtime/layer.js +43 -0
  81. package/dist/runtime/service.d.ts +40 -0
  82. package/dist/runtime/service.js +5 -0
  83. package/dist/store/memory/background.d.ts +39 -0
  84. package/dist/store/memory/background.js +206 -0
  85. package/dist/store/memory/model.d.ts +72 -0
  86. package/dist/store/memory/model.js +149 -0
  87. package/dist/store/memory/ownership.d.ts +11 -0
  88. package/dist/store/memory/ownership.js +82 -0
  89. package/dist/store/memory/service.d.ts +3 -0
  90. package/dist/store/memory/service.js +32 -0
  91. package/dist/store/memory/state.d.ts +13 -0
  92. package/dist/store/memory/state.js +109 -0
  93. package/dist/store/service.d.ts +89 -0
  94. package/dist/store/service.js +9 -0
  95. package/dist/store/sql/events.d.ts +13 -0
  96. package/dist/store/sql/events.js +89 -0
  97. package/dist/store/sql/model.d.ts +39 -0
  98. package/dist/store/sql/model.js +126 -0
  99. package/dist/store/sql/outbox.d.ts +15 -0
  100. package/dist/store/sql/outbox.js +130 -0
  101. package/dist/store/sql/ownership.d.ts +44 -0
  102. package/dist/store/sql/ownership.js +130 -0
  103. package/dist/store/sql/receipts.d.ts +25 -0
  104. package/dist/store/sql/receipts.js +93 -0
  105. package/dist/store/sql/schema.d.ts +9 -0
  106. package/dist/store/sql/schema.js +124 -0
  107. package/dist/store/sql/service.d.ts +5 -0
  108. package/dist/store/sql/service.js +42 -0
  109. package/dist/store/sql/state.d.ts +13 -0
  110. package/dist/store/sql/state.js +109 -0
  111. package/dist/store/sql/timers.d.ts +10 -0
  112. package/dist/store/sql/timers.js +90 -0
  113. package/dist/store/sql/transaction.d.ts +6 -0
  114. package/dist/store/sql/transaction.js +57 -0
  115. package/dist/store/sql/transition.d.ts +15 -0
  116. package/dist/store/sql/transition.js +154 -0
  117. package/dist/testing/index.d.ts +6 -0
  118. package/dist/testing/index.js +27 -0
  119. package/package.json +80 -0
@@ -0,0 +1,110 @@
1
+ import { Result } from "effect";
2
+ import { RegistrationError as InvalidDefinition } from "../errors/index.js";
3
+ import { makeClient } from "./client.js";
4
+ import { assertIdentifier, decodeJson, encodeJson, prepareMigrations, validateTransition, } from "./state/codec.js";
5
+ import { layer } from "./state/layer.js";
6
+ export const make = (name, options) => {
7
+ assertIdentifier(name);
8
+ if (!Number.isSafeInteger(options.protocolVersion) ||
9
+ options.protocolVersion < 1 ||
10
+ !Number.isSafeInteger(options.state.version) ||
11
+ options.state.version < 1) {
12
+ throw new InvalidDefinition({
13
+ message: "Actor protocol and state versions must be positive safe integers",
14
+ });
15
+ }
16
+ const state = Object.freeze({
17
+ ...options.state,
18
+ migrations: Object.freeze([...options.state.migrations]),
19
+ });
20
+ const migrations = prepareMigrations(state);
21
+ const build = (commands, queries, reducers, readers) => {
22
+ const compiled = {
23
+ name,
24
+ protocolVersion: options.protocolVersion,
25
+ stateVersion: state.version,
26
+ initial: () => encodeJson(state.schema, state.initial()),
27
+ migrations,
28
+ commands,
29
+ queries,
30
+ reducers,
31
+ readers,
32
+ };
33
+ const protocol = Object.freeze({
34
+ kind: "state",
35
+ actorType: name,
36
+ version: options.protocolVersion,
37
+ commands,
38
+ queries,
39
+ eventSchema: options.events,
40
+ });
41
+ return Object.freeze({
42
+ name,
43
+ protocolVersion: options.protocolVersion,
44
+ protocol,
45
+ state,
46
+ eventSchema: options.events,
47
+ commands,
48
+ queries,
49
+ client: makeClient({
50
+ name,
51
+ protocolVersion: options.protocolVersion,
52
+ commands,
53
+ queries,
54
+ eventSchema: options.events,
55
+ }),
56
+ toLayer: (entityOptions) => layer(compiled, entityOptions),
57
+ command(operation, procedure) {
58
+ assertIdentifier(operation);
59
+ if (Object.hasOwn(commands, operation) || Object.hasOwn(queries, operation))
60
+ throw new InvalidDefinition({ message: "Duplicate actor operation" });
61
+ const metadata = Object.freeze({
62
+ payload: procedure.payload,
63
+ success: procedure.success,
64
+ error: procedure.error,
65
+ });
66
+ const nextCommands = Object.freeze({ ...commands, [operation]: metadata });
67
+ const reduce = (address, commandId, payload, turn, limits) => {
68
+ const result = procedure.reduce(decodeJson(state.schema, turn.snapshot.state), decodeJson(procedure.payload, payload), {
69
+ address,
70
+ commandId,
71
+ nowMillis: turn.nowMillis,
72
+ revision: turn.snapshot.revision,
73
+ });
74
+ if (Result.isFailure(result))
75
+ return Result.fail(encodeJson(procedure.error, result.failure));
76
+ const transition = result.success;
77
+ const encoded = {
78
+ state: encodeJson(state.schema, transition.state),
79
+ value: encodeJson(procedure.success, transition.value),
80
+ events: (transition.events ?? []).map((event) => encodeJson(options.events, event)),
81
+ timers: transition.timers ?? [],
82
+ outbox: transition.outbox ?? [],
83
+ };
84
+ validateTransition(address, encoded, limits);
85
+ return Result.succeed(encoded);
86
+ };
87
+ return build(nextCommands, queries, { ...reducers, [operation]: reduce }, readers);
88
+ },
89
+ query(operation, procedure) {
90
+ assertIdentifier(operation);
91
+ if (Object.hasOwn(commands, operation) || Object.hasOwn(queries, operation))
92
+ throw new InvalidDefinition({ message: "Duplicate actor operation" });
93
+ const metadata = Object.freeze({
94
+ payload: procedure.payload,
95
+ success: procedure.success,
96
+ error: procedure.error,
97
+ });
98
+ const nextQueries = Object.freeze({ ...queries, [operation]: metadata });
99
+ const read = (snapshot, payload) => {
100
+ const result = procedure.read(decodeJson(state.schema, snapshot), decodeJson(procedure.payload, payload));
101
+ return Result.isFailure(result)
102
+ ? { _tag: "Failure", error: encodeJson(procedure.error, result.failure) }
103
+ : { _tag: "Success", value: encodeJson(procedure.success, result.success) };
104
+ };
105
+ return build(commands, nextQueries, reducers, { ...readers, [operation]: read });
106
+ },
107
+ });
108
+ };
109
+ return build(Object.freeze({}), Object.freeze({}), {}, {});
110
+ };
@@ -0,0 +1,10 @@
1
+ import type { Address, Json, JsonSchema } from "../../protocol/index.js";
2
+ import type { Migration, StateDefinition } from "../definition.js";
3
+ import type { Configuration } from "../../runtime/service.js";
4
+ import type { EncodedTransition } from "../../store/service.js";
5
+ export declare const encodeJson: <S extends JsonSchema>(schema: S, value: S["Type"]) => Json;
6
+ export declare const decodeJson: <S extends JsonSchema>(schema: S, value: Json) => S["Type"];
7
+ export declare const assertIdentifier: (name: string) => void;
8
+ export declare const prepareMigrations: <S extends JsonSchema>(state: StateDefinition<S>) => ReadonlyArray<Migration>;
9
+ export declare const byteLength: (value: Json) => number;
10
+ export declare const validateTransition: (address: Address, transition: EncodedTransition, limits: Configuration) => void;
@@ -0,0 +1,117 @@
1
+ import { Result, Schema } from "effect";
2
+ import { InvalidInput, MigrationRejected, RegistrationError } from "../../errors/index.js";
3
+ import { addressId, canonicalJson, validateId } from "../../protocol/encoding.js";
4
+ export const encodeJson = (schema, value) => {
5
+ const result = Schema.encodeResult(schema)(value);
6
+ if (Result.isFailure(result))
7
+ throw new InvalidInput({ message: "Actor output does not match its schema" });
8
+ canonicalJson(result.success);
9
+ return result.success;
10
+ };
11
+ export const decodeJson = (schema, value) => {
12
+ const result = Schema.decodeUnknownResult(schema)(value, { onExcessProperty: "error" });
13
+ if (Result.isFailure(result))
14
+ throw new InvalidInput({ message: "Actor value does not match its schema" });
15
+ return result.success;
16
+ };
17
+ export const assertIdentifier = (name) => {
18
+ if (typeof name !== "string" ||
19
+ name.trim() !== name ||
20
+ !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(name) ||
21
+ ["constructor", "prototype", "__proto__"].includes(name)) {
22
+ throw new RegistrationError({
23
+ message: "Actor and operation names must be safe nonempty identifiers",
24
+ });
25
+ }
26
+ };
27
+ export const prepareMigrations = (state) => {
28
+ const sources = new Set();
29
+ return state.migrations.map((migration) => {
30
+ if (!Number.isSafeInteger(migration.fromVersion) ||
31
+ !Number.isSafeInteger(migration.toVersion) ||
32
+ migration.fromVersion < 1 ||
33
+ migration.toVersion <= migration.fromVersion ||
34
+ migration.toVersion > state.version ||
35
+ sources.has(migration.fromVersion)) {
36
+ throw new RegistrationError({
37
+ message: "State migration chain contains an invalid or ambiguous step",
38
+ });
39
+ }
40
+ sources.add(migration.fromVersion);
41
+ return {
42
+ fromVersion: migration.fromVersion,
43
+ toVersion: migration.toVersion,
44
+ migrate: (input) => {
45
+ try {
46
+ const result = migration.migrate(input);
47
+ if (Result.isFailure(result))
48
+ return result;
49
+ canonicalJson(result.success);
50
+ if (migration.toVersion === state.version)
51
+ decodeJson(state.schema, result.success);
52
+ return result;
53
+ }
54
+ catch {
55
+ return Result.fail(new MigrationRejected({
56
+ fromVersion: migration.fromVersion,
57
+ toVersion: migration.toVersion,
58
+ message: "State migration failed validation",
59
+ }));
60
+ }
61
+ },
62
+ };
63
+ });
64
+ };
65
+ export const byteLength = (value) => new TextEncoder().encode(canonicalJson(value)).byteLength;
66
+ export const validateTransition = (address, transition, limits) => {
67
+ if (byteLength(transition.state) > limits.maxStateBytes ||
68
+ byteLength(transition.value) > limits.maxStateBytes) {
69
+ throw new InvalidInput({ message: "Actor state or result exceeds the configured byte limit" });
70
+ }
71
+ if (transition.events.length > limits.maxEventsPerTurn ||
72
+ transition.outbox.length > limits.maxOutboxCallsPerTurn ||
73
+ transition.timers.length > limits.maxTimerChangesPerTurn) {
74
+ throw new InvalidInput({ message: "Actor turn exceeds its side-effect count limit" });
75
+ }
76
+ for (const event of transition.events) {
77
+ if (byteLength(event) > limits.maxEventBytes)
78
+ throw new InvalidInput({ message: "Actor event exceeds the configured byte limit" });
79
+ }
80
+ for (const timer of transition.timers) {
81
+ if (!timer ||
82
+ typeof timer.timerId !== "string" ||
83
+ timer.timerId.length === 0 ||
84
+ timer.timerId.length > 256) {
85
+ throw new InvalidInput({ message: "Invalid timer identity" });
86
+ }
87
+ validateId(timer.timerId);
88
+ if (timer._tag === "Cancel")
89
+ continue;
90
+ if (timer._tag !== "Set" || !Number.isSafeInteger(timer.dueAtMillis) || timer.dueAtMillis < 0) {
91
+ throw new InvalidInput({ message: "Invalid timer schedule" });
92
+ }
93
+ if (byteLength(timer.payload) > limits.maxCommandBytes)
94
+ throw new InvalidInput({ message: "Timer payload is too large" });
95
+ }
96
+ for (const call of transition.outbox) {
97
+ validateId(call.commandId);
98
+ addressId(call.target);
99
+ if (call.target.namespace !== address.namespace || call.target.tenant !== address.tenant) {
100
+ throw new InvalidInput({
101
+ message: "Outgoing calls must stay within the source namespace and tenant",
102
+ });
103
+ }
104
+ if (!["command", "hosted"].includes(call.kind) ||
105
+ typeof call.operation !== "string" ||
106
+ call.operation.length === 0 ||
107
+ typeof call.commandId !== "string" ||
108
+ call.commandId.length === 0 ||
109
+ call.commandId.length > 256 ||
110
+ !Number.isSafeInteger(call.protocolVersion) ||
111
+ call.protocolVersion < 1) {
112
+ throw new InvalidInput({ message: "Invalid outgoing call" });
113
+ }
114
+ if (byteLength(call.payload) > limits.maxCommandBytes)
115
+ throw new InvalidInput({ message: "Outgoing payload is too large" });
116
+ }
117
+ };
@@ -0,0 +1,17 @@
1
+ import type { Result } from "effect";
2
+ import type { Json, Outcome, Procedures } from "../../protocol/index.js";
3
+ import type { EncodedTransition, Turn } from "../../store/service.js";
4
+ import type { Address } from "../../protocol/index.js";
5
+ import type { Migration } from "../definition.js";
6
+ import type { Configuration } from "../../runtime/service.js";
7
+ export interface CompiledActor {
8
+ readonly name: string;
9
+ readonly protocolVersion: number;
10
+ readonly stateVersion: number;
11
+ readonly initial: () => Json;
12
+ readonly migrations: ReadonlyArray<Migration>;
13
+ readonly commands: Procedures;
14
+ readonly queries: Procedures;
15
+ readonly reducers: Readonly<Record<string, (address: Address, commandId: string, payload: Json, turn: Turn, limits: Configuration) => Result.Result<EncodedTransition, Json>>>;
16
+ readonly readers: Readonly<Record<string, (state: Json, payload: Json) => Outcome>>;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Layer } from "effect";
2
+ import { RegistrationError } from "../../errors/index.js";
3
+ import { Runtime } from "../../runtime/service.js";
4
+ import { Store } from "../../store/service.js";
5
+ import type { EntityOptions } from "../definition.js";
6
+ import type { CompiledActor } from "./compiled.js";
7
+ export declare const layer: (actor: CompiledActor, options?: EntityOptions) => Layer.Layer<never, RegistrationError, Runtime | Store>;
@@ -0,0 +1,167 @@
1
+ import { Duration, Effect, Layer, Option, Result } from "effect";
2
+ import { Entity } from "effect/unstable/cluster";
3
+ import { HostUnavailable, InvalidInput, MigrationRejected, RegistrationError, UnsupportedVersion, } from "../../errors/index.js";
4
+ import { boundary } from "../../protocol/encoding.js";
5
+ import { Runtime } from "../../runtime/service.js";
6
+ import { decodeAddress } from "../../runtime/cluster/address.js";
7
+ import { validateInvocation } from "../../runtime/cluster/client.js";
8
+ import { makeEntity } from "../../runtime/cluster/protocol.js";
9
+ import { Store } from "../../store/service.js";
10
+ import { byteLength, decodeJson } from "./codec.js";
11
+ const retryInfrastructure = (effect) => effect.pipe(Effect.catchIf((error) => typeof error === "object" &&
12
+ error !== null &&
13
+ "_tag" in error &&
14
+ (error._tag === "effect-actors/OwnershipLost" ||
15
+ (error._tag === "effect-actors/StorageUnavailable" &&
16
+ "retryable" in error &&
17
+ error.retryable === true)), () => Effect.die("Actor storage ownership is temporarily unavailable")));
18
+ export const layer = (actor, options = {}) => Layer.effectDiscard(Effect.gen(function* () {
19
+ yield* Effect.try({
20
+ try: () => {
21
+ if (options.mailboxCapacity !== undefined &&
22
+ (!Number.isSafeInteger(options.mailboxCapacity) || options.mailboxCapacity < 1))
23
+ throw new Error();
24
+ if (options.maxIdleTime !== undefined && !(Duration.toMillis(options.maxIdleTime) > 0))
25
+ throw new Error();
26
+ },
27
+ catch: () => new RegistrationError({ message: "Invalid actor mailbox capacity or idle duration" }),
28
+ });
29
+ const runtime = yield* Runtime;
30
+ const store = yield* Store;
31
+ const configuration = runtime.configuration;
32
+ const maximumTurnMillis = Duration.toMillis(configuration.maxTurnDuration);
33
+ const migrations = actor.migrations.map((migration) => ({
34
+ ...migration,
35
+ migrate: (value) => {
36
+ const result = migration.migrate(value);
37
+ if (Result.isSuccess(result) &&
38
+ byteLength(result.success) > configuration.maxStateBytes) {
39
+ return Result.fail(new MigrationRejected({
40
+ fromVersion: migration.fromVersion,
41
+ toVersion: migration.toVersion,
42
+ message: "Migrated state exceeds the configured byte limit",
43
+ }));
44
+ }
45
+ return result;
46
+ },
47
+ }));
48
+ const entity = makeEntity(configuration.namespace, actor.name);
49
+ const build = Effect.gen(function* () {
50
+ const current = yield* Entity.CurrentAddress;
51
+ const address = yield* Effect.orDie(boundary(() => decodeAddress(configuration.namespace, actor.name, current.entityId)));
52
+ const ready = yield* Effect.result(Effect.gen(function* () {
53
+ const owner = yield* store.acquire(address, {
54
+ leaseDuration: configuration.actorLeaseDuration,
55
+ renewEvery: configuration.actorLeaseRenewEvery,
56
+ });
57
+ const initial = yield* boundary(actor.initial);
58
+ yield* boundary(() => {
59
+ if (byteLength(initial) > configuration.maxStateBytes)
60
+ throw new InvalidInput({
61
+ message: "Initial state exceeds the configured byte limit",
62
+ });
63
+ });
64
+ const snapshot = yield* store.initialize(owner, {
65
+ currentVersion: actor.stateVersion,
66
+ initial,
67
+ migrations,
68
+ });
69
+ yield* boundary(() => {
70
+ if (byteLength(snapshot.state) > configuration.maxStateBytes)
71
+ throw new InvalidInput({
72
+ message: "Stored state exceeds the configured byte limit",
73
+ });
74
+ });
75
+ return owner;
76
+ }));
77
+ const withOwner = (run) => retryInfrastructure(Result.isFailure(ready)
78
+ ? Effect.fail(ready.failure)
79
+ : Effect.raceFirst(run(ready.success), ready.success.lost));
80
+ const evaluate = (command, commandId, payload, turn) => {
81
+ const reduce = Object.hasOwn(actor.reducers, command)
82
+ ? actor.reducers[command]
83
+ : undefined;
84
+ if (!reduce)
85
+ throw new InvalidInput({ message: "Unknown actor command" });
86
+ const start = performance.now();
87
+ const result = reduce(address, commandId, payload, turn, configuration);
88
+ if (performance.now() - start > maximumTurnMillis)
89
+ throw new InvalidInput({ message: "Actor reducer exceeded its turn budget" });
90
+ if (Result.isFailure(result) && byteLength(result.failure) > configuration.maxStateBytes)
91
+ throw new InvalidInput({ message: "Actor error exceeds the configured byte limit" });
92
+ if (Result.isFailure(result))
93
+ return result;
94
+ for (const timer of result.success.timers) {
95
+ if (timer._tag !== "Set")
96
+ continue;
97
+ const target = Object.hasOwn(actor.commands, timer.command)
98
+ ? actor.commands[timer.command]
99
+ : undefined;
100
+ if (!target)
101
+ throw new InvalidInput({ message: "Timer targets an unknown command" });
102
+ decodeJson(target.payload, timer.payload);
103
+ }
104
+ return result;
105
+ };
106
+ return entity.of({
107
+ Command: ({ payload }) => withOwner((owner) => Effect.gen(function* () {
108
+ const input = { ...payload, address };
109
+ yield* boundary(() => validateInvocation(input, configuration));
110
+ if (payload.protocolVersion !== actor.protocolVersion)
111
+ return yield* new UnsupportedVersion({
112
+ actorType: actor.name,
113
+ version: payload.protocolVersion,
114
+ });
115
+ const reduce = Object.hasOwn(actor.reducers, payload.command)
116
+ ? actor.reducers[payload.command]
117
+ : undefined;
118
+ if (!reduce)
119
+ return yield* new InvalidInput({ message: "Unknown actor command" });
120
+ return yield* store.transact(owner, input, (turn) => evaluate(payload.command, payload.commandId, payload.payload, turn));
121
+ })),
122
+ Query: ({ payload }) => withOwner((owner) => Effect.gen(function* () {
123
+ if (payload.protocolVersion !== actor.protocolVersion)
124
+ return yield* new UnsupportedVersion({
125
+ actorType: actor.name,
126
+ version: payload.protocolVersion,
127
+ });
128
+ const read = Object.hasOwn(actor.readers, payload.query)
129
+ ? actor.readers[payload.query]
130
+ : undefined;
131
+ if (!read)
132
+ return yield* new InvalidInput({ message: "Unknown actor query" });
133
+ yield* boundary(() => {
134
+ if (byteLength(payload.payload) > configuration.maxCommandBytes)
135
+ throw new InvalidInput({
136
+ message: "Query payload exceeds the configured byte limit",
137
+ });
138
+ });
139
+ return yield* store.query(owner, (snapshot) => {
140
+ const start = performance.now();
141
+ const value = read(snapshot.state, payload.payload);
142
+ if (performance.now() - start > maximumTurnMillis)
143
+ throw new InvalidInput({ message: "Actor query exceeded its turn budget" });
144
+ if (byteLength(value._tag === "Success" ? value.value : value.error) >
145
+ configuration.maxStateBytes)
146
+ throw new InvalidInput({
147
+ message: "Query output exceeds the configured byte limit",
148
+ });
149
+ return { revision: snapshot.revision, cursor: snapshot.cursor, value };
150
+ });
151
+ })),
152
+ Receipt: ({ payload }) => retryInfrastructure(store.receipt(address, payload.commandId)).pipe(Effect.map(Option.getOrNull)),
153
+ Events: ({ payload }) => retryInfrastructure(store.events(address, payload)),
154
+ Wake: ({ payload }) => withOwner((owner) => store.fireTimer(owner, { address, ...payload }, (command, value, turn) => {
155
+ return evaluate(command, `~effect-actors/timer/${JSON.stringify([payload.timerId, payload.generation])}`, value, turn);
156
+ })).pipe(Effect.map(Option.getOrNull)),
157
+ Action: () => Effect.fail(new HostUnavailable({ retryable: false })),
158
+ DurableAction: () => Effect.fail(new HostUnavailable({ retryable: false })),
159
+ });
160
+ });
161
+ yield* runtime.register({
162
+ actorType: actor.name,
163
+ protocolVersion: actor.protocolVersion,
164
+ kind: "state",
165
+ entity: entity.toLayer(build, { ...options, concurrency: 1 }),
166
+ });
167
+ }));
@@ -0,0 +1,30 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { CursorExpired, HostUnavailable, InvalidInput, StorageUnavailable } from "../../errors/index.js";
3
+ import type { CommandError, ReadError } from "../../errors/index.js";
4
+ import type { Json } from "../../protocol/index.js";
5
+ import type { GatewayError as RemoteError } from "../../gateway/wire.js";
6
+ export interface ErrorContext {
7
+ readonly commandId?: string;
8
+ readonly actorType: string;
9
+ readonly protocolVersion?: number;
10
+ }
11
+ export declare const isSuccess: (status: number) => boolean;
12
+ export declare const invalid: () => InvalidInput;
13
+ export declare const unavailable: () => StorageUnavailable;
14
+ export declare const remoteCommandError: (error: RemoteError, context: ErrorContext) => CommandError;
15
+ export declare const remoteReadError: (error: RemoteError, context: ErrorContext) => ReadError | CursorExpired;
16
+ export declare const parseCommandError: (response: {
17
+ readonly json: Effect.Effect<Json, unknown>;
18
+ }, context: ErrorContext, fallback: CommandError) => Effect.Effect<never, CommandError>;
19
+ export declare const parseReadError: (response: {
20
+ readonly json: Effect.Effect<Json, unknown>;
21
+ }, context: ErrorContext) => Effect.Effect<never, ReadError | CursorExpired>;
22
+ export declare const parseReadOnlyError: (response: {
23
+ readonly json: Effect.Effect<Json, unknown>;
24
+ }, context: ErrorContext) => Effect.Effect<never, ReadError>;
25
+ export declare const parseHostedError: (response: {
26
+ readonly json: Effect.Effect<Json, unknown>;
27
+ }, context: ErrorContext) => Effect.Effect<never, CommandError | HostUnavailable>;
28
+ export declare const responseJson: <S extends Schema.ConstraintDecoder<unknown>, E>(response: {
29
+ readonly json: Effect.Effect<Json, unknown>;
30
+ }, schema: S, fallback: E) => Effect.Effect<S["Type"], E>;
@@ -0,0 +1,72 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { CommandConflict, CursorExpired, HostUnavailable, InvalidInput, OutcomeUnknown, Overloaded, RevisionConflict, StorageUnavailable, Unauthorized, UnsupportedVersion, } from "../../errors/index.js";
3
+ import { decode } from "../../protocol/index.js";
4
+ import { GatewayErrorResponse } from "../../gateway/wire.js";
5
+ export const isSuccess = (status) => status >= 200 && status < 300;
6
+ export const invalid = () => new InvalidInput({ message: "Invalid HTTP actor request" });
7
+ export const unavailable = () => new StorageUnavailable({ retryable: true });
8
+ export const remoteCommandError = (error, context) => {
9
+ switch (error.code) {
10
+ case "InvalidInput":
11
+ case "NotFound":
12
+ return invalid();
13
+ case "Unauthorized":
14
+ case "Unauthenticated":
15
+ return new Unauthorized({ message: "Gateway rejected access" });
16
+ case "Overloaded":
17
+ return new Overloaded({ retryAfterMillis: error.retryAfterMillis ?? 1_000 });
18
+ case "UnsupportedVersion":
19
+ return new UnsupportedVersion({
20
+ actorType: context.actorType,
21
+ version: context.protocolVersion ?? 0,
22
+ });
23
+ case "CommandConflict":
24
+ return new CommandConflict({ commandId: error.commandId ?? context.commandId ?? "" });
25
+ case "RevisionConflict":
26
+ return new RevisionConflict({ expected: error.expected ?? "", actual: error.actual ?? "" });
27
+ case "OutcomeUnknown":
28
+ return new OutcomeUnknown({ commandId: error.commandId ?? context.commandId ?? "" });
29
+ case "HostUnavailable":
30
+ case "Unavailable":
31
+ case "SlowConsumer":
32
+ case "CursorExpired":
33
+ return context.commandId === undefined
34
+ ? unavailable()
35
+ : new OutcomeUnknown({ commandId: context.commandId });
36
+ default:
37
+ return context.commandId === undefined
38
+ ? unavailable()
39
+ : new OutcomeUnknown({ commandId: context.commandId });
40
+ }
41
+ };
42
+ export const remoteReadError = (error, context) => {
43
+ if (error.code === "CursorExpired")
44
+ return new CursorExpired({ message: "Event cursor has expired" });
45
+ const commandError = remoteCommandError(error, context);
46
+ switch (commandError._tag) {
47
+ case "effect-actors/InvalidInput":
48
+ case "effect-actors/Unauthorized":
49
+ case "effect-actors/Overloaded":
50
+ case "effect-actors/UnsupportedVersion":
51
+ return commandError;
52
+ default:
53
+ return unavailable();
54
+ }
55
+ };
56
+ export const parseCommandError = (response, context, fallback) => response.json.pipe(Effect.flatMap((body) => decode(GatewayErrorResponse, body)), Effect.matchEffect({
57
+ onFailure: () => Effect.fail(fallback),
58
+ onSuccess: ({ error }) => Effect.fail(remoteCommandError(error, context)),
59
+ }));
60
+ export const parseReadError = (response, context) => response.json.pipe(Effect.flatMap((body) => decode(GatewayErrorResponse, body)), Effect.matchEffect({
61
+ onFailure: () => Effect.fail(unavailable()),
62
+ onSuccess: ({ error }) => Effect.fail(remoteReadError(error, context)),
63
+ }));
64
+ export const parseReadOnlyError = (response, context) => parseReadError(response, context).pipe(Effect.catchIf((error) => error instanceof CursorExpired, () => Effect.fail(unavailable())));
65
+ const remoteHostedError = (error, context) => error.code === "HostUnavailable"
66
+ ? new HostUnavailable({ retryable: true })
67
+ : remoteCommandError(error, context);
68
+ export const parseHostedError = (response, context) => response.json.pipe(Effect.flatMap((body) => decode(GatewayErrorResponse, body)), Effect.matchEffect({
69
+ onFailure: () => Effect.fail(new HostUnavailable({ retryable: true })),
70
+ onSuccess: ({ error }) => Effect.fail(remoteHostedError(error, context)),
71
+ }));
72
+ export const responseJson = (response, schema, fallback) => response.json.pipe(Effect.flatMap((body) => decode(schema, body)), Effect.mapError(() => fallback));
@@ -0,0 +1,10 @@
1
+ import { Effect, Stream } from "effect";
2
+ import { CursorExpired, InvalidInput } from "../../errors/index.js";
3
+ import type { ReadError } from "../../errors/index.js";
4
+ import type { Address, Event, Json } from "../../protocol/index.js";
5
+ import type { ErrorContext } from "./errors.js";
6
+ export declare const eventsUrl: (baseUrl: string, address: Address, after: string) => string;
7
+ export declare const validateCursor: (address: Address, cursor: string) => Effect.Effect<string, InvalidInput>;
8
+ export declare const decodeEventStream: (stream: Stream.Stream<Uint8Array, unknown>, context: ErrorContext & {
9
+ readonly address: Address;
10
+ }) => Stream.Stream<Event<Json>, ReadError | CursorExpired>;
@@ -0,0 +1,30 @@
1
+ import { Effect, Encoding, Stream } from "effect";
2
+ import { Sse } from "effect/unstable/encoding";
3
+ import { CursorExpired, InvalidInput } from "../../errors/index.js";
4
+ import { boundary, canonicalJson, decode, readCursor } from "../../protocol/index.js";
5
+ import { Event as EventSchema } from "../../protocol/schemas.js";
6
+ import { GatewayError } from "../../gateway/wire.js";
7
+ import { invalid, remoteReadError, unavailable } from "./errors.js";
8
+ const segment = (value) => encodeURIComponent(value);
9
+ export const eventsUrl = (baseUrl, address, after) => {
10
+ const key = Encoding.encodeBase64Url(canonicalJson(address.key));
11
+ return `${baseUrl}/${segment(address.tenant)}/${segment(address.actorType)}/events?key=${key}&after=${segment(after)}`;
12
+ };
13
+ export const validateCursor = (address, cursor) => boundary(() => readCursor(address, cursor));
14
+ const decodeSse = (event, context) => {
15
+ if (event.event === "actor-error") {
16
+ return boundary(() => JSON.parse(event.data)).pipe(Effect.flatMap((body) => decode(GatewayError, body)), Effect.mapError(() => unavailable()), Effect.flatMap((error) => Effect.fail(remoteReadError(error, context))));
17
+ }
18
+ if (event.event !== "actor-event" || event.id === undefined)
19
+ return Effect.fail(invalid());
20
+ return boundary(() => JSON.parse(event.data)).pipe(Effect.flatMap((body) => decode(EventSchema, body)), Effect.flatMap((decoded) => decoded.cursor === event.id
21
+ ? validateCursor(context.address, decoded.cursor).pipe(Effect.as(decoded))
22
+ : Effect.fail(invalid())), Effect.mapError(() => invalid()));
23
+ };
24
+ export const decodeEventStream = (stream, context) => stream.pipe(Stream.decodeText, Stream.pipeThroughChannel(Sse.decode()), Stream.mapEffect((event) => decodeSse(event, context)), Stream.mapError((error) => {
25
+ if (error instanceof InvalidInput || error instanceof CursorExpired)
26
+ return error;
27
+ if (typeof error === "object" && error !== null && error._tag)
28
+ return error;
29
+ return unavailable();
30
+ }));
@@ -0,0 +1,5 @@
1
+ import { Layer } from "effect";
2
+ import { HttpClient } from "effect/unstable/http";
3
+ import { Client } from "../service.js";
4
+ import type { HttpOptions } from "../service.js";
5
+ export declare const layerHttp: (options: HttpOptions) => Layer.Layer<Client, never, HttpClient.HttpClient>;