@rikalabs/akter 0.2.2 → 0.3.0-next.40953534015.1.ge41bf251cdeb
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/dist/client/transport.d.ts +1 -1
- package/dist/client/transport.js +3 -3
- package/dist/contract/actor.d.ts +12 -13
- package/dist/contract/actor.js +3 -3
- package/dist/contract/caller.d.ts +1 -1
- package/dist/contract/caller.js +1 -1
- package/dist/contract/context.d.ts +30 -25
- package/dist/contract/cron.d.ts +1 -1
- package/dist/contract/cron.js +1 -1
- package/dist/contract/errors.d.ts +7 -7
- package/dist/contract/errors.js +5 -5
- package/dist/contract/handle.d.ts +18 -18
- package/dist/contract/handle.js +8 -8
- package/dist/contract/make.d.ts +21 -18
- package/dist/contract/make.js +15 -10
- package/dist/contract/members.d.ts +51 -31
- package/dist/contract/members.js +33 -10
- package/dist/execution/action.d.ts +2 -2
- package/dist/execution/action.js +7 -7
- package/dist/execution/durable.js +3 -3
- package/dist/execution/engine.js +1 -1
- package/dist/execution/entries.d.ts +8 -1
- package/dist/execution/entries.js +7 -1
- package/dist/execution/jobs.js +76 -11
- package/dist/execution/transport.d.ts +1 -1
- package/dist/execution/transport.js +8 -8
- package/dist/handlers/registry.d.ts +1 -1
- package/dist/handlers/registry.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/rpc/group.d.ts +10 -9
- package/dist/rpc/group.js +16 -15
- package/dist/runtime/app.d.ts +8 -1
- package/dist/runtime/sharding.js +2 -2
- package/dist/serve/handlers.js +4 -4
- package/dist/snapshots/checkpoint.d.ts +2 -0
- package/dist/snapshots/recovery.js +1 -1
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/simulation.d.ts +2 -2
- package/dist/testing/simulation.js +3 -3
- package/dist/testing/story.d.ts +177 -0
- package/dist/testing/story.js +404 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
The framework and CLI release together at `0.3.0` on the `latest` npm dist-tag. Akter now uses domain-driven names: a caller's request is a **command** and a live, never-stored value is a **broadcast**. Stores written by 0.2 open unchanged: every spelling written to a store, an audit record or the `/rpc` wire keeps its 0.2 form.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- `Actor.action` is `Actor.command`, the `actions` option is `commands`, and `ActionContext`, `ActionHandler` and `ActionDefect` are `CommandContext`, `CommandHandler` and `CommandDefect`. A defect reaches a client named `CommandDefect`.
|
|
10
|
+
- `Actor.event` is `Actor.broadcast`, the `events` option is `broadcasts`, `ctx.emit` in commands and jobs is `ctx.broadcast`, and the `Event` type is `Broadcast`. `Transport.event` is `Transport.broadcast`.
|
|
11
|
+
- `access` sees request kinds `"command"` and `"broadcast"` instead of `"action"` and `"event"`; `classifyRpc` returns `"command"` instead of `"action"`.
|
|
12
|
+
- `Actor.job` no longer takes `retry` or `timeout`. Attach them as policies: `Actor.job({ ... }).pipe(Job.retry({ times, schedule }), Job.timeout("30 seconds"))`. `times` counts retries after the first attempt, so 0.2's `attempts: 3` is `times: 2`; `schedule` is an Effect `Schedule` for the delay before each retry. `Retry` is `{ times, schedule? }`.
|
|
13
|
+
- None of the 0.2 spellings is kept as an alias.
|
|
14
|
+
- The `effect`, `@effect/platform-bun` and `@effect/platform-node` peers are `~4.0.3`: Effect 4.0 patch releases only. Effect 4.1.0 removed `Crypto` from the `effect` root entry, which Akter imports, so 0.3.0 does not support Effect 4.1.
|
|
15
|
+
|
|
16
|
+
### New
|
|
17
|
+
|
|
18
|
+
- `Actor.job({ onSuccess })` names an internal command that receives the job's success value. The value is journaled before it is delivered, under an id derived from the job, so a crash or a retry delivers it exactly once. A job without `onSuccess` behaves as in 0.2.
|
|
19
|
+
- Each retry delay is computed from the job's `Schedule` when the failure is journaled and replayed from the log, never recomputed. A schedule that ends stops the retries early.
|
|
20
|
+
- `Story` in `@rikalabs/akter/testing` runs one actor's command and view handlers with no engine and no store. A story gives a state and a command and checks the reply or failure, the new state and every staged send, timer, job start and broadcast; `Story.effect` runs the same steps as an Effect.
|
|
21
|
+
- `Job` is exported from `@rikalabs/akter` as both the type and the policy namespace.
|
|
22
|
+
- `App.make` names the actor its layer leaves unimplemented and the `toLayer` call to add.
|
|
23
|
+
- The naming guide documents imperative commands and jobs, past-tense results (`succeeded<Job>`, `failed<Job>`) and broadcasts, and dotted actor names such as `Billing.Invoice`.
|
|
24
|
+
|
|
3
25
|
## 0.2.2 (2026-10-15)
|
|
4
26
|
|
|
5
27
|
The framework and CLI release together at `0.2.2` on the `latest` npm dist-tag. Stores written by 0.2.0 and 0.2.1 open unchanged.
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ export const Counter = Actor.make("Counter", {
|
|
|
24
24
|
by: "id",
|
|
25
25
|
}),
|
|
26
26
|
}),
|
|
27
|
-
|
|
27
|
+
commands: { add: Actor.command(Schema.Finite, { success: Schema.Finite }) },
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
export const CounterLive = Counter.toLayer({
|
|
@@ -35,7 +35,7 @@ export const CounterLive = Counter.toLayer({
|
|
|
35
35
|
})
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Commands are synchronous and deterministic. They publish no reply, broadcast, send, timer or job until their bucket-log segment is durable. Jobs are the only place for I/O.
|
|
39
39
|
|
|
40
40
|
## Client and server
|
|
41
41
|
|
|
@@ -46,7 +46,7 @@ const app = App.make({ actors: [Counter], layer: CounterLive })
|
|
|
46
46
|
const server = Akter.serve({ app, auth: Auth.none })
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Provide `Store.fileSystem(".akter/store")` for local development or `Store.s3({ bucket, credentials })` for an S3-compatible store. The single Effect client is `Counter.client(id)`. Its
|
|
49
|
+
Provide `Store.fileSystem(".akter/store")` for local development or `Store.s3({ bucket, credentials })` for an S3-compatible store. The single Effect client is `Counter.client(id)`. Its commands are Effects, state and views are Streams, lists expose `range` and watch, and static `Counter.find({ where, orderBy, limit })` returns ids only.
|
|
50
50
|
|
|
51
51
|
The one RPC group is `/rpc`: WebSocket JSON and HTTP NDJSON. The server can opt into `/metrics` with `prometheus: true`; `Telemetry.layer` composes OTLP traces/metrics and Effect DevTools without an OTLP logs exporter.
|
|
52
52
|
|
|
@@ -26,7 +26,7 @@ export interface ClientOptions {
|
|
|
26
26
|
readonly reauthenticateEvery?: Duration.Input;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* The `Client` every handle calls through, over `/rpc`. Each
|
|
29
|
+
* The `Client` every handle calls through, over `/rpc`. Each command mints a command id from the
|
|
30
30
|
* client's clock and retries with that same id after a lost connection or a retryable refusal;
|
|
31
31
|
* each stream resubscribes after a lost connection and starts again from a full value. A WebSocket
|
|
32
32
|
* session that lapses at its reauthorization bound is replaced by reconnecting.
|
package/dist/client/transport.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ActionFailed, ErrorEnvelope, FindCodec, groupOf, SPAN_PREFIX, Tags } fr
|
|
|
9
9
|
const isClientError = (error) => Predicate.isTagged(error, "RpcClientError");
|
|
10
10
|
/**
|
|
11
11
|
* A response that never arrived as a valid envelope. A lost connection is safe to retry: an
|
|
12
|
-
*
|
|
12
|
+
* command resends its command id, so the server runs it at most once, and a read resubscribes.
|
|
13
13
|
*/
|
|
14
14
|
const transportError = (error) => ActorError.make({
|
|
15
15
|
reason: Predicate.isTagged(error.reason, "RpcClientDefect")
|
|
@@ -84,7 +84,7 @@ const makeTransport = (options, connection) => Effect.gen(function* () {
|
|
|
84
84
|
id: request.id,
|
|
85
85
|
params: request.params,
|
|
86
86
|
})),
|
|
87
|
-
|
|
87
|
+
broadcast: (request) => streamed(Tags.event(request.actor.name, request.member), { id: request.id }),
|
|
88
88
|
range: (request) => retrying(unary(Tags.range(request.actor.name, request.member), {
|
|
89
89
|
id: request.id,
|
|
90
90
|
...request.range,
|
|
@@ -169,7 +169,7 @@ const connected = (options, connection) => isSocket(options.url)
|
|
|
169
169
|
transformClient: (client) => refusalsAsExits(HttpClient.mapRequestEffect(client, (request) => Effect.map(headersOf(options), (headers) => HttpClientRequest.setHeaders(request, headers)))),
|
|
170
170
|
}).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerNdjson]));
|
|
171
171
|
/**
|
|
172
|
-
* The `Client` every handle calls through, over `/rpc`. Each
|
|
172
|
+
* The `Client` every handle calls through, over `/rpc`. Each command mints a command id from the
|
|
173
173
|
* client's clock and retries with that same id after a lost connection or a retryable refusal;
|
|
174
174
|
* each stream resubscribes after a lost connection and starts again from a full value. A WebSocket
|
|
175
175
|
* session that lapses at its reauthorization bound is replaced by reconnecting.
|
package/dist/contract/actor.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Actor as ActorContract, Def } from "./make.ts";
|
|
2
2
|
import { JobFailure } from "./members.ts";
|
|
3
3
|
/**
|
|
4
|
-
* Declares actors: `Actor.make(name, { state,
|
|
4
|
+
* Declares actors: `Actor.make(name, { state, commands, ... })` for the contract and the member
|
|
5
5
|
* constructors, schema first and options second.
|
|
6
6
|
*/
|
|
7
7
|
export declare const Actor: {
|
|
8
|
-
make: <const Name extends string, S extends import("./make.ts").State, A extends Record<string, import("./members.ts").
|
|
8
|
+
make: <const Name extends string, S extends import("./make.ts").State, A extends Record<string, import("./members.ts").Command>, K extends import("./members.ts").MemberSchema = import("effect/Schema").String, V extends Record<string, import("./members.ts").View> = {}, E extends Record<string, import("./members.ts").Broadcast> = {}, J extends Record<string, import("./members.ts").Job> = {}, P extends import("./members.ts").OptionalSchema = undefined, Acc extends (caller: import("./caller.ts").Caller, request: import("./make.ts").AccessRequest) => boolean = (caller: import("./caller.ts").Caller) => caller is import("./caller.ts").User | import("./caller.ts").System>(name: Name, options: import("./make.ts").MakeOptions<K, S, A, V, E, J, P, Acc> & ([(keyof A & (keyof E | keyof J | keyof V | import("./context.ts").ListKeys<S["fields"]> | ("connect" | "disconnect" | "join" | "state"))) | (keyof E & (keyof J | import("./context.ts").ListKeys<S["fields"]> | ("connect" | "disconnect" | "join" | "state"))) | (keyof J & (import("./context.ts").ListKeys<S["fields"]> | ("connect" | "disconnect" | "join" | "state"))) | (keyof V & (keyof E | keyof J | import("./context.ts").ListKeys<S["fields"]> | ("connect" | "disconnect" | "join" | "state"))) | (import("./context.ts").ListKeys<S["fields"]> & ("connect" | "disconnect" | "join" | "state"))] extends [never] ? unknown : import("./make.ts").TypeError<`member name '${keyof A & "connect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & "disconnect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & "join"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & "state"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & "connect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & "disconnect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & "join"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & "state"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof J & "connect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof J & "disconnect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof J & "join"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof J & "state"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & "connect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & "disconnect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & "join"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & "state"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${import("./context.ts").ListKeys<S["fields"]> & "connect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${import("./context.ts").ListKeys<S["fields"]> & "disconnect"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${import("./context.ts").ListKeys<S["fields"]> & "join"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${import("./context.ts").ListKeys<S["fields"]> & "state"}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & keyof E & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & keyof J & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & keyof V & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof A & import("./context.ts").ListKeys<S["fields"]> & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & keyof J & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof E & import("./context.ts").ListKeys<S["fields"]> & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof J & import("./context.ts").ListKeys<S["fields"]> & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & keyof E & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & keyof J & string}' is used twice or reserved (state, join, connect, disconnect)` | `member name '${keyof V & import("./context.ts").ListKeys<S["fields"]> & string}' is used twice or reserved (state, join, connect, disconnect)`>)) => ActorContract<Name, {
|
|
9
9
|
readonly key: K;
|
|
10
10
|
readonly state: S;
|
|
11
|
-
readonly
|
|
11
|
+
readonly commands: A;
|
|
12
12
|
readonly views: V;
|
|
13
|
-
readonly
|
|
13
|
+
readonly broadcasts: E;
|
|
14
14
|
readonly jobs: J;
|
|
15
15
|
readonly presence: P;
|
|
16
16
|
readonly caller: Acc extends (caller: any, ...rest: Array<any>) => caller is infer C ? Extract<C, {
|
|
@@ -37,9 +37,9 @@ export declare const Actor: {
|
|
|
37
37
|
readonly subject: string;
|
|
38
38
|
};
|
|
39
39
|
}>;
|
|
40
|
-
|
|
41
|
-
(): import("./members.ts").
|
|
42
|
-
<P extends import("./members.ts").MemberSchema, const O extends import("./members.ts").
|
|
40
|
+
command: {
|
|
41
|
+
(): import("./members.ts").Command<undefined, undefined, undefined, false>;
|
|
42
|
+
<P extends import("./members.ts").MemberSchema, const O extends import("./members.ts").PayloadCommandOptions = {}>(payload: P, options?: O): import("./members.ts").Command<P, O extends {
|
|
43
43
|
readonly success: infer S extends import("./members.ts").MemberSchema;
|
|
44
44
|
} ? S : undefined, O extends {
|
|
45
45
|
readonly fails: infer E extends import("./members.ts").DeclaredError;
|
|
@@ -48,7 +48,7 @@ export declare const Actor: {
|
|
|
48
48
|
} ? true : O extends {
|
|
49
49
|
readonly internal: true;
|
|
50
50
|
} ? true : false>;
|
|
51
|
-
<const O extends import("./members.ts").
|
|
51
|
+
<const O extends import("./members.ts").CommandOptions>(options: O): import("./members.ts").Command<undefined, O extends {
|
|
52
52
|
readonly success: infer S extends import("./members.ts").MemberSchema;
|
|
53
53
|
} ? S : undefined, O extends {
|
|
54
54
|
readonly fails: infer E extends import("./members.ts").DeclaredError;
|
|
@@ -61,14 +61,13 @@ export declare const Actor: {
|
|
|
61
61
|
view: <A extends import("./members.ts").MemberSchema, P extends import("./members.ts").MemberSchema | undefined = undefined>(success: A, options?: {
|
|
62
62
|
readonly params?: P;
|
|
63
63
|
}) => import("./members.ts").View<A, NoInfer<P>>;
|
|
64
|
-
|
|
65
|
-
job: <P extends import("./members.ts").MemberSchema | undefined = undefined, const F extends string = string>(options: {
|
|
64
|
+
broadcast: <A extends import("./members.ts").MemberSchema>(schema: A) => import("./members.ts").Broadcast<A>;
|
|
65
|
+
job: <P extends import("./members.ts").MemberSchema | undefined = undefined, const F extends string = string, const S extends string | undefined = undefined>(options: {
|
|
66
66
|
readonly payload?: P;
|
|
67
|
-
readonly retry?: import("./members.ts").Retry;
|
|
68
|
-
readonly timeout?: import("effect/Duration").Input;
|
|
69
67
|
readonly concurrency?: number;
|
|
68
|
+
readonly onSuccess?: S;
|
|
70
69
|
readonly onFailure: F;
|
|
71
|
-
}) => import("./members.ts").Job<NoInfer<P>, NoInfer<F>>;
|
|
70
|
+
}) => import("./members.ts").Job<NoInfer<P>, NoInfer<F>, NoInfer<S>>;
|
|
72
71
|
list: <I extends import("./members.ts").MemberSchema, const B extends import("./members.ts").KeyField<I["Type"]>, const K extends import("./members.ts").KeyField<I["Type"]> = B>(item: I, options: {
|
|
73
72
|
readonly key?: K;
|
|
74
73
|
readonly by: B;
|
package/dist/contract/actor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Contract } from "./make.js";
|
|
2
2
|
import { JobFailure, Members } from "./members.js";
|
|
3
3
|
/**
|
|
4
|
-
* Declares actors: `Actor.make(name, { state,
|
|
4
|
+
* Declares actors: `Actor.make(name, { state, commands, ... })` for the contract and the member
|
|
5
5
|
* constructors, schema first and options second.
|
|
6
6
|
*/
|
|
7
7
|
export const Actor = {
|
|
8
8
|
make: Contract.make,
|
|
9
|
-
|
|
9
|
+
command: Members.command,
|
|
10
10
|
view: Members.view,
|
|
11
|
-
|
|
11
|
+
broadcast: Members.broadcast,
|
|
12
12
|
job: Members.job,
|
|
13
13
|
list: Members.list,
|
|
14
14
|
JobFailure,
|
|
@@ -14,7 +14,7 @@ export type Anonymous = typeof Anonymous.Type;
|
|
|
14
14
|
* `cron`, `job`, `operator`, `process`), `subject` names the origin (`actor:<Type>/<id>`,
|
|
15
15
|
* `job:<jobId>`, `cron:<Type>`, `operator:<id>`) so that `caller.subject` types for every caller
|
|
16
16
|
* an actor admits once `access` excludes `Anonymous`, `ref` is the originating actor's address and
|
|
17
|
-
* `onBehalfOf` the principal of the
|
|
17
|
+
* `onBehalfOf` the principal of the command that caused the call.
|
|
18
18
|
*/
|
|
19
19
|
export declare const System: Schema.TaggedStruct<"System", {
|
|
20
20
|
readonly source: Schema.Literals<readonly ["send", "timer", "cron", "job", "operator", "process"]>;
|
package/dist/contract/caller.js
CHANGED
|
@@ -8,7 +8,7 @@ export const Anonymous = Schema.TaggedStruct("Anonymous", {});
|
|
|
8
8
|
* `cron`, `job`, `operator`, `process`), `subject` names the origin (`actor:<Type>/<id>`,
|
|
9
9
|
* `job:<jobId>`, `cron:<Type>`, `operator:<id>`) so that `caller.subject` types for every caller
|
|
10
10
|
* an actor admits once `access` excludes `Anonymous`, `ref` is the originating actor's address and
|
|
11
|
-
* `onBehalfOf` the principal of the
|
|
11
|
+
* `onBehalfOf` the principal of the command that caused the call.
|
|
12
12
|
*/
|
|
13
13
|
export const System = Schema.TaggedStruct("System", {
|
|
14
14
|
source: Schema.Literals(["send", "timer", "cron", "job", "operator", "process"]),
|
|
@@ -2,7 +2,7 @@ import type { DateTime, Duration, Effect, Schema, Types } from "effect";
|
|
|
2
2
|
import type { Caller, System } from "./caller.ts";
|
|
3
3
|
import type { SelfHandle } from "./handle.ts";
|
|
4
4
|
import type { Actor, AnyActor, Def, DefOf } from "./make.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Broadcast, Command, Job, List, ListDraft, ListReader, MemberSchema, OptionalSchema, View } from "./members.ts";
|
|
6
6
|
/** The state struct's fields. */
|
|
7
7
|
export type Fields = {
|
|
8
8
|
readonly [name: string]: MemberSchema;
|
|
@@ -33,7 +33,7 @@ export type ListKeys<F extends Fields> = {
|
|
|
33
33
|
type DraftField<S> = S extends List ? ListDraft<ItemOf<S>, ListKey<S>, ListBy<S>> : S extends Schema.Top ? Types.DeepMutable<S["Type"]> : never;
|
|
34
34
|
type ReadField<S> = S extends List ? ListReader<ItemOf<S>, ListKey<S>, ListBy<S>> : S extends Schema.Top ? S["Type"] : never;
|
|
35
35
|
/**
|
|
36
|
-
* The state
|
|
36
|
+
* The state a command handler changes: deep-mutable fields, each list as its draft API.
|
|
37
37
|
* `Schema.optional` fields are optional keys, so `delete draft.field` compiles for them only.
|
|
38
38
|
*/
|
|
39
39
|
export type Draft<F extends Fields> = {
|
|
@@ -57,9 +57,9 @@ export type ClientState<F extends Fields> = {
|
|
|
57
57
|
export type FullState<F extends Fields> = ClientState<F> & {
|
|
58
58
|
readonly [K in ListKeys<F>]: F[K] extends List ? ReadonlyArray<ItemOf<F[K]>> : never;
|
|
59
59
|
};
|
|
60
|
-
/** The staged-call surface of `send` and `schedule`: one method per target
|
|
60
|
+
/** The staged-call surface of `send` and `schedule`: one method per target command, fire-and-forget. */
|
|
61
61
|
export type Sender<X> = {
|
|
62
|
-
readonly [A in keyof DefOf<X>["
|
|
62
|
+
readonly [A in keyof DefOf<X>["commands"]]: (...args: Args<DefOf<X>["commands"][A]["payload"]>) => void;
|
|
63
63
|
};
|
|
64
64
|
/** `ctx.start`: one method per job, staging a start. */
|
|
65
65
|
export type Starters<J extends Record<string, Job>> = {
|
|
@@ -67,20 +67,20 @@ export type Starters<J extends Record<string, Job>> = {
|
|
|
67
67
|
readonly key?: string;
|
|
68
68
|
}]) => void;
|
|
69
69
|
};
|
|
70
|
-
/** `ctx.
|
|
71
|
-
export type
|
|
70
|
+
/** `ctx.broadcast` in commands: stages a broadcast for publication after durability. */
|
|
71
|
+
export type Broadcasters<E extends Record<string, Broadcast>> = {
|
|
72
72
|
readonly [K in keyof E]: (value: E[K]["schema"]["Type"]) => void;
|
|
73
73
|
};
|
|
74
|
-
/** `
|
|
75
|
-
export type
|
|
74
|
+
/** `broadcast` in jobs: publishes at once. */
|
|
75
|
+
export type JobBroadcasters<E extends Record<string, Broadcast>> = {
|
|
76
76
|
readonly [K in keyof E]: (value: E[K]["schema"]["Type"]) => Effect.Effect<void>;
|
|
77
77
|
};
|
|
78
|
-
/** When a scheduled
|
|
78
|
+
/** When a scheduled command runs: after a delay, or at an epoch-millisecond instant. */
|
|
79
79
|
export type When = Duration.Input | {
|
|
80
80
|
readonly at: number;
|
|
81
81
|
};
|
|
82
|
-
/** The capabilities of
|
|
83
|
-
export interface
|
|
82
|
+
/** The capabilities of a command handler; nothing else is reachable from a command. */
|
|
83
|
+
export interface CommandContext<Name extends string, D extends Def, C extends Caller> {
|
|
84
84
|
readonly caller: C;
|
|
85
85
|
readonly self: D["key"]["Type"];
|
|
86
86
|
readonly id: () => string;
|
|
@@ -89,10 +89,10 @@ export interface ActionContext<Name extends string, D extends Def, C extends Cal
|
|
|
89
89
|
readonly send: <X extends AnyActor>(target: X, id: DefOf<X>["key"]["Type"]) => Sender<X>;
|
|
90
90
|
readonly schedule: (when: When) => Sender<Actor<Name, D>>;
|
|
91
91
|
readonly start: Starters<D["jobs"]>;
|
|
92
|
-
readonly
|
|
92
|
+
readonly broadcast: Broadcasters<D["broadcasts"]>;
|
|
93
93
|
}
|
|
94
|
-
/** The context of `connect` and `disconnect`:
|
|
95
|
-
export interface PresenceContext<Name extends string, D extends Def> extends
|
|
94
|
+
/** The context of `connect` and `disconnect`: a command context with the connection id. */
|
|
95
|
+
export interface PresenceContext<Name extends string, D extends Def> extends CommandContext<Name, D, D["caller"] | System> {
|
|
96
96
|
readonly connection: string;
|
|
97
97
|
}
|
|
98
98
|
/** A view handler reads durable state only and stages nothing. */
|
|
@@ -113,27 +113,32 @@ export interface JobContext<Name extends string, D extends Def> {
|
|
|
113
113
|
readonly self: SelfHandle<Name, D>;
|
|
114
114
|
readonly id: () => string;
|
|
115
115
|
readonly attempt: number;
|
|
116
|
-
readonly
|
|
116
|
+
readonly broadcast: JobBroadcasters<D["broadcasts"]>;
|
|
117
117
|
readonly step: Step;
|
|
118
118
|
}
|
|
119
|
-
type SuccessType<A extends
|
|
120
|
-
type FailsType<A extends
|
|
121
|
-
/** The caller
|
|
122
|
-
export type CallerFor<D extends Def, A extends
|
|
119
|
+
type SuccessType<A extends Command> = A["success"] extends Schema.Top ? A["success"]["Type"] : undefined;
|
|
120
|
+
type FailsType<A extends Command> = A["fails"] extends Schema.Top ? A["fails"]["Type"] : never;
|
|
121
|
+
/** The caller a command handler sees: internal commands are reached by the engine only. */
|
|
122
|
+
export type CallerFor<D extends Def, A extends Command> = A["internal"] extends true ? System : D["caller"] | System;
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
124
|
+
* A command handler: synchronous, over a copy-on-write draft. Returning the declared failure
|
|
125
125
|
* discards the draft and every staged effect; returning a value is the reply.
|
|
126
126
|
*/
|
|
127
|
-
export type
|
|
127
|
+
export type CommandHandler<Name extends string, D extends Def, A extends Command> = (draft: Draft<D["state"]["fields"]>, payload: PayloadOf<A["payload"]>, ctx: CommandContext<Name, D, CallerFor<D, A>>) => SuccessType<A> | FailsType<A>;
|
|
128
128
|
/** A view handler: a pure, synchronous function of durable state and its parameters. */
|
|
129
129
|
export type ViewHandler<D extends Def, V extends View> = (state: ReadState<D["state"]["fields"]>, params: PayloadOf<V["params"]>, ctx: ViewContext<D>) => V["success"]["Type"];
|
|
130
|
+
/**
|
|
131
|
+
* What a job's Effect succeeds with: the payload of its `onSuccess` command, which receives it, or
|
|
132
|
+
* `void` when that command takes none; anything when the job has no `onSuccess`.
|
|
133
|
+
*/
|
|
134
|
+
export type JobSuccess<D extends Def, J extends Job> = J["onSuccess"] extends keyof D["commands"] ? D["commands"][J["onSuccess"]]["payload"] extends Schema.Top ? PayloadOf<D["commands"][J["onSuccess"]]["payload"]> : void : unknown;
|
|
130
135
|
/** A job handler: the only place for I/O. Its requirements flow into the layer type. */
|
|
131
|
-
export type JobHandler<Name extends string, D extends Def, J extends Job> = (payload: PayloadOf<J["payload"]>, ctx: JobContext<Name, D>) => Effect.Effect<
|
|
132
|
-
/** `connect` or `disconnect`:
|
|
136
|
+
export type JobHandler<Name extends string, D extends Def, J extends Job> = (payload: PayloadOf<J["payload"]>, ctx: JobContext<Name, D>) => Effect.Effect<JobSuccess<D, J>, unknown, any>;
|
|
137
|
+
/** `connect` or `disconnect`: a command over the draft receiving the presence value. */
|
|
133
138
|
export type PresenceHandler<Name extends string, D extends Def> = (draft: Draft<D["state"]["fields"]>, presence: PayloadOf<D["presence"]>, ctx: PresenceContext<Name, D>) => undefined;
|
|
134
|
-
/** Every handler `X.toLayer` takes:
|
|
139
|
+
/** Every handler `X.toLayer` takes: commands, views, jobs and, with presence, `connect`/`disconnect`. */
|
|
135
140
|
export type Handlers<Name extends string, D extends Def> = {
|
|
136
|
-
readonly [A in keyof D["
|
|
141
|
+
readonly [A in keyof D["commands"]]: CommandHandler<Name, D, D["commands"][A]>;
|
|
137
142
|
} & {
|
|
138
143
|
readonly [V in keyof D["views"]]: ViewHandler<D, D["views"][V]>;
|
|
139
144
|
} & {
|
package/dist/contract/cron.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface CronSchedule {
|
|
|
9
9
|
* and unknown zones are refused. Throws on anything else.
|
|
10
10
|
*/
|
|
11
11
|
declare const parse: (expression: string) => CronSchedule;
|
|
12
|
-
/** Cron schedules of `cron`
|
|
12
|
+
/** Cron schedules of `cron` commands. */
|
|
13
13
|
export declare const Crons: {
|
|
14
14
|
parse: typeof parse;
|
|
15
15
|
};
|
package/dist/contract/cron.js
CHANGED
|
@@ -29,11 +29,11 @@ export declare const InvalidCommandId: Schema.TaggedStruct<"InvalidCommandId", {
|
|
|
29
29
|
readonly code: Schema.Literals<readonly ["malformed", "version", "future", "window", "conflict"]>;
|
|
30
30
|
readonly now: Schema.optionalKey<Schema.Finite>;
|
|
31
31
|
}>;
|
|
32
|
-
/** The owner could not run the
|
|
32
|
+
/** The owner could not run the command now; retry with the same command id. */
|
|
33
33
|
export declare const ActorUnavailable: Schema.TaggedStruct<"ActorUnavailable", {}>;
|
|
34
34
|
/** The shard is draining or was lost by its owner; retry with the same command id. */
|
|
35
35
|
export declare const ShardMoved: Schema.TaggedStruct<"ShardMoved", {}>;
|
|
36
|
-
/** The caller stopped waiting; the
|
|
36
|
+
/** The caller stopped waiting; the command may still become durable. */
|
|
37
37
|
export declare const Timeout: Schema.TaggedStruct<"Timeout", {
|
|
38
38
|
readonly commandId: Schema.optionalKey<Schema.String>;
|
|
39
39
|
}>;
|
|
@@ -126,21 +126,21 @@ export declare class ActorError extends ActorError_base {
|
|
|
126
126
|
/** The base wait before a retry in milliseconds, before the client's jitter. */
|
|
127
127
|
get retryAfter(): number | undefined;
|
|
128
128
|
}
|
|
129
|
-
/** Which defect ended
|
|
129
|
+
/** Which defect ended a command: its staged work is discarded and the defect is its outcome. */
|
|
130
130
|
export declare const DefectKind: Schema.Literals<readonly ["SynchronousActionViolation", "StateTooLarge", "ReplayMismatch", "DecodeFailure", "Thrown"]>;
|
|
131
|
-
/** Which defect ended
|
|
131
|
+
/** Which defect ended a command. */
|
|
132
132
|
export type DefectKind = typeof DefectKind.Type;
|
|
133
|
-
declare const
|
|
133
|
+
declare const CommandDefect_base: Schema.Class<CommandDefect, Schema.TaggedStruct<"CommandDefect", {
|
|
134
134
|
readonly kind: Schema.Literals<readonly ["SynchronousActionViolation", "StateTooLarge", "ReplayMismatch", "DecodeFailure", "Thrown"]>;
|
|
135
135
|
readonly actor: Schema.String;
|
|
136
136
|
readonly member: Schema.String;
|
|
137
137
|
readonly detail: Schema.String;
|
|
138
138
|
}>, import("effect/Cause").YieldableError>;
|
|
139
139
|
/**
|
|
140
|
-
* A defect a caller observes as the outcome of its
|
|
140
|
+
* A defect a caller observes as the outcome of its command. It is never an `ActorError` reason:
|
|
141
141
|
* nothing it staged survived, and retrying the same command id returns the same defect.
|
|
142
142
|
*/
|
|
143
|
-
export declare class
|
|
143
|
+
export declare class CommandDefect extends CommandDefect_base {
|
|
144
144
|
get message(): string;
|
|
145
145
|
}
|
|
146
146
|
export {};
|
package/dist/contract/errors.js
CHANGED
|
@@ -28,11 +28,11 @@ export const InvalidCommandId = Schema.TaggedStruct("InvalidCommandId", {
|
|
|
28
28
|
code: Schema.Literals(["malformed", "version", "future", "window", "conflict"]),
|
|
29
29
|
now: Schema.optionalKey(Schema.Finite),
|
|
30
30
|
});
|
|
31
|
-
/** The owner could not run the
|
|
31
|
+
/** The owner could not run the command now; retry with the same command id. */
|
|
32
32
|
export const ActorUnavailable = Schema.TaggedStruct("ActorUnavailable", {});
|
|
33
33
|
/** The shard is draining or was lost by its owner; retry with the same command id. */
|
|
34
34
|
export const ShardMoved = Schema.TaggedStruct("ShardMoved", {});
|
|
35
|
-
/** The caller stopped waiting; the
|
|
35
|
+
/** The caller stopped waiting; the command may still become durable. */
|
|
36
36
|
export const Timeout = Schema.TaggedStruct("Timeout", {
|
|
37
37
|
commandId: Schema.optionalKey(Schema.String),
|
|
38
38
|
});
|
|
@@ -114,7 +114,7 @@ export class ActorError extends Schema.TaggedError()("ActorError", {
|
|
|
114
114
|
return retryAfterMillis[this.reason._tag];
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
/** Which defect ended
|
|
117
|
+
/** Which defect ended a command: its staged work is discarded and the defect is its outcome. */
|
|
118
118
|
export const DefectKind = Schema.Literals([
|
|
119
119
|
"SynchronousActionViolation",
|
|
120
120
|
"StateTooLarge",
|
|
@@ -123,10 +123,10 @@ export const DefectKind = Schema.Literals([
|
|
|
123
123
|
"Thrown",
|
|
124
124
|
]);
|
|
125
125
|
/**
|
|
126
|
-
* A defect a caller observes as the outcome of its
|
|
126
|
+
* A defect a caller observes as the outcome of its command. It is never an `ActorError` reason:
|
|
127
127
|
* nothing it staged survived, and retrying the same command id returns the same defect.
|
|
128
128
|
*/
|
|
129
|
-
export class
|
|
129
|
+
export class CommandDefect extends Schema.TaggedError()("CommandDefect", {
|
|
130
130
|
kind: DefectKind,
|
|
131
131
|
actor: Schema.String,
|
|
132
132
|
member: Schema.String,
|
|
@@ -3,7 +3,7 @@ import type { Scope } from "effect";
|
|
|
3
3
|
import { ActorError } from "./errors.ts";
|
|
4
4
|
import type { Args, ClientState, FindQuery, Fields, ListKeys, ListTypes } from "./context.ts";
|
|
5
5
|
import type { AnyActor, Def } from "./make.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { Command, List, MemberSchema, OptionalSchema, RangeOptions } from "./members.ts";
|
|
7
7
|
/** A call's reply as the transport carries it: the encoded success value or declared failure. */
|
|
8
8
|
export type Reply = {
|
|
9
9
|
readonly kind: "Success";
|
|
@@ -27,12 +27,12 @@ export interface Target {
|
|
|
27
27
|
readonly actor: AnyActor;
|
|
28
28
|
readonly id: Schema.Json;
|
|
29
29
|
}
|
|
30
|
-
/** A call to one
|
|
30
|
+
/** A call to one command. */
|
|
31
31
|
export interface CallRequest extends Target {
|
|
32
32
|
readonly member: string;
|
|
33
33
|
readonly payload: Schema.Json;
|
|
34
34
|
}
|
|
35
|
-
/** A read of one member (a view,
|
|
35
|
+
/** A read of one member (a view, a broadcast or a list). */
|
|
36
36
|
export interface MemberRequest extends Target {
|
|
37
37
|
readonly member: string;
|
|
38
38
|
readonly params: Schema.Json;
|
|
@@ -57,13 +57,13 @@ export interface FindRequest {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* What a handle is derived from, in encoded values. The engine implements it in process; the
|
|
60
|
-
* `/rpc` client implements it over the wire. A defect reaches the caller as
|
|
60
|
+
* `/rpc` client implements it over the wire. A defect reaches the caller as a `CommandDefect`.
|
|
61
61
|
*/
|
|
62
62
|
export interface Transport {
|
|
63
63
|
call(request: CallRequest): Effect.Effect<Reply, ActorError>;
|
|
64
64
|
state(request: Target): Stream.Stream<StateFrame, ActorError>;
|
|
65
65
|
view(request: MemberRequest): Stream.Stream<Schema.Json, ActorError>;
|
|
66
|
-
|
|
66
|
+
broadcast(request: MemberRequest): Stream.Stream<Schema.Json, ActorError>;
|
|
67
67
|
range(request: RangeRequest): Effect.Effect<ReadonlyArray<Schema.Json>, ActorError>;
|
|
68
68
|
watch(request: RangeRequest): Stream.Stream<ReadonlyArray<Schema.Json>, ActorError>;
|
|
69
69
|
join(request: JoinRequest): Effect.Effect<void, ActorError, Scope.Scope>;
|
|
@@ -73,13 +73,13 @@ declare const Client_base: Context.ServiceClass<Client, "@rikalabs/akter/contrac
|
|
|
73
73
|
/** The connection handles talk through: the in-process engine, a job, or the `/rpc` client. */
|
|
74
74
|
export declare class Client extends Client_base {
|
|
75
75
|
}
|
|
76
|
-
type SuccessType<A extends
|
|
77
|
-
type FailsType<A extends
|
|
78
|
-
type
|
|
79
|
-
[A in keyof D["
|
|
80
|
-
}[keyof D["
|
|
81
|
-
type
|
|
82
|
-
readonly [A in Keys]: (...args: Args<D["
|
|
76
|
+
type SuccessType<A extends Command> = A["success"] extends MemberSchema ? A["success"]["Type"] : void;
|
|
77
|
+
type FailsType<A extends Command> = A["fails"] extends MemberSchema ? A["fails"]["Type"] : never;
|
|
78
|
+
type PublicCommands<D extends Def> = {
|
|
79
|
+
[A in keyof D["commands"]]: D["commands"][A]["internal"] extends true ? never : A;
|
|
80
|
+
}[keyof D["commands"]];
|
|
81
|
+
type CommandMethods<D extends Def, Keys extends keyof D["commands"]> = {
|
|
82
|
+
readonly [A in Keys]: (...args: Args<D["commands"][A]["payload"]>) => Effect.Effect<SuccessType<D["commands"][A]>, FailsType<D["commands"][A]> | ActorError>;
|
|
83
83
|
};
|
|
84
84
|
/** A list on a client handle: a one-shot window and a watched one. */
|
|
85
85
|
export interface ListHandle<Item, By> {
|
|
@@ -91,18 +91,18 @@ type Reads<D extends Def> = {
|
|
|
91
91
|
} & {
|
|
92
92
|
readonly [V in keyof D["views"]]: (...args: Args<D["views"][V]["params"]>) => Stream.Stream<D["views"][V]["success"]["Type"], ActorError>;
|
|
93
93
|
} & {
|
|
94
|
-
readonly [E in keyof D["
|
|
94
|
+
readonly [E in keyof D["broadcasts"]]: Stream.Stream<D["broadcasts"][E]["schema"]["Type"], ActorError>;
|
|
95
95
|
} & {
|
|
96
96
|
readonly state: Stream.Stream<ClientState<D["state"]["fields"]>, ActorError>;
|
|
97
97
|
} & (D["presence"] extends MemberSchema ? {
|
|
98
98
|
join(presence: D["presence"]["Type"]): Effect.Effect<void, ActorError, Scope.Scope>;
|
|
99
99
|
} : {});
|
|
100
|
-
/** The client handle of one actor instance: public
|
|
101
|
-
export type Handle<Name extends string, D extends Def> =
|
|
100
|
+
/** The client handle of one actor instance: public commands, lists, views, broadcasts, state, `join`. */
|
|
101
|
+
export type Handle<Name extends string, D extends Def> = CommandMethods<D, PublicCommands<D>> & Reads<D> & {
|
|
102
102
|
readonly "~name"?: Name;
|
|
103
103
|
};
|
|
104
|
-
/** A job's `self`: the handle with internal
|
|
105
|
-
export type SelfHandle<Name extends string, D extends Def> =
|
|
104
|
+
/** A job's `self`: the handle with internal commands included. */
|
|
105
|
+
export type SelfHandle<Name extends string, D extends Def> = CommandMethods<D, keyof D["commands"]> & Reads<D> & {
|
|
106
106
|
readonly "~name"?: Name;
|
|
107
107
|
};
|
|
108
108
|
/** The JSON codec of any member schema. */
|
|
@@ -120,7 +120,7 @@ export declare const MemberCodec: {
|
|
|
120
120
|
};
|
|
121
121
|
/** The client's `state` frame schema: every state field except lists. */
|
|
122
122
|
export declare const clientStateSchema: (actor: AnyActor) => MemberSchema;
|
|
123
|
-
/** Derives one handle; `self` includes internal
|
|
123
|
+
/** Derives one handle; `self` includes internal commands. A new owner's full state frame resets its version clock before schema-equivalent values are suppressed. */
|
|
124
124
|
declare const derive: <H>(target: Target & {
|
|
125
125
|
readonly self: boolean;
|
|
126
126
|
}, transport: Transport) => H;
|
package/dist/contract/handle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context, Effect, JsonPatch, Result, Schema, Stream } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import { ActorError, CommandDefect, InvalidInput } from "./errors.js";
|
|
3
3
|
import { isList } from "./members.js";
|
|
4
4
|
/** A value a list orders by. */
|
|
5
5
|
export const ByValue = Schema.Union([Schema.String, Schema.Finite]);
|
|
@@ -42,7 +42,7 @@ export const MemberCodec = {
|
|
|
42
42
|
const decode = Schema.decodeResult(json(schema));
|
|
43
43
|
return (value) => Result.match(decode(value), {
|
|
44
44
|
onSuccess: Effect.succeed,
|
|
45
|
-
onFailure: (error) => Effect.die(
|
|
45
|
+
onFailure: (error) => Effect.die(CommandDefect.make({ kind: "DecodeFailure", actor, member, detail: error.message })),
|
|
46
46
|
});
|
|
47
47
|
},
|
|
48
48
|
};
|
|
@@ -62,15 +62,15 @@ export const clientStateSchema = (actor) => {
|
|
|
62
62
|
clientStateSchemas.set(actor, schema);
|
|
63
63
|
return schema;
|
|
64
64
|
};
|
|
65
|
-
/** Derives one handle; `self` includes internal
|
|
65
|
+
/** Derives one handle; `self` includes internal commands. A new owner's full state frame resets its version clock before schema-equivalent values are suppressed. */
|
|
66
66
|
const derive = (target, transport) => {
|
|
67
67
|
const { actor, id } = target;
|
|
68
68
|
const handle = new Map();
|
|
69
|
-
const
|
|
69
|
+
const commands = actor.commands;
|
|
70
70
|
const fields = actor.state.fields;
|
|
71
71
|
const views = actor.views;
|
|
72
|
-
const
|
|
73
|
-
for (const [member, declaration] of Object.entries(
|
|
72
|
+
const broadcasts = actor.broadcasts;
|
|
73
|
+
for (const [member, declaration] of Object.entries(commands)) {
|
|
74
74
|
if (declaration.internal && !target.self)
|
|
75
75
|
continue;
|
|
76
76
|
const encode = MemberCodec.encode(declaration.payload);
|
|
@@ -103,9 +103,9 @@ const derive = (target, transport) => {
|
|
|
103
103
|
const decode = MemberCodec.decode(declaration.success, actor.name, member);
|
|
104
104
|
handle.set(member, (params) => Stream.unwrap(Effect.map(encode(params), (encoded) => transport.view({ actor, id, member, params: encoded }).pipe(Stream.mapEffect(decode)))));
|
|
105
105
|
}
|
|
106
|
-
for (const [member, declaration] of Object.entries(
|
|
106
|
+
for (const [member, declaration] of Object.entries(broadcasts)) {
|
|
107
107
|
const decode = MemberCodec.decode(declaration.schema, actor.name, member);
|
|
108
|
-
handle.set(member, transport.
|
|
108
|
+
handle.set(member, transport.broadcast({ actor, id, member, params: null }).pipe(Stream.mapEffect(decode)));
|
|
109
109
|
}
|
|
110
110
|
const decodeState = MemberCodec.decode(clientStateSchema(actor), actor.name, "state");
|
|
111
111
|
handle.set("state", Stream.suspend(() => {
|