@akterjs/client 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.
@@ -0,0 +1,28 @@
1
+ import { ActorKind, ActorRequest } from "./actor-operations.pure.mjs";
2
+ import { Cause, Duration, Effect, Schema, Scope, Stream } from "effect";
3
+ import { RpcClient } from "effect/rpc";
4
+ interface WireFailure {
5
+ readonly _tag: string;
6
+ }
7
+ type WireRequest = Readonly<Record<string, Schema.Json>>;
8
+ type WireSend = (tag: string, request: WireRequest) => Effect.Effect<Schema.Json | undefined, WireFailure>;
9
+ type WireWatch = (tag: string, request: WireRequest) => Stream.Stream<Schema.Json, WireFailure>;
10
+ type WireKind = ActorKind | "files/upload/append" | "files/upload/close" | "files/upload/open";
11
+ interface Addressed {
12
+ readonly kind: WireKind;
13
+ readonly tag: string;
14
+ }
15
+ declare function unaryClient(addressed: Addressed): Effect.Effect<WireSend, never, RpcClient.Protocol | Scope.Scope>;
16
+ declare function streamClient(addressed: Addressed): Effect.Effect<WireWatch, never, RpcClient.Protocol | Scope.Scope>;
17
+ declare const uploadChunkBytes = 1048576;
18
+ type Transferring = RpcClient.Protocol | Scope.Scope;
19
+ type TransferFailure = Cause.TimeoutError | WireFailure;
20
+ interface Transfer {
21
+ readonly request: ActorRequest;
22
+ readonly wait: Duration.Input;
23
+ }
24
+ declare const uploadSession: (upload: Transfer & {
25
+ readonly content: Uint8Array;
26
+ }) => Effect.Effect<Schema.Json | undefined, TransferFailure, Transferring>;
27
+ declare const downloadChunks: ({ request, wait }: Transfer) => Effect.Effect<readonly Schema.Json[], TransferFailure, Transferring>;
28
+ export { type Transfer, type TransferFailure, type WireFailure, type WireKind, type WireRequest, downloadChunks, streamClient, unaryClient, uploadChunkBytes, uploadSession };
@@ -0,0 +1,154 @@
1
+ import { Array, Effect, Schema, Stream } from "effect";
2
+ import { Rpc, RpcClient, RpcGroup } from "effect/rpc";
3
+ // src/actor-rpc.pure.ts
4
+ const commandFields = {
5
+ commandId: Schema.String,
6
+ key: Schema.Json,
7
+ payload: Schema.Json
8
+ };
9
+ const queryFields = {
10
+ key: Schema.Json,
11
+ payload: Schema.Json
12
+ };
13
+ const keyFields = { key: Schema.Json };
14
+ const computerFields = {
15
+ computer: Schema.Json,
16
+ key: Schema.Json
17
+ };
18
+ const listFields = {
19
+ cursor: Schema.optionalKey(Schema.Json),
20
+ key: Schema.Json,
21
+ limit: Schema.optionalKey(Schema.Json),
22
+ prefix: Schema.optionalKey(Schema.Json)
23
+ };
24
+ const fieldsOf = {
25
+ call: commandFields,
26
+ "computers/cp": {
27
+ ...computerFields,
28
+ computerPath: Schema.Json,
29
+ direction: Schema.Json,
30
+ path: Schema.Json
31
+ },
32
+ "computers/delete": computerFields,
33
+ "computers/get": computerFields,
34
+ "computers/list": keyFields,
35
+ "computers/logs": {
36
+ ...computerFields,
37
+ after: Schema.optionalKey(Schema.Json),
38
+ limit: Schema.optionalKey(Schema.Json)
39
+ },
40
+ "computers/pause": computerFields,
41
+ "computers/resume": computerFields,
42
+ "computers/stop": computerFields,
43
+ create: commandFields,
44
+ ensure: commandFields,
45
+ "files/download": {
46
+ key: Schema.Json,
47
+ length: Schema.optionalKey(Schema.Json),
48
+ offset: Schema.optionalKey(Schema.Json),
49
+ path: Schema.Json
50
+ },
51
+ "files/list": listFields,
52
+ "files/upload": keyFields,
53
+ "files/upload/append": {
54
+ bytes: Schema.Json,
55
+ key: Schema.Json,
56
+ offset: Schema.Json,
57
+ upload: Schema.Json
58
+ },
59
+ "files/upload/close": {
60
+ bytes: Schema.Json,
61
+ key: Schema.Json,
62
+ upload: Schema.Json
63
+ },
64
+ "files/upload/open": keyFields,
65
+ "files/usage": listFields,
66
+ fork: {
67
+ commandId: Schema.String,
68
+ into: Schema.Json,
69
+ key: Schema.Json
70
+ },
71
+ query: queryFields,
72
+ state: keyFields,
73
+ "steps/get": {
74
+ key: Schema.Json,
75
+ step: Schema.Json
76
+ },
77
+ "steps/list": {
78
+ key: Schema.Json,
79
+ limit: Schema.optionalKey(Schema.Json)
80
+ },
81
+ warm: keyFields,
82
+ watch: queryFields
83
+ };
84
+ function unaryClient({ kind, tag }) {
85
+ const rpc = Rpc.make(tag, {
86
+ error: Schema.Json,
87
+ payload: Schema.Struct(fieldsOf[kind]),
88
+ success: Schema.UndefinedOr(Schema.Json)
89
+ });
90
+ return RpcClient.make(RpcGroup.make(rpc), { flatten: true });
91
+ }
92
+ function streamClient({ kind, tag }) {
93
+ const rpc = Rpc.make(tag, {
94
+ error: Schema.Json,
95
+ payload: Schema.Struct(fieldsOf[kind]),
96
+ stream: true,
97
+ success: Schema.Json
98
+ });
99
+ return RpcClient.make(RpcGroup.make(rpc), { flatten: true });
100
+ }
101
+ const uploadChunkBytes = 1048576;
102
+ const Base64 = Schema.Uint8ArrayFromBase64;
103
+ const Opened = Schema.Struct({ upload: Schema.String });
104
+ const appended = Effect.fnUntraced(function* appended({ content, request, upload, wait }) {
105
+ const tag = `${request.tag}/append`;
106
+ const append = yield* unaryClient({
107
+ kind: "files/upload/append",
108
+ tag
109
+ });
110
+ const count = Math.ceil(content.length / uploadChunkBytes);
111
+ const chunks = Array.makeBy(count, (index) => ({
112
+ bytes: content.subarray(index * uploadChunkBytes, index * uploadChunkBytes + uploadChunkBytes),
113
+ offset: index * uploadChunkBytes
114
+ }));
115
+ yield* Effect.forEach(chunks, ({ bytes, offset }) => Effect.flatMap(Effect.orDie(Schema.encodeEffect(Base64)(bytes)), (encoded) => Effect.timeout(append(tag, {
116
+ ...request.payload,
117
+ bytes: encoded,
118
+ offset,
119
+ upload
120
+ }), wait)));
121
+ });
122
+ const uploadSession = (upload) => Effect.gen(function* runUploadSession() {
123
+ const { content, request, wait } = upload;
124
+ const open = yield* unaryClient({
125
+ kind: "files/upload/open",
126
+ tag: `${request.tag}/open`
127
+ });
128
+ const close = yield* unaryClient({
129
+ kind: "files/upload/close",
130
+ tag: `${request.tag}/close`
131
+ });
132
+ const opened = yield* Effect.timeout(open(`${request.tag}/open`, request.payload), wait);
133
+ const session = yield* Effect.orDie(Schema.decodeUnknownEffect(Opened)(opened));
134
+ yield* appended({
135
+ content,
136
+ request,
137
+ upload: session.upload,
138
+ wait
139
+ });
140
+ const closing = {
141
+ ...request.payload,
142
+ bytes: content.length,
143
+ upload: session.upload
144
+ };
145
+ return yield* Effect.timeout(close(`${request.tag}/close`, closing), wait);
146
+ });
147
+ const downloadChunks = ({ request, wait }) => Effect.gen(function* collectDownload() {
148
+ const download = yield* streamClient({
149
+ kind: "files/download",
150
+ tag: request.tag
151
+ });
152
+ return yield* Effect.timeout(Stream.runCollect(download(request.tag, request.payload)), wait);
153
+ });
154
+ export { downloadChunks, streamClient, unaryClient, uploadChunkBytes, uploadSession };
@@ -0,0 +1,13 @@
1
+ import { Layer } from "effect";
2
+ import { RpcClient } from "effect/rpc";
3
+ import { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, CallerNotAllowedError, CommandIdExpiredError, HandlerFailedError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError } from "@akterjs/effect/contract/errors";
4
+ import { ActorClients } from "@akterjs/effect/contract/actor-client";
5
+ import { ActorFileClients } from "@akterjs/effect/contract/file-clients";
6
+ interface AkterClientOptions {
7
+ readonly fetch?: typeof globalThis.fetch;
8
+ readonly headers?: () => Readonly<Record<string, string>>;
9
+ readonly url: string;
10
+ readonly webSocket?: typeof globalThis.WebSocket;
11
+ }
12
+ export declare const akterClient: ({ fetch, headers, url, webSocket }: AkterClientOptions) => Layer.Layer<ActorClients | ActorFileClients | RpcClient.Protocol>;
13
+ export { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, type AkterClientOptions, BillingUnavailableError, CallerNotAllowedError, CommandIdExpiredError, HandlerFailedError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError };
@@ -0,0 +1,27 @@
1
+ import { actorProtocol } from "./actor-protocol.layer.mjs";
2
+ import { Effect, Layer } from "effect";
3
+ import { SecureRandom } from "effect/crypto";
4
+ import { RpcClient, RpcSerialization } from "effect/rpc";
5
+ import { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, CallerNotAllowedError, CommandIdExpiredError, HandlerFailedError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError } from "@akterjs/effect/contract/errors";
6
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
7
+ import { ActorClients } from "@akterjs/effect/contract/actor-client";
8
+ import { rpcFileClients } from "@akterjs/effect/contract/file-clients";
9
+ // src/akter-client.layer.ts
10
+ const noHeaders = () => ({});
11
+ const rpcPath = "/rpc";
12
+ const akterClient = ({ fetch = globalThis.fetch, headers = noHeaders, url, webSocket = globalThis.WebSocket }) => {
13
+ const rpcUrl = new globalThis.URL(rpcPath, url).href;
14
+ const httpClient = Layer.effect(HttpClient.HttpClient, Effect.gen(function* withHeaders() {
15
+ const client = yield* HttpClient.HttpClient;
16
+ return HttpClient.mapRequest(client, (request) => HttpClientRequest.setHeaders(HttpClientRequest.prependUrl(request, rpcUrl), headers()));
17
+ })).pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetch)));
18
+ const protocol = Layer.effect(RpcClient.Protocol, Effect.flatMap(HttpClient.HttpClient, (http) => actorProtocol({
19
+ headers,
20
+ http,
21
+ url: rpcUrl,
22
+ webSocket
23
+ }))).pipe(Layer.provide([httpClient, RpcSerialization.layerNdjson]));
24
+ const clients = Layer.merge(ActorClients.layer.pipe(Layer.provide(SecureRandom.layerWebCrypto)), rpcFileClients);
25
+ return Layer.provideMerge(clients, protocol);
26
+ };
27
+ export { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, CallerNotAllowedError, CommandIdExpiredError, HandlerFailedError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError, akterClient };
@@ -0,0 +1,55 @@
1
+ import { AkterClientOptions } from "./akter-client.layer.mjs";
2
+ import { FileListing, FilePage, FileRange, FileRef, FileUsage, PromiseFiles } from "./promise-files.pure.mjs";
3
+ import { UploadSourceError } from "./upload-source.errors.mjs";
4
+ import { Effect, Option, Schema, Scope, Stream } from "effect";
5
+ import { RpcClient } from "effect/rpc";
6
+ import { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, BlobTransferError, CallerNotAllowedError, CommandIdExpiredError, FileAccessDeniedError, FileNotFoundError, FileTooLargeError, FilesUnavailableError, HandlerFailedError, InvalidFileRequestError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError } from "@akterjs/effect/contract/errors";
7
+ import { ActorClients } from "@akterjs/effect/contract/actor-client";
8
+ import { FileClient } from "@akterjs/effect/contract";
9
+ type ClientServices = ActorClients | RpcClient.Protocol | Scope.Scope;
10
+ interface ClientActor {
11
+ readonly client: (key: never) => Effect.Effect<object, never, ClientServices>;
12
+ readonly fileAccess: Option.Option<unknown>;
13
+ readonly files: (key: never) => Effect.Effect<FileClient, Error>;
14
+ readonly key: Schema.Top & {
15
+ readonly DecodingServices: never;
16
+ readonly Encoded: string;
17
+ };
18
+ readonly name: string;
19
+ readonly queries: Readonly<Record<string, {
20
+ readonly payload: Schema.Top;
21
+ }>>;
22
+ }
23
+ type EffectClientOf<Actor extends ClientActor> = Effect.Success<ReturnType<Actor["client"]>>;
24
+ type Stop = () => void;
25
+ type OnError = (error: Error) => void;
26
+ type PromiseCall<Call> = Call extends ((payload: infer Payload) => Effect.Effect<infer Success, infer _Failure>) ? (payload: Payload) => Promise<Success> : never;
27
+ /** A query declared without a payload subscribes as `subscribe(onUpdate, onError?)`; one with a payload takes it first. */
28
+ type PromiseSubscribe<Call> = Call extends {
29
+ readonly subscribe: (payload: infer Payload) => Stream.Stream<infer Update, infer _Failure>;
30
+ } ? undefined extends Payload ? {
31
+ readonly subscribe: (onUpdate: (update: Update) => void, onError?: OnError) => Stop;
32
+ } : {
33
+ readonly subscribe: (payload: Payload, onUpdate: (update: Update) => void, onError?: OnError) => Stop;
34
+ } : unknown;
35
+ type PromiseState<Updates> = Updates extends Stream.Stream<infer Update, infer _Failure> ? {
36
+ readonly subscribe: (onUpdate: (update: Update) => void, onError?: OnError) => Stop;
37
+ } : never;
38
+ /** An actor's files, when its contract declares file calls (`files: Akter.fileAccess(...)`); otherwise none. */
39
+ type PromiseFilesOf<Actor extends ClientActor> = Actor["fileAccess"] extends Option.None<unknown> ? unknown : {
40
+ readonly files: PromiseFiles;
41
+ };
42
+ type PromiseActor<Actor extends ClientActor> = { readonly [Member in Exclude<keyof EffectClientOf<Actor>, "files" | "fork" | "state" | "warm">]: PromiseCall<EffectClientOf<Actor>[Member]> & PromiseSubscribe<EffectClientOf<Actor>[Member]>; } & PromiseFilesOf<Actor> & {
43
+ /** Forks this actor into a new one at `into` and answers the copy's client (ADR 0017). */
44
+ readonly fork: (into: Actor["key"]["Encoded"]) => Promise<PromiseActor<Actor>>;
45
+ readonly state: PromiseState<EffectClientOf<Actor>["state" & keyof EffectClientOf<Actor>]>;
46
+ readonly warm: () => Promise<void>;
47
+ };
48
+ type PromiseClient<Actors extends readonly ClientActor[]> = { readonly [Actor in Actors[number] as Actor["name"]]: (key: Actor["key"]["Encoded"]) => PromiseActor<Actor>; } & {
49
+ readonly close: () => Promise<void>;
50
+ };
51
+ interface PromiseClientOptions<Actors extends readonly ClientActor[]> extends AkterClientOptions {
52
+ readonly actors: Actors;
53
+ }
54
+ declare function createClient<const Actors extends readonly ClientActor[]>(options: PromiseClientOptions<Actors>): PromiseClient<Actors>;
55
+ export { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, BlobTransferError, CallerNotAllowedError, CommandIdExpiredError, FileAccessDeniedError, type FileListing, FileNotFoundError, type FilePage, type FileRange, type FileRef, FileTooLargeError, type FileUsage, FilesUnavailableError, HandlerFailedError, InvalidFileRequestError, InvalidRequestError, type PromiseActor, type PromiseClient, type PromiseClientOptions, type PromiseFiles, SpendCapReachedError, StorageLimitError, UnauthenticatedError, UploadSourceError, createClient };
@@ -0,0 +1,114 @@
1
+ import { akterClient } from "./akter-client.layer.mjs";
2
+ import { promiseFiles } from "./promise-files.pure.mjs";
3
+ import { UploadSourceError } from "./upload-source.errors.mjs";
4
+ import { Array, Effect, Fiber, ManagedRuntime, Option, Record, Schema, Stream } from "effect";
5
+ import { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, BlobTransferError, CallerNotAllowedError, CommandIdExpiredError, FileAccessDeniedError, FileNotFoundError, FileTooLargeError, FilesUnavailableError, HandlerFailedError, InvalidFileRequestError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError } from "@akterjs/effect/contract/errors";
6
+ // src/promise-client.adapter.ts
7
+ const filesMember = "files";
8
+ const stateMember = "state";
9
+ const warmMember = "warm";
10
+ const forkMember = "fork";
11
+ const thenMember = "then";
12
+ function wireActor(actor) {
13
+ return actor;
14
+ }
15
+ function asCall(member) {
16
+ return member;
17
+ }
18
+ function asWarm(member) {
19
+ return member;
20
+ }
21
+ function asFork(member) {
22
+ return member;
23
+ }
24
+ function asUpdates(member) {
25
+ return member;
26
+ }
27
+ const listen = (runtime, { onError, onUpdate, updates }) => {
28
+ const delivered = Stream.runForEach(updates, (update) => Effect.sync(() => {
29
+ onUpdate(update);
30
+ }));
31
+ const reported = Effect.matchEffect(delivered, {
32
+ onFailure: (error) => Effect.sync(() => {
33
+ onError?.(error);
34
+ }),
35
+ onSuccess: () => Effect.void
36
+ });
37
+ const fiber = runtime.runFork(reported);
38
+ return () => {
39
+ runtime.runFork(Fiber.interrupt(fiber));
40
+ };
41
+ };
42
+ const listenTo = (runtime, updates) => (onUpdate, onError) => listen(runtime, {
43
+ onError,
44
+ onUpdate,
45
+ updates: Stream.unwrap(updates)
46
+ });
47
+ const memberOf = (actor, key, member) => Effect.gen(function* findMember() {
48
+ const decoded = yield* Schema.decodeEffect(actor.key)(key);
49
+ const client = yield* actor.client(decoded);
50
+ return yield* Option.match(Record.get(client, member), {
51
+ onNone: () => Effect.die(`${actor.name} has no member ${member}`),
52
+ onSome: Effect.succeed
53
+ });
54
+ });
55
+ const callOf = ({ actor, key, runtime }) => (member) => {
56
+ const found = memberOf(actor, key, member);
57
+ const call = (payload) => runtime.runPromise(Effect.scoped(Effect.flatMap(found, (send) => asCall(send)(payload))));
58
+ if (Option.exists(Record.get(actor.queries, member), (query) => query.payload === Schema.Void)) return Object.assign(call, { subscribe: listenTo(runtime, Effect.map(found, (query) => asCall(query).subscribe())) });
59
+ const withPayload = (payload, onUpdate, onError) => listenTo(runtime, Effect.map(found, (query) => asCall(query).subscribe(payload)))(onUpdate, onError);
60
+ return Object.assign(call, { subscribe: withPayload });
61
+ };
62
+ const forkOf = ({ actor, key, open, runtime }) => (into) => {
63
+ const forked = Effect.gen(function* forkActor() {
64
+ yield* (yield* Effect.map(memberOf(actor, key, forkMember), asFork))(yield* Schema.decodeEffect(actor.key)(into));
65
+ });
66
+ return runtime.runPromise(Effect.as(Effect.scoped(forked), open(into)));
67
+ };
68
+ const stateOf = ({ actor, key, runtime }) => ({ subscribe: (onUpdate, onError) => {
69
+ const updates = Stream.unwrap(Effect.map(memberOf(actor, key, stateMember), asUpdates));
70
+ return listen(runtime, {
71
+ onError,
72
+ onUpdate,
73
+ updates
74
+ });
75
+ } });
76
+ const warmOf = ({ actor, key, runtime }) => {
77
+ const warmed = Effect.flatMap(memberOf(actor, key, warmMember), (warm) => Effect.asVoid(asWarm(warm)));
78
+ return () => runtime.runPromise(Effect.scoped(warmed));
79
+ };
80
+ const filesOf = ({ actor, key, runtime }) => {
81
+ const connected = Effect.flatMap(Schema.decodeEffect(actor.key)(key), actor.files);
82
+ return promiseFiles({
83
+ connected,
84
+ run: (effect) => runtime.runPromise(effect)
85
+ });
86
+ };
87
+ const actionMember = (handle, member) => {
88
+ if (member === forkMember) return forkOf(handle);
89
+ if (member === warmMember) return warmOf(handle);
90
+ return callOf(handle)(member);
91
+ };
92
+ const handleMember = (handle, member) => {
93
+ if (member === thenMember) return Option.getOrUndefined(Option.none());
94
+ if (member === stateMember) return stateOf(handle);
95
+ if (member === filesMember) return Option.getOrUndefined(Option.map(handle.actor.fileAccess, () => filesOf(handle)));
96
+ return actionMember(handle, member);
97
+ };
98
+ const handleOf = (runtime, actor) => (key) => {
99
+ const handle = {
100
+ actor,
101
+ key,
102
+ open: handleOf(runtime, actor),
103
+ runtime
104
+ };
105
+ return new Proxy({}, { get: (_target, property) => handleMember(handle, String(property)) });
106
+ };
107
+ function createClient({ actors, ...connection }) {
108
+ const runtime = ManagedRuntime.make(akterClient(connection));
109
+ return {
110
+ ...Record.fromEntries(Array.map(actors, (actor) => [actor.name, handleOf(runtime, wireActor(actor))])),
111
+ close: () => runtime.dispose()
112
+ };
113
+ }
114
+ export { ActorAlreadyExistsError, ActorErasedError, ActorNotCreatedError, ActorUnavailableError, BillingUnavailableError, BlobTransferError, CallerNotAllowedError, CommandIdExpiredError, FileAccessDeniedError, FileNotFoundError, FileTooLargeError, FilesUnavailableError, HandlerFailedError, InvalidFileRequestError, InvalidRequestError, SpendCapReachedError, StorageLimitError, UnauthenticatedError, UploadSourceError, createClient };
@@ -0,0 +1,48 @@
1
+ import { Effect } from "effect";
2
+ import { FileClient } from "@akterjs/effect/contract";
3
+ /** One file an actor holds. */
4
+ interface FileRef {
5
+ readonly bytes: number;
6
+ readonly path: string;
7
+ readonly sha256: string;
8
+ readonly writtenAt: number;
9
+ }
10
+ /** Which files to list: those under `prefix`, at most `limit`, after the `cursor` a previous page returned. */
11
+ interface FileListing {
12
+ readonly cursor?: string | undefined;
13
+ readonly limit?: number | undefined;
14
+ readonly prefix?: string | undefined;
15
+ }
16
+ /** A byte range of one file. */
17
+ interface FileRange {
18
+ readonly length: number;
19
+ readonly offset: number;
20
+ }
21
+ /** One page of files; `cursor` is present when there are more. */
22
+ interface FilePage {
23
+ readonly cursor?: string;
24
+ readonly entries: readonly FileRef[];
25
+ }
26
+ /** One page of files and the total bytes every file the listing matches holds. */
27
+ interface FileUsage extends FilePage {
28
+ readonly bytes: number;
29
+ }
30
+ /**
31
+ * An actor's files over its `<Actor>/files/*` RPCs (ADR 0016). `upload` returns the receipt a command attaches the file
32
+ * with; the others read what the actor holds. Each rejects with the hosted error classes the Effect client fails with.
33
+ */
34
+ interface PromiseFiles {
35
+ readonly download: (path: string, range?: FileRange) => Promise<ReadableStream<Uint8Array>>;
36
+ readonly list: (listing?: FileListing) => Promise<FilePage>;
37
+ readonly upload: (body: ReadableStream<Uint8Array> | Uint8Array) => Promise<string>;
38
+ readonly usage: (listing?: FileListing) => Promise<FileUsage>;
39
+ }
40
+ interface Filing {
41
+ /** Connects to one actor's files. */
42
+ readonly connected: Effect.Effect<FileClient, Error>;
43
+ /** Runs an effect on the client's runtime. */
44
+ readonly run: <Success>(effect: Effect.Effect<Success, Error>) => Promise<Success>;
45
+ }
46
+ /** The Promise face of one actor's file client. */
47
+ declare const promiseFiles: ({ connected, run }: Filing) => PromiseFiles;
48
+ export { type FileListing, type FilePage, type FileRange, type FileRef, type FileUsage, type PromiseFiles, promiseFiles };
@@ -0,0 +1,35 @@
1
+ import { UploadSourceError } from "./upload-source.errors.mjs";
2
+ import { Effect, Option, Predicate, Record, Stream } from "effect";
3
+ // src/promise-files.pure.ts
4
+ const pageOf = ({ cursor, entries }) => Option.match(cursor, {
5
+ onNone: () => ({ entries }),
6
+ onSome: (next) => ({
7
+ cursor: next,
8
+ entries
9
+ })
10
+ });
11
+ const listingOf = ({ cursor, limit, prefix } = {}) => ({
12
+ ...Record.getSomes({
13
+ cursor: Option.fromUndefinedOr(cursor),
14
+ prefix: Option.fromUndefinedOr(prefix)
15
+ }),
16
+ ...Record.getSomes({ limit: Option.fromUndefinedOr(limit) })
17
+ });
18
+ const sourceOf = (body) => {
19
+ if (Predicate.isUint8Array(body)) return Stream.make(body);
20
+ return Stream.fromReadableStream({
21
+ evaluate: () => body,
22
+ onError: (cause) => UploadSourceError.make({ message: `the upload's source stream failed: ${String(cause)}` })
23
+ });
24
+ };
25
+ /** The Promise face of one actor's file client. */
26
+ const promiseFiles = ({ connected, run }) => ({
27
+ download: (path, range) => run(Effect.map(connected, (files) => Stream.toReadableStream(files.download(path, range)))),
28
+ list: (listing) => run(Effect.map(Effect.flatMap(connected, (files) => files.list(listingOf(listing))), pageOf)),
29
+ upload: (body) => run(Effect.flatMap(connected, (files) => files.upload(sourceOf(body)))),
30
+ usage: (listing) => run(Effect.map(Effect.flatMap(connected, (files) => files.fileUsage(listingOf(listing))), (usage) => ({
31
+ ...pageOf(usage),
32
+ bytes: Number(usage.bytes)
33
+ })))
34
+ });
35
+ export { promiseFiles };
@@ -0,0 +1,7 @@
1
+ import { RpcMessage } from "effect/rpc";
2
+ interface ReadFloors {
3
+ readonly received: (clientId: number, response: RpcMessage.FromServerEncoded) => void;
4
+ readonly sent: (clientId: number, message: RpcMessage.FromClientEncoded) => RpcMessage.FromClientEncoded;
5
+ }
6
+ declare const trackedFloors: () => ReadFloors;
7
+ export { type ReadFloors, trackedFloors };
@@ -0,0 +1,49 @@
1
+ import { Array, Option, Predicate, Schema, String as String$1 } from "effect";
2
+ // src/read-floors.pure.ts
3
+ const floorHeader = "akter-floor";
4
+ const Keyed = Schema.Struct({ key: Schema.Unknown });
5
+ const Versioned = Schema.Struct({
6
+ requestId: Schema.Union([Schema.String, Schema.Finite]),
7
+ version: Schema.Int
8
+ });
9
+ const KeyText = Schema.fromJsonString(Schema.Unknown);
10
+ const keyedOf = Schema.decodeUnknownOption(Keyed);
11
+ const versionedOf = Schema.decodeUnknownOption(Versioned);
12
+ const keyTextOf = Schema.encodeUnknownOption(KeyText);
13
+ const actorKeyOf = ({ payload, tag }) => Option.flatMap(keyedOf(payload), ({ key }) => Option.map(keyTextOf(key), (keyText) => `${Option.getOrElse(Array.head(String$1.split(tag, "/")), () => tag)}\n${keyText}`));
14
+ const pendingOf = (clientId, requestId) => `${clientId}/${requestId}`;
15
+ const trackedFloors = () => {
16
+ const floors = /* @__PURE__ */ new Map();
17
+ const pending = /* @__PURE__ */ new Map();
18
+ const floored = (clientId, request) => Option.match(actorKeyOf(request), {
19
+ onNone: () => request,
20
+ onSome: (actorKey) => {
21
+ pending.set(pendingOf(clientId, request.id), actorKey);
22
+ return Option.match(Option.fromNullishOr(floors.get(actorKey)), {
23
+ onNone: () => request,
24
+ onSome: (floor) => ({
25
+ ...request,
26
+ headers: [...request.headers, [floorHeader, String(floor)]]
27
+ })
28
+ });
29
+ }
30
+ });
31
+ const sent = (clientId, message) => {
32
+ if (Predicate.isTagged(message, "Request")) return floored(clientId, message);
33
+ return message;
34
+ };
35
+ const received = (clientId, response) => {
36
+ if (!Predicate.isTagged(response, "Exit")) return;
37
+ const id = pendingOf(clientId, response.requestId);
38
+ const actorKey = pending.get(id);
39
+ pending.delete(id);
40
+ Option.map(Option.all([Option.fromNullishOr(actorKey), versionedOf(response)]), ([key, { version }]) => {
41
+ floors.set(key, Math.max(version, floors.get(key) ?? version));
42
+ });
43
+ };
44
+ return {
45
+ received,
46
+ sent
47
+ };
48
+ };
49
+ export { trackedFloors };
@@ -0,0 +1,13 @@
1
+ import { RpcMessage } from "effect/rpc";
2
+ /**
3
+ * What a server's answers become before they reach a caller: an `Exit` whose cause is only interrupts says the server
4
+ * stopped the request without an answer. Left alone, the caller's fiber ends interrupted with no message, and a CLI
5
+ * built on Effect's `runMain` exits 130 and prints nothing. This answers it as `ActorUnavailableError` for the request's
6
+ * actor instead, a typed error every call can fail with, which says to retry.
7
+ */
8
+ interface RemoteInterrupts {
9
+ readonly received: (clientId: number, response: RpcMessage.FromServerEncoded) => RpcMessage.FromServerEncoded;
10
+ readonly sent: (clientId: number, message: RpcMessage.FromClientEncoded) => void;
11
+ }
12
+ declare const remoteInterrupts: () => RemoteInterrupts;
13
+ export { type RemoteInterrupts, remoteInterrupts };
@@ -0,0 +1,45 @@
1
+ import { Array, Option, Predicate, Schema, String } from "effect";
2
+ import { ActorUnavailableError } from "@akterjs/effect/contract/errors";
3
+ // src/remote-interrupt.pure.ts
4
+ const InterruptReasons = Schema.NonEmptyArray(Schema.TaggedStruct("Interrupt", {}));
5
+ const InterruptedOnly = Schema.Struct({ exit: Schema.TaggedStruct("Failure", { cause: InterruptReasons }) });
6
+ const UnavailableWire = Schema.toEncoded(ActorUnavailableError);
7
+ const FailReason = Schema.TaggedStruct("Fail", { error: UnavailableWire });
8
+ /** An encoded exit that failed with one error, as Effect RPC writes it. */
9
+ const FailedExit = Schema.TaggedStruct("Failure", { cause: Schema.Tuple([FailReason]) });
10
+ const isInterruptedOnly = Schema.is(InterruptedOnly);
11
+ const encodeUnavailable = Schema.encodeOption(ActorUnavailableError);
12
+ const message = "the server stopped this request before it answered. Retry shortly.";
13
+ const requestOf = (clientId, requestId) => `${clientId}/${requestId}`;
14
+ /** `Room/files/list` and `Room/post` are both Room's. */
15
+ const actorOf = (tag) => Option.getOrElse(Array.head(String.split(tag, "/")), () => tag);
16
+ const failedWith = (error) => FailedExit.make({ cause: [FailReason.make({ error })] });
17
+ const answered = (actor, response) => Option.match(encodeUnavailable(ActorUnavailableError.make({
18
+ actor,
19
+ message
20
+ })), {
21
+ onNone: () => response,
22
+ onSome: (error) => ({
23
+ ...response,
24
+ exit: failedWith(error)
25
+ })
26
+ });
27
+ const remoteInterrupts = () => {
28
+ const actors = /* @__PURE__ */ new Map();
29
+ const sent = (clientId, outgoing) => {
30
+ if (Predicate.isTagged(outgoing, "Request")) actors.set(requestOf(clientId, outgoing.id), actorOf(outgoing.tag));
31
+ };
32
+ const received = (clientId, response) => {
33
+ if (!Predicate.isTagged(response, "Exit")) return response;
34
+ const id = requestOf(clientId, response.requestId);
35
+ const actor = Option.fromNullishOr(actors.get(id));
36
+ actors.delete(id);
37
+ if (Option.isNone(actor) || !isInterruptedOnly(response)) return response;
38
+ return answered(actor.value, response);
39
+ };
40
+ return {
41
+ received,
42
+ sent
43
+ };
44
+ };
45
+ export { remoteInterrupts };
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+ declare const UploadSourceError_base: Schema.Class<UploadSourceError, Schema.TaggedStruct<"UploadSourceError", {
3
+ readonly message: Schema.String;
4
+ }>, import("effect/Cause").YieldableError & {
5
+ readonly brand: unique symbol;
6
+ }>;
7
+ /** The upload's source stream failed before the upload finished. */
8
+ declare class UploadSourceError extends UploadSourceError_base {}
9
+ export { UploadSourceError };
@@ -0,0 +1,5 @@
1
+ import { Schema } from "effect";
2
+ // src/upload-source.errors.ts
3
+ /** The upload's source stream failed before the upload finished. */
4
+ var UploadSourceError = class extends Schema.TaggedError()("UploadSourceError", { message: Schema.String }) {};
5
+ export { UploadSourceError };