@net-mesh/browser 0.37.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/CHANGELOG.md +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- package/package.json +72 -0
package/dist/org.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organization-scoped streaming, as a page wants it (plan §4.5).
|
|
3
|
+
*
|
|
4
|
+
* The eight org verbs — {@link OrgUnaryHandler call}, streaming,
|
|
5
|
+
* client-streaming and duplex, each calling and serving — share one
|
|
6
|
+
* credential/option vocabulary and one terminal error taxonomy
|
|
7
|
+
* (`errors.ts`'s `OrgStreamError` family). The same eight names exist
|
|
8
|
+
* on {@link BrowserNode} and on {@link MeshSession}; the wrappers here
|
|
9
|
+
* serve both, so the two surfaces cannot drift into two spellings of
|
|
10
|
+
* one contract.
|
|
11
|
+
*
|
|
12
|
+
* **Suspension and closure (plan §4.5).** Deadlines are absolute: a
|
|
13
|
+
* frozen tab extends no lease and no window, so a call whose deadline
|
|
14
|
+
* passes while its tab is suspended retires with its deadline
|
|
15
|
+
* terminal on wake — {@link OrgTimeoutError}, or
|
|
16
|
+
* {@link OrgIndeterminateError} where the work may already be
|
|
17
|
+
* executing in another tab. Nothing resumes automatically: a retired
|
|
18
|
+
* call is never transparently re-opened, because re-opening is a
|
|
19
|
+
* fresh call with a fresh proof and MAY repeat effects. Node or tab
|
|
20
|
+
* close retires ownership — pending calls fail typed, and each
|
|
21
|
+
* dropped caller handle emits exactly one CANCEL.
|
|
22
|
+
*
|
|
23
|
+
* **Handler drop (F-S3.1-2), verbatim.** The retire supervisor may
|
|
24
|
+
* drop the handler future without a final poll — cancellation is
|
|
25
|
+
* observed through the retirement observables (the terminal item, the
|
|
26
|
+
* sink's typed closed refusal, and `retired`), never assumed as a
|
|
27
|
+
* handler-side event; a detached observer holding `retired` observes
|
|
28
|
+
* the signal. After retirement a handler sees typed refusals from
|
|
29
|
+
* `send`/`close`, and any return value it produces is discarded.
|
|
30
|
+
*/
|
|
31
|
+
import { fromWasmError, orgRetireReason, orgTerminalError, OrgCancelledError, ORG_SINK_CLOSED_REFUSAL, UnknownLeafError, } from './errors.js';
|
|
32
|
+
/**
|
|
33
|
+
* The live org handles one node or session handed out, so its
|
|
34
|
+
* teardown can retire them: node close retires ownership — pending
|
|
35
|
+
* calls fail typed, one CANCEL per dropped caller handle.
|
|
36
|
+
*/
|
|
37
|
+
export class OrgHandleRegistry {
|
|
38
|
+
live = new Set();
|
|
39
|
+
add(handle) {
|
|
40
|
+
this.live.add(handle);
|
|
41
|
+
}
|
|
42
|
+
delete(handle) {
|
|
43
|
+
this.live.delete(handle);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Retire every live handle with one typed terminal. Drains first,
|
|
47
|
+
* so a handle's own teardown cannot re-enter this loop. Every
|
|
48
|
+
* handle is attempted; the raw failures come back rather than being
|
|
49
|
+
* swallowed, for the owner's teardown ledger.
|
|
50
|
+
*/
|
|
51
|
+
retireAll(error) {
|
|
52
|
+
const open = [...this.live];
|
|
53
|
+
this.live.clear();
|
|
54
|
+
const failures = [];
|
|
55
|
+
for (const handle of open) {
|
|
56
|
+
try {
|
|
57
|
+
handle.retire(error);
|
|
58
|
+
}
|
|
59
|
+
catch (failure) {
|
|
60
|
+
failures.push(failure);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return failures;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The response half of an org call, wrapped: terminal items become
|
|
68
|
+
* the §4.3 typed errors, and the async iterator throws them.
|
|
69
|
+
*/
|
|
70
|
+
export class OrgStream {
|
|
71
|
+
inner;
|
|
72
|
+
onCancel;
|
|
73
|
+
onDone;
|
|
74
|
+
terminal = null;
|
|
75
|
+
/** A boundary rejection, latched so a second pull sees the same typed failure. */
|
|
76
|
+
failure = null;
|
|
77
|
+
handleDropped = false;
|
|
78
|
+
waiters = [];
|
|
79
|
+
/**
|
|
80
|
+
* @param `onCancel` drops the underlying handle — at most once, so
|
|
81
|
+
* one CANCEL leaves however many halves initiate the drop.
|
|
82
|
+
*/
|
|
83
|
+
constructor(inner, onCancel, onDone) {
|
|
84
|
+
this.inner = inner;
|
|
85
|
+
this.onCancel = onCancel;
|
|
86
|
+
this.onDone = onDone;
|
|
87
|
+
}
|
|
88
|
+
next() {
|
|
89
|
+
if (this.failure !== null)
|
|
90
|
+
return Promise.reject(this.failure);
|
|
91
|
+
if (this.terminal !== null)
|
|
92
|
+
return Promise.resolve(this.terminal);
|
|
93
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
94
|
+
const waiter = { resolve, reject };
|
|
95
|
+
this.waiters.push(waiter);
|
|
96
|
+
void this.inner.next().then((raw) => {
|
|
97
|
+
// BROWSER-4: this pull's waiter leaves the list the moment the
|
|
98
|
+
// pull settles — a completed pull retains nothing to the
|
|
99
|
+
// terminal.
|
|
100
|
+
spliceWaiter(this.waiters, waiter);
|
|
101
|
+
const item = byteItem(raw);
|
|
102
|
+
if (item.done)
|
|
103
|
+
this.complete(item);
|
|
104
|
+
// With-resolvers settlement is first-wins, so a pull a
|
|
105
|
+
// retirement already settled simply keeps that outcome.
|
|
106
|
+
resolve(item);
|
|
107
|
+
}, (error) => {
|
|
108
|
+
// A boundary rejection propagates as its classified self —
|
|
109
|
+
// a proxied `LeaderLost` is the existing `RpcError` kind,
|
|
110
|
+
// re-typed by the one mapper — and is latched for the next
|
|
111
|
+
// pull. Not folded into an error item: the item vocabulary
|
|
112
|
+
// is the §4.3 terminal set, and inventing a translation for
|
|
113
|
+
// a throw this package cannot name is how a taxonomy turns
|
|
114
|
+
// into guessing.
|
|
115
|
+
spliceWaiter(this.waiters, waiter);
|
|
116
|
+
const failure = fromWasmError(error);
|
|
117
|
+
// `fail` settles every still-parked pull and latches the
|
|
118
|
+
// failure for the next one; this pull settles itself.
|
|
119
|
+
this.fail(failure);
|
|
120
|
+
reject(failure);
|
|
121
|
+
});
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
cancel() {
|
|
125
|
+
this.retire(new OrgCancelledError());
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Retire this stream: one CANCEL at most, every parked and future
|
|
129
|
+
* `next()` settled with `error` as the terminal item.
|
|
130
|
+
*/
|
|
131
|
+
retire(error) {
|
|
132
|
+
if (this.terminal !== null || this.failure !== null)
|
|
133
|
+
return;
|
|
134
|
+
this.dropHandle();
|
|
135
|
+
this.complete({ done: true, error });
|
|
136
|
+
}
|
|
137
|
+
[Symbol.asyncIterator]() {
|
|
138
|
+
// A terminal's final body is yielded BEFORE the end (BROWSER-1):
|
|
139
|
+
// one done item carrying bytes is one yield plus one end, so the
|
|
140
|
+
// end is this iterator's own latch, not the item's `done`.
|
|
141
|
+
let ended = false;
|
|
142
|
+
return {
|
|
143
|
+
next: async () => {
|
|
144
|
+
if (ended)
|
|
145
|
+
return { done: true, value: undefined };
|
|
146
|
+
const item = await this.next();
|
|
147
|
+
// THROWS the typed error at the terminal error item — the
|
|
148
|
+
// idiomatic `for await` shape, so `catch` sees the §4.3 class.
|
|
149
|
+
if (item.error !== undefined)
|
|
150
|
+
throw item.error;
|
|
151
|
+
if (item.value === undefined) {
|
|
152
|
+
ended = true;
|
|
153
|
+
return { done: true, value: undefined };
|
|
154
|
+
}
|
|
155
|
+
if (item.done)
|
|
156
|
+
ended = true;
|
|
157
|
+
return { done: false, value: item.value };
|
|
158
|
+
},
|
|
159
|
+
return: async () => {
|
|
160
|
+
ended = true;
|
|
161
|
+
this.cancel();
|
|
162
|
+
return { done: true, value: undefined };
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
complete(item) {
|
|
167
|
+
if (this.terminal !== null)
|
|
168
|
+
return;
|
|
169
|
+
this.terminal = item;
|
|
170
|
+
this.onDone?.();
|
|
171
|
+
const waiters = [...this.waiters];
|
|
172
|
+
this.waiters.length = 0;
|
|
173
|
+
for (const waiter of waiters)
|
|
174
|
+
waiter.resolve(item);
|
|
175
|
+
}
|
|
176
|
+
fail(error) {
|
|
177
|
+
if (this.terminal !== null || this.failure !== null)
|
|
178
|
+
return;
|
|
179
|
+
this.failure = error;
|
|
180
|
+
this.onDone?.();
|
|
181
|
+
const waiters = [...this.waiters];
|
|
182
|
+
this.waiters.length = 0;
|
|
183
|
+
for (const waiter of waiters)
|
|
184
|
+
waiter.reject(error);
|
|
185
|
+
}
|
|
186
|
+
dropHandle() {
|
|
187
|
+
if (this.handleDropped)
|
|
188
|
+
return;
|
|
189
|
+
this.handleDropped = true;
|
|
190
|
+
try {
|
|
191
|
+
this.onCancel();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// A handle that refuses its own drop is already retired at the
|
|
195
|
+
// boundary; the typed terminal below is what the consumer sees.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** A client-streaming upload, wrapped. */
|
|
200
|
+
export class OrgUpload {
|
|
201
|
+
inner;
|
|
202
|
+
onDone;
|
|
203
|
+
failure = null;
|
|
204
|
+
handleDropped = false;
|
|
205
|
+
waiters = [];
|
|
206
|
+
constructor(inner, onDone) {
|
|
207
|
+
this.inner = inner;
|
|
208
|
+
this.onDone = onDone;
|
|
209
|
+
}
|
|
210
|
+
async send(payload) {
|
|
211
|
+
if (this.failure !== null)
|
|
212
|
+
throw this.failure;
|
|
213
|
+
try {
|
|
214
|
+
await this.inner.send(payload);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
throw fromWasmError(error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
finish() {
|
|
221
|
+
if (this.failure !== null)
|
|
222
|
+
return Promise.reject(this.failure);
|
|
223
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
224
|
+
const waiter = { reject };
|
|
225
|
+
this.waiters.push(waiter);
|
|
226
|
+
void this.inner.finish().then((body) => {
|
|
227
|
+
spliceWaiter(this.waiters, waiter);
|
|
228
|
+
this.onDone?.();
|
|
229
|
+
resolve(body);
|
|
230
|
+
}, (error) => {
|
|
231
|
+
spliceWaiter(this.waiters, waiter);
|
|
232
|
+
this.onDone?.();
|
|
233
|
+
// finish() rejects with the typed terminal error (§4.3).
|
|
234
|
+
reject(fromWasmError(error));
|
|
235
|
+
});
|
|
236
|
+
return promise;
|
|
237
|
+
}
|
|
238
|
+
cancel() {
|
|
239
|
+
this.retire(new OrgCancelledError());
|
|
240
|
+
}
|
|
241
|
+
/** Retire this upload: one CANCEL at most; parked `finish()` rejects typed. */
|
|
242
|
+
retire(error) {
|
|
243
|
+
if (this.failure !== null)
|
|
244
|
+
return;
|
|
245
|
+
this.failure = error;
|
|
246
|
+
if (!this.handleDropped) {
|
|
247
|
+
this.handleDropped = true;
|
|
248
|
+
try {
|
|
249
|
+
this.inner.cancel();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Already retired at the boundary; `error` is the outcome.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.onDone?.();
|
|
256
|
+
const waiters = [...this.waiters];
|
|
257
|
+
this.waiters.length = 0;
|
|
258
|
+
for (const waiter of waiters)
|
|
259
|
+
waiter.reject(error);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** A duplex call, wrapped: one call-level CANCEL however both halves end. */
|
|
263
|
+
export class OrgDuplex {
|
|
264
|
+
inner;
|
|
265
|
+
onDone;
|
|
266
|
+
/** The two halves share one drop, so one CANCEL leaves per call. */
|
|
267
|
+
handleDropped = false;
|
|
268
|
+
failure = null;
|
|
269
|
+
sink;
|
|
270
|
+
stream;
|
|
271
|
+
constructor(inner, onDone) {
|
|
272
|
+
this.inner = inner;
|
|
273
|
+
this.onDone = onDone;
|
|
274
|
+
this.stream = new OrgStream(inner.stream(), () => this.dropHandle(), () => this.onDone?.());
|
|
275
|
+
this.sink = {
|
|
276
|
+
send: async (payload) => {
|
|
277
|
+
if (this.failure !== null)
|
|
278
|
+
throw this.failure;
|
|
279
|
+
try {
|
|
280
|
+
await this.inner.send(payload);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw fromWasmError(error);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
finishSending: async () => {
|
|
287
|
+
if (this.failure !== null)
|
|
288
|
+
throw this.failure;
|
|
289
|
+
try {
|
|
290
|
+
await this.inner.finish_sending();
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
throw fromWasmError(error);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
cancel: () => {
|
|
297
|
+
this.retire(new OrgCancelledError());
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** Retire the whole call: one CANCEL, both halves settled typed. */
|
|
302
|
+
retire(error) {
|
|
303
|
+
this.failure ??= error;
|
|
304
|
+
this.dropHandle();
|
|
305
|
+
// Idempotent one level down: a stream that already reached its
|
|
306
|
+
// own terminal keeps that outcome.
|
|
307
|
+
this.stream.retire(error);
|
|
308
|
+
this.onDone?.();
|
|
309
|
+
}
|
|
310
|
+
dropHandle() {
|
|
311
|
+
if (this.handleDropped)
|
|
312
|
+
return;
|
|
313
|
+
this.handleDropped = true;
|
|
314
|
+
try {
|
|
315
|
+
this.inner.cancel();
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Already retired at the boundary; `error` is the outcome.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The response sink a handler receives, wrapped. `retired` is
|
|
324
|
+
* memoized at construction — the boundary's `retired()` is called
|
|
325
|
+
* exactly once and the one signal is what a detached observer holds.
|
|
326
|
+
*/
|
|
327
|
+
export class OrgSink {
|
|
328
|
+
inner;
|
|
329
|
+
retired;
|
|
330
|
+
verdict = null;
|
|
331
|
+
constructor(inner) {
|
|
332
|
+
this.inner = inner;
|
|
333
|
+
this.retired = inner.retired().then((raw) => {
|
|
334
|
+
this.verdict ??= orgRetireReason(raw);
|
|
335
|
+
return this.verdict;
|
|
336
|
+
}, () => {
|
|
337
|
+
// A `retired()` that itself fails is reported as a teardown
|
|
338
|
+
// verdict rather than left unsettled — an observer must get
|
|
339
|
+
// ONE signal, never a hang and never a rejection.
|
|
340
|
+
this.verdict ??= 'replaced';
|
|
341
|
+
return this.verdict;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async send(payload) {
|
|
345
|
+
if (this.verdict !== null)
|
|
346
|
+
throw new OrgCancelledError(ORG_SINK_CLOSED_REFUSAL);
|
|
347
|
+
try {
|
|
348
|
+
await this.inner.send(payload);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
throw fromWasmError(error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** Half-close the response direction: the call's normal ending. */
|
|
355
|
+
async close() {
|
|
356
|
+
if (this.verdict !== null)
|
|
357
|
+
throw new OrgCancelledError(ORG_SINK_CLOSED_REFUSAL);
|
|
358
|
+
try {
|
|
359
|
+
await this.inner.close();
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
throw fromWasmError(error);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The request stream a handler receives, wrapped. Its terminal is the
|
|
368
|
+
* `done` item; the abnormal record is `retired`, memoized once as
|
|
369
|
+
* {@link OrgResponseSink.retired} is.
|
|
370
|
+
*/
|
|
371
|
+
export class OrgRequests {
|
|
372
|
+
inner;
|
|
373
|
+
retired;
|
|
374
|
+
verdict = null;
|
|
375
|
+
waiters = [];
|
|
376
|
+
constructor(inner) {
|
|
377
|
+
this.inner = inner;
|
|
378
|
+
this.retired = inner.retired().then((raw) => {
|
|
379
|
+
this.settle(orgRetireReason(raw));
|
|
380
|
+
return this.verdict;
|
|
381
|
+
}, () => {
|
|
382
|
+
this.settle('replaced');
|
|
383
|
+
return this.verdict;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
next() {
|
|
387
|
+
if (this.verdict !== null)
|
|
388
|
+
return Promise.resolve({ done: true });
|
|
389
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
390
|
+
this.waiters.push(resolve);
|
|
391
|
+
void this.inner.next().then((raw) => {
|
|
392
|
+
spliceWaiter(this.waiters, resolve);
|
|
393
|
+
resolve(requestItem(raw));
|
|
394
|
+
}, () => {
|
|
395
|
+
// A boundary throw here ends the iteration: the request
|
|
396
|
+
// direction is over one way or the other, and `retired` is
|
|
397
|
+
// where a handler reads which.
|
|
398
|
+
spliceWaiter(this.waiters, resolve);
|
|
399
|
+
resolve({ done: true });
|
|
400
|
+
});
|
|
401
|
+
return promise;
|
|
402
|
+
}
|
|
403
|
+
[Symbol.asyncIterator]() {
|
|
404
|
+
return {
|
|
405
|
+
next: async () => {
|
|
406
|
+
const item = await this.next();
|
|
407
|
+
if (item.done || item.value === undefined)
|
|
408
|
+
return { done: true, value: undefined };
|
|
409
|
+
return { done: false, value: item.value };
|
|
410
|
+
},
|
|
411
|
+
return: async () => ({ done: true, value: undefined }),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
settle(verdict) {
|
|
415
|
+
if (this.verdict !== null)
|
|
416
|
+
return;
|
|
417
|
+
this.verdict = verdict;
|
|
418
|
+
const waiters = [...this.waiters];
|
|
419
|
+
this.waiters.length = 0;
|
|
420
|
+
for (const waiter of waiters)
|
|
421
|
+
waiter({ done: true });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* One serve registration, wrapped. `close()` is C9's protected-split
|
|
426
|
+
* retirement: it retires the registration's live protected calls with
|
|
427
|
+
* their exact terminals and refuses new openings on the service.
|
|
428
|
+
*/
|
|
429
|
+
export class OrgServe {
|
|
430
|
+
inner;
|
|
431
|
+
onDone;
|
|
432
|
+
closed = false;
|
|
433
|
+
constructor(inner, onDone) {
|
|
434
|
+
this.inner = inner;
|
|
435
|
+
this.onDone = onDone;
|
|
436
|
+
}
|
|
437
|
+
get service() {
|
|
438
|
+
return this.inner.service();
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* **A retirement, not a graceful unregister (C9's protected
|
|
442
|
+
* split).** Every live protected call this registration admitted is
|
|
443
|
+
* retired with its exact retirement terminal — the
|
|
444
|
+
* deadline/cancel/revocation vocabulary — and new openings on the
|
|
445
|
+
* service are refused. Deliberately NOT the public path's
|
|
446
|
+
* let-existing-calls-finish behavior.
|
|
447
|
+
*/
|
|
448
|
+
close() {
|
|
449
|
+
if (this.closed)
|
|
450
|
+
return;
|
|
451
|
+
this.closed = true;
|
|
452
|
+
this.onDone?.();
|
|
453
|
+
this.inner.close();
|
|
454
|
+
}
|
|
455
|
+
/** The registry's teardown verb: for a registration it is `close()`. */
|
|
456
|
+
retire(_error) {
|
|
457
|
+
this.close();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Build exactly the options object `call_org*` reads.
|
|
462
|
+
*
|
|
463
|
+
* A fresh object, so a caller mutating its own afterwards cannot
|
|
464
|
+
* change a call already in flight. The byte fields are forwarded as
|
|
465
|
+
* the `Uint8Array`s they must be — the leaf refuses anything else
|
|
466
|
+
* loudly, and a silent re-encode here would be a second codec.
|
|
467
|
+
*/
|
|
468
|
+
export function toWasmOrgCallOptions(options) {
|
|
469
|
+
const credentials = {
|
|
470
|
+
membership: options.credentials.membership,
|
|
471
|
+
dispatcher: options.credentials.dispatcher,
|
|
472
|
+
...(options.credentials.capabilityGrant === undefined
|
|
473
|
+
? {}
|
|
474
|
+
: { capabilityGrant: options.credentials.capabilityGrant }),
|
|
475
|
+
actingOrg: options.credentials.actingOrg,
|
|
476
|
+
providerOwnerOrg: options.credentials.providerOwnerOrg,
|
|
477
|
+
provider: options.credentials.provider,
|
|
478
|
+
...(options.credentials.proofTtlSecs === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { proofTtlSecs: options.credentials.proofTtlSecs }),
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
credentials,
|
|
484
|
+
...(options.deadlineMs === undefined ? {} : { deadlineMs: options.deadlineMs }),
|
|
485
|
+
...(options.streamWindowInitial === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: { streamWindowInitial: options.streamWindowInitial }),
|
|
488
|
+
...(options.requestWindowInitial === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: { requestWindowInitial: options.requestWindowInitial }),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** Build exactly the options object `serve_org*` reads. */
|
|
494
|
+
export function toWasmOrgServeOptions(options) {
|
|
495
|
+
return { ownerOrg: options.ownerOrg };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Read one handler invocation's caller attribution.
|
|
499
|
+
*
|
|
500
|
+
* The boundary hands the crate's `to_json` JSON **string** (the same
|
|
501
|
+
* convention `on_event` uses), camelCase keys verbatim
|
|
502
|
+
* {@link OrgCaller}'s; anything that does not parse to an object is
|
|
503
|
+
* refused rather than served with an attribution this build cannot
|
|
504
|
+
* vouch for.
|
|
505
|
+
*/
|
|
506
|
+
export function parseOrgCaller(raw) {
|
|
507
|
+
const value = JSON.parse(raw);
|
|
508
|
+
if (value === null || typeof value !== 'object') {
|
|
509
|
+
throw new UnknownLeafError('the caller attribution is not an OrgCaller object');
|
|
510
|
+
}
|
|
511
|
+
const record = value;
|
|
512
|
+
return {
|
|
513
|
+
entity: String(record.entity ?? ''),
|
|
514
|
+
actingOrg: String(record.actingOrg ?? ''),
|
|
515
|
+
providerOrg: String(record.providerOrg ?? ''),
|
|
516
|
+
provider: String(record.provider ?? ''),
|
|
517
|
+
capability: String(record.capability ?? ''),
|
|
518
|
+
isSameOrg: record.isSameOrg === true,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/** Adapt a unary handler to the boundary's `(caller, request)` trampoline. */
|
|
522
|
+
export function unaryOrgTrampoline(handler) {
|
|
523
|
+
return async (caller, request) => await handler(parseOrgCaller(caller), request);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Adapt a streaming handler to `(caller, request, sink)`. The sink is
|
|
527
|
+
* wrapped once per invocation; its `retired` is the one signal the
|
|
528
|
+
* F-S3.1-2 level names.
|
|
529
|
+
*/
|
|
530
|
+
export function streamingOrgTrampoline(handler) {
|
|
531
|
+
return async (caller, request, sink) => {
|
|
532
|
+
await handler(parseOrgCaller(caller), request, new OrgSink(sink));
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
/** Adapt a client-streaming handler to `(caller, requests)`. */
|
|
536
|
+
export function clientStreamOrgTrampoline(handler) {
|
|
537
|
+
return async (caller, requests) => await handler(parseOrgCaller(caller), new OrgRequests(requests));
|
|
538
|
+
}
|
|
539
|
+
/** Adapt a duplex handler to `(caller, requests, sink)`. */
|
|
540
|
+
export function duplexOrgTrampoline(handler) {
|
|
541
|
+
return async (caller, requests, sink) => {
|
|
542
|
+
await handler(parseOrgCaller(caller), new OrgRequests(requests), new OrgSink(sink));
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* One waiter leaves its list the moment its pull settles (BROWSER-4):
|
|
547
|
+
* a resolved pull retains no closure until the terminal.
|
|
548
|
+
*/
|
|
549
|
+
function spliceWaiter(waiters, waiter) {
|
|
550
|
+
const index = waiters.indexOf(waiter);
|
|
551
|
+
if (index >= 0)
|
|
552
|
+
waiters.splice(index, 1);
|
|
553
|
+
}
|
|
554
|
+
/** The boundary's byte item, typed through the §4.3 vocabulary. */
|
|
555
|
+
function byteItem(raw) {
|
|
556
|
+
if (!raw.done)
|
|
557
|
+
return { done: false, value: raw.value };
|
|
558
|
+
if (raw.error !== undefined)
|
|
559
|
+
return { done: true, error: orgTerminalError(raw.error) };
|
|
560
|
+
// A terminal that carries a final body keeps it (BROWSER-1): the
|
|
561
|
+
// item is `{ done: true, value }`, never a flattened end.
|
|
562
|
+
return raw.value === undefined ? { done: true } : { done: true, value: raw.value };
|
|
563
|
+
}
|
|
564
|
+
/** The boundary's request item: no error arm, by contract. */
|
|
565
|
+
function requestItem(raw) {
|
|
566
|
+
return raw.done ? { done: true } : { done: false, value: raw.value };
|
|
567
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The peer-attempt drive loop, once, for both surfaces.
|
|
3
|
+
*
|
|
4
|
+
* `BrowserNode` drives an attempt on this tab's own node;
|
|
5
|
+
* `MeshSession` drives one on whichever tab holds the lock. The steps
|
|
6
|
+
* and the classification are identical, and they must stay identical:
|
|
7
|
+
* the offerer and the answerer disagreeing about what supersession or
|
|
8
|
+
* a terminal reading looks like is a defect neither side can detect.
|
|
9
|
+
* So the loop lives here and each surface supplies only the four
|
|
10
|
+
* primitives ({@link PeerPrimitives}).
|
|
11
|
+
*
|
|
12
|
+
* **Supersession now has two sources, and that is the reason this
|
|
13
|
+
* file exists rather than a second copy of the loop.**
|
|
14
|
+
*
|
|
15
|
+
* 1. *Client-side*, as before: the reading names the dialog that is
|
|
16
|
+
* live, so a reading for a different dialog means this attempt was
|
|
17
|
+
* replaced.
|
|
18
|
+
* 2. *Server-side*, new with the proxy: a request that named a dialog
|
|
19
|
+
* is refused by the leaf **before** it services anything, because
|
|
20
|
+
* the attempt it was issued for is no longer the live one. A
|
|
21
|
+
* proxied request crosses a channel and is served later, so this is
|
|
22
|
+
* the common case for a follower rather than an edge.
|
|
23
|
+
*
|
|
24
|
+
* Both are the same disposition to a caller, and both are classified
|
|
25
|
+
* here. A direct caller sees (1) almost always and (2) never; a
|
|
26
|
+
* proxied caller sees either.
|
|
27
|
+
*/
|
|
28
|
+
import type { PeerAttemptStatus, PeerConnectOutcome } from './node.js';
|
|
29
|
+
/**
|
|
30
|
+
* The four steps an attempt needs, as whichever surface provides
|
|
31
|
+
* them.
|
|
32
|
+
*
|
|
33
|
+
* `candidate` and `handshake` take the dialog because the leaf
|
|
34
|
+
* requires it: a peer-only poll is resolved against whatever attempt
|
|
35
|
+
* is live when it runs, which for a request that crossed a channel is
|
|
36
|
+
* how a superseded request drives its own replacement.
|
|
37
|
+
*/
|
|
38
|
+
export interface PeerPrimitives {
|
|
39
|
+
/** Mint an offer and start an attempt; resolves to its dialog. */
|
|
40
|
+
offer(peer: string): Promise<string>;
|
|
41
|
+
/** Answer the offer already filed; resolves to its dialog. */
|
|
42
|
+
acceptOffer(peer: string): Promise<string>;
|
|
43
|
+
/** Service one polling step of `dialog`; resolves to the reading JSON. */
|
|
44
|
+
candidate(peer: string, dialog: string): Promise<string>;
|
|
45
|
+
/** Run the Noise handshake for `dialog`; resolves to the dialog it ran for. */
|
|
46
|
+
handshake(peer: string, dialog: string): Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The exact prefix the leaf's "peer not discovered" refusal carries
|
|
50
|
+
* (`wasm.rs`'s `NO_ANNOUNCEMENT_PREFIX`).
|
|
51
|
+
*
|
|
52
|
+
* Pinned because the wasm boundary carries messages, so a message is
|
|
53
|
+
* a contract: a reworded refusal demotes a typed disposition to a
|
|
54
|
+
* thrown error with nothing red.
|
|
55
|
+
*/
|
|
56
|
+
export declare const NO_ANNOUNCEMENT_PREFIX = "no verified announcement for";
|
|
57
|
+
/** `wasm.rs`'s `NO_LIVE_ATTEMPT_PREFIX`. */
|
|
58
|
+
export declare const NO_LIVE_ATTEMPT_PREFIX = "no live attempt with";
|
|
59
|
+
/** `wasm.rs`'s refusal when no verified offer is waiting yet. */
|
|
60
|
+
export declare const NO_OFFER_PREFIX = "no verified offer from";
|
|
61
|
+
/**
|
|
62
|
+
* `wasm.rs`'s `attempt_for` refusal: the named dialog is not the one
|
|
63
|
+
* now live.
|
|
64
|
+
*
|
|
65
|
+
* Distinct from {@link NO_LIVE_ATTEMPT_PREFIX} on purpose — "your
|
|
66
|
+
* attempt was replaced" and "there is no attempt" are different
|
|
67
|
+
* facts, and the leaf says which. Both classify as `superseded`, but
|
|
68
|
+
* only this one can name the replacement.
|
|
69
|
+
*/
|
|
70
|
+
export declare const REPLACED_ATTEMPT_MARKER = "is not the live attempt";
|
|
71
|
+
/** How long the drive loop waits between candidate steps. */
|
|
72
|
+
export declare const PEER_TICK_MS = 50;
|
|
73
|
+
/**
|
|
74
|
+
* How long {@link acceptPeer} waits for the offer envelope to arrive
|
|
75
|
+
* before it treats "no offer" as the answer.
|
|
76
|
+
*
|
|
77
|
+
* Two anchor hops and one pump, so it is generous rather than tight.
|
|
78
|
+
*/
|
|
79
|
+
export declare const PEER_OFFER_WAIT_MS = 5000;
|
|
80
|
+
/** Offer, drive to an open channel, then handshake (§9 steps 3-4). */
|
|
81
|
+
export declare function connectPeer(peer: string, primitives: PeerPrimitives, parse: (json: string) => PeerAttemptStatus): Promise<PeerConnectOutcome>;
|
|
82
|
+
/**
|
|
83
|
+
* Answer an offer and drive to a direct session.
|
|
84
|
+
*
|
|
85
|
+
* The accept is retried while the leaf reports no offer yet: the
|
|
86
|
+
* envelope crosses A → anchor → B, so a page reacting to "the peer is
|
|
87
|
+
* here" a moment before it lands would be told there is no offer,
|
|
88
|
+
* which is true and not an answer. Every other refusal is returned or
|
|
89
|
+
* thrown at once, because retrying those would only hide them.
|
|
90
|
+
*/
|
|
91
|
+
export declare function acceptPeer(peer: string, primitives: PeerPrimitives, parse: (json: string) => PeerAttemptStatus): Promise<PeerConnectOutcome>;
|
|
92
|
+
/**
|
|
93
|
+
* Run the offerer's handshake on an attempt whose channel is open.
|
|
94
|
+
*
|
|
95
|
+
* The dialog is **checked** rather than assumed: the handshake
|
|
96
|
+
* resolves to the dialog it actually ran for, and a mismatch is this
|
|
97
|
+
* caller's attempt having been replaced underneath it. Reporting
|
|
98
|
+
* `direct` with the caller's own dialog on any success is how a stale
|
|
99
|
+
* caller was once told its superseded attempt had connected, while
|
|
100
|
+
* the leaf had handshaken the attempt that replaced it.
|
|
101
|
+
*/
|
|
102
|
+
export declare function handshakePeer(peer: string, dialog: string, primitives: PeerPrimitives): Promise<PeerConnectOutcome>;
|
|
103
|
+
/**
|
|
104
|
+
* Pump one attempt until `done`, its terminal transition, or a newer
|
|
105
|
+
* attempt replaces it.
|
|
106
|
+
*
|
|
107
|
+
* `{ type: 'direct' }` here means only "`done` answered true" — the
|
|
108
|
+
* caller decides what remains.
|
|
109
|
+
*
|
|
110
|
+
* **Every terminal state ends the loop**, not only the two ICE ones.
|
|
111
|
+
* An answerer waits for `direct`, and a channel that opened with a
|
|
112
|
+
* session that never installed satisfies neither `direct` nor
|
|
113
|
+
* `iceTimeout`: the leaf reports `failed`, which is this loop's exit.
|
|
114
|
+
* The bound is the leaf's, which owns the attempt's deadline — a
|
|
115
|
+
* timeout added here would be a second clock disagreeing with it.
|
|
116
|
+
*/
|
|
117
|
+
export declare function driveAttempt(peer: string, dialog: string, done: (status: PeerAttemptStatus) => boolean, primitives: PeerPrimitives, parse: (json: string) => PeerAttemptStatus): Promise<PeerConnectOutcome>;
|