posipaki 0.35.2 → 0.37.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/dist/{client-C4LZE65N.d.ts → client-Cg_MlE0m.d.ts} +17 -4
- package/dist/client-Cg_MlE0m.d.ts.map +1 -0
- package/dist/{define-actor-DqOW2StX.js → define-actor-CZysIKH4.js} +18 -4
- package/dist/define-actor-CZysIKH4.js.map +1 -0
- package/dist/hooks-CgXhmY4r.js.map +1 -1
- package/dist/{hooks-Dj59y_jN.d.ts → hooks-gmAsdJBN.d.ts} +20 -4
- package/dist/hooks-gmAsdJBN.d.ts.map +1 -0
- package/dist/hooks.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/json1-DYq9g7im.js +1124 -0
- package/dist/json1-DYq9g7im.js.map +1 -0
- package/dist/plugins/debug-logger.d.ts +2 -2
- package/dist/plugins/tree-introspection.d.ts +6 -6
- package/dist/plugins/tree-introspection.d.ts.map +1 -1
- package/dist/plugins/tree-introspection.js +5 -5
- package/dist/plugins/tree-introspection.js.map +1 -1
- package/dist/{process-pZDxIjLA.d.ts → process-DjRQSZef.d.ts} +2 -2
- package/dist/{process-pZDxIjLA.d.ts.map → process-DjRQSZef.d.ts.map} +1 -1
- package/dist/remote/index.d.ts +169 -8
- package/dist/remote/index.d.ts.map +1 -1
- package/dist/remote/index.js +2 -2
- package/dist/remote/node.d.ts +3 -3
- package/dist/remote/node.js +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/{types-CsE8xK3a.d.ts → types-DDc-stle.d.ts} +4 -2
- package/dist/types-DDc-stle.d.ts.map +1 -0
- package/dist/{version-0mlM8dbC.js → version-98a9sol-.js} +2 -2
- package/dist/{version-0mlM8dbC.js.map → version-98a9sol-.js.map} +1 -1
- package/dist/xfetch.d.ts +2 -2
- package/package.json +1 -1
- package/dist/client-C4LZE65N.d.ts.map +0 -1
- package/dist/define-actor-DqOW2StX.js.map +0 -1
- package/dist/hooks-Dj59y_jN.d.ts.map +0 -1
- package/dist/json1-DzPx9oX3.js +0 -200
- package/dist/json1-DzPx9oX3.js.map +0 -1
- package/dist/types-CsE8xK3a.d.ts.map +0 -1
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
import { r as makeWaiter } from "./util-Brtfgv68.js";
|
|
2
|
+
import { i as isProcess, t as defineActor } from "./define-actor-CZysIKH4.js";
|
|
3
|
+
import { s as stopPropagation } from "./hooks-CgXhmY4r.js";
|
|
4
|
+
//#region src/remote/process-ref.ts
|
|
5
|
+
/** The key that marks a process reference inside an otherwise JSON value. */
|
|
6
|
+
const PROCESS_REF = "$p";
|
|
7
|
+
/**
|
|
8
|
+
* The id a side gives its own root: the first id of its parity. The two ends of a
|
|
9
|
+
* connection therefore number their roots differently, and an id says whose root it is
|
|
10
|
+
* as it says who holds every other process. A side hands out its next id from two
|
|
11
|
+
* steps further on, the one in between belonging to the root of the side it talks to.
|
|
12
|
+
*/
|
|
13
|
+
function rootIdFor(parity) {
|
|
14
|
+
return parity === "odd" ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
/** What a served root is called when the client states no name for it. A client that
|
|
17
|
+
* states one — and the library's own does, in `$init` — is served under that name instead:
|
|
18
|
+
* the process across the seam is the one its proxy was forked as, and it should be called
|
|
19
|
+
* the same on both sides. This is the name for a connection that says nothing. */
|
|
20
|
+
const SERVED_ROOT_NAME = "remote";
|
|
21
|
+
/**
|
|
22
|
+
* The processes one connection knows, by the ids that name them. One table per
|
|
23
|
+
* connection, because an id means nothing on any other one, and it lives exactly
|
|
24
|
+
* as long as the connection that owns it.
|
|
25
|
+
*
|
|
26
|
+
* It holds two sets, told apart by parity rather than kept apart by a rule: the
|
|
27
|
+
* processes this side holds — the root, and anything it hands over — and the ones
|
|
28
|
+
* the far side holds, which this side knows as handles. A frame's address is
|
|
29
|
+
* looked up in both, and exactly one can answer.
|
|
30
|
+
*/
|
|
31
|
+
var ProcessTable = class {
|
|
32
|
+
constructor(parity, onCrossed) {
|
|
33
|
+
this.pvtMine = /* @__PURE__ */ new Map();
|
|
34
|
+
this.pvtMineIds = /* @__PURE__ */ new Map();
|
|
35
|
+
this.pvtTheirs = /* @__PURE__ */ new Map();
|
|
36
|
+
this.pvtRootId = rootIdFor(parity);
|
|
37
|
+
this.pvtFarRootId = rootIdFor(parity === "odd" ? "even" : "odd");
|
|
38
|
+
this.pvtNext = this.pvtRootId + 2;
|
|
39
|
+
this.pvtOnCrossed = onCrossed;
|
|
40
|
+
}
|
|
41
|
+
/** The id of this side's own end of the connection. */
|
|
42
|
+
rootId() {
|
|
43
|
+
return this.pvtRootId;
|
|
44
|
+
}
|
|
45
|
+
/** The id the far side knows its own end by: the root this side reaches as a handle,
|
|
46
|
+
* and the one it never hands out for anything of its own. */
|
|
47
|
+
farRootId() {
|
|
48
|
+
return this.pvtFarRootId;
|
|
49
|
+
}
|
|
50
|
+
/** Bind this side's own end of the connection — the root — to its id. */
|
|
51
|
+
bindRoot(proc) {
|
|
52
|
+
const bound = this.pvtMine.get(this.pvtRootId);
|
|
53
|
+
if (bound !== void 0 && bound !== proc) throw new Error("ProcessTable: the root is already bound");
|
|
54
|
+
this.pvtMine.set(this.pvtRootId, proc);
|
|
55
|
+
this.pvtMineIds.set(proc, this.pvtRootId);
|
|
56
|
+
}
|
|
57
|
+
/** The id this side gives a process it holds, allocating one the first time. */
|
|
58
|
+
handleFor(proc) {
|
|
59
|
+
const known = this.pvtMineIds.get(proc);
|
|
60
|
+
if (known !== void 0) return known;
|
|
61
|
+
const id = this.pvtNext;
|
|
62
|
+
this.pvtNext += 2;
|
|
63
|
+
this.pvtMine.set(id, proc);
|
|
64
|
+
this.pvtMineIds.set(proc, id);
|
|
65
|
+
this.pvtOnCrossed?.(proc, id);
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
/** A process this side holds, by the id it gave it. */
|
|
69
|
+
processFor(id) {
|
|
70
|
+
return this.pvtMine.get(id);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register a process the far side holds — a handle — under the id it arrived
|
|
74
|
+
* with. A root is one of these as much as anything else: both ends hold one at 0,
|
|
75
|
+
* so the far side's root is a handle here, and the one this side has of itself
|
|
76
|
+
* stays what it was.
|
|
77
|
+
*/
|
|
78
|
+
bindFar(id, proc) {
|
|
79
|
+
this.pvtTheirs.set(id, proc);
|
|
80
|
+
}
|
|
81
|
+
/** A process the far side holds, by the id it gave it. */
|
|
82
|
+
farHandleFor(id) {
|
|
83
|
+
return this.pvtTheirs.get(id);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whatever this connection knows `id` to mean: a process this side holds, or a
|
|
87
|
+
* handle on one the far side holds. Only one of the two can answer, because
|
|
88
|
+
* the two sides take their ids from different halves of the space — so what
|
|
89
|
+
* comes back does not depend on which way the frame was going.
|
|
90
|
+
*/
|
|
91
|
+
resolve(id) {
|
|
92
|
+
return this.pvtMine.get(id) ?? this.pvtTheirs.get(id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Let go of an id: the process behind it is not this connection's business any
|
|
96
|
+
* more, so it is no longer known by it. Both sides' ids go the same way — the
|
|
97
|
+
* side that lets a handle go names the far side's id, the side that is asked to
|
|
98
|
+
* forget names its own — and an id let go is never handed out again, so nothing
|
|
99
|
+
* that still names it can come to mean another process.
|
|
100
|
+
*
|
|
101
|
+
* Neither root is let go of: one is this side's own end of the connection, and the
|
|
102
|
+
* other is what this side is talking to.
|
|
103
|
+
*/
|
|
104
|
+
release(id) {
|
|
105
|
+
if (id === this.pvtRootId || id === this.pvtFarRootId) return void 0;
|
|
106
|
+
const mine = this.pvtMine.get(id);
|
|
107
|
+
if (mine !== void 0) {
|
|
108
|
+
this.pvtMine.delete(id);
|
|
109
|
+
this.pvtMineIds.delete(mine);
|
|
110
|
+
return mine;
|
|
111
|
+
}
|
|
112
|
+
const theirs = this.pvtTheirs.get(id);
|
|
113
|
+
if (theirs !== void 0) this.pvtTheirs.delete(id);
|
|
114
|
+
return theirs;
|
|
115
|
+
}
|
|
116
|
+
/** Every handle this side holds on a process of the far side. */
|
|
117
|
+
handles() {
|
|
118
|
+
return [...this.pvtTheirs.values()];
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function isPlainObject(value) {
|
|
122
|
+
if (typeof value !== "object" || value === null) return false;
|
|
123
|
+
const proto = Object.getPrototypeOf(value);
|
|
124
|
+
return proto === Object.prototype || proto === null;
|
|
125
|
+
}
|
|
126
|
+
/** Whether `value` is the wire form of a reference. */
|
|
127
|
+
function asProcessRef(value) {
|
|
128
|
+
if (typeof value !== "object" || value === null) return null;
|
|
129
|
+
const ref = value["$p"];
|
|
130
|
+
if (typeof ref !== "object" || ref === null) return null;
|
|
131
|
+
const { id, pname } = ref;
|
|
132
|
+
if (typeof id !== "number" || typeof pname !== "string") return null;
|
|
133
|
+
return {
|
|
134
|
+
id,
|
|
135
|
+
pname
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Whether `value` has the shape of a handle on a far-side process. Judged by
|
|
139
|
+
* what it holds rather than by `instanceof`, for the reason a process is: one
|
|
140
|
+
* build can hold two copies of the class. */
|
|
141
|
+
function isRemoteProcess(value) {
|
|
142
|
+
if (typeof value !== "object" || value === null) return false;
|
|
143
|
+
const candidate = value;
|
|
144
|
+
return typeof candidate.pname === "string" && typeof candidate.id === "symbol" && typeof candidate.send === "function" && asProcessRef({ ["$p"]: candidate.ref }) !== null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Replace every process in `value` with the reference this connection uses for
|
|
148
|
+
* it, so what is left is JSON that says which processes the answer is about.
|
|
149
|
+
*
|
|
150
|
+
* A handle goes back as the reference it came with, id and all: that id is the
|
|
151
|
+
* far side's own, the one it will be read by.
|
|
152
|
+
*
|
|
153
|
+
* Containers are copied rather than rewritten: the objects belong to the actor
|
|
154
|
+
* that returned them, and a state it is still running on is not ours to edit. A
|
|
155
|
+
* node already seen is left where it is — a cycle is the encoder's to report, not
|
|
156
|
+
* something to walk forever.
|
|
157
|
+
*/
|
|
158
|
+
function encodeProcessRefs(value, table, seen = /* @__PURE__ */ new Set()) {
|
|
159
|
+
if (isRemoteProcess(value)) return { ["$p"]: {
|
|
160
|
+
id: value.ref.id,
|
|
161
|
+
pname: value.ref.pname
|
|
162
|
+
} };
|
|
163
|
+
if (isProcess(value)) return { ["$p"]: {
|
|
164
|
+
id: table.handleFor(value),
|
|
165
|
+
pname: value.pname
|
|
166
|
+
} };
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
if (seen.has(value)) return value;
|
|
169
|
+
seen.add(value);
|
|
170
|
+
return value.map((item) => encodeProcessRefs(item, table, seen));
|
|
171
|
+
}
|
|
172
|
+
if (isPlainObject(value)) {
|
|
173
|
+
if (seen.has(value)) return value;
|
|
174
|
+
seen.add(value);
|
|
175
|
+
const copy = {};
|
|
176
|
+
for (const [key, item] of Object.entries(value)) copy[key] = encodeProcessRefs(item, table, seen);
|
|
177
|
+
return copy;
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
/** Replace every reference in `value` with what `resolve` makes of it. */
|
|
182
|
+
function decodeProcessRefs(value, resolve, seen = /* @__PURE__ */ new Set()) {
|
|
183
|
+
const ref = asProcessRef(value);
|
|
184
|
+
if (ref) return resolve(ref);
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
if (seen.has(value)) return value;
|
|
187
|
+
seen.add(value);
|
|
188
|
+
return value.map((item) => decodeProcessRefs(item, resolve, seen));
|
|
189
|
+
}
|
|
190
|
+
if (isPlainObject(value)) {
|
|
191
|
+
if (seen.has(value)) return value;
|
|
192
|
+
seen.add(value);
|
|
193
|
+
const copy = {};
|
|
194
|
+
for (const [key, item] of Object.entries(value)) copy[key] = decodeProcessRefs(item, resolve, seen);
|
|
195
|
+
return copy;
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/remote/channel.ts
|
|
201
|
+
/** The id a frame addresses, or null when it names no process. */
|
|
202
|
+
function frameTo(frame) {
|
|
203
|
+
const to = frame.to;
|
|
204
|
+
return typeof to === "number" ? to : null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A frame on its way out: every process in it becomes the reference this
|
|
208
|
+
* connection uses for that process, and the process the frame addresses is
|
|
209
|
+
* written down as `to`.
|
|
210
|
+
*/
|
|
211
|
+
function encodeFrame(frame, table, to) {
|
|
212
|
+
return {
|
|
213
|
+
...encodeProcessRefs(frame, table),
|
|
214
|
+
to
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A frame just read: every reference in it becomes a handle on that process — or
|
|
219
|
+
* the handle this side already made for it, since the same process handed over
|
|
220
|
+
* twice is one process.
|
|
221
|
+
*/
|
|
222
|
+
function decodeFrame(frame, resolve) {
|
|
223
|
+
return decodeProcessRefs(frame, resolve);
|
|
224
|
+
}
|
|
225
|
+
function isProto(msg) {
|
|
226
|
+
return "$proto" in msg;
|
|
227
|
+
}
|
|
228
|
+
function isInit(msg) {
|
|
229
|
+
return "$init" in msg;
|
|
230
|
+
}
|
|
231
|
+
function isState(msg) {
|
|
232
|
+
return "$state" in msg;
|
|
233
|
+
}
|
|
234
|
+
function isMsg(msg) {
|
|
235
|
+
return "$msg" in msg;
|
|
236
|
+
}
|
|
237
|
+
function isExit(msg) {
|
|
238
|
+
return "$exit" in msg;
|
|
239
|
+
}
|
|
240
|
+
/** The three, in the one order they are ever named in — the order a tune frame
|
|
241
|
+
* carries them in, so two ways of asking for the same thing look the same. */
|
|
242
|
+
const STREAM_KINDS = [
|
|
243
|
+
"message",
|
|
244
|
+
"state",
|
|
245
|
+
"exit"
|
|
246
|
+
];
|
|
247
|
+
function isStreamKind(value) {
|
|
248
|
+
return typeof value === "string" && STREAM_KINDS.includes(value);
|
|
249
|
+
}
|
|
250
|
+
function isStop(msg) {
|
|
251
|
+
return "$stop" in msg;
|
|
252
|
+
}
|
|
253
|
+
function isPause(msg) {
|
|
254
|
+
return "$pause" in msg;
|
|
255
|
+
}
|
|
256
|
+
function isResume(msg) {
|
|
257
|
+
return "$resume" in msg;
|
|
258
|
+
}
|
|
259
|
+
function isRelease(msg) {
|
|
260
|
+
return "$release" in msg;
|
|
261
|
+
}
|
|
262
|
+
function isTune(msg) {
|
|
263
|
+
return "$tune" in msg;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The categories a tune frame asks for, in the order they are named in — or null
|
|
267
|
+
* when the frame says something this vocabulary has no word for, which is a frame
|
|
268
|
+
* to ignore rather than to guess at.
|
|
269
|
+
*/
|
|
270
|
+
function tuneFrame(frame) {
|
|
271
|
+
const body = frame.$tune;
|
|
272
|
+
if (typeof body !== "object" || body === null) return null;
|
|
273
|
+
const streams = body.streams;
|
|
274
|
+
if (!Array.isArray(streams)) return null;
|
|
275
|
+
if (!streams.every(isStreamKind)) return null;
|
|
276
|
+
return STREAM_KINDS.filter((kind) => streams.includes(kind));
|
|
277
|
+
}
|
|
278
|
+
const REFLECT_METHODS = "$r.methods";
|
|
279
|
+
const REFLECT_CALL = "$r.call.";
|
|
280
|
+
const REFLECT_RESULT = "$r.result.";
|
|
281
|
+
function isReflectionMethods(msg) {
|
|
282
|
+
const names = msg[REFLECT_METHODS];
|
|
283
|
+
return Array.isArray(names) && names.every((name) => typeof name === "string");
|
|
284
|
+
}
|
|
285
|
+
function reflectionBody(frame, prefix) {
|
|
286
|
+
for (const key of Object.keys(frame)) {
|
|
287
|
+
if (!key.startsWith(prefix) || key.length === prefix.length) continue;
|
|
288
|
+
const body = frame[key];
|
|
289
|
+
if (typeof body !== "object" || body === null) return null;
|
|
290
|
+
return {
|
|
291
|
+
name: key.slice(prefix.length),
|
|
292
|
+
body
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
/** A call frame, or null — a malformed one is not worth an answer. */
|
|
298
|
+
function asReflectionCall(frame) {
|
|
299
|
+
const parsed = reflectionBody(frame, REFLECT_CALL);
|
|
300
|
+
if (!parsed || typeof parsed.body.seq !== "number") return null;
|
|
301
|
+
const args = parsed.body.args;
|
|
302
|
+
return {
|
|
303
|
+
name: parsed.name,
|
|
304
|
+
seq: parsed.body.seq,
|
|
305
|
+
args: Array.isArray(args) ? args : []
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/** An answer frame, or null. `error` present means the call was refused. */
|
|
309
|
+
function asReflectionResult(frame) {
|
|
310
|
+
const parsed = reflectionBody(frame, REFLECT_RESULT);
|
|
311
|
+
if (!parsed || typeof parsed.body.seq !== "number") return null;
|
|
312
|
+
const { seq, value, error } = parsed.body;
|
|
313
|
+
return typeof error === "string" ? {
|
|
314
|
+
name: parsed.name,
|
|
315
|
+
seq,
|
|
316
|
+
error
|
|
317
|
+
} : {
|
|
318
|
+
name: parsed.name,
|
|
319
|
+
seq,
|
|
320
|
+
value
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Why a value cannot cross a frame, or null when it can. Only JSON data
|
|
325
|
+
* crosses: a function, a symbol or a bigint cannot be written down at all, and
|
|
326
|
+
* a value with a cycle or a bigint inside it makes the encoder throw. What
|
|
327
|
+
* JSON itself would quietly drop — a function stored inside an object — is the
|
|
328
|
+
* caller's business, not something to invent a policy for here.
|
|
329
|
+
*/
|
|
330
|
+
function jsonProblem(value) {
|
|
331
|
+
const kind = typeof value;
|
|
332
|
+
if (kind === "function" || kind === "symbol" || kind === "bigint") return `a ${kind}`;
|
|
333
|
+
if (kind !== "object" || value === null) return null;
|
|
334
|
+
try {
|
|
335
|
+
JSON.stringify(value);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
return err instanceof Error ? err.message : String(err);
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/remote/remote-process.ts
|
|
343
|
+
var RemoteProcess = class {
|
|
344
|
+
constructor(ref, send, fromName, letGo) {
|
|
345
|
+
this.state = null;
|
|
346
|
+
this.$reflection = {};
|
|
347
|
+
this.pvtConnected = true;
|
|
348
|
+
this.pvtReleased = false;
|
|
349
|
+
this.pvtExit = null;
|
|
350
|
+
this.pvtExitWaiter = null;
|
|
351
|
+
this.pvtMessageSubs = [];
|
|
352
|
+
this.pvtStateSubs = [];
|
|
353
|
+
this.pvtWanted = /* @__PURE__ */ new Set();
|
|
354
|
+
this.ref = ref;
|
|
355
|
+
this.pname = ref.pname;
|
|
356
|
+
this.id = Symbol(ref.pname);
|
|
357
|
+
this.pvtSend = send;
|
|
358
|
+
this.pvtFromName = fromName;
|
|
359
|
+
this.pvtLetGo = letGo;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Whether the process this handle names can still be reached: the wire is there, and
|
|
363
|
+
* this side has not let the handle go.
|
|
364
|
+
*
|
|
365
|
+
* Reaching it is not the same as it being alive. A process that has ended is still
|
|
366
|
+
* one this side has a handle on — it is what the handle points at, and what it left
|
|
367
|
+
* behind is here — so an end does not put it out of reach; `hasEnded()` is that
|
|
368
|
+
* answer, and the two are asked separately. From the two ways out there is no coming
|
|
369
|
+
* back: no reconnect, and no way to un-release.
|
|
370
|
+
*/
|
|
371
|
+
isConnected() {
|
|
372
|
+
return this.pvtConnected && !this.pvtReleased;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The connection is gone. Every handle this side holds goes out of reach with it,
|
|
376
|
+
* and says so: there is no reconnect to wait for, so a send fails here and now
|
|
377
|
+
* rather than disappearing into nothing.
|
|
378
|
+
*/
|
|
379
|
+
disconnect() {
|
|
380
|
+
if (!this.pvtConnected) return;
|
|
381
|
+
this.pvtConnected = false;
|
|
382
|
+
this.pvtStateSubs.forEach((fn) => fn());
|
|
383
|
+
this.pvtExitWaiter?.reject(/* @__PURE__ */ new Error(`${this.pname} cannot be reached: the connection is closed`));
|
|
384
|
+
}
|
|
385
|
+
/** Whether the process it names has ended, as far as this side has been told. */
|
|
386
|
+
hasEnded() {
|
|
387
|
+
return this.pvtExit !== null;
|
|
388
|
+
}
|
|
389
|
+
/** Why nothing can be asked *of the process* — let go, ended, or out of reach — or
|
|
390
|
+
* nothing when something can. Those are not one thing said three ways: an ended
|
|
391
|
+
* process is still reached, it simply has nothing left to receive. */
|
|
392
|
+
pvtWhyNotAskable() {
|
|
393
|
+
if (this.pvtReleased) return `${this.pname} was released`;
|
|
394
|
+
if (this.pvtExit !== null) return `${this.pname} has ended`;
|
|
395
|
+
if (!this.pvtConnected) return `${this.pname} cannot be reached: the connection is closed`;
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
pvtRequireAskable() {
|
|
399
|
+
const why = this.pvtWhyNotAskable();
|
|
400
|
+
if (why !== null) throw new Error(why);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Wait for the process to end. Its exit crosses back like everything else it
|
|
404
|
+
* does, and what it left behind is the answer.
|
|
405
|
+
*
|
|
406
|
+
* A handle whose connection is gone rejects instead: what is left to wait for
|
|
407
|
+
* after that is nothing, and a process that keeps running is not something this
|
|
408
|
+
* side can tell from one that died with the wire.
|
|
409
|
+
*/
|
|
410
|
+
wait() {
|
|
411
|
+
if (this.pvtExit !== null) return Promise.resolve(this.pvtExit);
|
|
412
|
+
const why = this.pvtWhyNotAskable();
|
|
413
|
+
if (why !== null) return Promise.reject(new Error(why));
|
|
414
|
+
this.pvtWant("exit");
|
|
415
|
+
this.pvtExitWaiter ??= makeWaiter();
|
|
416
|
+
return this.pvtExitWaiter.promise;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Ask the far side to stop it. Stops the way it always does there, and this
|
|
420
|
+
* resolves when its exit has crossed back — which is all this side can know.
|
|
421
|
+
* There is no deadline, so a process that refuses to stop leaves this pending.
|
|
422
|
+
*/
|
|
423
|
+
stop() {
|
|
424
|
+
this.pvtRequireAskable();
|
|
425
|
+
this.pvtWant("exit");
|
|
426
|
+
this.pvtSend({ $stop: {} }, this.ref.id);
|
|
427
|
+
return this.wait().then(() => void 0);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Let it go: the far side forgets this id and stops telling this side anything
|
|
431
|
+
* about the process behind it. The process itself is untouched — it runs on, it
|
|
432
|
+
* is simply nobody's here any more — and this handle is done: nothing reaches it,
|
|
433
|
+
* nothing is waited for, and what arrives for it later is not news.
|
|
434
|
+
*/
|
|
435
|
+
release() {
|
|
436
|
+
this.pvtRequireAskable();
|
|
437
|
+
this.pvtReleased = true;
|
|
438
|
+
this.pvtLetGo?.(this.ref.id);
|
|
439
|
+
this.pvtSend({ $release: {} }, this.ref.id);
|
|
440
|
+
}
|
|
441
|
+
/** Stop feeding it messages. It is still there: `send` still reaches it. */
|
|
442
|
+
pause() {
|
|
443
|
+
this.pvtRequireAskable();
|
|
444
|
+
this.pvtSend({ $pause: {} }, this.ref.id);
|
|
445
|
+
}
|
|
446
|
+
/** Feed it messages again. */
|
|
447
|
+
resume() {
|
|
448
|
+
this.pvtRequireAskable();
|
|
449
|
+
this.pvtSend({ $resume: {} }, this.ref.id);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* What to be told about it from now on: any of `message`, `state` and `exit`,
|
|
453
|
+
* or `"silent"` for none of it.
|
|
454
|
+
*
|
|
455
|
+
* A process that crossed is silent to begin with, so this is how a handle asks
|
|
456
|
+
* to hear anything at all — and what is heard is only ever what was asked for.
|
|
457
|
+
* An exit nobody asked for is not sent: silence is silence, which makes this the
|
|
458
|
+
* one way to leave a handle that will not answer a `wait()`, and the far side's
|
|
459
|
+
* books the one place an end can go unrecorded.
|
|
460
|
+
*/
|
|
461
|
+
tune(kinds) {
|
|
462
|
+
this.pvtRequireAskable();
|
|
463
|
+
this.pvtTune(kinds === "silent" ? [] : kinds);
|
|
464
|
+
}
|
|
465
|
+
/** Ask the far side to say exactly this much, and nothing when it is already
|
|
466
|
+
* saying it. The list goes in the order the three are named in, so asking for
|
|
467
|
+
* the same thing twice sends the same frame twice. */
|
|
468
|
+
pvtTune(wanted) {
|
|
469
|
+
const kinds = STREAM_KINDS.filter((kind) => wanted.includes(kind));
|
|
470
|
+
if (kinds.length === this.pvtWanted.size && kinds.every((kind) => this.pvtWanted.has(kind))) return;
|
|
471
|
+
this.pvtWanted = new Set(kinds);
|
|
472
|
+
this.pvtSend({ $tune: { streams: kinds } }, this.ref.id);
|
|
473
|
+
}
|
|
474
|
+
/** Ask for one more category, because something here now wants it. A handle
|
|
475
|
+
* that is gone asks nothing: there is no far side left to hear it, and nothing
|
|
476
|
+
* was asked for, so what was wanted stays what it was. */
|
|
477
|
+
pvtWant(kind) {
|
|
478
|
+
if (this.pvtWanted.has(kind)) return;
|
|
479
|
+
if (!this.pvtConnected || this.pvtReleased) return;
|
|
480
|
+
this.pvtTune([...this.pvtWanted, kind]);
|
|
481
|
+
}
|
|
482
|
+
/** Hand the far process a message. */
|
|
483
|
+
send(msg) {
|
|
484
|
+
this.pvtRequireAskable();
|
|
485
|
+
this.pvtSend({ $msg: {
|
|
486
|
+
fromName: this.pvtFromName,
|
|
487
|
+
body: msg
|
|
488
|
+
} }, this.ref.id);
|
|
489
|
+
}
|
|
490
|
+
subscribe(channel, cb) {
|
|
491
|
+
if (channel === "message") {
|
|
492
|
+
const fn = cb;
|
|
493
|
+
this.pvtMessageSubs.push(fn);
|
|
494
|
+
this.pvtWant("message");
|
|
495
|
+
return () => this.pvtUnsubscribe(this.pvtMessageSubs, fn);
|
|
496
|
+
}
|
|
497
|
+
const fn = cb;
|
|
498
|
+
this.pvtStateSubs.push(fn);
|
|
499
|
+
this.pvtWant("state");
|
|
500
|
+
return () => this.pvtUnsubscribe(this.pvtStateSubs, fn);
|
|
501
|
+
}
|
|
502
|
+
pvtUnsubscribe(list, fn) {
|
|
503
|
+
const idx = list.indexOf(fn);
|
|
504
|
+
if (idx >= 0) list.splice(idx, 1);
|
|
505
|
+
}
|
|
506
|
+
/** Its state, as the far side just published it. */
|
|
507
|
+
receiveState(state) {
|
|
508
|
+
if (this.pvtReleased) return;
|
|
509
|
+
this.state = Object.assign(this.state ?? {}, state);
|
|
510
|
+
this.pvtStateSubs.forEach((fn) => fn());
|
|
511
|
+
}
|
|
512
|
+
/** A message it emitted. */
|
|
513
|
+
receiveMessage(msg, fromName) {
|
|
514
|
+
if (this.pvtReleased) return;
|
|
515
|
+
this.pvtMessageSubs.forEach((fn) => fn(msg, fromName));
|
|
516
|
+
}
|
|
517
|
+
/** It has ended, and this is what it left behind. The last thing said about a
|
|
518
|
+
* process: nothing crosses about it after its exit, though the handle still
|
|
519
|
+
* reaches it for what it left. */
|
|
520
|
+
receiveExit(exit) {
|
|
521
|
+
if (this.pvtExit !== null || this.pvtReleased) return;
|
|
522
|
+
this.pvtExit = exit;
|
|
523
|
+
this.pvtExitWaiter?.resolve(exit);
|
|
524
|
+
}
|
|
525
|
+
/** The methods it can answer, as functions that ask it for an answer. `call`
|
|
526
|
+
* is the connection's own: a handle knows what to ask for, not how a call is
|
|
527
|
+
* made or paired with its answer. */
|
|
528
|
+
receiveMethods(names, call) {
|
|
529
|
+
for (const name of names) this.$reflection[name] = (...args) => call(name, args);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/remote/process-streams.ts
|
|
534
|
+
/** What a process can be asked over the wire: the functions on its reflection
|
|
535
|
+
* surface, read once. On the side that serves it the list *is* the dispatch
|
|
536
|
+
* table, so a name that was never on it can never be reached. */
|
|
537
|
+
const pvtReflectionNames = /* @__PURE__ */ new WeakMap();
|
|
538
|
+
function reflectionNames(target) {
|
|
539
|
+
const known = pvtReflectionNames.get(target);
|
|
540
|
+
if (known) return known;
|
|
541
|
+
const surface = target.$reflection ?? {};
|
|
542
|
+
const names = Object.keys(surface).filter((name) => typeof surface[name] === "function");
|
|
543
|
+
pvtReflectionNames.set(target, names);
|
|
544
|
+
return names;
|
|
545
|
+
}
|
|
546
|
+
var ProcessStreams = class {
|
|
547
|
+
constructor(sink, rootId) {
|
|
548
|
+
this.pvtPending = [];
|
|
549
|
+
this.pvtRecords = /* @__PURE__ */ new Map();
|
|
550
|
+
this.pvtFlushing = false;
|
|
551
|
+
this.pvtSink = sink;
|
|
552
|
+
this.pvtRootId = rootId;
|
|
553
|
+
}
|
|
554
|
+
/** A process was numbered: it is about to cross. Anything that is not a
|
|
555
|
+
* process — a handle travelling back to the side that holds it — crosses as
|
|
556
|
+
* the reference it came with and is nobody's to stream. */
|
|
557
|
+
crossed(proc, id) {
|
|
558
|
+
if (!isProcess(proc)) return;
|
|
559
|
+
this.pvtPending.push([proc, id]);
|
|
560
|
+
}
|
|
561
|
+
/** Say what there is to say about whatever crossed while a frame was written. */
|
|
562
|
+
flush() {
|
|
563
|
+
if (this.pvtFlushing) return;
|
|
564
|
+
this.pvtFlushing = true;
|
|
565
|
+
try {
|
|
566
|
+
while (this.pvtPending.length > 0) {
|
|
567
|
+
const next = this.pvtPending.shift();
|
|
568
|
+
if (next) this.pvtStream(next[0], next[1]);
|
|
569
|
+
}
|
|
570
|
+
} finally {
|
|
571
|
+
this.pvtFlushing = false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* What to say about it from now on: the categories named, and none of the
|
|
576
|
+
* others. Asking for its state says it there and then — a handle that has just
|
|
577
|
+
* asked what it holds should not have to wait for it to change.
|
|
578
|
+
*
|
|
579
|
+
* An id with no stream here is not this side's to tune, and the frame is
|
|
580
|
+
* dropped: both sides tune only what they hold, so an id names a stream on the
|
|
581
|
+
* side that numbered it.
|
|
582
|
+
*/
|
|
583
|
+
tune(id, kinds) {
|
|
584
|
+
const record = this.pvtRecords.get(id);
|
|
585
|
+
if (!record) return;
|
|
586
|
+
const wanted = new Set(kinds);
|
|
587
|
+
for (const kind of STREAM_KINDS) {
|
|
588
|
+
const isOn = record.kinds.has(kind);
|
|
589
|
+
const want = wanted.has(kind);
|
|
590
|
+
if (want === isOn) continue;
|
|
591
|
+
if (want) {
|
|
592
|
+
record.kinds.add(kind);
|
|
593
|
+
this.pvtArm(record, id, kind);
|
|
594
|
+
} else {
|
|
595
|
+
record.kinds.delete(kind);
|
|
596
|
+
this.pvtDisarm(record, kind);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Say nothing more about this one. It is not that the process ended — the far
|
|
602
|
+
* side asked to be left out of it, so its exit is nobody's news over here, and
|
|
603
|
+
* dropping the subscription is what keeps it from being sent.
|
|
604
|
+
*/
|
|
605
|
+
stop(id) {
|
|
606
|
+
const record = this.pvtRecords.get(id);
|
|
607
|
+
if (!record) return;
|
|
608
|
+
for (const stop of record.stops.values()) stop();
|
|
609
|
+
this.pvtRecords.delete(id);
|
|
610
|
+
}
|
|
611
|
+
/** Nothing more is said about any of them: the connection is done. */
|
|
612
|
+
stopAll() {
|
|
613
|
+
for (const id of [...this.pvtRecords.keys()]) this.stop(id);
|
|
614
|
+
this.pvtPending.length = 0;
|
|
615
|
+
}
|
|
616
|
+
pvtStream(target, id) {
|
|
617
|
+
const record = {
|
|
618
|
+
target,
|
|
619
|
+
kinds: /* @__PURE__ */ new Set(),
|
|
620
|
+
stops: /* @__PURE__ */ new Map()
|
|
621
|
+
};
|
|
622
|
+
this.pvtRecords.set(id, record);
|
|
623
|
+
this.pvtSink({ [REFLECT_METHODS]: reflectionNames(target) }, id);
|
|
624
|
+
for (const kind of record.kinds) this.pvtArm(record, id, kind);
|
|
625
|
+
if (id !== this.pvtRootId) target.wait().then(() => void this.sayEnded(id, 0), () => void this.sayEnded(id, 1));
|
|
626
|
+
}
|
|
627
|
+
/** Start saying this much about it, and say what there is to say now. */
|
|
628
|
+
pvtArm(record, id, kind) {
|
|
629
|
+
if (kind === "message") {
|
|
630
|
+
record.stops.set("message", record.target.subscribe("message", (msg, sender) => {
|
|
631
|
+
this.pvtSink({ $msg: {
|
|
632
|
+
fromName: sender.fromName,
|
|
633
|
+
body: msg
|
|
634
|
+
} }, id);
|
|
635
|
+
}));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (kind === "state") {
|
|
639
|
+
record.stops.set("state", record.target.subscribe("state", () => {
|
|
640
|
+
this.pvtSink({ $state: record.target.state }, id);
|
|
641
|
+
}));
|
|
642
|
+
this.pvtSink({ $state: record.target.state }, id);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
pvtDisarm(record, kind) {
|
|
646
|
+
const stop = record.stops.get(kind);
|
|
647
|
+
if (!stop) return;
|
|
648
|
+
stop();
|
|
649
|
+
record.stops.delete(kind);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* It has ended: say so once, if that is among the categories asked for, and say
|
|
653
|
+
* nothing more about it either way. Awaited by whoever has to have the frame out
|
|
654
|
+
* before the wire closes, which is the root's end and nobody else's — no other
|
|
655
|
+
* process can end after this side has stopped saying anything about it.
|
|
656
|
+
*/
|
|
657
|
+
async sayEnded(id, code) {
|
|
658
|
+
const record = this.pvtRecords.get(id);
|
|
659
|
+
if (!record) return;
|
|
660
|
+
const told = record.kinds.has("exit");
|
|
661
|
+
const state = record.target.state;
|
|
662
|
+
this.stop(id);
|
|
663
|
+
if (told) await this.pvtSink({ $exit: {
|
|
664
|
+
code,
|
|
665
|
+
state
|
|
666
|
+
} }, id);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/remote/sender.ts
|
|
671
|
+
function makeSender(fromName, parentName, parentId) {
|
|
672
|
+
if (parentId && fromName === parentName) return {
|
|
673
|
+
fromName,
|
|
674
|
+
fromId: parentId
|
|
675
|
+
};
|
|
676
|
+
return {
|
|
677
|
+
fromName,
|
|
678
|
+
fromId: Symbol()
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/remote/call-side.ts
|
|
683
|
+
var CallSide = class {
|
|
684
|
+
constructor(write, table) {
|
|
685
|
+
this.pvtCalls = 0;
|
|
686
|
+
this.pvtWaiting = /* @__PURE__ */ new Map();
|
|
687
|
+
this.pvtWrite = write;
|
|
688
|
+
this.pvtTable = table;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Ask a process on the far side for one of its methods. Processes among the
|
|
692
|
+
* arguments become references first — this side's own, with ids from its table, or
|
|
693
|
+
* the far side's coming back — and what is left has to be JSON, or nothing is sent
|
|
694
|
+
* at all.
|
|
695
|
+
*/
|
|
696
|
+
call(method, callArgs, to) {
|
|
697
|
+
return new Promise((resolve, reject) => {
|
|
698
|
+
const seq = ++this.pvtCalls;
|
|
699
|
+
const frame = encodeFrame({ [`${REFLECT_CALL}${method}`]: {
|
|
700
|
+
seq,
|
|
701
|
+
args: callArgs
|
|
702
|
+
} }, this.pvtTable, to);
|
|
703
|
+
const problem = jsonProblem(frame);
|
|
704
|
+
if (problem !== null) {
|
|
705
|
+
reject(/* @__PURE__ */ new Error(`cannot send ${problem} to ${method}`));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.pvtWaiting.set(seq, {
|
|
709
|
+
name: method,
|
|
710
|
+
resolve,
|
|
711
|
+
reject
|
|
712
|
+
});
|
|
713
|
+
this.pvtWrite(frame).catch((error) => {
|
|
714
|
+
this.pvtWaiting.delete(seq);
|
|
715
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
/** One function per announced name, on the surface whoever calls reads. */
|
|
720
|
+
install(surface, methods, to) {
|
|
721
|
+
for (const method of methods) surface[method] = (...callArgs) => this.call(method, callArgs, to);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* An answer to a call this side made: the call waiting under that `seq` is settled
|
|
725
|
+
* by it, and the frame says whether it was one at all. An answer is about the call
|
|
726
|
+
* and not about a process, so a dispatcher reads it before it looks anything up.
|
|
727
|
+
*/
|
|
728
|
+
settle(frame) {
|
|
729
|
+
const result = asReflectionResult(frame);
|
|
730
|
+
if (!result) return false;
|
|
731
|
+
const waiting = this.pvtWaiting.get(result.seq);
|
|
732
|
+
if (waiting) {
|
|
733
|
+
this.pvtWaiting.delete(result.seq);
|
|
734
|
+
if (result.error === void 0) waiting.resolve(result.value);
|
|
735
|
+
else waiting.reject(/* @__PURE__ */ new Error(`${waiting.name}: ${result.error}`));
|
|
736
|
+
}
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
/** A call that was in flight when the wire went has no answer coming; it fails
|
|
740
|
+
* here rather than hanging for ever. */
|
|
741
|
+
rejectAll() {
|
|
742
|
+
for (const waiting of this.pvtWaiting.values()) waiting.reject(/* @__PURE__ */ new Error(`connection closed before ${waiting.name} answered`));
|
|
743
|
+
this.pvtWaiting.clear();
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
/**
|
|
747
|
+
* Answer one call with what the process said, or why it could not. A method may be
|
|
748
|
+
* written for a call that answers later; the wire has no opinion about that, it just
|
|
749
|
+
* waits for the frame.
|
|
750
|
+
*/
|
|
751
|
+
async function answerCall(call, target, write, table) {
|
|
752
|
+
const reply = (body) => write(encodeFrame({ [`${REFLECT_RESULT}${call.name}`]: body }, table, table.farRootId()));
|
|
753
|
+
if (!isProcess(target)) {
|
|
754
|
+
await reply({
|
|
755
|
+
seq: call.seq,
|
|
756
|
+
error: "no process with that id on this connection"
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const names = reflectionNames(target);
|
|
761
|
+
const surface = target.$reflection;
|
|
762
|
+
const method = names.includes(call.name) ? surface[call.name] : void 0;
|
|
763
|
+
if (typeof method !== "function") {
|
|
764
|
+
await reply({
|
|
765
|
+
seq: call.seq,
|
|
766
|
+
error: `no reflection method named ${call.name}`
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const value = encodeProcessRefs(await method(...call.args), table);
|
|
772
|
+
const problem = jsonProblem(value);
|
|
773
|
+
if (problem !== null) {
|
|
774
|
+
await reply({
|
|
775
|
+
seq: call.seq,
|
|
776
|
+
error: `${call.name} returned ${problem}, which cannot cross a frame`
|
|
777
|
+
});
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
await reply({
|
|
781
|
+
seq: call.seq,
|
|
782
|
+
value
|
|
783
|
+
});
|
|
784
|
+
} catch (err) {
|
|
785
|
+
await reply({
|
|
786
|
+
seq: call.seq,
|
|
787
|
+
error: err instanceof Error ? err.message : String(err)
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/remote/server.ts
|
|
793
|
+
/** The name the served actor is spawned under, and the one it answers to as a sender when
|
|
794
|
+
* it talks to a process of the far side's: what the client states in `$init`, since the
|
|
795
|
+
* process being served is the one its proxy was forked as over there. The fallback is for
|
|
796
|
+
* a connection that states none — an older client, or a hand-written `$init` — where the
|
|
797
|
+
* far side's name for its own end is all there is to go on. */
|
|
798
|
+
let rootName = SERVED_ROOT_NAME;
|
|
799
|
+
async function serveRemoteActor(actor, spawner) {
|
|
800
|
+
const channel = await spawner();
|
|
801
|
+
const table = new ProcessTable("even", (proc, id) => streams.crossed(proc, id));
|
|
802
|
+
/** Put an encoded frame on the wire, and then say what it numbered. A frame that
|
|
803
|
+
* carried a reference has to be out before anything about that process is. */
|
|
804
|
+
const write = async (frame) => {
|
|
805
|
+
await channel.send(frame);
|
|
806
|
+
streams.flush();
|
|
807
|
+
};
|
|
808
|
+
const sendToFrame = (frame, to) => write(encodeFrame(frame, table, to));
|
|
809
|
+
/** Every frame leaves this way. The answer is handed back, since the one frame that
|
|
810
|
+
* has to be out before the wire closes is awaited by the side that owns the wire. */
|
|
811
|
+
const sendTo = (frame, to) => sendToFrame(frame, to).catch((e) => {
|
|
812
|
+
console.error("Error sending out the frame", e);
|
|
813
|
+
});
|
|
814
|
+
const streams = new ProcessStreams(sendTo, table.rootId());
|
|
815
|
+
const calls = new CallSide(write, table);
|
|
816
|
+
/** The handle for a reference that arrived: the same one every time, and the process
|
|
817
|
+
* itself when the reference names a process of this side's own. The roots are
|
|
818
|
+
* numbered apart, so a reference to the far side's root is a reference to a process
|
|
819
|
+
* of theirs, and a handle like any of those. */
|
|
820
|
+
const handleFor = (ref) => {
|
|
821
|
+
const held = table.processFor(ref.id);
|
|
822
|
+
if (held) return held;
|
|
823
|
+
const known = table.farHandleFor(ref.id);
|
|
824
|
+
if (isRemoteProcess(known)) return known;
|
|
825
|
+
const handle = new RemoteProcess(ref, sendTo, rootName, (id) => table.release(id));
|
|
826
|
+
table.bindFar(ref.id, handle);
|
|
827
|
+
return handle;
|
|
828
|
+
};
|
|
829
|
+
const waiting = [];
|
|
830
|
+
await new Promise((resolve) => {
|
|
831
|
+
channel.onMessage((frame) => {
|
|
832
|
+
waiting.push(frame);
|
|
833
|
+
resolve();
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
channel.removeHandler();
|
|
837
|
+
const initFrame = decodeFrame(waiting.shift(), handleFor);
|
|
838
|
+
if (!isInit(initFrame)) throw new Error("serveRemoteActor: expected $init");
|
|
839
|
+
const init = initFrame.$init;
|
|
840
|
+
const stated = init.rootName;
|
|
841
|
+
if (typeof stated === "string" && stated !== "") rootName = stated;
|
|
842
|
+
const parentName = init.parentName ?? null;
|
|
843
|
+
const parentIdName = init.parentIdName ?? null;
|
|
844
|
+
const parentId = parentIdName ? Symbol.for(parentIdName) : null;
|
|
845
|
+
const { parentName: _pn, parentIdName: _pid, ...initArgs } = init;
|
|
846
|
+
const proc = await actor.spawn(initArgs, {
|
|
847
|
+
name: rootName,
|
|
848
|
+
parentName,
|
|
849
|
+
parentId
|
|
850
|
+
});
|
|
851
|
+
table.bindRoot(proc);
|
|
852
|
+
await proc.ready();
|
|
853
|
+
streams.crossed(proc, table.rootId());
|
|
854
|
+
streams.flush();
|
|
855
|
+
function dispatch(raw) {
|
|
856
|
+
const frame = decodeFrame(raw, handleFor);
|
|
857
|
+
if (calls.settle(frame)) return;
|
|
858
|
+
const to = frameTo(frame);
|
|
859
|
+
if (to === null) return;
|
|
860
|
+
const news = table.farHandleFor(to);
|
|
861
|
+
const mine = table.processFor(to);
|
|
862
|
+
if (isRelease(frame)) {
|
|
863
|
+
if (mine !== void 0) {
|
|
864
|
+
streams.stop(to);
|
|
865
|
+
table.release(to);
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (isTune(frame)) {
|
|
870
|
+
if (mine !== void 0) {
|
|
871
|
+
const kinds = tuneFrame(frame);
|
|
872
|
+
if (kinds !== null) streams.tune(to, kinds);
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (isReflectionMethods(frame)) {
|
|
877
|
+
if (isRemoteProcess(news)) news.receiveMethods(frame[REFLECT_METHODS], (method, args) => calls.call(method, args, to));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (isState(frame)) {
|
|
881
|
+
if (isRemoteProcess(news)) news.receiveState(frame.$state);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (isExit(frame)) {
|
|
885
|
+
if (isRemoteProcess(news)) news.receiveExit(frame.$exit);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (isStop(frame) || isPause(frame) || isResume(frame)) {
|
|
889
|
+
if (isProcess(mine)) {
|
|
890
|
+
const sender = makeSender(rootName, parentName, parentId);
|
|
891
|
+
if (isStop(frame)) mine.stop({ from: sender });
|
|
892
|
+
else if (isPause(frame)) mine.pause();
|
|
893
|
+
else mine.resume();
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (isMsg(frame)) {
|
|
898
|
+
const { fromName, body } = frame.$msg;
|
|
899
|
+
if (isProcess(mine)) mine.send(body, makeSender(fromName, parentName, parentId));
|
|
900
|
+
else if (isRemoteProcess(news)) news.receiveMessage(body, fromName);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const call = asReflectionCall(frame);
|
|
904
|
+
if (call) answerCall(call, mine, write, table);
|
|
905
|
+
}
|
|
906
|
+
channel.removeHandler();
|
|
907
|
+
channel.onMessage(dispatch);
|
|
908
|
+
for (const frame of waiting.splice(0)) dispatch(frame);
|
|
909
|
+
let code = 0;
|
|
910
|
+
try {
|
|
911
|
+
await proc.wait();
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error("server actor error:", err);
|
|
914
|
+
code = 1;
|
|
915
|
+
}
|
|
916
|
+
await streams.sayEnded(table.rootId(), code);
|
|
917
|
+
streams.stopAll();
|
|
918
|
+
calls.rejectAll();
|
|
919
|
+
await channel.close();
|
|
920
|
+
}
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/remote/client.ts
|
|
923
|
+
function remoteClient(name, spawner) {
|
|
924
|
+
return defineActor({
|
|
925
|
+
name,
|
|
926
|
+
handlers: {},
|
|
927
|
+
async setup(args) {
|
|
928
|
+
const table = new ProcessTable("odd", (proc, id) => streams.crossed(proc, id));
|
|
929
|
+
const root = this.ctx;
|
|
930
|
+
table.bindRoot(root);
|
|
931
|
+
const channel = await spawner(args);
|
|
932
|
+
const fromName = this.ctx.pname;
|
|
933
|
+
const parentName = this.ctx.parentName ?? this.ctx.pname;
|
|
934
|
+
const streams = new ProcessStreams((frame, to) => sendTo(frame, to), table.rootId());
|
|
935
|
+
/** Put an encoded frame on the wire, and then say what it numbered. A frame that
|
|
936
|
+
* carried a reference has to be out first, or the far side hears about a process
|
|
937
|
+
* it cannot name yet. */
|
|
938
|
+
const write = (frame) => Promise.resolve(channel.send(frame)).then(() => streams.flush());
|
|
939
|
+
/** Every frame leaves this way: the processes in it become references, and
|
|
940
|
+
* what the frame is addressed to becomes the id the far side knows. */
|
|
941
|
+
const sendTo = (frame, to) => {
|
|
942
|
+
write(encodeFrame(frame, table, to)).catch((error) => {
|
|
943
|
+
console.error("Error sending out the frame", error);
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
sendTo({ $init: {
|
|
947
|
+
...args,
|
|
948
|
+
rootName: fromName,
|
|
949
|
+
parentName,
|
|
950
|
+
parentIdName: parentName
|
|
951
|
+
} }, table.farRootId());
|
|
952
|
+
let currentState = {};
|
|
953
|
+
const calls = new CallSide(write, table);
|
|
954
|
+
/**
|
|
955
|
+
* The handle for a reference that arrived. A process handed over twice is one
|
|
956
|
+
* process, so a reference that names one this side already knows gives back the
|
|
957
|
+
* handle it gave last time, and one that names a process of this side's own —
|
|
958
|
+
* handed back to its owner — gives back that process.
|
|
959
|
+
*
|
|
960
|
+
* The roots are numbered apart, so the numbers say what belongs to whom: 1 is this
|
|
961
|
+
* side's own end, and 0 is the far side's root, which is a handle like any of the
|
|
962
|
+
* processes they hold.
|
|
963
|
+
*/
|
|
964
|
+
const handleFor = (ref) => {
|
|
965
|
+
const held = table.processFor(ref.id);
|
|
966
|
+
if (held) return held;
|
|
967
|
+
const known = table.farHandleFor(ref.id);
|
|
968
|
+
if (isRemoteProcess(known)) return known;
|
|
969
|
+
const handle = new RemoteProcess(ref, sendTo, fromName, (id) => table.release(id));
|
|
970
|
+
table.bindFar(ref.id, handle);
|
|
971
|
+
return handle;
|
|
972
|
+
};
|
|
973
|
+
/**
|
|
974
|
+
* The far side's root, which is what this proxy stands in for. It is bound here
|
|
975
|
+
* like any other reference the far side sends: what it holds, what it says, when
|
|
976
|
+
* it is over and what it can answer reach it as they reach any handle.
|
|
977
|
+
*/
|
|
978
|
+
const farRoot = new RemoteProcess({
|
|
979
|
+
id: table.farRootId(),
|
|
980
|
+
pname: fromName
|
|
981
|
+
}, sendTo, fromName, (id) => table.release(id));
|
|
982
|
+
table.bindFar(table.farRootId(), farRoot);
|
|
983
|
+
let started = null;
|
|
984
|
+
const start = new Promise((resolve) => {
|
|
985
|
+
started = resolve;
|
|
986
|
+
});
|
|
987
|
+
farRoot.tune([
|
|
988
|
+
"message",
|
|
989
|
+
"state",
|
|
990
|
+
"exit"
|
|
991
|
+
]);
|
|
992
|
+
farRoot.subscribe("state", () => {
|
|
993
|
+
Object.assign(currentState, farRoot.state ?? {});
|
|
994
|
+
root.notify();
|
|
995
|
+
started?.();
|
|
996
|
+
started = null;
|
|
997
|
+
});
|
|
998
|
+
farRoot.subscribe("message", (msg) => {
|
|
999
|
+
this.emit(msg);
|
|
1000
|
+
});
|
|
1001
|
+
const exitPromise = farRoot.wait().then((exit) => ({
|
|
1002
|
+
code: exit.code,
|
|
1003
|
+
state: exit.state
|
|
1004
|
+
}), () => ({
|
|
1005
|
+
code: null,
|
|
1006
|
+
state: currentState
|
|
1007
|
+
}));
|
|
1008
|
+
channel.onMessage((raw) => {
|
|
1009
|
+
const frame = decodeFrame(raw, handleFor);
|
|
1010
|
+
if (calls.settle(frame)) return;
|
|
1011
|
+
const to = frameTo(frame);
|
|
1012
|
+
if (to === null) return;
|
|
1013
|
+
const news = table.farHandleFor(to);
|
|
1014
|
+
const mine = table.processFor(to);
|
|
1015
|
+
if (isRelease(frame)) {
|
|
1016
|
+
if (mine !== void 0) {
|
|
1017
|
+
streams.stop(to);
|
|
1018
|
+
table.release(to);
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (isReflectionMethods(frame)) {
|
|
1023
|
+
if (isRemoteProcess(news)) {
|
|
1024
|
+
news.receiveMethods(frame[REFLECT_METHODS], (method, callArgs) => calls.call(method, callArgs, to));
|
|
1025
|
+
if (to === table.farRootId()) calls.install(this.reflection, frame[REFLECT_METHODS], table.farRootId());
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (isState(frame)) {
|
|
1030
|
+
if (isRemoteProcess(news)) news.receiveState(frame.$state);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (isExit(frame)) {
|
|
1034
|
+
if (isRemoteProcess(news)) news.receiveExit(frame.$exit);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (isMsg(frame)) {
|
|
1038
|
+
if (isProcess(mine)) mine.send(frame.$msg.body, makeSender(frame.$msg.fromName, null, null));
|
|
1039
|
+
else if (isRemoteProcess(news)) news.receiveMessage(frame.$msg.body, frame.$msg.fromName);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
if (isTune(frame)) {
|
|
1043
|
+
if (mine !== void 0) {
|
|
1044
|
+
const kinds = tuneFrame(frame);
|
|
1045
|
+
if (kinds !== null) streams.tune(to, kinds);
|
|
1046
|
+
}
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (isStop(frame) || isPause(frame) || isResume(frame)) {
|
|
1050
|
+
if (isProcess(mine)) {
|
|
1051
|
+
if (isStop(frame)) mine.stop({ from: makeSender(fromName, null, null) });
|
|
1052
|
+
else if (isPause(frame)) mine.pause();
|
|
1053
|
+
else mine.resume();
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const call = asReflectionCall(frame);
|
|
1058
|
+
if (call) answerCall(call, mine, write, table);
|
|
1059
|
+
});
|
|
1060
|
+
channel.onClose(() => {
|
|
1061
|
+
calls.rejectAll();
|
|
1062
|
+
for (const held of table.handles()) if (isRemoteProcess(held)) held.disconnect();
|
|
1063
|
+
streams.stopAll();
|
|
1064
|
+
});
|
|
1065
|
+
await start;
|
|
1066
|
+
return {
|
|
1067
|
+
public: currentState,
|
|
1068
|
+
private: {
|
|
1069
|
+
channel,
|
|
1070
|
+
farRoot,
|
|
1071
|
+
exitPromise
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
},
|
|
1075
|
+
async onMessage(msg) {
|
|
1076
|
+
const { farRoot } = this.state.private;
|
|
1077
|
+
if (farRoot.isConnected()) farRoot.send(msg);
|
|
1078
|
+
return stopPropagation();
|
|
1079
|
+
},
|
|
1080
|
+
async onStopRequested() {
|
|
1081
|
+
const { channel, farRoot, exitPromise } = this.state.private;
|
|
1082
|
+
if (channel) {
|
|
1083
|
+
if (farRoot.isConnected() && !farRoot.hasEnded()) farRoot.stop().catch(() => void 0);
|
|
1084
|
+
await exitPromise;
|
|
1085
|
+
}
|
|
1086
|
+
this.agreeToStop();
|
|
1087
|
+
},
|
|
1088
|
+
async afterEnd() {
|
|
1089
|
+
if (this.state.private.channel) await this.state.private.channel.close();
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
//#endregion
|
|
1094
|
+
//#region src/remote/protocols/json1.ts
|
|
1095
|
+
const VERSION = "json.v1";
|
|
1096
|
+
function encode(frame) {
|
|
1097
|
+
return JSON.stringify(frame);
|
|
1098
|
+
}
|
|
1099
|
+
function decode(str) {
|
|
1100
|
+
return JSON.parse(str);
|
|
1101
|
+
}
|
|
1102
|
+
function json1Channel(transport) {
|
|
1103
|
+
return {
|
|
1104
|
+
send(frame) {
|
|
1105
|
+
return transport.send(encode(frame));
|
|
1106
|
+
},
|
|
1107
|
+
onMessage(handler) {
|
|
1108
|
+
transport.onMessage((str) => handler(decode(str)));
|
|
1109
|
+
},
|
|
1110
|
+
removeHandler() {
|
|
1111
|
+
transport.removeHandler();
|
|
1112
|
+
},
|
|
1113
|
+
onClose(handler) {
|
|
1114
|
+
transport.onClose(handler);
|
|
1115
|
+
},
|
|
1116
|
+
close() {
|
|
1117
|
+
return transport.close();
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
1122
|
+
export { isStop as _, remoteClient as a, rootIdFor as b, RemoteProcess as c, isMsg as d, isPause as f, isState as g, isResume as h, json1Channel as i, isExit as l, isRelease as m, decode as n, serveRemoteActor as o, isProto as p, encode as r, makeSender as s, VERSION as t, isInit as u, PROCESS_REF as v, isRemoteProcess as y };
|
|
1123
|
+
|
|
1124
|
+
//# sourceMappingURL=json1-DYq9g7im.js.map
|