@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.
- package/LICENSE +201 -0
- package/README.md +17 -2
- package/dist/actor-fields.pure.d.mts +77 -0
- package/dist/actor-fields.pure.mjs +85 -0
- package/dist/actor-operations.pure.d.mts +49 -0
- package/dist/actor-operations.pure.mjs +512 -0
- package/dist/actor-protocol.layer.d.mts +18 -0
- package/dist/actor-protocol.layer.mjs +172 -0
- package/dist/actor-rpc.pure.d.mts +28 -0
- package/dist/actor-rpc.pure.mjs +154 -0
- package/dist/akter-client.layer.d.mts +13 -0
- package/dist/akter-client.layer.mjs +27 -0
- package/dist/promise-client.adapter.d.mts +55 -0
- package/dist/promise-client.adapter.mjs +114 -0
- package/dist/promise-files.pure.d.mts +48 -0
- package/dist/promise-files.pure.mjs +35 -0
- package/dist/read-floors.pure.d.mts +7 -0
- package/dist/read-floors.pure.mjs +49 -0
- package/dist/remote-interrupt.pure.d.mts +13 -0
- package/dist/remote-interrupt.pure.mjs +45 -0
- package/dist/upload-source.errors.d.mts +9 -0
- package/dist/upload-source.errors.mjs +5 -0
- package/package.json +68 -3
- package/src/actor-fields.pure.ts +60 -0
- package/src/actor-operations.pure.ts +484 -0
- package/src/actor-protocol.layer.ts +331 -0
- package/src/actor-rpc.pure.ts +179 -0
- package/src/akter-client.layer.ts +67 -0
- package/src/promise-client.adapter.ts +354 -0
- package/src/promise-files.pure.ts +106 -0
- package/src/read-floors.pure.ts +82 -0
- package/src/remote-interrupt.pure.ts +79 -0
- package/src/upload-source.errors.ts +9 -0
|
@@ -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 };
|