@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,89 @@
1
+ import { Effect } from "effect";
2
+ import { ComputerClient } from "@akterjs/effect/contract";
3
+ /** A computer's size: memory in MiB and virtual CPUs. */
4
+ interface ComputerSize {
5
+ readonly memoryMib: number;
6
+ readonly vcpu: number;
7
+ }
8
+ /**
9
+ * A computer to keep: its image, its size and the hosts it may reach. `name` is lowercase letters, digits and hyphens,
10
+ * starting with a letter. `egress` lists hosts or rules, never credentials.
11
+ */
12
+ interface ComputerSpec {
13
+ readonly egress: readonly string[];
14
+ readonly image: string;
15
+ readonly name: string;
16
+ readonly size: ComputerSize;
17
+ }
18
+ /** `paused` keeps memory, processes and files; `stopped` and `failed` keep files; `gone` means the disk is gone. */
19
+ type ComputerState = "starting" | "running" | "paused" | "stopped" | "failed" | "gone";
20
+ interface Computer extends ComputerSpec {
21
+ readonly state: ComputerState;
22
+ }
23
+ /**
24
+ * One exec step: at most 100 KiB of command and stdin together, and 300 seconds. `key` names the logical operation,
25
+ * such as the `step.key` of the `steps.run` it runs in, so the same step sent again reads its outcome back.
26
+ */
27
+ interface ExecRequest {
28
+ readonly command: string;
29
+ readonly cwd?: string;
30
+ readonly key: string;
31
+ readonly stdin?: Uint8Array;
32
+ readonly timeoutSeconds?: number;
33
+ }
34
+ /**
35
+ * `Ran`: the step ran to an end, or its recorded outcome was read back. `exitCode` is absent when the process ended
36
+ * without one; `truncated` is set past 4 MiB of a stream. `Unknown`: the step started but recorded no outcome, so it
37
+ * is not run again; reconcile with the outside system by its key.
38
+ */
39
+ type ExecResult = {
40
+ readonly _tag: "Ran";
41
+ readonly exitCode?: number;
42
+ readonly stderr: string;
43
+ readonly stdout: string;
44
+ readonly truncated: boolean;
45
+ } | {
46
+ readonly _tag: "Unknown";
47
+ };
48
+ /** Where a fork goes: another actor by name and key, and optionally a new name for the copy. */
49
+ interface ComputerFork {
50
+ readonly into: {
51
+ readonly actor: string;
52
+ readonly key: string;
53
+ };
54
+ readonly name?: string;
55
+ }
56
+ /** A hold on a paused computer, such as a fork's id: only a `resume` with the same hold runs it again. */
57
+ interface ComputerHold {
58
+ readonly hold?: string;
59
+ }
60
+ /**
61
+ * One actor's computers, from `steps.computer(Actor, key)` in a job. Each call reaches a provider, so make it inside
62
+ * `steps.run`. Each rejects with the error the Effect `ComputerClient` fails with, such as `ComputerHeldError`, by
63
+ * its `_tag`; a spec or exec request past the limits rejects with `InvalidRequestError` before it reaches a provider.
64
+ */
65
+ interface Computers {
66
+ readonly ensure: (spec: ComputerSpec) => Promise<Computer>;
67
+ readonly exec: (computer: string, request: ExecRequest) => Promise<ExecResult>;
68
+ /** Copies the computer, its files and memory, to the target actor. A resend finds the copy already made. */
69
+ readonly fork: (computer: string, fork: ComputerFork) => Promise<Computer>;
70
+ readonly get: (computer: string) => Promise<Computer>;
71
+ readonly list: () => Promise<readonly Computer[]>;
72
+ readonly pause: (computer: string, hold?: ComputerHold) => Promise<Computer>;
73
+ readonly remove: (computer: string) => Promise<void>;
74
+ readonly resume: (computer: string, hold?: ComputerHold) => Promise<Computer>;
75
+ readonly stop: (computer: string) => Promise<Computer>;
76
+ }
77
+ interface Connecting {
78
+ readonly actor: string;
79
+ /** The actor's Effect computer client, as `Actor.computer(key)` returns it. */
80
+ readonly connected: Effect.Effect<ComputerClient, Error>;
81
+ /** Runs an effect in the job's context; a failure rejects with the error. */
82
+ readonly run: <Success>(effect: Effect.Effect<Success, Error>) => Promise<Success>;
83
+ }
84
+ /** The Promise face of one actor's Effect computer client. */
85
+ declare const computersOf: ({ actor, connected, run }: Connecting) => Computers;
86
+ export declare const PlainComputers: {
87
+ readonly computersOf: typeof computersOf;
88
+ };
89
+ export type { Computer, ComputerFork, ComputerHold, ComputerSize, ComputerSpec, ComputerState, Computers, ExecRequest, ExecResult };
@@ -0,0 +1,34 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ import { ComputerName, ComputerSpec, ExecRequest } from "@akterjs/effect/contract/computer";
3
+ import { InvalidRequestError } from "@akterjs/effect/contract/errors";
4
+ // src/actor/resource/plain-computers.contract.ts
5
+ /** The Promise face of one actor's Effect computer client. */
6
+ const computersOf = ({ actor, connected, run }) => {
7
+ const refused = (invalid) => InvalidRequestError.make({
8
+ actor,
9
+ message: invalid.message
10
+ });
11
+ const spec = (wanted) => Effect.mapError(Schema.decodeEffect(ComputerSpec)(wanted), refused);
12
+ const request = (wanted) => Effect.mapError(Schema.decodeEffect(ExecRequest)(wanted), refused);
13
+ const name = (wanted) => Effect.mapError(Schema.decodeEffect(ComputerName)(wanted), refused);
14
+ const call = (use) => run(Effect.flatMap(connected, use));
15
+ return {
16
+ ensure: (wanted) => call((client) => Effect.flatMap(spec(wanted), client.ensure)),
17
+ exec: (computer, wanted) => call((client) => Effect.flatMap(request(wanted), (valid) => client.exec(computer, valid))),
18
+ fork: (computer, { into, name: renamed }) => call((client) => Option.match(Option.fromUndefinedOr(renamed), {
19
+ onNone: () => client.fork(computer, { into }),
20
+ onSome: (wanted) => Effect.flatMap(name(wanted), (valid) => client.fork(computer, {
21
+ into,
22
+ name: valid
23
+ }))
24
+ })),
25
+ get: (computer) => call((client) => client.get(computer)),
26
+ list: () => call((client) => client.list),
27
+ pause: (computer, hold) => call((client) => client.pause(computer, hold)),
28
+ remove: (computer) => call((client) => client.remove(computer)),
29
+ resume: (computer, hold) => call((client) => client.resume(computer, hold)),
30
+ stop: (computer) => call((client) => client.stop(computer))
31
+ };
32
+ };
33
+ const PlainComputers = { computersOf };
34
+ export { PlainComputers };
@@ -0,0 +1,38 @@
1
+ import { FileAccess } from "@akterjs/effect/contract";
2
+ /** Which caller kinds may make file calls to an actor, as `Akter.fileAccess` declares them. */
3
+ interface PlainFileAccess<Kinds extends string = string> {
4
+ readonly callers: readonly [Kinds, ...Kinds[]];
5
+ readonly kind: "files";
6
+ }
7
+ /**
8
+ * What a file call asks the actor's `files.authorize` rule: `read` for a download, a listing or the usage (`path` is
9
+ * the file, or the listing's prefix), `write` for an upload (`path` is empty: the command that attaches it names it).
10
+ */
11
+ interface FileRequest {
12
+ readonly access: "read" | "write";
13
+ readonly path: string;
14
+ }
15
+ /**
16
+ * The `files` an implementation needs when its actor declares file calls: `authorize` decides each call from `ctx.state`
17
+ * and `ctx.principal`, the verified caller, never the request. Only `true` allows the call.
18
+ */
19
+ type FileRulesOf<Options, Context> = Options extends {
20
+ readonly files: PlainFileAccess;
21
+ } ? {
22
+ readonly files: {
23
+ readonly authorize: (ctx: Context, request: FileRequest) => boolean;
24
+ };
25
+ } : unknown;
26
+ type FileAccessOf = <const Kinds extends string>(options: {
27
+ readonly callers: readonly [Kinds, ...Kinds[]];
28
+ }) => PlainFileAccess<Kinds>;
29
+ /**
30
+ * The Effect declaration a plain actor compiles to. An actor that declares no file calls compiles to one admitting no
31
+ * caller kind, which the engine reads as none, so every compiled actor has the same type.
32
+ */
33
+ declare const compiledFileAccess: (declared: PlainFileAccess | undefined) => FileAccess;
34
+ export declare const PlainFileAccesses: {
35
+ readonly compiledFileAccess: typeof compiledFileAccess;
36
+ readonly fileAccess: FileAccessOf;
37
+ };
38
+ export type { FileAccessOf, FileRequest, FileRulesOf, PlainFileAccess };
@@ -0,0 +1,23 @@
1
+ import { Option } from "effect";
2
+ import { Akter } from "@akterjs/effect/contract";
3
+ // src/actor/resource/plain-file-access.contract.ts
4
+ const fileAccess = (options) => ({
5
+ callers: options.callers,
6
+ kind: "files"
7
+ });
8
+ /**
9
+ * The Effect declaration a plain actor compiles to. An actor that declares no file calls compiles to one admitting no
10
+ * caller kind, which the engine reads as none, so every compiled actor has the same type.
11
+ */
12
+ const compiledFileAccess = (declared) => Option.match(Option.fromUndefinedOr(declared), {
13
+ onNone: () => ({
14
+ ...Akter.fileAccess({ callers: ["user"] }),
15
+ callers: Option.some([])
16
+ }),
17
+ onSome: ({ callers }) => Akter.fileAccess({ callers })
18
+ });
19
+ const PlainFileAccesses = {
20
+ compiledFileAccess,
21
+ fileAccess
22
+ };
23
+ export { PlainFileAccesses };
@@ -0,0 +1,53 @@
1
+ import { Effect } from "effect";
2
+ import { Akter } from "@akterjs/effect/contract";
3
+ import { FileAccessDeniedError } from "@akterjs/effect/contract/errors";
4
+ /** One file an actor holds. */
5
+ interface FileRef {
6
+ readonly bytes: number;
7
+ readonly path: string;
8
+ readonly sha256: string;
9
+ readonly writtenAt: number;
10
+ }
11
+ /** Which files to list: those under `prefix`, at most `limit`, after the `cursor` a previous page returned. */
12
+ interface FileListing {
13
+ readonly cursor?: string | undefined;
14
+ readonly limit?: number | undefined;
15
+ readonly prefix?: string | undefined;
16
+ }
17
+ /** One page of files; `cursor` is present when there are more. */
18
+ interface FilePage {
19
+ readonly cursor?: string;
20
+ readonly entries: readonly FileRef[];
21
+ }
22
+ /** One page of files and the total bytes every file the listing matches holds. */
23
+ interface FileUsage extends FilePage {
24
+ readonly bytes: number;
25
+ }
26
+ /** What a query reads of its actor's files: pages of them, and the bytes they hold. */
27
+ interface ReadingFiles {
28
+ readonly list: (listing?: FileListing) => FilePage;
29
+ readonly usage: (listing?: FileListing) => FileUsage;
30
+ }
31
+ /**
32
+ * The actor's files in a command or result handler. `attach(path, receipt)` binds a file a client uploaded (the receipt
33
+ * its `files.upload` returned) at `path` in this command's transaction, replacing any file there; `remove` drops one.
34
+ * Both commit or roll back with the command. File bytes never pass through a handler: clients upload and download them.
35
+ */
36
+ interface Files extends ReadingFiles {
37
+ readonly attach: (path: string, receipt: string) => FileRef;
38
+ readonly remove: (path: string) => void;
39
+ }
40
+ type ReadRun = <Success, Failure>(effect: Effect.Effect<Success, Failure, Akter>) => Success;
41
+ type ChangeRun = <Success, Failure>(effect: Effect.Effect<Success, Failure, Effect.Services<ReturnType<typeof Akter.cancel>> | Akter>) => Success;
42
+ declare const readingFilesOf: (run: ReadRun) => ReadingFiles;
43
+ declare const filesOf: (run: ChangeRun) => Files;
44
+ /** What a file call asks an actor's `files.authorize` rule: the same `access` and `path` its refusal names. */
45
+ type FileQuestion = Pick<FileAccessDeniedError, "access" | "path">;
46
+ /** The refusal of a file call whose rule didn't return `true`. */
47
+ declare const denied: ({ access, path }: FileQuestion) => FileAccessDeniedError;
48
+ export declare const PlainFiles: {
49
+ readonly denied: typeof denied;
50
+ readonly filesOf: typeof filesOf;
51
+ readonly readingFilesOf: typeof readingFilesOf;
52
+ };
53
+ export type { FileListing, FilePage, FileQuestion, FileRef, FileUsage, Files, ReadingFiles };
@@ -0,0 +1,47 @@
1
+ import { Effect, Option, Record } from "effect";
2
+ import { Akter } from "@akterjs/effect/contract";
3
+ import { FileAccessDeniedError } from "@akterjs/effect/contract/errors";
4
+ // src/actor/resource/plain-files.contract.ts
5
+ const pageOf = ({ cursor, entries }) => Option.match(cursor, {
6
+ onNone: () => ({ entries }),
7
+ onSome: (next) => ({
8
+ cursor: next,
9
+ entries
10
+ })
11
+ });
12
+ const listingOf = ({ cursor, limit, prefix } = {}) => ({
13
+ ...Record.getSomes({
14
+ cursor: Option.fromUndefinedOr(cursor),
15
+ prefix: Option.fromUndefinedOr(prefix)
16
+ }),
17
+ ...Record.getSomes({ limit: Option.fromUndefinedOr(limit) })
18
+ });
19
+ const readingFilesOf = (run) => ({
20
+ list: (listing) => pageOf(run(Effect.flatMap(Akter.files, (files) => files.list(listingOf(listing))))),
21
+ usage: (listing) => {
22
+ const usage = run(Effect.flatMap(Akter.files, (files) => files.fileUsage(listingOf(listing))));
23
+ return {
24
+ ...pageOf(usage),
25
+ bytes: Number(usage.bytes)
26
+ };
27
+ }
28
+ });
29
+ const filesOf = (run) => ({
30
+ ...readingFilesOf(run),
31
+ attach: (path, receipt) => run(Effect.flatMap(Akter.files, (files) => files.attach(path, receipt))),
32
+ remove: (path) => {
33
+ run(Effect.flatMap(Akter.files, (files) => files.remove(path)));
34
+ }
35
+ });
36
+ /** The refusal of a file call whose rule didn't return `true`. */
37
+ const denied = ({ access, path }) => FileAccessDeniedError.make({
38
+ access,
39
+ message: "the actor's files.authorize rule refused this call",
40
+ path
41
+ });
42
+ const PlainFiles = {
43
+ denied,
44
+ filesOf,
45
+ readingFilesOf
46
+ };
47
+ export { PlainFiles };
@@ -0,0 +1,76 @@
1
+ import { JsonNode } from "./json-document.schema.mjs";
2
+ import { PlainDefinitionError } from "./plain-definition.errors.mjs";
3
+ import { StateFields, Validator } from "./plain-types.contract.mjs";
4
+ import { Effect, Schema, SchemaIssue } from "effect";
5
+ /** Where a declaration sits in an actor, so a refusal names it. */
6
+ interface Site {
7
+ readonly actor: string;
8
+ readonly member: string;
9
+ readonly part: string;
10
+ }
11
+ interface Documents {
12
+ readonly input: JsonNode;
13
+ readonly output: JsonNode;
14
+ }
15
+ type JsonCodec = Schema.Codec<Schema.Json, Schema.Json>;
16
+ type KeyCodec = Schema.Codec<string, string>;
17
+ type StateCodec = Schema.Codec<StateFields, Schema.Json>;
18
+ /** What Akter asks a validator to check: a JSON value it received, or an error instance a handler threw. */
19
+ type Checked = Schema.Json | Error;
20
+ interface Declaring {
21
+ readonly site: Site;
22
+ readonly validator: Validator;
23
+ }
24
+ interface Refusing {
25
+ readonly reasons: readonly string[];
26
+ readonly site: Site;
27
+ }
28
+ declare const refuse: ({ reasons, site }: Refusing) => PlainDefinitionError;
29
+ interface Bridged<Codec> {
30
+ readonly codec: Codec;
31
+ readonly documents: Documents;
32
+ }
33
+ declare const documentsOf: ({ site, validator: declared }: Declaring) => Documents;
34
+ /** Runs a validator as a schema step: a refusal becomes the issue the engine reports. */
35
+ declare const validating: (declared: Validator) => (input: Checked) => Effect.Effect<Schema.Json, SchemaIssue.Issue>;
36
+ /** Bridges a Standard validator to an Effect codec over JSON values: decode and encode both run the validator and keep its output, which must stay JSON. */
37
+ declare const bridge: (declaring: Declaring) => Bridged<JsonCodec>;
38
+ declare const bridgeState: (declaring: Declaring) => Bridged<StateCodec>;
39
+ declare const bridgeKey: (declaring: Declaring) => Bridged<KeyCodec>;
40
+ /** The fields of an error declared without any: an empty object. */
41
+ declare const noFields: Validator;
42
+ /** The default principal of a `user` caller. */
43
+ declare const principal: import("effect/StandardSchema").StandardJSONSchemaV1<{
44
+ readonly id: string;
45
+ }, {
46
+ readonly id: string;
47
+ }> & import("effect/StandardSchema").StandardSchemaV1<{
48
+ readonly id: string;
49
+ }, {
50
+ readonly id: string;
51
+ }> & Schema.Struct<{
52
+ readonly id: Schema.String;
53
+ }>;
54
+ /** The principal another actor calls with. */
55
+ declare const actorPrincipal: import("effect/StandardSchema").StandardJSONSchemaV1<{
56
+ readonly actor: string;
57
+ readonly id: string;
58
+ readonly key: string;
59
+ }, {
60
+ readonly actor: string;
61
+ readonly id: string;
62
+ readonly key: string;
63
+ }> & import("effect/StandardSchema").StandardSchemaV1<{
64
+ readonly actor: string;
65
+ readonly id: string;
66
+ readonly key: string;
67
+ }, {
68
+ readonly actor: string;
69
+ readonly id: string;
70
+ readonly key: string;
71
+ }> & Schema.Struct<{
72
+ readonly actor: Schema.String;
73
+ readonly id: Schema.String;
74
+ readonly key: Schema.String;
75
+ }>;
76
+ export { type Bridged, type Checked, type Declaring, type Documents, type JsonCodec, type KeyCodec, type Site, type StateCodec, actorPrincipal, bridge, bridgeKey, bridgeState, documentsOf, noFields, principal, refuse, validating };
@@ -0,0 +1,113 @@
1
+ import { exportDocument } from "./json-document.schema.mjs";
2
+ import { PlainDefinitionError } from "./plain-definition.errors.mjs";
3
+ import { Array, Effect, Option, Predicate, Result, Schema, SchemaIssue, SchemaTransformation, pipe } from "effect";
4
+ // src/actor/standard-schema.schema.ts
5
+ const refuse = ({ reasons, site }) => PlainDefinitionError.make({
6
+ ...site,
7
+ message: `${site.actor}.${site.member} ${site.part}: ${Array.join(reasons, "; ")}`,
8
+ reasons
9
+ });
10
+ const asyncRefusal = "the validator is asynchronous; Akter validates synchronously";
11
+ const isValidator = (declared) => Predicate.hasProperty(declared, "~standard") && Predicate.hasProperty(declared["~standard"], "validate") && Predicate.isFunction(declared["~standard"].validate);
12
+ const hasExporter = (declared) => Predicate.hasProperty(declared["~standard"], "jsonSchema") && Predicate.hasProperty(declared["~standard"].jsonSchema, "input") && Predicate.isFunction(declared["~standard"].jsonSchema.input) && Predicate.hasProperty(declared["~standard"].jsonSchema, "output") && Predicate.isFunction(declared["~standard"].jsonSchema.output);
13
+ const keyOfSegment = (segment) => {
14
+ if (Predicate.isString(segment) || Predicate.isNumber(segment) || Predicate.isSymbol(segment)) return String(segment);
15
+ return String(segment.key);
16
+ };
17
+ const describeIssue = (issue) => pipe(Option.fromNullishOr(issue.path), Option.filter(Array.isReadonlyArrayNonEmpty), Option.match({
18
+ onNone: () => issue.message,
19
+ onSome: (path) => `${Array.join(Array.map(path, keyOfSegment), ".")}: ${issue.message}`
20
+ }));
21
+ const decodeJson = Schema.decodeUnknownOption(Schema.Json);
22
+ /** Runs a validator synchronously and requires a JSON output, or names why it failed. */
23
+ const validatedJson = (declared, input) => {
24
+ const outcome = declared["~standard"].validate(input);
25
+ if (Predicate.isPromiseLike(outcome)) return Result.fail(asyncRefusal);
26
+ if (Predicate.hasProperty(outcome, "value")) return Result.fromOption(decodeJson(outcome.value), () => "the validator's output is not JSON");
27
+ return Result.fail(Array.join(Array.map(outcome.issues, describeIssue), "; "));
28
+ };
29
+ const exported = (converter, side) => pipe(Result.try({
30
+ catch: (failure) => `its ${side} JSON Schema export failed: ${String(failure)}`,
31
+ try: () => exportDocument({
32
+ converter,
33
+ side
34
+ })
35
+ }), Result.flatMap((checked) => Array.match(checked.refusals, {
36
+ onEmpty: () => Result.succeed(checked.document),
37
+ onNonEmpty: (refusals) => Result.fail(`its ${side} JSON Schema: ${Array.join(refusals, "; ")}`)
38
+ })));
39
+ const failureOf = (result) => Option.toArray(Result.getFailure(result));
40
+ const documentsOf = ({ site, validator: declared }) => {
41
+ if (!isValidator(declared)) throw refuse({
42
+ reasons: ["is not a Standard Schema validator"],
43
+ site
44
+ });
45
+ if (!hasExporter(declared)) throw refuse({
46
+ reasons: ["has no Standard JSON Schema export; use a validator that exports JSON Schema (ADR 0015)"],
47
+ site
48
+ });
49
+ const input = exported(declared["~standard"].jsonSchema, "input");
50
+ const output = exported(declared["~standard"].jsonSchema, "output");
51
+ if (Result.isSuccess(input) && Result.isSuccess(output)) return {
52
+ input: input.success,
53
+ output: output.success
54
+ };
55
+ throw refuse({
56
+ reasons: [...failureOf(input), ...failureOf(output)],
57
+ site
58
+ });
59
+ };
60
+ const describedJson = (document) => Schema.Json.check(Schema.makeFilter(() => true, { toJsonSchema: () => document }));
61
+ const describedString = (document) => Schema.String.check(Schema.makeFilter(() => true, { toJsonSchema: () => document }));
62
+ /** Runs a validator as a schema step: a refusal becomes the issue the engine reports. */
63
+ const validating = (declared) => (input) => Effect.fromResult(Result.mapError(validatedJson(declared, input), (message) => new SchemaIssue.InvalidValue({ message }, Option.some(input))));
64
+ /** Bridges a Standard validator to an Effect codec over JSON values: decode and encode both run the validator and keep its output, which must stay JSON. */
65
+ const bridge = (declaring) => {
66
+ const documents = documentsOf(declaring);
67
+ const run = validating(declaring.validator);
68
+ return {
69
+ codec: describedJson(documents.output).pipe(Schema.decodeTo(Schema.Json, SchemaTransformation.transformEffect({
70
+ decode: run,
71
+ encode: run
72
+ }))),
73
+ documents
74
+ };
75
+ };
76
+ const StateRecord = Schema.Record(Schema.String, Schema.Json);
77
+ const isStateRecord = Schema.is(StateRecord);
78
+ const stateOf = (value) => Effect.mapError(Effect.fromOption(Option.liftPredicate(value, isStateRecord)), () => new SchemaIssue.InvalidValue({ message: "the state must be an object" }, Option.some(value)));
79
+ const bridgeState = (declaring) => {
80
+ const documents = documentsOf(declaring);
81
+ const run = validating(declaring.validator);
82
+ return {
83
+ codec: describedJson(documents.output).pipe(Schema.decodeTo(StateRecord, SchemaTransformation.transformEffect({
84
+ decode: (input) => Effect.flatMap(run(input), stateOf),
85
+ encode: run
86
+ }))),
87
+ documents
88
+ };
89
+ };
90
+ const textOf = (value) => Effect.mapError(Effect.fromOption(Option.liftPredicate(value, Predicate.isString)), () => new SchemaIssue.InvalidValue({ message: "a key must be a string" }, Option.some(value)));
91
+ const bridgeKey = (declaring) => {
92
+ const documents = documentsOf(declaring);
93
+ const check = validating(declaring.validator);
94
+ const run = (input) => Effect.flatMap(check(input), textOf);
95
+ return {
96
+ codec: describedString(documents.output).pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transformEffect({
97
+ decode: run,
98
+ encode: run
99
+ }))),
100
+ documents
101
+ };
102
+ };
103
+ /** The fields of an error declared without any: an empty object. */
104
+ const noFields = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(Schema.Struct({})));
105
+ /** The default principal of a `user` caller. */
106
+ const principal = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(Schema.Struct({ id: Schema.String })));
107
+ /** The principal another actor calls with. */
108
+ const actorPrincipal = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(Schema.Struct({
109
+ actor: Schema.String,
110
+ id: Schema.String,
111
+ key: Schema.String
112
+ })));
113
+ export { actorPrincipal, bridge, bridgeKey, bridgeState, documentsOf, noFields, principal, refuse, validating };
@@ -0,0 +1,23 @@
1
+ import { PlainApp } from "./actor/plain-types.contract.mjs";
2
+ /** A running local engine: the same `akter dev` engine, in this process. */
3
+ interface LocalServer {
4
+ readonly close: () => Promise<void>;
5
+ /** Explicit local administration: stops the actor and erases its database, files and computers. Not an app RPC. */
6
+ readonly erase: (address: {
7
+ readonly actor: string;
8
+ readonly key: string;
9
+ }) => Promise<void>;
10
+ readonly url: string;
11
+ }
12
+ interface LocalServerOptions {
13
+ readonly app: PlainApp;
14
+ /** Overrides AKTER_LOCAL_COMPUTERS for this server. Local steps run on this machine, without isolation. */
15
+ readonly computers?: boolean;
16
+ /** The folder that holds every actor's database. A fresh temporary folder, removed on close, when omitted. */
17
+ readonly directory?: string;
18
+ /** The port to listen on, on 127.0.0.1. Any free port when omitted. */
19
+ readonly port?: number;
20
+ }
21
+ /** Serves an app on the local engine, for tests and scripts. Point `createClient` at its `url`. */
22
+ declare const serveLocal: (options: LocalServerOptions) => Promise<LocalServer>;
23
+ export { type LocalServer, type LocalServerOptions, serveLocal };
@@ -0,0 +1,43 @@
1
+ import { engineOf } from "./plain-engine.layer.mjs";
2
+ import { layer } from "@effect/platform-node/NodeFileSystem";
3
+ import { ActorHost, Akter } from "@akterjs/effect";
4
+ import { ConfigProvider, Context, Effect, Exit, FileSystem, Layer, Option, Scope } from "effect";
5
+ import { HttpServer } from "effect/http";
6
+ // src/local-server.adapter.ts
7
+ const anyPort = 0;
8
+ const localServerLayer = Effect.fnUntraced(function* localServerLayer({ app, computers, port = anyPort }, folder) {
9
+ const { actors, layer } = yield* Effect.sync(() => engineOf(app));
10
+ const engine = Akter.local(folder).pipe(Layer.provide(layer), Layer.provide(Akter.jobs));
11
+ const served = Akter.serve({
12
+ actors,
13
+ port
14
+ }).pipe(Layer.provideMerge(engine));
15
+ return Option.match(Option.fromNullishOr(computers), {
16
+ onNone: () => served,
17
+ onSome: (enabled) => {
18
+ const provider = ConfigProvider.fromUnknown({ AKTER_LOCAL_COMPUTERS: String(enabled) });
19
+ const setting = ConfigProvider.layerAdd(provider, { asPrimary: true });
20
+ return served.pipe(Layer.provide(setting));
21
+ }
22
+ });
23
+ });
24
+ /** Serves an app on the local engine, for tests and scripts. Point `createClient` at its `url`. */
25
+ const serveLocal = (options) => Effect.runPromise(Effect.gen(function* startLocal() {
26
+ const scope = yield* Scope.make();
27
+ const temporary = Scope.provide(scope)(FileSystem.FileSystem.use((fileSystem) => fileSystem.makeTempDirectoryScoped({ prefix: "akter-local-" })));
28
+ const folder = yield* Option.match(Option.fromUndefinedOr(options.directory), {
29
+ onNone: () => Effect.orDie(temporary),
30
+ onSome: Effect.succeed
31
+ });
32
+ const served = yield* localServerLayer(options, folder);
33
+ const services = yield* Effect.orDie(Layer.buildWithScope(served, scope));
34
+ const url = yield* HttpServer.addressFormattedWith(Effect.succeed).pipe(Effect.provide(services));
35
+ const context = yield* Effect.context();
36
+ const host = Context.get(services, ActorHost);
37
+ return {
38
+ close: () => Effect.runPromiseWith(context)(Scope.close(scope, Exit.void)),
39
+ erase: (address) => Effect.runPromiseWith(context)(host.erase?.(address) ?? Effect.die("the local engine has no actor erasure")),
40
+ url
41
+ };
42
+ }).pipe(Effect.provide(layer)));
43
+ export { serveLocal };
@@ -0,0 +1,60 @@
1
+ import { PlainFileAccess } from "./actor/resource/plain-file-access.contract.mjs";
2
+ import { ActorContract, ActorOptions, CreateOf, DeclaredInput, KeyOf, MemberNames, OptionsOf, PayloadArgs, PayloadIn, QueriesOf, StateOf, SuccessOut } from "./actor/plain-types.contract.mjs";
3
+ import { PromiseFiles, PromiseFiles as PromiseFiles$1 } from "@akterjs/client";
4
+ /** One delivery of a subscribed value: the value and the actor's version after the commit that produced it. */
5
+ interface LiveUpdate<Value> {
6
+ readonly value: Value;
7
+ readonly version: number;
8
+ }
9
+ type Stop = () => void;
10
+ type OnError = (error: Error) => void;
11
+ type CommandCall<Options> = (...payload: PayloadArgs<PayloadIn<Options>>) => Promise<SuccessOut<Options>>;
12
+ type OnUpdate<Options> = (update: LiveUpdate<SuccessOut<Options>>) => void;
13
+ /** A query declared without a payload subscribes as `subscribe(onUpdate, onError?)`; one with a payload takes it first. */
14
+ type QueryCall<Options> = CommandCall<Options> & {
15
+ readonly subscribe: [PayloadIn<Options>] extends [undefined] ? (onUpdate: OnUpdate<Options>, onError?: OnError) => Stop : (payload: PayloadIn<Options>, onUpdate: OnUpdate<Options>, onError?: OnError) => Stop;
16
+ };
17
+ type CreationCalls<Options extends ActorOptions> = [CreateOf<Options>] extends [never] ? unknown : {
18
+ readonly create: (input: DeclaredInput<CreateOf<Options>>) => Promise<void>;
19
+ readonly ensure: (input: DeclaredInput<CreateOf<Options>>) => Promise<void>;
20
+ };
21
+ /** The actor's files, when it declares file calls: `upload` returns the receipt a command passes to `ctx.files.attach`. */
22
+ type FilesOf<Options extends ActorOptions> = Options extends {
23
+ readonly files: PlainFileAccess;
24
+ } ? {
25
+ readonly files: PromiseFiles$1;
26
+ } : unknown;
27
+ /** One actor, addressed by key, called over the network. Calls reject with the declared error classes or an Akter failure carrying `_tag`. */
28
+ type ActorHandle<Options extends ActorOptions> = { readonly [Member in MemberNames<Options["commands"]>]: CommandCall<OptionsOf<Options["commands"][Member]>>; } & { readonly [Member in MemberNames<QueriesOf<Options>>]: QueryCall<OptionsOf<QueriesOf<Options>[Member]>>; } & FilesOf<Options> & {
29
+ /** Copies this actor into a new one at `into` and resolves with the copy's handle. The copy starts quiet (ADR 0017). */
30
+ readonly fork: (into: KeyOf<Options>) => Promise<ActorHandle<Options>>;
31
+ readonly state: {
32
+ readonly subscribe: (onUpdate: (update: LiveUpdate<StateOf<Options>>) => void, onError?: OnError) => Stop;
33
+ };
34
+ readonly warm: () => Promise<void>;
35
+ } & CreationCalls<Options>;
36
+ /** Any actor `Akter.make` defined, as the client takes it. */
37
+ interface AnyPlainActor {
38
+ readonly "~options"?: ActorOptions;
39
+ readonly contract: ActorContract;
40
+ readonly name: string;
41
+ }
42
+ type OptionsOfActor<Actor> = Actor extends {
43
+ readonly "~options"?: infer Options extends ActorOptions;
44
+ } ? Options : never;
45
+ type HandleOf<Actor> = ActorHandle<OptionsOfActor<Actor>>;
46
+ type KeyOfActor<Actor> = KeyOf<OptionsOfActor<Actor>>;
47
+ /** A client for a set of actors: `client.Ledger("acct-1").deposit({ amount: 5 })`. */
48
+ type PlainClient<Actors extends readonly AnyPlainActor[]> = { readonly [Actor in Actors[number] as Actor["name"]]: (key: KeyOfActor<Actor>) => HandleOf<Actor>; } & {
49
+ readonly close: () => Promise<void>;
50
+ };
51
+ interface PlainClientOptions<Actors extends readonly AnyPlainActor[]> {
52
+ readonly actors: Actors;
53
+ readonly fetch?: typeof globalThis.fetch;
54
+ readonly headers?: () => Readonly<Record<string, string>>;
55
+ readonly url: string;
56
+ readonly webSocket?: typeof globalThis.WebSocket;
57
+ }
58
+ /** Connects to `akter dev` or Akter Cloud. Calls return Promises; queries and state can be subscribed to. */
59
+ declare function createClient<const Actors extends readonly AnyPlainActor[]>(options: PlainClientOptions<Actors>): PlainClient<Actors>;
60
+ export { type ActorHandle, type AnyPlainActor, type LiveUpdate, type OnError, type PlainClient, type PlainClientOptions, type PromiseFiles, type Stop, createClient };
@@ -0,0 +1,12 @@
1
+ import { PlainCompiler } from "./actor/plain-compile.contract.mjs";
2
+ import { Array, Option } from "effect";
3
+ import { createClient as createClient$1 } from "@akterjs/client";
4
+ // src/plain-client.adapter.ts
5
+ const effectActorOf = (actor) => Option.getOrThrowWith(PlainCompiler.compilationOf(actor), () => /* @__PURE__ */ new TypeError(`${actor.name} was not defined with Akter.make from @akterjs/sdk`)).compiled.actor;
6
+ function createClient({ actors, ...connection }) {
7
+ return createClient$1({
8
+ ...connection,
9
+ actors: Array.map(actors, effectActorOf)
10
+ });
11
+ }
12
+ export { createClient };
@@ -0,0 +1,9 @@
1
+ import { PlainImplement, Served } from "./actor/plain-implement.contract.mjs";
2
+ import { PlainApp } from "./actor/plain-types.contract.mjs";
3
+ /**
4
+ * The Effect actors and layer a plain app serves, for engines and the conformance suite rather than apps:
5
+ * `akter dev`, `akter deploy` and Akter Cloud read the same two fields from the app module.
6
+ * The layer needs the `WorkflowEngine`; provide `Akter.jobs` from `@akterjs/effect` beside the engine.
7
+ */
8
+ declare const engineOf: (app: PlainApp) => ReturnType<typeof PlainImplement.app>;
9
+ export { type Served, engineOf };
@@ -0,0 +1,10 @@
1
+ import { PlainImplement } from "./actor/plain-implement.contract.mjs";
2
+ import { Option } from "effect";
3
+ // src/plain-engine.layer.ts
4
+ /**
5
+ * The Effect actors and layer a plain app serves, for engines and the conformance suite rather than apps:
6
+ * `akter dev`, `akter deploy` and Akter Cloud read the same two fields from the app module.
7
+ * The layer needs the `WorkflowEngine`; provide `Akter.jobs` from `@akterjs/effect` beside the engine.
8
+ */
9
+ const engineOf = (app) => Option.getOrThrowWith(PlainImplement.servedOf(app), () => /* @__PURE__ */ new TypeError("engineOf takes an app made by Akter.app"));
10
+ export { engineOf };
@@ -0,0 +1,7 @@
1
+ import { Akter } from "./actor/plain-actor.contract.mjs";
2
+ import { ActorContract, ActorOptions, CommandContext, CommandRef, CreateContext, Documents, FinishedJob, Implementation, InputOf, JobOptions, JobRef, JobResult, JobStep, JobSteps, Json, MemberContract, MemberOptions, Migrations, OutputOf, PlainActor, PlainAkter, PlainApp, PlainCommand, PlainDefinitionFailure, PlainError, PlainErrorClass, PlainImplementation, PlainJob, PlainQuery, QueryContext, StateFields, StateUpgrade, Validator } from "./actor/plain-types.contract.mjs";
3
+ import { FileRequest, PlainFileAccess } from "./actor/resource/plain-file-access.contract.mjs";
4
+ import { Delay, Sql, SqlRow, SqlValue } from "./actor/plain-values.contract.mjs";
5
+ import { FileListing, FilePage, FileRef, FileUsage, Files, ReadingFiles } from "./actor/resource/plain-files.contract.mjs";
6
+ import { Computer, ComputerFork, ComputerHold, ComputerSize, ComputerSpec, ComputerState, Computers, ExecRequest, ExecResult } from "./actor/resource/plain-computers.contract.mjs";
7
+ export { type ActorContract, type ActorOptions, Akter, type CommandContext, type CommandRef, type Computer, type ComputerFork, type ComputerHold, type ComputerSize, type ComputerSpec, type ComputerState, type Computers, type CreateContext, type Delay, type Documents, type ExecRequest, type ExecResult, type FileListing, type FilePage, type FileRef, type FileRequest, type FileUsage, type Files, type FinishedJob, type Implementation, type InputOf, type JobOptions, type JobRef, type JobResult, type JobStep, type JobSteps, type Json, type MemberContract, type MemberOptions, type Migrations, type OutputOf, type PlainActor, type PlainAkter, type PlainApp, type PlainCommand, type PlainDefinitionFailure, type PlainError, type PlainErrorClass, type PlainFileAccess, type PlainImplementation, type PlainJob, type PlainQuery, type QueryContext, type ReadingFiles, type Sql, type SqlRow, type SqlValue, type StateFields, type StateUpgrade, type Validator };