@phreshos/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/channel.d.ts +6 -0
- package/dist/channel.js +19 -0
- package/dist/current.d.ts +21 -0
- package/dist/current.js +50 -0
- package/dist/deadline.d.ts +7 -0
- package/dist/deadline.js +13 -0
- package/dist/domain.d.ts +91 -0
- package/dist/domain.js +313 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.js +92 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.js +54 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +4 -0
- package/dist/served.d.ts +3 -0
- package/dist/served.js +41 -0
- package/dist/storage.d.ts +14 -0
- package/dist/storage.js +172 -0
- package/dist/wire.d.ts +38 -0
- package/dist/wire.js +234 -0
- package/package.json +31 -0
package/dist/wire.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import Deadline from "./deadline.js";
|
|
3
|
+
import { defaultTimeout } from "./events.js";
|
|
4
|
+
/** The server endpoint's sole IPC adapter. */
|
|
5
|
+
class Wire {
|
|
6
|
+
pending = new Map();
|
|
7
|
+
subscribers = new Map();
|
|
8
|
+
every = new Map();
|
|
9
|
+
answerers = new Map();
|
|
10
|
+
waiting = new Map();
|
|
11
|
+
impossible = new Map();
|
|
12
|
+
identity;
|
|
13
|
+
constructor() {
|
|
14
|
+
let identify = () => undefined;
|
|
15
|
+
this.identity = new Promise(resolve => { identify = resolve; });
|
|
16
|
+
process.on("message", message => {
|
|
17
|
+
if (!Array.isArray(message))
|
|
18
|
+
return;
|
|
19
|
+
const [route, ...values] = message;
|
|
20
|
+
if (route === "boundary") {
|
|
21
|
+
const [operation, ...rest] = values;
|
|
22
|
+
if (operation === "forget" && typeof rest[0] === "string")
|
|
23
|
+
this.forgetIncoming(rest[0]);
|
|
24
|
+
if (operation === "impossible" && typeof rest[0] === "string" && typeof rest[1] === "string") {
|
|
25
|
+
this.impossible.get(rest[0])?.(new Error(rest[1]));
|
|
26
|
+
this.impossible.delete(rest[0]);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (route === "host-end" && values[0] === "identity" && typeof values[1] === "string" && typeof values[2] === "string") {
|
|
31
|
+
identify({ process: values[1], program: values[2] });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (values[0] === "answer" && typeof values[1] === "string") {
|
|
35
|
+
this.settle(values[1], values.at(-1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (values[0] === "wait" && typeof values[1] === "string" && typeof values[2] === "string" && typeof values[3] === "string") {
|
|
39
|
+
this.receiveQuestion(route, values.slice(3), { address: values[1], id: values[2] });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.deliver(route, values);
|
|
43
|
+
});
|
|
44
|
+
process.send?.(["boundary", "identity"]);
|
|
45
|
+
process.send?.(["boundary", "ready"]);
|
|
46
|
+
}
|
|
47
|
+
send(route, ...values) {
|
|
48
|
+
process.send?.([route, ...values]);
|
|
49
|
+
}
|
|
50
|
+
request(values, timeout = defaultTimeout) {
|
|
51
|
+
return this.requestWithin(values, new Deadline(timeout));
|
|
52
|
+
}
|
|
53
|
+
requestWithin(values, deadline) {
|
|
54
|
+
const question = randomUUID();
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
this.send("boundary", "expect", question);
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
this.pending.delete(question);
|
|
59
|
+
this.send("boundary", "forget", question);
|
|
60
|
+
reject(new Error(`Answer timeout ${deadline.milliseconds}ms`));
|
|
61
|
+
}, deadline.remaining());
|
|
62
|
+
this.pending.set(question, { resolve, reject, timer });
|
|
63
|
+
this.send("end-host", "wait", question, ...values);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
expectWithin(question, deadline) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
this.send("boundary", "expect", question);
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
this.pending.delete(question);
|
|
71
|
+
this.send("boundary", "forget", question);
|
|
72
|
+
reject(new Error(`Answer timeout ${deadline.milliseconds}ms`));
|
|
73
|
+
}, deadline.remaining());
|
|
74
|
+
this.pending.set(question, { resolve, reject, timer });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
forget(question) {
|
|
78
|
+
const pending = this.pending.get(question);
|
|
79
|
+
if (pending)
|
|
80
|
+
clearTimeout(pending.timer);
|
|
81
|
+
this.pending.delete(question);
|
|
82
|
+
this.send("boundary", "forget", question);
|
|
83
|
+
}
|
|
84
|
+
on(route, event, handler, subject = null, impossible) {
|
|
85
|
+
const key = `${route}:${event}`;
|
|
86
|
+
const handlers = this.subscribers.get(key) ?? new Set();
|
|
87
|
+
handlers.add(handler);
|
|
88
|
+
this.subscribers.set(key, handlers);
|
|
89
|
+
const subscription = this.register("publish", route, event, subject, impossible);
|
|
90
|
+
return once(() => {
|
|
91
|
+
handlers.delete(handler);
|
|
92
|
+
if (!handlers.size)
|
|
93
|
+
this.subscribers.delete(key);
|
|
94
|
+
this.unregister(subscription);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
onAll(route, handler, subject = null) {
|
|
98
|
+
const handlers = this.every.get(route) ?? new Set();
|
|
99
|
+
handlers.add(handler);
|
|
100
|
+
this.every.set(route, handlers);
|
|
101
|
+
const subscription = this.register("publish", route, null, subject);
|
|
102
|
+
return once(() => {
|
|
103
|
+
handlers.delete(handler);
|
|
104
|
+
if (!handlers.size)
|
|
105
|
+
this.every.delete(route);
|
|
106
|
+
this.unregister(subscription);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
answer(route, event, handler) {
|
|
110
|
+
const key = `${route}:${event}`;
|
|
111
|
+
if (this.answerers.has(key))
|
|
112
|
+
throw new Error(`The "${event}" event already has an answerer`);
|
|
113
|
+
this.answerers.set(key, handler);
|
|
114
|
+
const subscription = this.register("ask", route, event, null);
|
|
115
|
+
this.releaseWaiting(route, event);
|
|
116
|
+
return once(() => {
|
|
117
|
+
if (this.answerers.get(key) === handler)
|
|
118
|
+
this.answerers.delete(key);
|
|
119
|
+
this.unregister(subscription);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
observe(target, half, kind, event, handler, impossible) {
|
|
123
|
+
const subscription = randomUUID();
|
|
124
|
+
const stop = this.on("observed", subscription, handler);
|
|
125
|
+
if (impossible)
|
|
126
|
+
this.impossible.set(subscription, impossible);
|
|
127
|
+
this.send("end-host", "observe", subscription, target, half, kind, event, impossible !== undefined);
|
|
128
|
+
return once(() => {
|
|
129
|
+
stop();
|
|
130
|
+
this.impossible.delete(subscription);
|
|
131
|
+
this.send("end-host", "unobserve", subscription);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
register(kind, route, event, subject, impossible) {
|
|
135
|
+
const subscription = randomUUID();
|
|
136
|
+
if (impossible)
|
|
137
|
+
this.impossible.set(subscription, impossible);
|
|
138
|
+
this.send("boundary", "subscribe", subscription, kind, route, event, subject, impossible !== undefined);
|
|
139
|
+
return subscription;
|
|
140
|
+
}
|
|
141
|
+
unregister(subscription) {
|
|
142
|
+
this.impossible.delete(subscription);
|
|
143
|
+
this.send("boundary", "unsubscribe", subscription);
|
|
144
|
+
}
|
|
145
|
+
deliver(route, values) {
|
|
146
|
+
const [event, ...message] = values;
|
|
147
|
+
if (typeof event !== "string")
|
|
148
|
+
return;
|
|
149
|
+
const handlers = [...this.subscribers.get(`${route}:${event}`) ?? []];
|
|
150
|
+
for (const handler of handlers)
|
|
151
|
+
invoke(handler, message);
|
|
152
|
+
const observers = [...this.every.get(route) ?? []];
|
|
153
|
+
for (const observer of observers)
|
|
154
|
+
invoke(observer, [event, ...message]);
|
|
155
|
+
}
|
|
156
|
+
receiveQuestion(route, values, question) {
|
|
157
|
+
const [event, ...message] = values;
|
|
158
|
+
if (typeof event !== "string")
|
|
159
|
+
return;
|
|
160
|
+
const answerer = this.answerers.get(`${route}:${event}`);
|
|
161
|
+
if (!answerer) {
|
|
162
|
+
const key = `${route}:${event}`;
|
|
163
|
+
const waiting = this.waiting.get(key) ?? [];
|
|
164
|
+
waiting.push({ values, question });
|
|
165
|
+
this.waiting.set(key, waiting);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let result;
|
|
169
|
+
try {
|
|
170
|
+
result = answerer(...message);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.sendAnswer(route, question, event, failed(error));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
Promise.resolve(result).then(answer => this.sendAnswer(route, question, event, { success: true, result: answer }), error => this.sendAnswer(route, question, event, failed(error)));
|
|
177
|
+
}
|
|
178
|
+
sendAnswer(route, question, event, outcome) {
|
|
179
|
+
this.send(route, "answer", question.address, question.id, event, outcome);
|
|
180
|
+
}
|
|
181
|
+
releaseWaiting(route, event) {
|
|
182
|
+
const key = `${route}:${event}`;
|
|
183
|
+
const waiting = this.waiting.get(key);
|
|
184
|
+
if (!waiting)
|
|
185
|
+
return;
|
|
186
|
+
this.waiting.delete(key);
|
|
187
|
+
for (const held of waiting)
|
|
188
|
+
this.receiveQuestion(route, held.values, held.question);
|
|
189
|
+
}
|
|
190
|
+
forgetIncoming(question) {
|
|
191
|
+
for (const [key, waiting] of this.waiting) {
|
|
192
|
+
const remaining = waiting.filter(entry => entry.question.address !== question);
|
|
193
|
+
if (remaining.length)
|
|
194
|
+
this.waiting.set(key, remaining);
|
|
195
|
+
else
|
|
196
|
+
this.waiting.delete(key);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
settle(question, outcome) {
|
|
200
|
+
const pending = this.pending.get(question);
|
|
201
|
+
if (!pending)
|
|
202
|
+
return;
|
|
203
|
+
this.pending.delete(question);
|
|
204
|
+
clearTimeout(pending.timer);
|
|
205
|
+
this.send("boundary", "forget", question);
|
|
206
|
+
if (outcome?.success === true)
|
|
207
|
+
pending.resolve(outcome.result);
|
|
208
|
+
else if (outcome?.success === false && typeof outcome.error === "string")
|
|
209
|
+
pending.reject(new Error(outcome.error));
|
|
210
|
+
else
|
|
211
|
+
pending.reject(new Error("The boundary returned an invalid outcome"));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function failed(error) {
|
|
215
|
+
return {
|
|
216
|
+
success: false,
|
|
217
|
+
error: error instanceof Error ? error.message : "An unknown exception occurred"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function invoke(handler, values) {
|
|
221
|
+
Promise.resolve().then(() => handler(...values)).catch(error => {
|
|
222
|
+
queueMicrotask(() => { throw error; });
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function once(cleanup) {
|
|
226
|
+
let active = true;
|
|
227
|
+
return () => {
|
|
228
|
+
if (!active)
|
|
229
|
+
return;
|
|
230
|
+
active = false;
|
|
231
|
+
cleanup();
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export default new Wire();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phreshos/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The SDK used by a Program's server endpoint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/main.js",
|
|
7
|
+
"types": "dist/main.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/main.d.ts",
|
|
11
|
+
"default": "./dist/main.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"check": "tsc --noEmit",
|
|
20
|
+
"build": "tsc --noEmit false --outDir dist --rootDir source",
|
|
21
|
+
"prepack": "node --run build"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@phreshos/core": "^0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@phreshos/core": "0.1.0",
|
|
28
|
+
"@types/node": "^26.2.0",
|
|
29
|
+
"typescript": "^6.0.3"
|
|
30
|
+
}
|
|
31
|
+
}
|