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.
- package/LICENSE +21 -0
- package/README.md +467 -0
- package/dist/actor/client.d.ts +11 -0
- package/dist/actor/client.js +68 -0
- package/dist/actor/definition.d.ts +77 -0
- package/dist/actor/definition.js +110 -0
- package/dist/actor/state/codec.d.ts +10 -0
- package/dist/actor/state/codec.js +117 -0
- package/dist/actor/state/compiled.d.ts +17 -0
- package/dist/actor/state/compiled.js +1 -0
- package/dist/actor/state/layer.d.ts +7 -0
- package/dist/actor/state/layer.js +167 -0
- package/dist/client/http/errors.d.ts +30 -0
- package/dist/client/http/errors.js +72 -0
- package/dist/client/http/events.d.ts +10 -0
- package/dist/client/http/events.js +30 -0
- package/dist/client/http/layer.d.ts +5 -0
- package/dist/client/http/layer.js +133 -0
- package/dist/client/http.d.ts +1 -0
- package/dist/client/http.js +1 -0
- package/dist/client/outcome.d.ts +4 -0
- package/dist/client/outcome.js +10 -0
- package/dist/client/service.d.ts +28 -0
- package/dist/client/service.js +4 -0
- package/dist/errors/index.d.ts +93 -0
- package/dist/errors/index.js +93 -0
- package/dist/gateway/access.d.ts +11 -0
- package/dist/gateway/access.js +38 -0
- package/dist/gateway/dispatch.d.ts +13 -0
- package/dist/gateway/dispatch.js +45 -0
- package/dist/gateway/fault.d.ts +32 -0
- package/dist/gateway/fault.js +85 -0
- package/dist/gateway/http/events.d.ts +8 -0
- package/dist/gateway/http/events.js +32 -0
- package/dist/gateway/http/response.d.ts +11 -0
- package/dist/gateway/http/response.js +57 -0
- package/dist/gateway/http/routes.d.ts +6 -0
- package/dist/gateway/http/routes.js +184 -0
- package/dist/gateway/http/validation.d.ts +19 -0
- package/dist/gateway/http/validation.js +92 -0
- package/dist/gateway/options.d.ts +31 -0
- package/dist/gateway/options.js +107 -0
- package/dist/gateway/security.d.ts +25 -0
- package/dist/gateway/security.js +4 -0
- package/dist/gateway/service.d.ts +113 -0
- package/dist/gateway/service.js +5 -0
- package/dist/gateway/websocket/operations.d.ts +20 -0
- package/dist/gateway/websocket/operations.js +96 -0
- package/dist/gateway/websocket.d.ts +8 -0
- package/dist/gateway/websocket.js +96 -0
- package/dist/gateway/wire.d.ts +172 -0
- package/dist/gateway/wire.js +102 -0
- package/dist/host/client.d.ts +9 -0
- package/dist/host/client.js +46 -0
- package/dist/host/definition.d.ts +71 -0
- package/dist/host/definition.js +68 -0
- package/dist/host/entity.d.ts +9 -0
- package/dist/host/entity.js +302 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/protocol/encoding.d.ts +29 -0
- package/dist/protocol/encoding.js +165 -0
- package/dist/protocol/index.d.ts +90 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/schemas.d.ts +49 -0
- package/dist/protocol/schemas.js +30 -0
- package/dist/runtime/cluster/address.d.ts +3 -0
- package/dist/runtime/cluster/address.js +21 -0
- package/dist/runtime/cluster/client.d.ts +9 -0
- package/dist/runtime/cluster/client.js +131 -0
- package/dist/runtime/cluster/protocol.d.ts +104 -0
- package/dist/runtime/cluster/protocol.js +69 -0
- package/dist/runtime/configuration.d.ts +4 -0
- package/dist/runtime/configuration.js +34 -0
- package/dist/runtime/dispatchers/layer.d.ts +6 -0
- package/dist/runtime/dispatchers/layer.js +53 -0
- package/dist/runtime/dispatchers/outbox.d.ts +4 -0
- package/dist/runtime/dispatchers/outbox.js +57 -0
- package/dist/runtime/layer.d.ts +6 -0
- package/dist/runtime/layer.js +43 -0
- package/dist/runtime/service.d.ts +40 -0
- package/dist/runtime/service.js +5 -0
- package/dist/store/memory/background.d.ts +39 -0
- package/dist/store/memory/background.js +206 -0
- package/dist/store/memory/model.d.ts +72 -0
- package/dist/store/memory/model.js +149 -0
- package/dist/store/memory/ownership.d.ts +11 -0
- package/dist/store/memory/ownership.js +82 -0
- package/dist/store/memory/service.d.ts +3 -0
- package/dist/store/memory/service.js +32 -0
- package/dist/store/memory/state.d.ts +13 -0
- package/dist/store/memory/state.js +109 -0
- package/dist/store/service.d.ts +89 -0
- package/dist/store/service.js +9 -0
- package/dist/store/sql/events.d.ts +13 -0
- package/dist/store/sql/events.js +89 -0
- package/dist/store/sql/model.d.ts +39 -0
- package/dist/store/sql/model.js +126 -0
- package/dist/store/sql/outbox.d.ts +15 -0
- package/dist/store/sql/outbox.js +130 -0
- package/dist/store/sql/ownership.d.ts +44 -0
- package/dist/store/sql/ownership.js +130 -0
- package/dist/store/sql/receipts.d.ts +25 -0
- package/dist/store/sql/receipts.js +93 -0
- package/dist/store/sql/schema.d.ts +9 -0
- package/dist/store/sql/schema.js +124 -0
- package/dist/store/sql/service.d.ts +5 -0
- package/dist/store/sql/service.js +42 -0
- package/dist/store/sql/state.d.ts +13 -0
- package/dist/store/sql/state.js +109 -0
- package/dist/store/sql/timers.d.ts +10 -0
- package/dist/store/sql/timers.js +90 -0
- package/dist/store/sql/transaction.d.ts +6 -0
- package/dist/store/sql/transaction.js +57 -0
- package/dist/store/sql/transition.d.ts +15 -0
- package/dist/store/sql/transition.js +154 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +27 -0
- package/package.json +80 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Client } from "../client/service.js";
|
|
3
|
+
import { unwrap } from "../client/outcome.js";
|
|
4
|
+
import { HostedErrorSchema } from "../errors/index.js";
|
|
5
|
+
import { boundary, decode, encode, fingerprint, validateId, validateKey, } from "../protocol/encoding.js";
|
|
6
|
+
const requestKeyFor = (action, payload) => {
|
|
7
|
+
if (action.delivery._tag === "Transient")
|
|
8
|
+
return undefined;
|
|
9
|
+
const requestKey = action.delivery.requestKey(payload);
|
|
10
|
+
return validateId(requestKey);
|
|
11
|
+
};
|
|
12
|
+
export const makeClient = (options) => Effect.map(Client, (client) => (key) => {
|
|
13
|
+
const identity = {
|
|
14
|
+
...key,
|
|
15
|
+
key: Array.isArray(key?.key) ? Object.freeze([...key.key]) : key?.key,
|
|
16
|
+
};
|
|
17
|
+
const address = () => ({
|
|
18
|
+
...validateKey(identity),
|
|
19
|
+
namespace: client.namespace,
|
|
20
|
+
actorType: options.name,
|
|
21
|
+
});
|
|
22
|
+
const actions = Object.fromEntries(Object.entries(options.actions).map(([name, action]) => [
|
|
23
|
+
name,
|
|
24
|
+
(payload) => Effect.gen(function* () {
|
|
25
|
+
const encoded = yield* encode(action.payload, payload);
|
|
26
|
+
const decoded = yield* decode(action.payload, encoded);
|
|
27
|
+
const target = yield* boundary(address);
|
|
28
|
+
const requestKey = yield* boundary(() => requestKeyFor(action, decoded));
|
|
29
|
+
const digest = yield* boundary(() => fingerprint({
|
|
30
|
+
command: name,
|
|
31
|
+
protocolVersion: options.protocolVersion,
|
|
32
|
+
payload: encoded,
|
|
33
|
+
}));
|
|
34
|
+
const outcome = yield* client.hosted({
|
|
35
|
+
address: target,
|
|
36
|
+
action: name,
|
|
37
|
+
protocolVersion: options.protocolVersion,
|
|
38
|
+
payload: encoded,
|
|
39
|
+
fingerprint: digest,
|
|
40
|
+
...(requestKey === undefined ? {} : { requestKey }),
|
|
41
|
+
});
|
|
42
|
+
return yield* unwrap(action.success, action.error, HostedErrorSchema, outcome);
|
|
43
|
+
}),
|
|
44
|
+
]));
|
|
45
|
+
return { actions };
|
|
46
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Duration, Fiber, Layer, Scope } from "effect";
|
|
3
|
+
import type { Client } from "../client/service.js";
|
|
4
|
+
import type { CommandError, HostUnavailable, RegistrationError } from "../errors/index.js";
|
|
5
|
+
import type { Address, Decoded, JsonSchema, Key, Procedure } from "../protocol/index.js";
|
|
6
|
+
import type { Runtime } from "../runtime/service.js";
|
|
7
|
+
export interface Action<P extends JsonSchema = JsonSchema, A extends JsonSchema = JsonSchema, E extends JsonSchema = JsonSchema> extends Procedure<P, A, E> {
|
|
8
|
+
readonly delivery: {
|
|
9
|
+
readonly _tag: "Transient";
|
|
10
|
+
} | {
|
|
11
|
+
readonly _tag: "Persisted";
|
|
12
|
+
readonly requestKey: (payload: Decoded<P>) => string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type Actions = Readonly<Record<string, Procedure & {
|
|
16
|
+
readonly delivery: {
|
|
17
|
+
readonly _tag: "Transient";
|
|
18
|
+
} | {
|
|
19
|
+
readonly _tag: "Persisted";
|
|
20
|
+
readonly requestKey: (payload: never) => string;
|
|
21
|
+
};
|
|
22
|
+
}>>;
|
|
23
|
+
export interface HostContext {
|
|
24
|
+
readonly address: Address;
|
|
25
|
+
readonly scope: Scope.Scope;
|
|
26
|
+
/**
|
|
27
|
+
* Runs authority-owned background work for this activation. The effect itself is not
|
|
28
|
+
* serialized; `make` must reconstruct pending work from durable application authority
|
|
29
|
+
* whenever the hosted actor activates again.
|
|
30
|
+
*/
|
|
31
|
+
readonly fork: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<Fiber.Fiber<A, E>, never, Exclude<R, Scope.Scope>>;
|
|
32
|
+
}
|
|
33
|
+
export type Handlers<A extends Actions> = {
|
|
34
|
+
readonly [N in keyof A]: (payload: Decoded<A[N]["payload"]>) => Effect.Effect<Decoded<A[N]["success"]>, Decoded<A[N]["error"]>>;
|
|
35
|
+
};
|
|
36
|
+
export interface Implementation<A extends Actions> {
|
|
37
|
+
readonly handlers: Handlers<A>;
|
|
38
|
+
readonly failure: Effect.Effect<never, HostUnavailable>;
|
|
39
|
+
}
|
|
40
|
+
export interface Reference<A extends Actions> {
|
|
41
|
+
readonly actions: {
|
|
42
|
+
readonly [N in keyof A]: (payload: Decoded<A[N]["payload"]>) => Effect.Effect<Decoded<A[N]["success"]>, Decoded<A[N]["error"]> | CommandError | HostUnavailable>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface Protocol<A extends Actions = Actions> {
|
|
46
|
+
readonly kind: "hosted";
|
|
47
|
+
readonly actorType: string;
|
|
48
|
+
readonly version: number;
|
|
49
|
+
readonly actions: A;
|
|
50
|
+
}
|
|
51
|
+
export interface Definition<A extends Actions> {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly protocolVersion: number;
|
|
54
|
+
readonly protocol: Protocol<A>;
|
|
55
|
+
readonly of: (handlers: Handlers<A>) => Handlers<A>;
|
|
56
|
+
readonly client: Effect.Effect<(key: Key) => Reference<A>, never, Client>;
|
|
57
|
+
/**
|
|
58
|
+
* Registers the hosted actor. `make` runs on every authority activation, including
|
|
59
|
+
* recovery driven by a persisted keep-alive without a new client request.
|
|
60
|
+
*/
|
|
61
|
+
readonly toLayer: <R>(make: (context: HostContext) => Effect.Effect<Implementation<A>, HostUnavailable, R>, options: {
|
|
62
|
+
readonly maxIdleTime: Duration.Input;
|
|
63
|
+
readonly mailboxCapacity: number;
|
|
64
|
+
readonly concurrency: number;
|
|
65
|
+
}) => Layer.Layer<never, RegistrationError, Runtime | Exclude<R, Scope.Scope>>;
|
|
66
|
+
}
|
|
67
|
+
export declare const action: <P extends JsonSchema, A extends JsonSchema, E extends JsonSchema>(options: Action<P, A, E>) => Action<P, A, E>;
|
|
68
|
+
export declare const make: <A extends Actions>(name: string, options: {
|
|
69
|
+
readonly protocolVersion: number;
|
|
70
|
+
readonly actions: A;
|
|
71
|
+
}) => Definition<A>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { assertIdentifier } from "../actor/state/codec.js";
|
|
3
|
+
import { makeClient } from "./client.js";
|
|
4
|
+
import { layer } from "./entity.js";
|
|
5
|
+
const validateVersion = (version) => {
|
|
6
|
+
if (!Number.isSafeInteger(version) || version < 1) {
|
|
7
|
+
throw new TypeError("Hosted actor protocol versions must be positive integers");
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const action = (options) => {
|
|
11
|
+
if (options === null || typeof options !== "object" || options.delivery === undefined) {
|
|
12
|
+
throw new TypeError("Hosted actions require a delivery policy");
|
|
13
|
+
}
|
|
14
|
+
if (options.payload === undefined ||
|
|
15
|
+
options.success === undefined ||
|
|
16
|
+
options.error === undefined) {
|
|
17
|
+
throw new TypeError("Hosted actions require payload, success, and error schemas");
|
|
18
|
+
}
|
|
19
|
+
if (options.delivery._tag !== "Transient" && options.delivery._tag !== "Persisted") {
|
|
20
|
+
throw new TypeError("Hosted action delivery must be transient or persisted");
|
|
21
|
+
}
|
|
22
|
+
if (options.delivery._tag === "Persisted" && typeof options.delivery.requestKey !== "function") {
|
|
23
|
+
throw new TypeError("Persisted hosted actions require a request key function");
|
|
24
|
+
}
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
payload: options.payload,
|
|
27
|
+
success: options.success,
|
|
28
|
+
error: options.error,
|
|
29
|
+
delivery: Object.freeze({ ...options.delivery }),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
export const make = (name, options) => {
|
|
33
|
+
if (options === null ||
|
|
34
|
+
typeof options !== "object" ||
|
|
35
|
+
options.actions === null ||
|
|
36
|
+
typeof options.actions !== "object") {
|
|
37
|
+
throw new TypeError("Hosted actors require an action definition");
|
|
38
|
+
}
|
|
39
|
+
assertIdentifier(name);
|
|
40
|
+
validateVersion(options.protocolVersion);
|
|
41
|
+
const entries = Object.entries(options.actions);
|
|
42
|
+
if (entries.length === 0)
|
|
43
|
+
throw new TypeError("Hosted actors require at least one action");
|
|
44
|
+
const actions = Object.freeze(Object.fromEntries(entries.map(([actionName, definition]) => {
|
|
45
|
+
assertIdentifier(actionName);
|
|
46
|
+
if (definition === null || typeof definition !== "object" || !("delivery" in definition)) {
|
|
47
|
+
throw new TypeError("Hosted actions must be created with HostActor.action");
|
|
48
|
+
}
|
|
49
|
+
return [actionName, action(definition)];
|
|
50
|
+
})));
|
|
51
|
+
const protocol = Object.freeze({
|
|
52
|
+
kind: "hosted",
|
|
53
|
+
actorType: name,
|
|
54
|
+
version: options.protocolVersion,
|
|
55
|
+
actions,
|
|
56
|
+
});
|
|
57
|
+
const of = (handlers) => Object.freeze({ ...handlers });
|
|
58
|
+
const client = makeClient({ name, protocolVersion: options.protocolVersion, actions });
|
|
59
|
+
const definition = {
|
|
60
|
+
name,
|
|
61
|
+
protocolVersion: options.protocolVersion,
|
|
62
|
+
protocol,
|
|
63
|
+
of,
|
|
64
|
+
client,
|
|
65
|
+
toLayer: (create, layerOptions) => layer(definition, create, layerOptions),
|
|
66
|
+
};
|
|
67
|
+
return Object.freeze(definition);
|
|
68
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect, Layer, Scope } from "effect";
|
|
2
|
+
import { HostUnavailable, RegistrationError } from "../errors/index.js";
|
|
3
|
+
import { Runtime } from "../runtime/service.js";
|
|
4
|
+
import type { Actions, Definition, HostContext, Implementation } from "./definition.js";
|
|
5
|
+
export declare const layer: <A extends Actions, R>(definition: Definition<A>, make: (context: HostContext) => Effect.Effect<Implementation<A>, HostUnavailable, R>, options: {
|
|
6
|
+
readonly maxIdleTime: import("effect").Duration.Input;
|
|
7
|
+
readonly mailboxCapacity: number;
|
|
8
|
+
readonly concurrency: number;
|
|
9
|
+
}) => Layer.Layer<never, RegistrationError, Runtime | Exclude<R, Scope.Scope>>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { Cause, Deferred, Duration, Effect, Exit, Layer, Option, Queue, Ref, Result, Schedule, Scope, Semaphore, } from "effect";
|
|
2
|
+
import { Entity, Sharding } from "effect/unstable/cluster";
|
|
3
|
+
import { HostUnavailable, InvalidInput, RegistrationError, UnsupportedVersion, } from "../errors/index.js";
|
|
4
|
+
import { boundary, canonicalJson, decode, encode, fingerprint, validateId, } from "../protocol/encoding.js";
|
|
5
|
+
import { decodeAddress } from "../runtime/cluster/address.js";
|
|
6
|
+
import { makeEntity } from "../runtime/cluster/protocol.js";
|
|
7
|
+
import { Runtime } from "../runtime/service.js";
|
|
8
|
+
const unavailable = (retryable) => new HostUnavailable({ retryable });
|
|
9
|
+
const reactivationPolicy = Schedule.min([
|
|
10
|
+
Schedule.exponential(500, 1.5),
|
|
11
|
+
Schedule.spaced("10 seconds"),
|
|
12
|
+
]);
|
|
13
|
+
const reject = () => Effect.fail(new InvalidInput({ message: "Operation is not available for this actor" }));
|
|
14
|
+
const validateOptions = (options) => {
|
|
15
|
+
const maxIdleMillis = Duration.toMillis(options.maxIdleTime);
|
|
16
|
+
if (!Number.isFinite(maxIdleMillis) ||
|
|
17
|
+
maxIdleMillis <= 0 ||
|
|
18
|
+
!Number.isSafeInteger(options.mailboxCapacity) ||
|
|
19
|
+
options.mailboxCapacity < 1 ||
|
|
20
|
+
!Number.isSafeInteger(options.concurrency) ||
|
|
21
|
+
options.concurrency < 1) {
|
|
22
|
+
throw new RegistrationError({
|
|
23
|
+
message: "Hosted actor idle duration, capacity, and concurrency must be positive",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const requestKeyFor = (action, payload) => {
|
|
28
|
+
if (action.delivery._tag !== "Persisted")
|
|
29
|
+
throw new InvalidInput({ message: "Hosted action is not persisted" });
|
|
30
|
+
const requestKey = action.delivery.requestKey(payload);
|
|
31
|
+
return validateId(requestKey);
|
|
32
|
+
};
|
|
33
|
+
const retainForRecovery = (background) => Ref.modify(background, (state) => {
|
|
34
|
+
const reactivate = state.active > 0;
|
|
35
|
+
return [reactivate, reactivate ? { ...state, retain: true } : state];
|
|
36
|
+
});
|
|
37
|
+
const hostContext = (address, scope, unavailableSignal, currentAddress, sharding) => Effect.gen(function* () {
|
|
38
|
+
const background = yield* Ref.make({ active: 0, retain: false });
|
|
39
|
+
const keepAlive = (enabled) => Entity.keepAlive(enabled).pipe(Effect.provideService(Sharding.Sharding, sharding), Effect.provideService(Entity.CurrentAddress, currentAddress));
|
|
40
|
+
const release = Effect.gen(function* () {
|
|
41
|
+
const shouldRelease = yield* Ref.modify(background, (state) => [
|
|
42
|
+
state.active === 1 && !state.retain,
|
|
43
|
+
{ ...state, active: Math.max(0, state.active - 1) },
|
|
44
|
+
]);
|
|
45
|
+
if (shouldRelease)
|
|
46
|
+
yield* keepAlive(false);
|
|
47
|
+
});
|
|
48
|
+
const fork = (effect) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
49
|
+
const shouldRetain = yield* Ref.modify(background, (state) => [state.active === 0, { ...state, active: state.active + 1 }]);
|
|
50
|
+
if (shouldRetain)
|
|
51
|
+
yield* keepAlive(true);
|
|
52
|
+
const work = restore(effect).pipe(Scope.provide(scope), Effect.onError((cause) => Cause.hasInterruptsOnly(cause)
|
|
53
|
+
? Effect.void
|
|
54
|
+
: Effect.flatMap(retainForRecovery(background), (reactivate) => Deferred.fail(unavailableSignal, {
|
|
55
|
+
error: unavailable(true),
|
|
56
|
+
reactivate,
|
|
57
|
+
}).pipe(Effect.asVoid))), Effect.ensuring(release));
|
|
58
|
+
return yield* Effect.forkIn(work, scope);
|
|
59
|
+
}));
|
|
60
|
+
return { context: { address, scope, fork }, background, keepAlive };
|
|
61
|
+
});
|
|
62
|
+
const unavailableResult = (durable, error) => durable && error.retryable
|
|
63
|
+
? Effect.die("Hosted actor is temporarily unavailable")
|
|
64
|
+
: Effect.fail(error);
|
|
65
|
+
const validateHandlers = (actions, handlers) => {
|
|
66
|
+
if (typeof handlers !== "object" || handlers === null)
|
|
67
|
+
return unavailable(false);
|
|
68
|
+
const names = Object.keys(handlers);
|
|
69
|
+
const expected = Object.keys(actions);
|
|
70
|
+
if (names.length !== expected.length)
|
|
71
|
+
return unavailable(false);
|
|
72
|
+
for (const name of expected) {
|
|
73
|
+
if (!Object.hasOwn(handlers, name) || typeof handlers[name] !== "function")
|
|
74
|
+
return unavailable(false);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
const activationFailure = (activation) => Effect.flatMap(Deferred.poll(activation.unavailableSignal), Option.match({
|
|
79
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
80
|
+
onSome: (failure) => Effect.map(Effect.flip(failure), Option.some),
|
|
81
|
+
}));
|
|
82
|
+
const byteLength = (value) => new TextEncoder().encode(canonicalJson(value)).byteLength;
|
|
83
|
+
const handlersForActivation = (entity, actorType, actions, protocolVersion, maxCommandBytes, maxStateBytes, activate) => {
|
|
84
|
+
const dispatch = (durable, input) => Effect.gen(function* () {
|
|
85
|
+
if (input.protocolVersion !== protocolVersion) {
|
|
86
|
+
return yield* Effect.fail(new UnsupportedVersion({ actorType, version: input.protocolVersion }));
|
|
87
|
+
}
|
|
88
|
+
if (!Object.hasOwn(actions, input.action)) {
|
|
89
|
+
return yield* Effect.fail(new InvalidInput({ message: "Unknown hosted action" }));
|
|
90
|
+
}
|
|
91
|
+
const action = actions[input.action];
|
|
92
|
+
if ((action.delivery._tag === "Persisted") !== durable) {
|
|
93
|
+
return yield* Effect.fail(new InvalidInput({ message: "Hosted action delivery does not match its definition" }));
|
|
94
|
+
}
|
|
95
|
+
yield* boundary(() => {
|
|
96
|
+
if (byteLength(input.payload) > maxCommandBytes) {
|
|
97
|
+
throw new InvalidInput({
|
|
98
|
+
message: "Hosted action payload exceeds the configured byte limit",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const digest = yield* boundary(() => fingerprint({
|
|
103
|
+
command: input.action,
|
|
104
|
+
protocolVersion: input.protocolVersion,
|
|
105
|
+
payload: input.payload,
|
|
106
|
+
}));
|
|
107
|
+
if (digest !== input.fingerprint) {
|
|
108
|
+
return yield* Effect.fail(new InvalidInput({ message: "Hosted action fingerprint mismatch" }));
|
|
109
|
+
}
|
|
110
|
+
const payload = yield* decode(action.payload, input.payload);
|
|
111
|
+
const requestKey = durable ? yield* boundary(() => requestKeyFor(action, payload)) : undefined;
|
|
112
|
+
if (durable && input.requestKey !== requestKey) {
|
|
113
|
+
return yield* Effect.fail(new InvalidInput({ message: "Hosted action request key mismatch" }));
|
|
114
|
+
}
|
|
115
|
+
if (!durable && input.requestKey !== undefined) {
|
|
116
|
+
return yield* Effect.fail(new InvalidInput({ message: "Transient hosted actions cannot include a request key" }));
|
|
117
|
+
}
|
|
118
|
+
const activation = yield* activate.pipe(Effect.catch((error) => unavailableResult(durable, error)));
|
|
119
|
+
const knownFailure = yield* activationFailure(activation);
|
|
120
|
+
if (Option.isSome(knownFailure)) {
|
|
121
|
+
return yield* unavailableResult(durable, knownFailure.value.error);
|
|
122
|
+
}
|
|
123
|
+
const handler = activation.implementation.handlers[input.action];
|
|
124
|
+
const handle = Effect.gen(function* () {
|
|
125
|
+
const result = yield* Effect.result(Effect.suspend(() => handler(payload)));
|
|
126
|
+
if (Result.isSuccess(result)) {
|
|
127
|
+
const value = yield* encode(action.success, result.success);
|
|
128
|
+
yield* boundary(() => {
|
|
129
|
+
if (byteLength(value) > maxStateBytes) {
|
|
130
|
+
throw new InvalidInput({
|
|
131
|
+
message: "Hosted action output exceeds the configured byte limit",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return { _tag: "Success", value };
|
|
136
|
+
}
|
|
137
|
+
const error = yield* encode(action.error, result.failure);
|
|
138
|
+
yield* boundary(() => {
|
|
139
|
+
if (byteLength(error) > maxStateBytes) {
|
|
140
|
+
throw new InvalidInput({
|
|
141
|
+
message: "Hosted action error exceeds the configured byte limit",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return { _tag: "Failure", error };
|
|
146
|
+
});
|
|
147
|
+
return yield* Deferred.await(activation.unavailableSignal).pipe(Effect.catch((failure) => unavailableResult(durable, failure.error)), Effect.raceFirst(handle), Effect.catchDefect(() => Effect.die("Hosted actor handler failed")));
|
|
148
|
+
});
|
|
149
|
+
return entity.of({
|
|
150
|
+
Command: reject,
|
|
151
|
+
Query: reject,
|
|
152
|
+
Receipt: reject,
|
|
153
|
+
Events: reject,
|
|
154
|
+
Wake: reject,
|
|
155
|
+
Action: (request) => dispatch(false, request.payload),
|
|
156
|
+
DurableAction: (request) => dispatch(true, request.payload),
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const makeActivation = (options) => Effect.gen(function* () {
|
|
160
|
+
const scope = yield* Scope.fork(options.transportScope);
|
|
161
|
+
const unavailableSignal = yield* Deferred.make();
|
|
162
|
+
const managed = yield* hostContext(options.address, scope, unavailableSignal, options.currentAddress, options.sharding);
|
|
163
|
+
const failed = (error) => Effect.map(retainForRecovery(managed.background), (active) => ({
|
|
164
|
+
_tag: "Unavailable",
|
|
165
|
+
failure: {
|
|
166
|
+
error,
|
|
167
|
+
reactivate: options.retainRecovery || active,
|
|
168
|
+
},
|
|
169
|
+
}));
|
|
170
|
+
const initialize = Effect.suspend(() => options.make(managed.context)).pipe(Effect.provideContext(options.dependencies), Scope.provide(scope), Effect.matchEffect({
|
|
171
|
+
onFailure: failed,
|
|
172
|
+
onSuccess: (implementation) => Effect.succeed({ _tag: "Ready", implementation }),
|
|
173
|
+
}), Effect.catchDefect(() => failed(unavailable(true))));
|
|
174
|
+
const initialized = yield* Deferred.await(unavailableSignal).pipe(Effect.catch((failure) => Effect.succeed({ _tag: "Unavailable", failure })), Effect.raceFirst(initialize));
|
|
175
|
+
if (initialized._tag === "Unavailable") {
|
|
176
|
+
if (!initialized.failure.error.retryable || !initialized.failure.reactivate) {
|
|
177
|
+
yield* managed.keepAlive(false);
|
|
178
|
+
}
|
|
179
|
+
yield* Scope.close(scope, Exit.fail(initialized.failure.error));
|
|
180
|
+
return yield* Effect.fail(initialized.failure);
|
|
181
|
+
}
|
|
182
|
+
const invalid = validateHandlers(options.actions, initialized.implementation.handlers);
|
|
183
|
+
if (invalid !== undefined) {
|
|
184
|
+
yield* managed.keepAlive(false);
|
|
185
|
+
yield* Scope.close(scope, Exit.fail(invalid));
|
|
186
|
+
return yield* Effect.fail({ error: invalid, reactivate: false });
|
|
187
|
+
}
|
|
188
|
+
if (options.retainRecovery) {
|
|
189
|
+
const state = yield* Ref.get(managed.background);
|
|
190
|
+
if (state.active === 0)
|
|
191
|
+
yield* managed.keepAlive(false);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
scope,
|
|
195
|
+
implementation: initialized.implementation,
|
|
196
|
+
unavailableSignal,
|
|
197
|
+
background: managed.background,
|
|
198
|
+
keepAlive: managed.keepAlive,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const watchActivation = (activation, transportScope, invalidate) => Effect.gen(function* () {
|
|
202
|
+
const closeOnFailure = Deferred.await(activation.unavailableSignal).pipe(Effect.catch((failure) => invalidate(activation, failure)));
|
|
203
|
+
const fail = (error) => Effect.flatMap(retainForRecovery(activation.background), (reactivate) => Deferred.fail(activation.unavailableSignal, { error, reactivate }).pipe(Effect.asVoid));
|
|
204
|
+
const watchAuthority = activation.implementation.failure.pipe(Effect.catch(fail), Effect.catchDefect(() => fail(unavailable(true))));
|
|
205
|
+
yield* Effect.forkIn(closeOnFailure, transportScope);
|
|
206
|
+
yield* Effect.forkIn(watchAuthority, activation.scope);
|
|
207
|
+
});
|
|
208
|
+
export const layer = (definition, make, options) => Layer.effectDiscard(Effect.gen(function* () {
|
|
209
|
+
yield* Effect.try({
|
|
210
|
+
try: () => validateOptions(options),
|
|
211
|
+
catch: () => new RegistrationError({ message: "Invalid hosted actor options" }),
|
|
212
|
+
});
|
|
213
|
+
const runtime = yield* Runtime;
|
|
214
|
+
const dependencies = yield* Effect.context();
|
|
215
|
+
const entity = makeEntity(runtime.configuration.namespace, definition.name);
|
|
216
|
+
const entityLayer = entity.toLayer(Effect.gen(function* () {
|
|
217
|
+
const currentAddress = yield* Entity.CurrentAddress;
|
|
218
|
+
const transportScope = yield* Effect.scope;
|
|
219
|
+
const address = yield* Effect.sync(() => decodeAddress(runtime.configuration.namespace, definition.name, currentAddress.entityId));
|
|
220
|
+
const sharding = yield* Sharding.Sharding;
|
|
221
|
+
const gate = yield* Semaphore.make(1);
|
|
222
|
+
const recoveries = yield* Queue.dropping(1);
|
|
223
|
+
let current;
|
|
224
|
+
let terminal;
|
|
225
|
+
let invalidate;
|
|
226
|
+
const attemptActivation = (retainRecovery) => gate.withPermit(Effect.gen(function* () {
|
|
227
|
+
if (terminal !== undefined) {
|
|
228
|
+
return {
|
|
229
|
+
_tag: "Unavailable",
|
|
230
|
+
failure: { error: terminal, reactivate: false },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (current !== undefined) {
|
|
234
|
+
const failure = yield* activationFailure(current);
|
|
235
|
+
return Option.isNone(failure)
|
|
236
|
+
? { _tag: "Ready", activation: current }
|
|
237
|
+
: { _tag: "Unavailable", failure: failure.value };
|
|
238
|
+
}
|
|
239
|
+
const result = yield* Effect.result(makeActivation({
|
|
240
|
+
actions: definition.protocol.actions,
|
|
241
|
+
address,
|
|
242
|
+
currentAddress,
|
|
243
|
+
dependencies,
|
|
244
|
+
make,
|
|
245
|
+
retainRecovery,
|
|
246
|
+
sharding,
|
|
247
|
+
transportScope,
|
|
248
|
+
}));
|
|
249
|
+
if (Result.isFailure(result)) {
|
|
250
|
+
if (!result.failure.error.retryable)
|
|
251
|
+
terminal = result.failure.error;
|
|
252
|
+
return { _tag: "Unavailable", failure: result.failure };
|
|
253
|
+
}
|
|
254
|
+
current = result.success;
|
|
255
|
+
yield* watchActivation(result.success, transportScope, invalidate);
|
|
256
|
+
return { _tag: "Ready", activation: result.success };
|
|
257
|
+
}));
|
|
258
|
+
const activateManaged = (retainRecovery, superviseFailure) => Effect.flatMap(attemptActivation(retainRecovery), (result) => {
|
|
259
|
+
if (result._tag === "Ready")
|
|
260
|
+
return Effect.succeed(result.activation);
|
|
261
|
+
const supervise = superviseFailure && result.failure.error.retryable && result.failure.reactivate
|
|
262
|
+
? Queue.offer(recoveries, undefined).pipe(Effect.asVoid)
|
|
263
|
+
: Effect.void;
|
|
264
|
+
return supervise.pipe(Effect.andThen(Effect.fail(result.failure)));
|
|
265
|
+
});
|
|
266
|
+
invalidate = (activation, failure) => Effect.flatMap(gate.withPermit(Effect.gen(function* () {
|
|
267
|
+
const active = current === activation;
|
|
268
|
+
if (active) {
|
|
269
|
+
current = undefined;
|
|
270
|
+
if (!failure.error.retryable)
|
|
271
|
+
terminal = failure.error;
|
|
272
|
+
}
|
|
273
|
+
yield* Scope.close(activation.scope, Exit.fail(failure.error));
|
|
274
|
+
if (active && !failure.error.retryable)
|
|
275
|
+
yield* activation.keepAlive(false);
|
|
276
|
+
return active && failure.error.retryable && failure.reactivate;
|
|
277
|
+
})), (reactivate) => reactivate ? Queue.offer(recoveries, undefined).pipe(Effect.asVoid) : Effect.void);
|
|
278
|
+
const initial = yield* Effect.result(Effect.suspend(() => activateManaged(true, false)).pipe(Effect.retry({
|
|
279
|
+
while: (failure) => failure.error.retryable,
|
|
280
|
+
schedule: reactivationPolicy,
|
|
281
|
+
})));
|
|
282
|
+
if (Result.isFailure(initial) && initial.failure.error.retryable) {
|
|
283
|
+
return yield* Effect.die("Hosted actor activation retry policy terminated");
|
|
284
|
+
}
|
|
285
|
+
yield* Effect.forkIn(Queue.take(recoveries).pipe(Effect.andThen(Effect.suspend(() => activateManaged(true, false)).pipe(Effect.retry({
|
|
286
|
+
while: (failure) => failure.error.retryable,
|
|
287
|
+
schedule: reactivationPolicy,
|
|
288
|
+
}), Effect.catch(() => Effect.void))), Effect.forever), transportScope);
|
|
289
|
+
const activate = Effect.suspend(() => activateManaged(false, true).pipe(Effect.mapError((failure) => failure.error)));
|
|
290
|
+
return handlersForActivation(entity, definition.name, definition.protocol.actions, definition.protocolVersion, runtime.configuration.maxCommandBytes, runtime.configuration.maxStateBytes, activate);
|
|
291
|
+
}), {
|
|
292
|
+
maxIdleTime: options.maxIdleTime,
|
|
293
|
+
mailboxCapacity: options.mailboxCapacity,
|
|
294
|
+
concurrency: options.concurrency,
|
|
295
|
+
});
|
|
296
|
+
yield* runtime.register({
|
|
297
|
+
actorType: definition.name,
|
|
298
|
+
protocolVersion: definition.protocolVersion,
|
|
299
|
+
kind: "hosted",
|
|
300
|
+
entity: entityLayer,
|
|
301
|
+
});
|
|
302
|
+
}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as Actor from "./actor/definition.js";
|
|
2
|
+
export * as ActorRuntime from "./runtime/service.js";
|
|
3
|
+
export * as ActorStore from "./store/service.js";
|
|
4
|
+
export * as ActorClient from "./client/service.js";
|
|
5
|
+
export * as HostActor from "./host/definition.js";
|
|
6
|
+
export * as Gateway from "./gateway/service.js";
|
|
7
|
+
export * as Protocol from "./protocol/index.js";
|
|
8
|
+
export * as Errors from "./errors/index.js";
|
|
9
|
+
export * as Testing from "./testing/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as Actor from "./actor/definition.js";
|
|
2
|
+
export * as ActorRuntime from "./runtime/service.js";
|
|
3
|
+
export * as ActorStore from "./store/service.js";
|
|
4
|
+
export * as ActorClient from "./client/service.js";
|
|
5
|
+
export * as HostActor from "./host/definition.js";
|
|
6
|
+
export * as Gateway from "./gateway/service.js";
|
|
7
|
+
export * as Protocol from "./protocol/index.js";
|
|
8
|
+
export * as Errors from "./errors/index.js";
|
|
9
|
+
export * as Testing from "./testing/index.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { InvalidInput } from "../errors/index.js";
|
|
3
|
+
import type { Address, Json, Key } from "./index.js";
|
|
4
|
+
export declare const KeySchema: Schema.Struct<{
|
|
5
|
+
readonly tenant: Schema.String;
|
|
6
|
+
readonly key: Schema.$Array<Schema.String>;
|
|
7
|
+
}>;
|
|
8
|
+
export declare const AddressSchema: Schema.Struct<{
|
|
9
|
+
readonly namespace: Schema.String;
|
|
10
|
+
readonly tenant: Schema.String;
|
|
11
|
+
readonly actorType: Schema.String;
|
|
12
|
+
readonly key: Schema.$Array<Schema.String>;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const validateKey: (input: Key) => Key;
|
|
15
|
+
export declare const addressId: (input: Address) => string;
|
|
16
|
+
export declare const validateId: (value: string, maximumBytes?: number) => string;
|
|
17
|
+
export declare const canonicalJson: (input: Json) => string;
|
|
18
|
+
export declare const fingerprint: (input: {
|
|
19
|
+
readonly command: string;
|
|
20
|
+
readonly protocolVersion: number;
|
|
21
|
+
readonly payload: Json;
|
|
22
|
+
readonly expectedRevision?: string;
|
|
23
|
+
}) => string;
|
|
24
|
+
export declare const isSequence: (value: string) => boolean;
|
|
25
|
+
export declare const makeCursor: (address: Address, sequence: string) => string;
|
|
26
|
+
export declare const readCursor: (address: Address, cursor: string) => string;
|
|
27
|
+
export declare const decode: <S extends Schema.ConstraintDecoder<unknown>>(schema: S, input: unknown) => Effect.Effect<S["Type"], InvalidInput, S["DecodingServices"]>;
|
|
28
|
+
export declare const encode: <S extends Schema.ConstraintEncoder<unknown>>(schema: S, input: S["Type"]) => Effect.Effect<S["Encoded"], InvalidInput, S["EncodingServices"]>;
|
|
29
|
+
export declare const boundary: <A>(evaluate: () => A) => Effect.Effect<A, InvalidInput>;
|