@akterjs/sdk 0.0.0-stage → 0.1.0

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 (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +105 -2
  3. package/dist/actor/json-document.schema.d.mts +14 -0
  4. package/dist/actor/json-document.schema.mjs +177 -0
  5. package/dist/actor/plain-actor.contract.d.mts +3 -0
  6. package/dist/actor/plain-actor.contract.mjs +63 -0
  7. package/dist/actor/plain-compile.contract.d.mts +100 -0
  8. package/dist/actor/plain-compile.contract.mjs +267 -0
  9. package/dist/actor/plain-definition.errors.d.mts +12 -0
  10. package/dist/actor/plain-definition.errors.mjs +10 -0
  11. package/dist/actor/plain-error.schema.d.mts +25 -0
  12. package/dist/actor/plain-error.schema.mjs +90 -0
  13. package/dist/actor/plain-handlers.contract.d.mts +87 -0
  14. package/dist/actor/plain-handlers.contract.mjs +94 -0
  15. package/dist/actor/plain-implement.contract.d.mts +27 -0
  16. package/dist/actor/plain-implement.contract.mjs +184 -0
  17. package/dist/actor/plain-types.contract.d.mts +316 -0
  18. package/dist/actor/plain-types.contract.mjs +1 -0
  19. package/dist/actor/plain-values.contract.d.mts +11 -0
  20. package/dist/actor/plain-values.contract.mjs +1 -0
  21. package/dist/actor/resource/plain-computers.contract.d.mts +89 -0
  22. package/dist/actor/resource/plain-computers.contract.mjs +34 -0
  23. package/dist/actor/resource/plain-file-access.contract.d.mts +38 -0
  24. package/dist/actor/resource/plain-file-access.contract.mjs +23 -0
  25. package/dist/actor/resource/plain-files.contract.d.mts +53 -0
  26. package/dist/actor/resource/plain-files.contract.mjs +47 -0
  27. package/dist/actor/standard-schema.schema.d.mts +76 -0
  28. package/dist/actor/standard-schema.schema.mjs +113 -0
  29. package/dist/local-server.adapter.d.mts +23 -0
  30. package/dist/local-server.adapter.mjs +43 -0
  31. package/dist/plain-client.adapter.d.mts +60 -0
  32. package/dist/plain-client.adapter.mjs +12 -0
  33. package/dist/plain-engine.layer.d.mts +9 -0
  34. package/dist/plain-engine.layer.mjs +10 -0
  35. package/dist/plain-sdk.contract.d.mts +7 -0
  36. package/dist/plain-sdk.contract.mjs +1 -0
  37. package/package.json +67 -3
@@ -0,0 +1,94 @@
1
+ import { PlainCompiler } from "./plain-compile.contract.mjs";
2
+ import { PlainErrors } from "./plain-error.schema.mjs";
3
+ import { PlainFiles } from "./resource/plain-files.contract.mjs";
4
+ import { Array, Cause, Effect, Exit, Option, Predicate, Record, Result, Schema } from "effect";
5
+ import { Akter } from "@akterjs/effect/contract";
6
+ import { DurableDeferred } from "effect/workflow";
7
+ // src/actor/plain-handlers.contract.ts
8
+ const TimerId = Schema.String.pipe(Schema.brand("TimerId"));
9
+ const notSynchronous = "A plain command, query, create or result handler must return synchronously; do I/O in a job (ADR 0007).";
10
+ const sqlOf = (scope, run) => (strings, ...values) => run(scope.sql(strings, ...values));
11
+ /** What a handler threw: a declared error fails the call, anything else is a defect. */
12
+ const failureOf = (errors, cause) => {
13
+ const isDeclared = Schema.is(PlainErrors.errorsCodec(errors));
14
+ return Option.match(Option.liftPredicate(cause, isDeclared), {
15
+ onNone: () => Effect.die(cause),
16
+ onSome: Effect.fail
17
+ });
18
+ };
19
+ const returned = (value) => {
20
+ if (Predicate.isPromiseLike(value)) return Effect.die(/* @__PURE__ */ new TypeError(notSynchronous));
21
+ return Effect.succeed(value);
22
+ };
23
+ /** Runs a handler: a declared error it throws fails the call; anything else it throws, or a Promise it returns, is a defect. */
24
+ const settle = (errors, call) => Effect.suspend(() => Result.match(Result.try({
25
+ catch: (cause) => cause,
26
+ try: call
27
+ }), {
28
+ onFailure: (cause) => failureOf(errors, cause),
29
+ onSuccess: returned
30
+ }));
31
+ const undeclared = (ref, kind) => /* @__PURE__ */ new TypeError(`${ref.actor}.${ref.member} is not a ${kind} Akter.make declared`);
32
+ const addressed = (ref) => {
33
+ const target = Option.getOrThrowWith(PlainCompiler.commandTargetOf(ref), () => undeclared(ref, "command"));
34
+ return Option.getOrThrow(PlainCompiler.addressedOf(target.compilation(), target.member));
35
+ };
36
+ const startable = (ref) => {
37
+ const target = Option.getOrThrowWith(PlainCompiler.jobTargetOf(ref), () => undeclared(ref, "job"));
38
+ return Option.getOrThrow(PlainCompiler.startableOf(target.compilation(), target.member));
39
+ };
40
+ /** The durable deferred a plain job waits on with `steps.waitFor(name)` and a command fills with `ctx.signal`. */
41
+ const signalNamed = (name) => DurableDeferred.make(name, { success: Schema.Json });
42
+ /** The payload after a target, as `ctx.send(Ledger.commands.credit, key, payload)` passes it. */
43
+ const firstOf = (payload) => Option.getOrUndefined(Array.head(payload));
44
+ const commandContext = ({ run, scope, state, update }) => ({
45
+ cancel: (timer) => {
46
+ run(Akter.cancel(TimerId.make(timer)));
47
+ },
48
+ files: PlainFiles.filesOf(run),
49
+ id: () => run(scope.id),
50
+ key: scope.key,
51
+ now: () => run(scope.now),
52
+ principal: scope.principal,
53
+ schedule: (delay, command, ...payload) => run(Akter.schedule(delay, addressed(command), firstOf(payload))),
54
+ send: (command, key, ...payload) => {
55
+ run(Akter.send(addressed(command), key, firstOf(payload)));
56
+ },
57
+ signal: (job, name, value) => {
58
+ run(Akter.signal(signalNamed(name), {
59
+ job,
60
+ value
61
+ }));
62
+ },
63
+ sql: sqlOf(scope, run),
64
+ start: (job, ...payload) => run(Akter.start(startable(job), { input: firstOf(payload) })),
65
+ state,
66
+ update
67
+ });
68
+ const migrationsOf = (migrations) => Record.map(migrations, (statements) => (sql) => Effect.forEach(Array.ensure(statements), (text) => sql.unsafe(text), { discard: true }));
69
+ const jobResultOf = (exit) => Exit.match(exit, {
70
+ onFailure: (cause) => Option.match(Cause.findErrorOption(cause), {
71
+ onNone: () => ({
72
+ defect: Cause.pretty(cause),
73
+ outcome: "defect"
74
+ }),
75
+ onSome: (error) => ({
76
+ error,
77
+ outcome: "failure"
78
+ })
79
+ }),
80
+ onSuccess: (value) => ({
81
+ outcome: "success",
82
+ value
83
+ })
84
+ });
85
+ const PlainHandlerBridge = {
86
+ commandContext,
87
+ failureOf,
88
+ jobResultOf,
89
+ migrationsOf,
90
+ settle,
91
+ signalNamed,
92
+ sqlOf
93
+ };
94
+ export { PlainHandlerBridge };
@@ -0,0 +1,27 @@
1
+ import { Compilation } from "./plain-compile.contract.mjs";
2
+ import { PlainHandlers } from "./plain-handlers.contract.mjs";
3
+ import { PlainApp, PlainImplementation } from "./plain-types.contract.mjs";
4
+ import { Layer, Option } from "effect";
5
+ import { WorkflowEngine } from "effect/workflow";
6
+ type ServedLayer = Layer.Layer<never, never, WorkflowEngine.WorkflowEngine>;
7
+ /** Compiles a plain implementation to the actor's Effect layer and its job layers. */
8
+ declare const implement: (compilation: Compilation, handlers: PlainHandlers) => ServedLayer;
9
+ interface Implemented {
10
+ readonly actor: Compilation["compiled"]["actor"];
11
+ readonly layer: ServedLayer;
12
+ }
13
+ /** The Effect actors and layer an app serves, in the form `akter dev`, `akter deploy` and `Akter.serve` take. */
14
+ interface Served {
15
+ readonly actors: readonly Implemented["actor"][];
16
+ readonly layer: ServedLayer;
17
+ }
18
+ declare const register: (implementation: PlainImplementation, implemented: Implemented) => void;
19
+ declare const app: (declared: readonly PlainImplementation[]) => Served;
20
+ declare const servedOf: (declared: PlainApp) => Option.Option<Served>;
21
+ export declare const PlainImplement: {
22
+ readonly app: typeof app;
23
+ readonly implement: typeof implement;
24
+ readonly register: typeof register;
25
+ readonly servedOf: typeof servedOf;
26
+ };
27
+ export type { Served };
@@ -0,0 +1,184 @@
1
+ import { PlainCompiler } from "./plain-compile.contract.mjs";
2
+ import { PlainComputers } from "./resource/plain-computers.contract.mjs";
3
+ import { PlainFiles } from "./resource/plain-files.contract.mjs";
4
+ import { PlainHandlerBridge } from "./plain-handlers.contract.mjs";
5
+ import { bridgeState } from "./standard-schema.schema.mjs";
6
+ import { Array, Effect, Equal, Layer, MutableRef, Option, Record, Schema } from "effect";
7
+ import { Akter } from "@akterjs/effect/contract";
8
+ import { Activity, DurableClock, DurableDeferred } from "effect/workflow";
9
+ // src/actor/plain-implement.contract.ts
10
+ const errorsFor = (compilation, member) => Option.getOrElse(Record.get(compilation.errors, member), () => []);
11
+ /** The context of a handler that may change the actor: commands and result handlers. */
12
+ const changingContext = Effect.fnUntraced(function* changingContext(compilation, state) {
13
+ const scope = yield* Akter;
14
+ const context = yield* Effect.context();
15
+ const run = Effect.runSyncWith(context);
16
+ return PlainHandlerBridge.commandContext({
17
+ run,
18
+ scope,
19
+ state,
20
+ update: (patch) => {
21
+ run(compilation.compiled.actor.update(patch));
22
+ }
23
+ });
24
+ });
25
+ /** The context of a handler that only reads. */
26
+ const readingContext = Effect.fnUntraced(function* readingContext() {
27
+ const scope = yield* Akter;
28
+ const context = yield* Effect.context();
29
+ const run = Effect.runSyncWith(context);
30
+ return {
31
+ files: PlainFiles.readingFilesOf(run),
32
+ id: () => run(scope.id),
33
+ key: scope.key,
34
+ now: () => run(scope.now),
35
+ principal: scope.principal,
36
+ sql: PlainHandlerBridge.sqlOf(scope, run)
37
+ };
38
+ });
39
+ const commandHandler = ({ compilation, handle, member }) => (state, payload) => Effect.flatMap(changingContext(compilation, state), (ctx) => PlainHandlerBridge.settle(errorsFor(compilation, member), () => handle(ctx, payload)));
40
+ const queryHandler = ({ compilation, handle, member }) => (state, payload) => Effect.flatMap(readingContext(), (reading) => PlainHandlerBridge.settle(errorsFor(compilation, member), () => handle({
41
+ files: reading.files,
42
+ key: reading.key,
43
+ principal: reading.principal,
44
+ sql: reading.sql,
45
+ state
46
+ }, payload)));
47
+ /** Whether the actor's `files.authorize` rule allows the call: only `true` does, and an actor without the rule allows none. */
48
+ const allows = (rules, ctx, request) => Option.match(Option.fromUndefinedOr(rules), {
49
+ onNone: () => Effect.succeed(false),
50
+ onSome: (rule) => Effect.map(Effect.orDie(PlainHandlerBridge.settle([], () => rule.authorize(ctx, request))), (allowed) => Equal.equals(allowed, true))
51
+ });
52
+ /** The actor's `files.authorize` rule, run as a query: the call is refused unless the rule returns `true`. */
53
+ const filesRule = (rules) => (state, request) => readingContext().pipe(Effect.flatMap((reading) => allows(rules, {
54
+ files: reading.files,
55
+ key: reading.key,
56
+ principal: reading.principal,
57
+ sql: reading.sql,
58
+ state
59
+ }, request)), Effect.filterOrFail((allowed) => allowed, () => PlainFiles.denied(request)), Effect.asVoid);
60
+ const createHandler = (handle) => (input) => Effect.flatMap(readingContext(), (ctx) => Effect.orDie(PlainHandlerBridge.settle([], () => handle(ctx, input))));
61
+ const resultHandler = ({ compilation, handle }) => (state, exit, job) => Effect.flatMap(changingContext(compilation, state), (ctx) => Effect.orDie(PlainHandlerBridge.settle([], () => {
62
+ handle(ctx, PlainHandlerBridge.jobResultOf(exit), { id: job.id });
63
+ })));
64
+ /** Runs `effect` unless the job was suspended, in which case it stays interrupted. */
65
+ const unlessSuspended = (suspended, effect) => Effect.suspend(() => {
66
+ if (MutableRef.get(suspended)) return Effect.interrupt;
67
+ return effect;
68
+ });
69
+ const computerOf = (actor, key) => {
70
+ const { compiled } = Option.getOrThrowWith(PlainCompiler.compilationOf(actor), () => /* @__PURE__ */ new TypeError(`${actor.name} is not an actor Akter.make defined`));
71
+ return compiled.actor.computer(key);
72
+ };
73
+ /** Runs a step's code with its idempotency key, as the activity named `name`. */
74
+ const keyed = (name, execute) => Effect.flatMap(Activity.idempotencyKey(name), (key) => Effect.promise(() => execute({ key })));
75
+ /** The job's steps as Promises: each runs as a recorded activity, a durable sleep or a durable wait in the workflow. */
76
+ const stepsOf = ({ call, patched, run }) => ({
77
+ computer: (actor, key) => PlainComputers.computersOf({
78
+ actor: actor.name,
79
+ connected: computerOf(actor, key),
80
+ run: call
81
+ }),
82
+ patched,
83
+ run: (name, execute) => run(Activity.make({
84
+ execute: keyed(name, execute),
85
+ name,
86
+ success: Schema.Json
87
+ })),
88
+ sleep: (name, delay) => run(DurableClock.sleep({
89
+ duration: delay,
90
+ name
91
+ })),
92
+ waitFor: (name) => run(DurableDeferred.await(PlainHandlerBridge.signalNamed(name)))
93
+ });
94
+ const jobWork = ({ handling, input, workflow }) => Effect.gen(function* runPlainJob() {
95
+ const context = yield* Effect.context();
96
+ const run = Effect.runPromiseWith(context);
97
+ const suspended = MutableRef.make(false);
98
+ const steps = stepsOf({
99
+ call: run,
100
+ patched: (name) => Effect.runSyncWith(context)(workflow.patched(name)),
101
+ run: (effect) => run(Effect.onInterrupt(unlessSuspended(suspended, effect), () => Effect.sync(() => MutableRef.set(suspended, true))))
102
+ });
103
+ const value = yield* Effect.catchDefect(Effect.promise(() => handling.handle(input, steps)), (defect) => unlessSuspended(suspended, PlainHandlerBridge.failureOf(errorsFor(handling.compilation, handling.member), defect)));
104
+ return yield* unlessSuspended(suspended, Effect.succeed(value));
105
+ });
106
+ const jobLayer = (handling) => Option.map(PlainCompiler.startableOf(handling.compilation, handling.member), (workflow) => workflow.toLayer(({ input }) => jobWork({
107
+ handling,
108
+ input,
109
+ workflow
110
+ })));
111
+ const upgradeOf = (compilation, upgrading) => Akter.migrateState(bridgeState({
112
+ site: {
113
+ actor: compilation.name,
114
+ member: "state",
115
+ part: "from"
116
+ },
117
+ validator: upgrading.from
118
+ }).codec, upgrading.upgrade);
119
+ const actorLayer = (compilation, handlers, create) => {
120
+ if (compilation.compiled.mode === "initial") return compilation.compiled.actor.toLayer(handlers);
121
+ const handle = Option.getOrThrowWith(create, () => /* @__PURE__ */ new TypeError(`${compilation.name} declares create, so implement needs create`));
122
+ return compilation.compiled.actor.toLayer({
123
+ ...handlers,
124
+ create: createHandler(handle)
125
+ });
126
+ };
127
+ /** Compiles a plain implementation to the actor's Effect layer and its job layers. */
128
+ const implement = (compilation, handlers) => {
129
+ const actor = actorLayer(compilation, {
130
+ commands: Record.map(handlers.commands, (handle, member) => commandHandler({
131
+ compilation,
132
+ handle,
133
+ member
134
+ })),
135
+ files: { authorize: filesRule(handlers.files) },
136
+ migrations: PlainHandlerBridge.migrationsOf(handlers.migrations ?? {}),
137
+ queries: Record.map(handlers.queries ?? {}, (handle, member) => queryHandler({
138
+ compilation,
139
+ handle,
140
+ member
141
+ })),
142
+ results: Record.map(handlers.results ?? {}, (handle, member) => resultHandler({
143
+ compilation,
144
+ handle,
145
+ member
146
+ })),
147
+ ...Option.match(Option.fromUndefinedOr(handlers.state), {
148
+ onNone: () => ({}),
149
+ onSome: (upgrading) => ({ state: upgradeOf(compilation, upgrading) })
150
+ })
151
+ }, Option.fromUndefinedOr(handlers.create));
152
+ const jobs = Array.flatMap(Record.toEntries(handlers.jobs ?? {}), ([member, handle]) => Option.toArray(jobLayer({
153
+ compilation,
154
+ handle,
155
+ member
156
+ })));
157
+ return Layer.mergeAll(actor, ...jobs);
158
+ };
159
+ const implementations = /* @__PURE__ */ new WeakMap();
160
+ const apps = /* @__PURE__ */ new WeakMap();
161
+ const register = (implementation, implemented) => {
162
+ implementations.set(implementation, implemented);
163
+ };
164
+ const served = (implemented) => ({
165
+ actors: Array.map(implemented, (each) => each.actor),
166
+ layer: Array.match(implemented, {
167
+ onEmpty: () => Layer.empty,
168
+ onNonEmpty: (all) => Layer.mergeAll(...Array.map(all, (each) => each.layer))
169
+ })
170
+ });
171
+ const app = (declared) => {
172
+ const implemented = Array.map(declared, (implementation) => Option.getOrThrowWith(Option.fromUndefinedOr(implementations.get(implementation)), () => /* @__PURE__ */ new TypeError(`${implementation.actor} was not implemented with implement()`)));
173
+ const result = served(implemented);
174
+ apps.set(result, implemented);
175
+ return result;
176
+ };
177
+ const servedOf = (declared) => Option.map(Option.fromUndefinedOr(apps.get(declared)), served);
178
+ const PlainImplement = {
179
+ app,
180
+ implement,
181
+ register,
182
+ servedOf
183
+ };
184
+ export { PlainImplement };
@@ -0,0 +1,316 @@
1
+ import { Computers } from "./resource/plain-computers.contract.mjs";
2
+ import { FileAccessOf, FileRulesOf, PlainFileAccess } from "./resource/plain-file-access.contract.mjs";
3
+ import { Files, ReadingFiles } from "./resource/plain-files.contract.mjs";
4
+ import { Delay, Sql, SqlRow, SqlValue } from "./plain-values.contract.mjs";
5
+ import { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
6
+ /** A JSON value: what the plain SDK stores, journals and sends (ADR 0015). */
7
+ type Json = null | boolean | number | string | readonly Json[] | {
8
+ readonly [key: string]: Json;
9
+ };
10
+ /** An actor's state: a JSON object. */
11
+ type StateFields = Readonly<Record<string, Json>>;
12
+ /** A Standard Schema validator that also implements Standard JSON Schema, such as a Zod 4 or ArkType declaration, or Valibot through `toStandardJsonSchema`. */
13
+ interface Validator<Input = unknown, Output = Input> {
14
+ readonly "~standard": StandardSchemaV1.Props<Input, Output> & StandardJSONSchemaV1.Props<Input, Output>;
15
+ }
16
+ type InputOf<Declaration extends Validator> = NonNullable<Declaration["~standard"]["types"]>["input"];
17
+ type OutputOf<Declaration extends Validator> = NonNullable<Declaration["~standard"]["types"]>["output"];
18
+ type DeclaredInput<Declaration> = Declaration extends Validator ? InputOf<Declaration> : never;
19
+ type DeclaredOutput<Declaration> = Declaration extends Validator ? OutputOf<Declaration> : never;
20
+ /** An error a handler throws and a caller catches by class. `_tag` names it on the wire. */
21
+ type PlainError<Tag extends string> = Error & {
22
+ readonly _tag: Tag;
23
+ };
24
+ type NoFields = Readonly<Record<string, never>>;
25
+ /** A declared error class: `new OverdrawnError({ balance })`, thrown by a handler and rejected to the caller. */
26
+ type PlainErrorClass<Tag extends string, Input, Output> = (Input extends NoFields ? new () => PlainError<Tag> : new (fields: Input) => PlainError<Tag> & Readonly<Output>) & {
27
+ readonly _tag: Tag;
28
+ };
29
+ type AnyErrorClass = (abstract new (...fields: never) => Error) & {
30
+ readonly _tag: string;
31
+ };
32
+ type ErrorInput<Fields> = Fields extends Validator ? InputOf<Fields> : NoFields;
33
+ type ErrorOutput<Fields> = Fields extends Validator ? OutputOf<Fields> : NoFields;
34
+ interface MemberOptions {
35
+ readonly callers?: readonly string[];
36
+ readonly errors?: readonly AnyErrorClass[];
37
+ readonly payload?: Validator;
38
+ readonly success?: Validator;
39
+ }
40
+ interface JobOptions {
41
+ readonly errors?: readonly AnyErrorClass[];
42
+ /** Named changes to the job's code, which `steps.patched` answers by the code that started the job. */
43
+ readonly patches?: readonly string[];
44
+ readonly payload: Validator;
45
+ readonly success?: Validator;
46
+ }
47
+ /** A declared command: it changes the actor and commits with everything it did. */
48
+ interface PlainCommand<Options extends MemberOptions = MemberOptions> {
49
+ readonly kind: "command";
50
+ readonly options: Options;
51
+ }
52
+ /** A declared query: it reads the actor and can be subscribed to. */
53
+ interface PlainQuery<Options extends MemberOptions = MemberOptions> {
54
+ readonly kind: "query";
55
+ readonly options: Options;
56
+ }
57
+ /** A declared job: durable work a command starts, which runs after the command commits. */
58
+ interface PlainJob<Options extends JobOptions = JobOptions> {
59
+ readonly kind: "job";
60
+ readonly options: Options;
61
+ }
62
+ /** What a caller sends: the payload's input, or `undefined` for a member declared without one. */
63
+ type PayloadIn<Options> = Options extends {
64
+ readonly payload: infer Declaration;
65
+ } ? DeclaredInput<Declaration> : undefined;
66
+ /** What a handler receives: the payload's output, or `undefined` for a member declared without one. */
67
+ type PayloadOut<Options> = Options extends {
68
+ readonly payload: infer Declaration;
69
+ } ? DeclaredOutput<Declaration> : undefined;
70
+ /** What a caller receives: the success output, or `undefined` for a member declared without one. */
71
+ type SuccessOut<Options> = Options extends {
72
+ readonly success: infer Declaration extends Validator;
73
+ } ? OutputOf<Declaration> : undefined;
74
+ type ErrorsOf<Options> = Options extends {
75
+ readonly errors: infer Classes extends readonly AnyErrorClass[];
76
+ } ? InstanceType<Classes[number]> : never;
77
+ type OptionsOf<Declaration> = Declaration extends {
78
+ readonly options: infer Options;
79
+ } ? Options : never;
80
+ /** The arguments after a target: none when the payload is `undefined`. */
81
+ type PayloadArgs<Payload> = [Payload] extends [undefined] ? [] : [payload: Payload];
82
+ /** The members of a kind an actor declares none of. */
83
+ type NoMembers = Readonly<Record<string, never>>;
84
+ /** The names of the declared members, without the index signature of `NoMembers`. */
85
+ type MemberNames<Members> = keyof { readonly [Member in keyof Members & string as Members[Member] extends never ? never : Member]: Member; };
86
+ interface ActorOptions {
87
+ readonly callers?: Readonly<Record<string, Validator>>;
88
+ readonly commands: Readonly<Record<string, PlainCommand>>;
89
+ readonly create?: Validator;
90
+ readonly files?: PlainFileAccess;
91
+ readonly initial?: () => StateFields;
92
+ readonly jobs?: Readonly<Record<string, PlainJob>>;
93
+ readonly key: Validator<string, string>;
94
+ readonly queries?: Readonly<Record<string, PlainQuery>>;
95
+ readonly state: Validator<StateFields, StateFields>;
96
+ }
97
+ type StateOf<Options extends ActorOptions> = OutputOf<Options["state"]>;
98
+ type KeyOf<Options extends ActorOptions> = OutputOf<Options["key"]>;
99
+ type PrincipalOf<Options extends ActorOptions> = Options extends {
100
+ readonly callers: infer Callers extends Readonly<Record<string, Validator>>;
101
+ } ? OutputOf<Callers[keyof Callers]> : {
102
+ readonly id: string;
103
+ };
104
+ type CommandsOf<Options extends ActorOptions> = Options["commands"];
105
+ type QueriesOf<Options extends ActorOptions> = Options extends {
106
+ readonly queries: infer Queries extends Readonly<Record<string, PlainQuery>>;
107
+ } ? Queries : NoMembers;
108
+ type JobsOf<Options extends ActorOptions> = Options extends {
109
+ readonly jobs: infer Jobs extends Readonly<Record<string, PlainJob>>;
110
+ } ? Jobs : NoMembers;
111
+ type CreateOf<Options extends ActorOptions> = Options extends {
112
+ readonly create: infer Declaration;
113
+ } ? Declaration : never;
114
+ /** Names one command of one actor, for `ctx.schedule`, `ctx.send` and history. */
115
+ interface CommandRef<Actor extends string = string, Key = unknown, Payload = unknown> {
116
+ readonly "~key"?: Key;
117
+ readonly "~payload"?: Payload;
118
+ readonly actor: Actor;
119
+ readonly member: string;
120
+ }
121
+ /** Names one job of one actor, for `ctx.start`. */
122
+ interface JobRef<Actor extends string = string, Payload = unknown> {
123
+ readonly "~payload"?: Payload;
124
+ readonly actor: Actor;
125
+ readonly member: string;
126
+ }
127
+ interface CreateContext<Options extends ActorOptions> {
128
+ readonly id: () => string;
129
+ readonly key: KeyOf<Options>;
130
+ readonly now: () => number;
131
+ readonly principal: PrincipalOf<Options>;
132
+ readonly sql: Sql;
133
+ }
134
+ interface QueryContext<Options extends ActorOptions> {
135
+ readonly files: ReadingFiles;
136
+ readonly key: KeyOf<Options>;
137
+ readonly principal: PrincipalOf<Options>;
138
+ readonly sql: Sql;
139
+ readonly state: StateOf<Options>;
140
+ }
141
+ interface CommandContext<Name extends string, Options extends ActorOptions> extends QueryContext<Options> {
142
+ readonly cancel: (timer: string) => void;
143
+ readonly files: Files;
144
+ readonly id: () => string;
145
+ readonly now: () => number;
146
+ readonly schedule: <Payload>(delay: Delay, command: CommandRef<Name, KeyOf<Options>, Payload>, ...payload: PayloadArgs<Payload>) => string;
147
+ readonly send: <Key, Payload>(command: CommandRef<string, Key, Payload>, key: Key, ...payload: PayloadArgs<Payload>) => void;
148
+ /**
149
+ * Signals one of this actor's jobs waiting in `steps.waitFor(name)` with `value`, such as an approval. The signal is
150
+ * stored with the command and wakes the job once the command commits: a command that throws signals nothing, and a
151
+ * retried command signals once. A job that has finished, was never started or already has that signal ignores it.
152
+ * `job` is the id `ctx.start` returned; signal another actor's job by sending that actor a command.
153
+ */
154
+ readonly signal: (job: string, name: string, value: Json) => void;
155
+ readonly start: <Payload>(job: JobRef<Name, Payload>, ...payload: PayloadArgs<Payload>) => string;
156
+ readonly update: (patch: Partial<StateOf<Options>>) => void;
157
+ }
158
+ /** Why a step's value is not JSON, as the type error names it, such as `NotJson<"a Date">`. */
159
+ interface NotJson<What extends string> {
160
+ readonly "~notJson": `${What} is not JSON; a step's value is recorded as JSON`;
161
+ }
162
+ /**
163
+ * `Value` when it is JSON: a JSON value, or an interface or type of one. Anything else becomes a `NotJson` naming the
164
+ * part that isn't, such as a function or method, a `Date`, a `bigint`, a symbol or an `undefined` field.
165
+ */
166
+ type JsonSafe<Value> = Value extends Json ? Value : Value extends bigint ? NotJson<"a bigint"> : Value extends symbol ? NotJson<"a symbol"> : Value extends undefined ? NotJson<"undefined"> : Value extends ((...args: never) => void) ? NotJson<"a function"> : Value extends Date ? NotJson<"a Date"> : Value extends object ? { readonly [Key in keyof Value]: JsonSafe<Value[Key]>; } : NotJson<"this value">;
167
+ /** The step `steps.run` is running. */
168
+ interface JobStep {
169
+ /** The step's idempotency key: the same each time this job runs this step, and distinct from every other step. Pass it as an exec step's `key`. */
170
+ readonly key: string;
171
+ }
172
+ /** A job's steps. Each completed step is recorded, so a job that resumes after a restart does not repeat it. */
173
+ interface JobSteps {
174
+ /** The computers of `actor` at `key`, such as `steps.computer(Task, key)`. Call them inside `steps.run`. */
175
+ readonly computer: <Name extends string, Options extends ActorOptions>(actor: PlainActor<Name, Options>, key: KeyOf<Options>) => Computers;
176
+ /** Whether the code that started this job declared the patch `name`, for the job's whole run. */
177
+ readonly patched: (name: string) => boolean;
178
+ /** Runs `execute` once and records its value, which must be JSON (`JsonSafe`); a resumed job reads it back. */
179
+ readonly run: <Value>(name: string, execute: (step: JobStep) => Promise<Value & JsonSafe<Value>>) => Promise<Value>;
180
+ readonly sleep: (name: string, delay: Delay) => Promise<void>;
181
+ /** Waits until a command signals this job with `ctx.signal(id, name, value)`, and returns the value, recorded. */
182
+ readonly waitFor: (name: string) => Promise<Json>;
183
+ }
184
+ /** The job a result belongs to: `id` is the id `ctx.start` returned when the job was started. */
185
+ interface FinishedJob {
186
+ readonly id: string;
187
+ }
188
+ /** How a job ended, delivered to the actor's result handler. */
189
+ type JobResult<Value, Failure> = {
190
+ readonly outcome: "defect";
191
+ readonly defect: string;
192
+ } | {
193
+ readonly outcome: "failure";
194
+ readonly error: Failure;
195
+ } | {
196
+ readonly outcome: "success";
197
+ readonly value: Value;
198
+ };
199
+ /** A command or query handler: it returns the success input, or nothing for a member declared without one. */
200
+ type HandlerOf<Context, Options> = Options extends {
201
+ readonly success: infer Declaration extends Validator;
202
+ } ? (ctx: Context, payload: PayloadOut<Options>) => InputOf<Declaration> : (ctx: Context, payload: PayloadOut<Options>) => void;
203
+ type JobHandlerOf<Options> = Options extends {
204
+ readonly success: infer Declaration extends Validator;
205
+ } ? (payload: PayloadOut<Options>, steps: JobSteps) => Promise<InputOf<Declaration>> : (payload: PayloadOut<Options>, steps: JobSteps) => Promise<void>;
206
+ type CommandHandlers<Name extends string, Options extends ActorOptions> = { readonly [Member in MemberNames<CommandsOf<Options>>]: HandlerOf<CommandContext<Name, Options>, OptionsOf<CommandsOf<Options>[Member]>>; };
207
+ type QueryHandlers<Options extends ActorOptions> = { readonly [Member in MemberNames<QueriesOf<Options>>]: HandlerOf<QueryContext<Options>, OptionsOf<QueriesOf<Options>[Member]>>; };
208
+ type JobHandlers<Options extends ActorOptions> = { readonly [Member in MemberNames<JobsOf<Options>>]: JobHandlerOf<OptionsOf<JobsOf<Options>[Member]>>; };
209
+ type ResultHandlers<Name extends string, Options extends ActorOptions> = { readonly [Member in MemberNames<JobsOf<Options>>]: (ctx: CommandContext<Name, Options>, result: JobResult<SuccessOut<OptionsOf<JobsOf<Options>[Member]>>, ErrorsOf<OptionsOf<JobsOf<Options>[Member]>>>, job: FinishedJob) => void; };
210
+ /** Reads state stored under an older declaration and returns the current one. */
211
+ interface StateUpgrade<State> {
212
+ readonly from: Validator<StateFields, StateFields>;
213
+ readonly upgrade: (stored: never) => State;
214
+ }
215
+ /** SQL statements per migration number. Each runs once, in order, when the actor first opens after a deploy. */
216
+ type Migrations = Readonly<Record<number, string | readonly string[]>>;
217
+ type Implementation<Name extends string, Options extends ActorOptions> = {
218
+ readonly commands: CommandHandlers<Name, Options>;
219
+ readonly migrations?: Migrations;
220
+ readonly state?: StateUpgrade<StateOf<Options>>;
221
+ } & ([MemberNames<QueriesOf<Options>>] extends [never] ? unknown : {
222
+ readonly queries: QueryHandlers<Options>;
223
+ }) & ([MemberNames<JobsOf<Options>>] extends [never] ? unknown : {
224
+ readonly jobs: JobHandlers<Options>;
225
+ readonly results: ResultHandlers<Name, Options>;
226
+ }) & ([CreateOf<Options>] extends [never] ? unknown : {
227
+ readonly create: (ctx: CreateContext<Options>, input: DeclaredOutput<CreateOf<Options>>) => StateOf<Options>;
228
+ }) & FileRulesOf<Options, QueryContext<Options>>;
229
+ /** An actor's handlers, ready to serve. Pass it to `Akter.app`. */
230
+ interface PlainImplementation<Name extends string = string> {
231
+ readonly actor: Name;
232
+ }
233
+ /** A JSON Schema document. */
234
+ type JsonDocument = Readonly<Record<string, Json>>;
235
+ /** The JSON Schema documents of one declaration, kept for history and devtools. */
236
+ interface Documents {
237
+ readonly input: JsonDocument;
238
+ readonly output: JsonDocument;
239
+ }
240
+ interface MemberContract {
241
+ /** The caller kinds the member admits; absent when it admits every kind the actor declares. */
242
+ readonly callers?: readonly string[];
243
+ readonly errors: Readonly<Record<string, Documents>>;
244
+ /** Absent when the member takes no payload. */
245
+ readonly payload?: Documents;
246
+ /** Absent when the member replies with nothing. */
247
+ readonly success?: Documents;
248
+ }
249
+ /** An actor's declarations as JSON Schema, captured when the actor is defined. */
250
+ interface ActorContract {
251
+ readonly callers: Readonly<Record<string, Documents>>;
252
+ readonly commands: Readonly<Record<string, MemberContract>>;
253
+ /** Absent when the actor starts from `initial`. */
254
+ readonly create?: Documents;
255
+ readonly jobs: Readonly<Record<string, MemberContract>>;
256
+ readonly key: Documents;
257
+ readonly name: string;
258
+ readonly queries: Readonly<Record<string, MemberContract>>;
259
+ readonly state: Documents;
260
+ }
261
+ /** A defined actor: its name, references to its commands and jobs, its contract, and `implement`. */
262
+ interface PlainActor<Name extends string = string, Options extends ActorOptions = ActorOptions> {
263
+ readonly "~options"?: Options;
264
+ readonly commands: { readonly [Member in MemberNames<CommandsOf<Options>>]: CommandRef<Name, KeyOf<Options>, PayloadIn<OptionsOf<CommandsOf<Options>[Member]>>>; };
265
+ readonly contract: ActorContract;
266
+ readonly implement: (implementation: Implementation<Name, Options>) => PlainImplementation<Name>;
267
+ readonly jobs: { readonly [Member in MemberNames<JobsOf<Options>>]: JobRef<Name, PayloadIn<OptionsOf<JobsOf<Options>[Member]>>>; };
268
+ readonly name: Name;
269
+ }
270
+ /** The module `akter dev` and `akter deploy` load: `export const app = Akter.app([...])`. */
271
+ interface PlainApp {
272
+ readonly actors: readonly object[];
273
+ readonly layer: object;
274
+ }
275
+ interface PlainAkter {
276
+ /** The principal another actor calls with: its name, key and id. Declare it as the `actor` caller to accept `ctx.send`. */
277
+ readonly ActorPrincipal: Validator<{
278
+ readonly actor: string;
279
+ readonly id: string;
280
+ readonly key: string;
281
+ }, {
282
+ readonly actor: string;
283
+ readonly id: string;
284
+ readonly key: string;
285
+ }>;
286
+ /** The default principal of a `user` caller: an id. */
287
+ readonly Principal: Validator<{
288
+ readonly id: string;
289
+ }, {
290
+ readonly id: string;
291
+ }>;
292
+ /** Declares an app from implemented actors. */
293
+ readonly app: (implementations: readonly PlainImplementation[]) => PlainApp;
294
+ /** Declares a command. */
295
+ readonly command: <const Options extends MemberOptions = MemberOptions>(options?: Options) => PlainCommand<Options>;
296
+ /** Declares an error class. Its fields, if any, are a Standard validator of an object. */
297
+ readonly error: <const Tag extends string, Fields extends Validator | undefined = undefined>(tag: Tag, fields?: Fields) => PlainErrorClass<Tag, ErrorInput<Fields>, ErrorOutput<Fields>>;
298
+ /** Declares which caller kinds may make file calls: `files: Akter.fileAccess({ callers: ["member"] })`. */
299
+ readonly fileAccess: FileAccessOf;
300
+ /** Declares a job. */
301
+ readonly job: <const Options extends JobOptions>(options: Options) => PlainJob<Options>;
302
+ /** Defines an actor. Refuses a declaration it cannot store as JSON with a PlainDefinitionError. */
303
+ readonly make: <const Name extends string, const Options extends ActorOptions>(name: Name, options: Options) => PlainActor<Name, Options>;
304
+ /** Declares a query. */
305
+ readonly query: <const Options extends MemberOptions = MemberOptions>(options?: Options) => PlainQuery<Options>;
306
+ }
307
+ /** What a definition throws for a declaration the plain SDK refuses (ADR 0015). */
308
+ interface PlainDefinitionFailure extends Error {
309
+ readonly _tag: "PlainDefinitionError";
310
+ readonly actor: string;
311
+ readonly member: string;
312
+ /** Which part of the member was refused, such as `payload`, `state` or `principal`. */
313
+ readonly part: string;
314
+ readonly reasons: readonly string[];
315
+ }
316
+ export type { ActorContract, ActorOptions, AnyErrorClass, CommandContext, CommandRef, CreateContext, CreateOf, DeclaredInput, DeclaredOutput, Delay, Documents, ErrorInput, ErrorOutput, ErrorsOf, FinishedJob, Implementation, InputOf, JobOptions, JobRef, JobResult, JobStep, JobSteps, Json, JsonDocument, KeyOf, MemberContract, MemberNames, MemberOptions, Migrations, OptionsOf, OutputOf, PayloadArgs, PayloadIn, PayloadOut, PlainActor, PlainAkter, PlainApp, PlainCommand, PlainDefinitionFailure, PlainError, PlainErrorClass, PlainImplementation, PlainJob, PlainQuery, PrincipalOf, QueriesOf, QueryContext, Sql, SqlRow, SqlValue, StateFields, StateOf, StateUpgrade, SuccessOut, Validator };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ /** Values a plain handler hands the engine: SQL against its own database, and delays for timers. */
2
+ /** A value bound into SQL. */
3
+ type SqlValue = string | number | boolean | null | Uint8Array | bigint;
4
+ /** One row a read returns, by column name. */
5
+ type SqlRow = Readonly<Record<string, SqlValue>>;
6
+ /** The actor's own SQLite database, as a tagged template that runs now and returns the rows. */
7
+ type Sql = (strings: TemplateStringsArray, ...values: readonly SqlValue[]) => readonly SqlRow[];
8
+ type DelayUnit = "day" | "days" | "hour" | "hours" | "milli" | "millis" | "minute" | "minutes" | "second" | "seconds" | "week" | "weeks";
9
+ /** A delay in milliseconds, or text such as `"5 minutes"`. */
10
+ type Delay = number | `${number} ${DelayUnit}`;
11
+ export type { Delay, Sql, SqlRow, SqlValue };
@@ -0,0 +1 @@
1
+ export {};