@akterjs/client 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,512 @@
1
+ import { inputOf } from "./actor-fields.pure.mjs";
2
+ import { Array, Effect, Option, Record, SchemaAST, String as String$1 } from "effect";
3
+ import { SecureRandom } from "effect/crypto";
4
+ import { RpcSchema } from "effect/rpc";
5
+ // src/actor-operations.pure.ts
6
+ const group = "actors";
7
+ const commandOfKind = (kind) => {
8
+ const [owner, name] = String$1.split("/")(kind);
9
+ return Option.match(Option.fromUndefinedOr(name), {
10
+ onNone: () => [group, kind],
11
+ onSome: (found) => [owner, found]
12
+ });
13
+ };
14
+ const subscribe = "/subscribe";
15
+ const declared = [
16
+ {
17
+ description: "Run one of an actor's commands and print its reply.",
18
+ fields: [
19
+ "actor",
20
+ "key",
21
+ "member",
22
+ "payload",
23
+ "commandId"
24
+ ],
25
+ idempotent: false,
26
+ kind: "call",
27
+ readOnly: false
28
+ },
29
+ {
30
+ description: "Run one of an actor's queries and print its reply.",
31
+ fields: [
32
+ "actor",
33
+ "key",
34
+ "member",
35
+ "payload"
36
+ ],
37
+ idempotent: true,
38
+ kind: "query",
39
+ readOnly: true
40
+ },
41
+ {
42
+ description: "Print an actor's state once.",
43
+ fields: ["actor", "key"],
44
+ idempotent: true,
45
+ kind: "state",
46
+ readOnly: true
47
+ },
48
+ {
49
+ description: "Print a query's reply, then each new reply as the actor changes, one JSON line each.",
50
+ fields: [
51
+ "actor",
52
+ "key",
53
+ "member",
54
+ "payload"
55
+ ],
56
+ idempotent: true,
57
+ kind: "watch",
58
+ readOnly: true
59
+ },
60
+ {
61
+ description: "Create an actor from its create input; fails when it already exists.",
62
+ fields: [
63
+ "actor",
64
+ "key",
65
+ "payload",
66
+ "commandId"
67
+ ],
68
+ idempotent: false,
69
+ kind: "create",
70
+ readOnly: false
71
+ },
72
+ {
73
+ description: "Create an actor from its create input unless it already exists.",
74
+ fields: [
75
+ "actor",
76
+ "key",
77
+ "payload",
78
+ "commandId"
79
+ ],
80
+ idempotent: true,
81
+ kind: "ensure",
82
+ readOnly: false
83
+ },
84
+ {
85
+ description: "List an actor's files a page at a time, with each path, size, digest and write time.",
86
+ fields: [
87
+ "actor",
88
+ "key",
89
+ "prefix",
90
+ "cursor",
91
+ "limit"
92
+ ],
93
+ idempotent: true,
94
+ kind: "files/list",
95
+ readOnly: true
96
+ },
97
+ {
98
+ description: "List an actor's files a page at a time, with the total bytes they hold.",
99
+ fields: [
100
+ "actor",
101
+ "key",
102
+ "prefix",
103
+ "cursor",
104
+ "limit"
105
+ ],
106
+ idempotent: true,
107
+ kind: "files/usage",
108
+ readOnly: true
109
+ },
110
+ {
111
+ description: "Download one of an actor's files, whole or the byte range offset and length give.",
112
+ fields: [
113
+ "actor",
114
+ "key",
115
+ "path",
116
+ "offset",
117
+ "length"
118
+ ],
119
+ idempotent: true,
120
+ kind: "files/download",
121
+ readOnly: true
122
+ },
123
+ {
124
+ description: "Upload a file for an actor and print its receipt, which a command attaches at a path.",
125
+ fields: [
126
+ "actor",
127
+ "key",
128
+ "content"
129
+ ],
130
+ idempotent: false,
131
+ kind: "files/upload",
132
+ readOnly: false
133
+ },
134
+ {
135
+ description: "Fork an actor into a new actor at another key: a copy of its state and files at one moment, with none of its work in flight (its jobs, timers and undelivered messages stay with the original). A retry with the same commandId makes one copy.",
136
+ fields: [
137
+ "actor",
138
+ "key",
139
+ "into",
140
+ "commandId"
141
+ ],
142
+ idempotent: false,
143
+ kind: "fork",
144
+ readOnly: false
145
+ },
146
+ {
147
+ description: "List an actor's computers, each with its image, size, egress and state.",
148
+ fields: ["actor", "key"],
149
+ idempotent: true,
150
+ kind: "computers/list",
151
+ readOnly: true
152
+ },
153
+ {
154
+ description: "Copy one file (at most 1 MiB) between an actor and its computer. fromComputer returns a receipt; attach it with an actor command.",
155
+ fields: [
156
+ "actor",
157
+ "key",
158
+ "computer",
159
+ "direction",
160
+ "path",
161
+ "computerPath"
162
+ ],
163
+ flag: "computerFiles",
164
+ idempotent: false,
165
+ kind: "computers/cp",
166
+ readOnly: false
167
+ },
168
+ {
169
+ description: "Show one of an actor's computers: its image, size, egress and state.",
170
+ fields: [
171
+ "actor",
172
+ "key",
173
+ "computer"
174
+ ],
175
+ idempotent: true,
176
+ kind: "computers/get",
177
+ readOnly: true
178
+ },
179
+ {
180
+ description: "Read a page of a computer's log, oldest first; pass the last seq printed as after to read on.",
181
+ fields: [
182
+ "actor",
183
+ "key",
184
+ "computer",
185
+ "after",
186
+ "limit"
187
+ ],
188
+ idempotent: true,
189
+ kind: "computers/logs",
190
+ readOnly: true
191
+ },
192
+ {
193
+ description: "Pause a computer, keeping its memory and disk, and print it.",
194
+ fields: [
195
+ "actor",
196
+ "key",
197
+ "computer"
198
+ ],
199
+ idempotent: true,
200
+ kind: "computers/pause",
201
+ readOnly: false
202
+ },
203
+ {
204
+ description: "Resume a paused computer and print it; fails while a fork holds it.",
205
+ fields: [
206
+ "actor",
207
+ "key",
208
+ "computer"
209
+ ],
210
+ idempotent: true,
211
+ kind: "computers/resume",
212
+ readOnly: false
213
+ },
214
+ {
215
+ description: "Stop a computer, keeping its disk, and print it.",
216
+ fields: [
217
+ "actor",
218
+ "key",
219
+ "computer"
220
+ ],
221
+ idempotent: true,
222
+ kind: "computers/stop",
223
+ readOnly: false
224
+ },
225
+ {
226
+ description: "Delete a computer and its disk. The actor's own jobs make computers; this only removes one.",
227
+ fields: [
228
+ "actor",
229
+ "key",
230
+ "computer"
231
+ ],
232
+ idempotent: true,
233
+ kind: "computers/delete",
234
+ readOnly: false
235
+ },
236
+ {
237
+ description: "List an actor's durable computer steps, newest first: each one's job, name, computer, command and state, and its exit code once it ran.",
238
+ fields: [
239
+ "actor",
240
+ "key",
241
+ "limit"
242
+ ],
243
+ flag: "computerSteps",
244
+ idempotent: true,
245
+ kind: "steps/list",
246
+ readOnly: true
247
+ },
248
+ {
249
+ description: "Show one of an actor's durable computer steps with its output. A step in state unknown may have done its work; reconcile it by its id.",
250
+ fields: [
251
+ "actor",
252
+ "key",
253
+ "step"
254
+ ],
255
+ flag: "computerSteps",
256
+ idempotent: true,
257
+ kind: "steps/get",
258
+ readOnly: true
259
+ },
260
+ {
261
+ description: "Load an actor ahead of its first call, so that call answers sooner.",
262
+ fields: ["actor", "key"],
263
+ idempotent: true,
264
+ kind: "warm",
265
+ readOnly: true
266
+ }
267
+ ];
268
+ const streams = [
269
+ "files/download",
270
+ "state",
271
+ "watch"
272
+ ];
273
+ const dangerous = ["computers/delete"];
274
+ const operationOf = ({ description, fields, flag, idempotent, kind, readOnly }) => {
275
+ const [owner, name] = commandOfKind(kind);
276
+ const confirm = Array.contains(dangerous, kind);
277
+ const operation = {
278
+ annotations: {
279
+ confirm,
280
+ destructive: confirm,
281
+ idempotent,
282
+ openWorld: false,
283
+ readOnly
284
+ },
285
+ command: [owner, name],
286
+ description,
287
+ fields,
288
+ flag: Option.fromUndefinedOr(flag),
289
+ id: `${owner}.${name}`,
290
+ input: inputOf(fields),
291
+ kind,
292
+ stream: Array.contains(streams, kind),
293
+ tool: `${owner}_${name}`
294
+ };
295
+ if (kind === "computers/cp") return {
296
+ ...operation,
297
+ tool: "computers_files"
298
+ };
299
+ return operation;
300
+ };
301
+ const unexposedKinds = ["warm"];
302
+ const actorOperations = Array.map(Array.filter(declared, ({ kind }) => !Array.contains(unexposedKinds, kind)), operationOf);
303
+ const tagOf = ({ actor, kind, member }) => {
304
+ return {
305
+ call: `${actor}/${member}`,
306
+ "computers/cp": `${actor}/computer/files`,
307
+ "computers/delete": `${actor}/computer/delete`,
308
+ "computers/get": `${actor}/computer/get`,
309
+ "computers/list": `${actor}/computer/list`,
310
+ "computers/logs": `${actor}/computer/logs`,
311
+ "computers/pause": `${actor}/computer/pause`,
312
+ "computers/resume": `${actor}/computer/resume`,
313
+ "computers/stop": `${actor}/computer/stop`,
314
+ create: `${actor}/create`,
315
+ ensure: `${actor}/ensure`,
316
+ "files/download": `${actor}/files/download`,
317
+ "files/list": `${actor}/files/list`,
318
+ "files/upload": `${actor}/files/upload`,
319
+ "files/usage": `${actor}/files/usage`,
320
+ fork: `${actor}/fork`,
321
+ query: `${actor}/${member}`,
322
+ state: `${actor}/state${subscribe}`,
323
+ "steps/get": `${actor}/computer/step`,
324
+ "steps/list": `${actor}/computer/steps`,
325
+ warm: `${actor}/warm`,
326
+ watch: `${actor}/${member}${subscribe}`
327
+ }[kind];
328
+ };
329
+ const payloadFields = (rpc) => {
330
+ const names = Option.match(Option.liftPredicate(rpc.payloadSchema.ast, SchemaAST.isObjects), {
331
+ onNone: () => [],
332
+ onSome: (ast) => Array.map(ast.propertySignatures, ({ name }) => String(name))
333
+ });
334
+ return Array.join(Array.sort(names, String$1.Order), ",");
335
+ };
336
+ const fixedMembers = {
337
+ create: {
338
+ fields: "commandId,key,payload",
339
+ kind: "create",
340
+ stream: false
341
+ },
342
+ ensure: {
343
+ fields: "commandId,key,payload",
344
+ kind: "ensure",
345
+ stream: false
346
+ },
347
+ fork: {
348
+ fields: "commandId,into,key",
349
+ kind: "fork",
350
+ stream: false
351
+ },
352
+ state: {
353
+ fields: "key",
354
+ kind: "state",
355
+ stream: true
356
+ },
357
+ warm: {
358
+ fields: "key",
359
+ kind: "warm",
360
+ stream: false
361
+ }
362
+ };
363
+ const anyMember = [
364
+ {
365
+ fields: "commandId,key,payload",
366
+ kind: "call",
367
+ stream: false
368
+ },
369
+ {
370
+ fields: "key,payload",
371
+ kind: "query",
372
+ stream: false
373
+ },
374
+ {
375
+ fields: "key,payload",
376
+ kind: "watch",
377
+ stream: true
378
+ }
379
+ ];
380
+ const fileRpcs = {
381
+ "files/download": {
382
+ fields: "key,length,offset,path",
383
+ kind: "files/download",
384
+ stream: true
385
+ },
386
+ "files/list": {
387
+ fields: "cursor,key,limit,prefix",
388
+ kind: "files/list",
389
+ stream: false
390
+ },
391
+ "files/upload/append": {
392
+ fields: "bytes,key,offset,upload",
393
+ kind: "files/upload",
394
+ stream: false
395
+ },
396
+ "files/upload/close": {
397
+ fields: "bytes,key,upload",
398
+ kind: "files/upload",
399
+ stream: false
400
+ },
401
+ "files/upload/open": {
402
+ fields: "key",
403
+ kind: "files/upload",
404
+ stream: false
405
+ },
406
+ "files/usage": {
407
+ fields: "cursor,key,limit,prefix",
408
+ kind: "files/usage",
409
+ stream: false
410
+ }
411
+ };
412
+ const computerRpcs = {
413
+ "computer/delete": {
414
+ fields: "computer,key",
415
+ kind: "computers/delete",
416
+ stream: false
417
+ },
418
+ "computer/files": {
419
+ fields: "computer,computerPath,direction,key,path",
420
+ kind: "computers/cp",
421
+ stream: false
422
+ },
423
+ "computer/get": {
424
+ fields: "computer,key",
425
+ kind: "computers/get",
426
+ stream: false
427
+ },
428
+ "computer/list": {
429
+ fields: "key",
430
+ kind: "computers/list",
431
+ stream: false
432
+ },
433
+ "computer/logs": {
434
+ fields: "after,computer,key,limit",
435
+ kind: "computers/logs",
436
+ stream: false
437
+ },
438
+ "computer/pause": {
439
+ fields: "computer,key",
440
+ kind: "computers/pause",
441
+ stream: false
442
+ },
443
+ "computer/resume": {
444
+ fields: "computer,key",
445
+ kind: "computers/resume",
446
+ stream: false
447
+ },
448
+ "computer/step": {
449
+ fields: "key,step",
450
+ kind: "steps/get",
451
+ stream: false
452
+ },
453
+ "computer/steps": {
454
+ fields: "key,limit",
455
+ kind: "steps/list",
456
+ stream: false
457
+ },
458
+ "computer/stop": {
459
+ fields: "computer,key",
460
+ kind: "computers/stop",
461
+ stream: false
462
+ }
463
+ };
464
+ const nestedRpcs = {
465
+ ...fileRpcs,
466
+ ...computerRpcs
467
+ };
468
+ const laterMembers = ["computer", "files"];
469
+ const actorRest = /^[^/]+\/(?<rest>.+)$/u;
470
+ const actorTag = /^[^/]+\/(?<member>[^/]+)(?:\/subscribe)?$/u;
471
+ const kindOfRpc = ([tag, rpc]) => {
472
+ const member = Option.fromUndefinedOr(actorTag.exec(tag)?.groups?.member);
473
+ const fields = payloadFields(rpc);
474
+ const stream = RpcSchema.isStreamSchema(rpc.successSchema);
475
+ const rest = Option.fromUndefinedOr(actorRest.exec(tag)?.groups?.rest);
476
+ const nested = Option.flatMap(rest, (name) => Record.get(nestedRpcs, name));
477
+ const candidates = Option.match(Option.filter(member, (name) => !Array.contains(laterMembers, name)), {
478
+ onNone: () => Option.toArray(nested),
479
+ onSome: (name) => Option.match(Record.get(fixedMembers, name), {
480
+ onNone: () => anyMember,
481
+ onSome: Array.of
482
+ })
483
+ });
484
+ return Option.map(Array.findFirst(candidates, (signature) => signature.fields === fields && signature.stream === stream), (signature) => signature.kind);
485
+ };
486
+ const kindsOf = (requests) => Array.map(Array.fromIterable(requests), ([tag, rpc]) => [tag, kindOfRpc([tag, rpc])]);
487
+ const someField = (name, value) => Option.match(value, {
488
+ onNone: () => ({}),
489
+ onSome: (found) => ({ [name]: found })
490
+ });
491
+ const actorRequestOf = (operation) => (input) => Effect.gen(function* buildRequest() {
492
+ const takes = (name) => Array.contains(operation.fields, name);
493
+ const minted = SecureRandom.SecureRandom.use((random) => random.uuidV7());
494
+ const fresh = yield* Effect.when(minted, Effect.succeed(takes("commandId") && Option.isNone(input.commandId)));
495
+ const commandId = Option.filter(Option.orElse(input.commandId, () => fresh), () => takes("commandId"));
496
+ const payload = Option.liftPredicate(Option.getOrNull(input.payload), () => takes("payload"));
497
+ return {
498
+ payload: {
499
+ ...Record.filter(input.fileFields, (_value, name) => Array.some(operation.fields, (field) => field === name)),
500
+ ...someField("commandId", commandId),
501
+ ...someField("into", Option.filter(input.into, () => takes("into"))),
502
+ ...someField("payload", payload),
503
+ key: input.key
504
+ },
505
+ tag: tagOf({
506
+ actor: input.actor,
507
+ kind: operation.kind,
508
+ member: input.member
509
+ })
510
+ };
511
+ });
512
+ export { actorOperations, actorRequestOf, kindsOf, tagOf, unexposedKinds };
@@ -0,0 +1,18 @@
1
+ import { Effect, Scope } from "effect";
2
+ import { RpcClientError, RpcMessage } from "effect/rpc";
3
+ import { HttpClient } from "effect/http";
4
+ type Headers = () => Readonly<Record<string, string>>;
5
+ interface ActorProtocolOptions {
6
+ readonly headers: Headers;
7
+ readonly http: HttpClient.HttpClient;
8
+ readonly url: string;
9
+ readonly webSocket: typeof globalThis.WebSocket;
10
+ }
11
+ declare const actorProtocol: (options: ActorProtocolOptions) => Effect.Effect<{
12
+ readonly run: (clientId: number, f: (data: RpcMessage.FromServerEncoded) => Effect.Effect<void>) => Effect.Effect<never>;
13
+ readonly send: (clientId: number, request: RpcMessage.FromClientEncoded, transferables?: ReadonlyArray<globalThis.Transferable>) => Effect.Effect<void, RpcClientError.RpcClientError>;
14
+ readonly supportsAck: boolean;
15
+ readonly supportsTransferables: boolean;
16
+ readonly codecFor: import("effect/rpc/RpcSerialization").CodecFor;
17
+ }, never, import("effect/rpc/RpcSerialization").RpcSerialization | Scope.Scope>;
18
+ export { type ActorProtocolOptions, actorProtocol };
@@ -0,0 +1,172 @@
1
+ import { trackedFloors } from "./read-floors.pure.mjs";
2
+ import { remoteInterrupts } from "./remote-interrupt.pure.mjs";
3
+ import { Array, Clock, Effect, FiberMap, Latch, Option, Predicate, Record, Ref, Schema, String } from "effect";
4
+ import { RpcClient, RpcClientError, RpcMessage } from "effect/rpc";
5
+ import { Socket } from "effect/socket";
6
+ // src/actor-protocol.layer.ts
7
+ const streamSuffix = "/subscribe";
8
+ const liveProtocol = "akter-rpc";
9
+ const authProtocol = "akter-auth.";
10
+ const authorizationHeader = "authorization";
11
+ const bearerPrefix = "Bearer ";
12
+ const pingEvery = "10 seconds";
13
+ const silentMillis = 25e3;
14
+ const openTimeout = "10 seconds";
15
+ const normalClose = 1e3;
16
+ const ClientProtocolError = Schema.TaggedStruct("ClientProtocolError", {
17
+ error: RpcClientError.RpcClientError,
18
+ requestIds: Schema.Array(Schema.Union([Schema.String, Schema.Finite]))
19
+ });
20
+ const JsonText = Schema.fromJsonString(Schema.Unknown);
21
+ const encodeJson = Schema.encodeEffect(JsonText);
22
+ const decodeJson = Schema.decodeEffect(JsonText);
23
+ const utf8 = new globalThis.TextDecoder();
24
+ function serverMessages(values) {
25
+ return values;
26
+ }
27
+ const textOf = (message) => Effect.orDie(encodeJson(message));
28
+ const tokenOf = (headers) => Option.map(Option.filter(Record.get(headers, authorizationHeader), String.startsWith(bearerPrefix)), String.slice(7));
29
+ const withoutAuthorization = (headers) => Array.filter(Record.toEntries(headers), ([name]) => String.toLowerCase(name) !== authorizationHeader);
30
+ const socketUrl = (url, request) => Effect.map(textOf({
31
+ ...request,
32
+ headers: []
33
+ }), (text) => {
34
+ const target = new globalThis.URL(url);
35
+ target.protocol = String.replace(/^http/u, "ws")(target.protocol);
36
+ target.searchParams.set("request", text);
37
+ return target.href;
38
+ });
39
+ const lost = (message, cause) => RpcClientError.RpcClientError.make({ reason: RpcClientError.RpcClientDefect.make({
40
+ cause,
41
+ message
42
+ }) });
43
+ const textFrom = (frame) => {
44
+ if (Predicate.isString(frame)) return frame;
45
+ return utf8.decode(frame);
46
+ };
47
+ const decodeFrame = (frame) => Effect.mapBoth(decodeJson(textFrom(frame)), {
48
+ onFailure: (cause) => lost("the live socket sent a frame that isn't RPC JSON", cause),
49
+ onSuccess: (decoded) => serverMessages(Array.ensure(decoded))
50
+ });
51
+ const opened = ({ headers, url, webSocket }, request) => Effect.gen(function* openLiveSocket() {
52
+ const sent = headers();
53
+ const credentials = Option.map(tokenOf(sent), (token) => `${authProtocol}${token}`);
54
+ const protocols = [liveProtocol, ...Option.toArray(credentials)];
55
+ const target = yield* socketUrl(url, request);
56
+ const socket = yield* Socket.fromWebSocket(Effect.acquireRelease(Effect.sync(() => new webSocket(target, protocols)), (native) => Effect.sync(() => {
57
+ native.close(normalClose);
58
+ })), { openTimeout });
59
+ const reader = yield* socket.reader;
60
+ const writer = yield* socket.writer;
61
+ const opening = yield* textOf({
62
+ ...request,
63
+ headers: [...request.headers, ...withoutAuthorization(sent)]
64
+ });
65
+ yield* writer.write(opening);
66
+ return {
67
+ reader,
68
+ writer
69
+ };
70
+ });
71
+ const received = ({ deliver, ended }) => (response) => {
72
+ if (Predicate.isTagged(response, "Pong")) return Effect.void;
73
+ if (RpcMessage.isTerminalResponse(response)) return Effect.andThen(deliver(response), Latch.open(ended));
74
+ return deliver(response);
75
+ };
76
+ const receiving = (live) => {
77
+ const onFrame = (frame) => Effect.flatMap(decodeFrame(frame), (responses) => Effect.andThen(Effect.flatMap(Clock.currentTimeMillis, (now) => Ref.set(live.seen, now)), Effect.forEach(responses, received(live), { discard: true })));
78
+ return Effect.forever(Effect.flatMap(live.reader.pull, (frames) => Effect.forEach(frames, onFrame)));
79
+ };
80
+ const pinging = ({ seen, writer }) => Effect.forever(Effect.gen(function* pingLiveSocket() {
81
+ yield* Effect.sleep(pingEvery);
82
+ const now = yield* Clock.currentTimeMillis;
83
+ const last = yield* Ref.get(seen);
84
+ if (now - last > silentMillis) return yield* lost("the live socket went silent", now - last);
85
+ const frame = yield* textOf(RpcMessage.constPing);
86
+ return yield* writer.write(frame);
87
+ }));
88
+ const transportFailure = (failure) => {
89
+ if (Predicate.isTagged(failure, "RpcClientError")) return failure;
90
+ return RpcClientError.RpcClientError.make({ reason: failure.reason });
91
+ };
92
+ const liveStream = (options, writers) => ({ deliver, request }) => {
93
+ const served = Effect.gen(function* serveLiveRequest() {
94
+ const { reader, writer } = yield* opened(options, request);
95
+ const ended = yield* Latch.make();
96
+ const now = yield* Clock.currentTimeMillis;
97
+ const live = {
98
+ deliver,
99
+ ended,
100
+ reader,
101
+ seen: yield* Ref.make(now),
102
+ writer
103
+ };
104
+ yield* Effect.acquireRelease(Effect.sync(() => writers.set(request.id, writer)), () => Effect.sync(() => writers.delete(request.id)));
105
+ yield* Effect.raceFirst(Effect.raceFirst(receiving(live), pinging(live)), ended.await);
106
+ });
107
+ return Effect.matchEffect(Effect.scoped(served), {
108
+ onFailure: (failure) => deliver(ClientProtocolError.make({
109
+ error: transportFailure(failure),
110
+ requestIds: [request.id]
111
+ })),
112
+ onSuccess: () => Effect.void
113
+ });
114
+ };
115
+ const liveRequestOf = (message) => {
116
+ if (Predicate.isTagged(message, "Request") && String.endsWith(streamSuffix)(message.tag)) return Option.some(message);
117
+ return Option.none();
118
+ };
119
+ const deliveries = (http) => {
120
+ const delivers = /* @__PURE__ */ new Map();
121
+ const floors = trackedFloors();
122
+ const interrupts = remoteInterrupts();
123
+ const deliverTo = (clientId) => (response) => Option.match(Option.fromNullishOr(delivers.get(clientId)), {
124
+ onNone: () => Effect.void,
125
+ onSome: (deliver) => deliver(interrupts.received(clientId, response))
126
+ });
127
+ const run = (clientId, deliver) => Effect.ensuring(Effect.suspend(() => {
128
+ delivers.set(clientId, deliver);
129
+ return http.run(clientId, (response) => Effect.andThen(Effect.sync(() => {
130
+ floors.received(clientId, response);
131
+ }), Effect.suspend(() => deliver(interrupts.received(clientId, response)))));
132
+ }), Effect.sync(() => delivers.delete(clientId)));
133
+ return {
134
+ deliverTo,
135
+ floors,
136
+ interrupts,
137
+ run
138
+ };
139
+ };
140
+ const actorProtocol = Effect.fnUntraced(function* actorProtocol(options) {
141
+ const http = yield* RpcClient.makeProtocolHttp(options.http);
142
+ const lives = yield* FiberMap.make();
143
+ const writers = /* @__PURE__ */ new Map();
144
+ const { deliverTo, floors, interrupts, run } = deliveries(http);
145
+ const serve = liveStream(options, writers);
146
+ const writeTo = (requestId, message) => Option.match(Option.fromNullishOr(writers.get(requestId)), {
147
+ onNone: () => Effect.void,
148
+ onSome: (writer) => Effect.ignore(Effect.flatMap(textOf(message), (frame) => writer.write(frame)))
149
+ });
150
+ const send = (clientId, message, transferables) => {
151
+ const live = liveRequestOf(message);
152
+ interrupts.sent(clientId, message);
153
+ if (Option.isSome(live)) {
154
+ const streamed = serve({
155
+ deliver: deliverTo(clientId),
156
+ request: live.value
157
+ });
158
+ return Effect.asVoid(FiberMap.run(lives, live.value.id, streamed));
159
+ }
160
+ if (Predicate.isTagged(message, "Ack")) return writeTo(message.requestId, message);
161
+ if (Predicate.isTagged(message, "Interrupt")) return Effect.andThen(writeTo(message.requestId, message), FiberMap.remove(lives, message.requestId));
162
+ return http.send(clientId, floors.sent(clientId, message), transferables);
163
+ };
164
+ return RpcClient.Protocol.of({
165
+ codecFor: http.codecFor,
166
+ run,
167
+ send,
168
+ supportsAck: true,
169
+ supportsTransferables: false
170
+ });
171
+ });
172
+ export { actorProtocol };