@net-mesh/browser 0.40.0 → 0.41.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/CHANGELOG.md +54 -0
- package/README.md +5 -3
- package/dist/index.bundle.js +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/leader/index.d.ts +1 -1
- package/dist/leader/session.d.ts +8 -1
- package/dist/leader/session.js +12 -4
- package/dist/lobby.js +24 -10
- package/dist/net_leaf.d.ts +2 -2
- package/dist/net_leaf.js +11 -11
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +2 -2
- package/dist/netcode/index.d.ts +3 -0
- package/dist/netcode/index.js +4 -0
- package/dist/node.d.ts +13 -1
- package/dist/node.js +28 -7
- package/dist/peer-driver.d.ts +77 -2
- package/dist/peer-driver.js +163 -3
- package/dist/store/host.d.ts +19 -3
- package/dist/store/host.js +21 -7
- package/dist/store/index.d.ts +23 -0
- package/dist/store/index.js +16 -0
- package/dist/store/join.d.ts +6 -1
- package/dist/store/join.js +14 -8
- package/dist/world/link.js +3 -3
- package/dist/world/regions.js +3 -3
- package/package.json +1 -1
package/dist/peer-driver.js
CHANGED
|
@@ -58,6 +58,16 @@ export const PEER_TICK_MS = 50;
|
|
|
58
58
|
* Two anchor hops and one pump, so it is generous rather than tight.
|
|
59
59
|
*/
|
|
60
60
|
export const PEER_OFFER_WAIT_MS = 5_000;
|
|
61
|
+
/**
|
|
62
|
+
* How long an offer that arrived stays worth answering in place of
|
|
63
|
+
* offering back: the offerer's own ICE deadline (`wasm.rs`'s
|
|
64
|
+
* `PEER_ICE_DEADLINE_MS`). An older offer's attempt has ended at its
|
|
65
|
+
* sender, so answering it would only retire this side's own offer for
|
|
66
|
+
* nothing — and the leaf holds an unanswered offer until something
|
|
67
|
+
* takes it, so without a bound a stale one would hijack every later
|
|
68
|
+
* `connectPeer`.
|
|
69
|
+
*/
|
|
70
|
+
export const PEER_OFFER_FRESH_MS = 10_000;
|
|
61
71
|
/**
|
|
62
72
|
* Whether an answer's outcome settles the pair for a `connectPeer` that
|
|
63
73
|
* waited on it, rather than calling for an offer of its own.
|
|
@@ -110,10 +120,57 @@ function answerSettles(outcome) {
|
|
|
110
120
|
*
|
|
111
121
|
* Settled attempts are forgotten: the next call starts fresh. Another
|
|
112
122
|
* tab's node is outside this gate, as it is outside this surface.
|
|
123
|
+
*
|
|
124
|
+
* **Between two nodes, the lower id offers (glare).** The gate above
|
|
125
|
+
* cannot stop the *peer's* offer cancelling this node's: when both
|
|
126
|
+
* ends call `connectPeer` at once — two pages reaching each other, or
|
|
127
|
+
* both re-trying after an attempt ended — each answer retires the
|
|
128
|
+
* answerer's own offer, both run out their ICE deadline, and the pair
|
|
129
|
+
* stays relayed. So when this node's id is the higher of the pair:
|
|
130
|
+
*
|
|
131
|
+
* - its `connectPeer` answers an offer from the peer that arrived
|
|
132
|
+
* within {@link PEER_OFFER_FRESH_MS}, instead of offering back;
|
|
133
|
+
* - while its own offer is under way, an offer arriving from the peer
|
|
134
|
+
* is answered, and the answer's outcome is the call's.
|
|
135
|
+
*
|
|
136
|
+
* And on either side, an `acceptPeer` while this surface's own
|
|
137
|
+
* `connectPeer` for the peer is under way takes that call's outcome:
|
|
138
|
+
* on the lower id the crossing offer is the one to ignore, and on the
|
|
139
|
+
* higher id the `connectPeer` already answers it.
|
|
140
|
+
*
|
|
141
|
+
* Offers are learned from the surface's `signal` events
|
|
142
|
+
* ({@link noteOffer}); with none noted, or this node's id unknown,
|
|
143
|
+
* `connectPeer` offers exactly as before.
|
|
113
144
|
*/
|
|
114
145
|
export class PeerAttempts {
|
|
146
|
+
self;
|
|
147
|
+
now;
|
|
115
148
|
#connecting = new Map();
|
|
116
149
|
#accepting = new Map();
|
|
150
|
+
/** When an offer from each peer last arrived, by {@link now}. */
|
|
151
|
+
#offers = new Map();
|
|
152
|
+
/**
|
|
153
|
+
* @param self This node's id, 16 hex, or `null` while unknown (no
|
|
154
|
+
* tie-break then).
|
|
155
|
+
* @param now The clock offer arrivals are stamped with.
|
|
156
|
+
*/
|
|
157
|
+
constructor(self = () => null, now = () => Date.now()) {
|
|
158
|
+
this.self = self;
|
|
159
|
+
this.now = now;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A verified offer from `peer` (16 hex) arrived. Arrivals past
|
|
163
|
+
* {@link PEER_OFFER_FRESH_MS} are dropped here, so the map holds only
|
|
164
|
+
* peers heard from lately rather than every sender for the node's life.
|
|
165
|
+
*/
|
|
166
|
+
noteOffer(peer) {
|
|
167
|
+
const now = this.now();
|
|
168
|
+
for (const [key, at] of this.#offers) {
|
|
169
|
+
if (at <= now - PEER_OFFER_FRESH_MS)
|
|
170
|
+
this.#offers.delete(key);
|
|
171
|
+
}
|
|
172
|
+
this.#offers.set(peer.toLowerCase(), now);
|
|
173
|
+
}
|
|
117
174
|
/**
|
|
118
175
|
* Run `drive` as this peer's offer, unless one is already under way,
|
|
119
176
|
* the pair is already direct, or an answer under way settles it.
|
|
@@ -123,8 +180,12 @@ export class PeerAttempts {
|
|
|
123
180
|
* a round trip, and an `acceptPeer` could take the pair direct and
|
|
124
181
|
* leave the gate between a caller's early reading and its offer, which
|
|
125
182
|
* would then replace the healthy link.
|
|
183
|
+
*
|
|
184
|
+
* `answer` answers the offer the peer filed, if one is waiting
|
|
185
|
+
* (`null` when none is): how the higher id of a pair yields to the
|
|
186
|
+
* lower id's offer rather than crossing it.
|
|
126
187
|
*/
|
|
127
|
-
connect(peer, healthy, drive) {
|
|
188
|
+
connect(peer, healthy, drive, answer) {
|
|
128
189
|
const key = peer.toLowerCase();
|
|
129
190
|
const joining = this.#connecting.get(key);
|
|
130
191
|
if (joining !== undefined)
|
|
@@ -139,18 +200,81 @@ export class PeerAttempts {
|
|
|
139
200
|
const dialog = await healthy();
|
|
140
201
|
if (dialog !== undefined && dialog !== null)
|
|
141
202
|
return { type: 'direct', peer, dialog };
|
|
142
|
-
|
|
203
|
+
if (answer === undefined || !this.#yields(key))
|
|
204
|
+
return drive();
|
|
205
|
+
// The lower id already offered: answer it rather than cross it.
|
|
206
|
+
if (this.#takeOffer(key, this.now() - PEER_OFFER_FRESH_MS)) {
|
|
207
|
+
const answered = await answer();
|
|
208
|
+
// Only an answer that settles the pair stands in for the offer.
|
|
209
|
+
if (answered !== null && answerSettles(answered))
|
|
210
|
+
return answered;
|
|
211
|
+
}
|
|
212
|
+
return this.#offerYielding(key, drive, answer);
|
|
143
213
|
})();
|
|
144
214
|
return PeerAttempts.#track(this.#connecting, key, started);
|
|
145
215
|
}
|
|
146
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Run `drive` as this peer's answer, unless one is already under way,
|
|
218
|
+
* or this surface's own `connectPeer` for the peer is (see the class
|
|
219
|
+
* comment).
|
|
220
|
+
*/
|
|
147
221
|
accept(peer, drive) {
|
|
148
222
|
const key = peer.toLowerCase();
|
|
149
223
|
const joining = this.#accepting.get(key);
|
|
150
224
|
if (joining !== undefined)
|
|
151
225
|
return joining;
|
|
226
|
+
const connecting = this.#connecting.get(key);
|
|
227
|
+
if (connecting !== undefined && this.self() !== null)
|
|
228
|
+
return connecting;
|
|
152
229
|
return PeerAttempts.#track(this.#accepting, key, drive());
|
|
153
230
|
}
|
|
231
|
+
/** Whether this node is the higher id of the pair, and so answers. */
|
|
232
|
+
#yields(key) {
|
|
233
|
+
const self = this.self()?.toLowerCase();
|
|
234
|
+
// 16 hex each, so the strings order as the ids do.
|
|
235
|
+
return self !== undefined && self.length === key.length && self > key;
|
|
236
|
+
}
|
|
237
|
+
/** Consume a noted offer that arrived after `since`, if there is one. */
|
|
238
|
+
#takeOffer(key, since) {
|
|
239
|
+
const at = this.#offers.get(key);
|
|
240
|
+
if (at === undefined || at <= since)
|
|
241
|
+
return false;
|
|
242
|
+
this.#offers.delete(key);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Offer, but answer an offer from the peer that crosses it: this
|
|
247
|
+
* node is the higher id, so the crossing offer is the one the pair
|
|
248
|
+
* keeps. The offer's own drive then ends `superseded` by the answer,
|
|
249
|
+
* and is not the call's outcome.
|
|
250
|
+
*
|
|
251
|
+
* The answer, once started, is always awaited: answering retires this
|
|
252
|
+
* node's offer, so the offer's drive can settle `superseded` while the
|
|
253
|
+
* answer is still running, and racing the two returned that instead of
|
|
254
|
+
* the answer's result. With no answer started, the call's outcome is
|
|
255
|
+
* the offer's, within one tick of its settling.
|
|
256
|
+
*/
|
|
257
|
+
async #offerYielding(key, drive, answer) {
|
|
258
|
+
const since = this.now();
|
|
259
|
+
const offering = drive();
|
|
260
|
+
let settled = false;
|
|
261
|
+
const done = () => {
|
|
262
|
+
settled = true;
|
|
263
|
+
};
|
|
264
|
+
offering.then(done, done);
|
|
265
|
+
const crossed = (async () => {
|
|
266
|
+
while (!settled) {
|
|
267
|
+
await tick();
|
|
268
|
+
if (settled || !this.#takeOffer(key, since))
|
|
269
|
+
continue;
|
|
270
|
+
const answered = await answer();
|
|
271
|
+
if (answered !== null)
|
|
272
|
+
return answered;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
})();
|
|
276
|
+
return crossed.then((answered) => answered ?? offering);
|
|
277
|
+
}
|
|
154
278
|
static #track(map, key, started) {
|
|
155
279
|
map.set(key, started);
|
|
156
280
|
// Forget it once settled, and only if no newer one took its place.
|
|
@@ -162,6 +286,26 @@ export class PeerAttempts {
|
|
|
162
286
|
return started;
|
|
163
287
|
}
|
|
164
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* The `kind` a `signal` event carries for an offer: `SignalKind::tag`
|
|
291
|
+
* in `control_plane.rs`, which the leaf writes as a bare number.
|
|
292
|
+
*/
|
|
293
|
+
export const SIGNAL_KIND_OFFER = '1';
|
|
294
|
+
/**
|
|
295
|
+
* Tell `attempts` about an offer, when `event` is a verified one (a
|
|
296
|
+
* `signal` event of kind {@link SIGNAL_KIND_OFFER}). Every other event
|
|
297
|
+
* is ignored, as is a sender id that is not an exact decimal u64.
|
|
298
|
+
*/
|
|
299
|
+
export function noteOffer(attempts, event) {
|
|
300
|
+
if (event.type !== 'signal' || event.kind !== SIGNAL_KIND_OFFER)
|
|
301
|
+
return;
|
|
302
|
+
if (event.from === undefined || !/^\d{1,20}$/.test(event.from))
|
|
303
|
+
return;
|
|
304
|
+
const id = BigInt(event.from);
|
|
305
|
+
if (id > 0xffffffffffffffffn)
|
|
306
|
+
return;
|
|
307
|
+
attempts.noteOffer(id.toString(16).padStart(16, '0'));
|
|
308
|
+
}
|
|
165
309
|
/** Offer, drive to an open channel, then handshake (§9 steps 3-4). */
|
|
166
310
|
export async function connectPeer(peer, primitives, parse) {
|
|
167
311
|
let dialog;
|
|
@@ -210,6 +354,22 @@ export async function acceptPeer(peer, primitives, parse) {
|
|
|
210
354
|
}
|
|
211
355
|
return driveAttempt(peer, dialog, (status) => status.direct, primitives, parse);
|
|
212
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Answer the offer `peer` filed if one is waiting now, without waiting
|
|
359
|
+
* for one: `null` when none is, or when the leaf refuses the answer —
|
|
360
|
+
* the caller ({@link PeerAttempts.connect}, yielding to the lower id)
|
|
361
|
+
* then offers as it would have.
|
|
362
|
+
*/
|
|
363
|
+
export async function answerWaitingOffer(peer, primitives, parse) {
|
|
364
|
+
let dialog;
|
|
365
|
+
try {
|
|
366
|
+
dialog = await primitives.acceptOffer(peer);
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
return driveAttempt(peer, dialog, (status) => status.direct, primitives, parse);
|
|
372
|
+
}
|
|
213
373
|
/**
|
|
214
374
|
* Run the offerer's handshake on an attempt whose channel is open.
|
|
215
375
|
*
|
package/dist/store/host.d.ts
CHANGED
|
@@ -115,7 +115,8 @@ export interface StoreTransport {
|
|
|
115
115
|
connectPeer?(peerHex: string): Promise<unknown>;
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
118
|
+
* The peer ON AN EVENT, in the spelling `openStream` takes: 16
|
|
119
|
+
* lowercase hex. For an id the caller holds, see {@link nodeHexOf}.
|
|
119
120
|
*
|
|
120
121
|
* Events carry the authenticated peer as an EXACT DECIMAL string, and
|
|
121
122
|
* `openStream({ peer })` requires 16 hex digits. Handing the decimal
|
|
@@ -126,8 +127,23 @@ export interface StoreTransport {
|
|
|
126
127
|
* reconciliation for the store's own seam.
|
|
127
128
|
*/
|
|
128
129
|
export declare function peerHexOf(peer: string): string | null;
|
|
129
|
-
/**
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* A node id the CALLER holds, in the same spelling: 16 lowercase hex.
|
|
132
|
+
*
|
|
133
|
+
* Not {@link peerHexOf}, which reads an event's peer and takes digits
|
|
134
|
+
* to mean decimal. An id a caller holds is nearly always one this
|
|
135
|
+
* package printed — `nodeIdHex()`, a descriptor's `peerIdHex`, a
|
|
136
|
+
* lobby's `host` — and one such id in ~1,800 has no letter in it. Read
|
|
137
|
+
* as decimal it names a DIFFERENT node: a joiner addressed its host
|
|
138
|
+
* 4798628394172323 as 00110c53bcd7ffa3, and nothing was there. So
|
|
139
|
+
* sixteen hex digits are hex, whatever they are made of.
|
|
140
|
+
*
|
|
141
|
+
* Any other length is read as {@link peerHexOf} reads it, so an app
|
|
142
|
+
* may still hand back the decimal `peerNode` an event gave it. A
|
|
143
|
+
* decimal exactly sixteen digits long is the one spelling that cannot
|
|
144
|
+
* be told from the canonical hex, and it is read as hex.
|
|
145
|
+
*/
|
|
146
|
+
export declare function nodeHexOf(id: string): string | null;
|
|
131
147
|
/**
|
|
132
148
|
* How a host decides what each replica sees: exactly one of these.
|
|
133
149
|
*
|
package/dist/store/host.js
CHANGED
|
@@ -50,7 +50,8 @@ export const HOST_SWEEP_MS = 5_000;
|
|
|
50
50
|
*/
|
|
51
51
|
export const FAREWELL_DEADLINE_MS = 500;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* The peer ON AN EVENT, in the spelling `openStream` takes: 16
|
|
54
|
+
* lowercase hex. For an id the caller holds, see {@link nodeHexOf}.
|
|
54
55
|
*
|
|
55
56
|
* Events carry the authenticated peer as an EXACT DECIMAL string, and
|
|
56
57
|
* `openStream({ peer })` requires 16 hex digits. Handing the decimal
|
|
@@ -74,11 +75,24 @@ export function peerHexOf(peer) {
|
|
|
74
75
|
}
|
|
75
76
|
return null;
|
|
76
77
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
/**
|
|
79
|
+
* A node id the CALLER holds, in the same spelling: 16 lowercase hex.
|
|
80
|
+
*
|
|
81
|
+
* Not {@link peerHexOf}, which reads an event's peer and takes digits
|
|
82
|
+
* to mean decimal. An id a caller holds is nearly always one this
|
|
83
|
+
* package printed — `nodeIdHex()`, a descriptor's `peerIdHex`, a
|
|
84
|
+
* lobby's `host` — and one such id in ~1,800 has no letter in it. Read
|
|
85
|
+
* as decimal it names a DIFFERENT node: a joiner addressed its host
|
|
86
|
+
* 4798628394172323 as 00110c53bcd7ffa3, and nothing was there. So
|
|
87
|
+
* sixteen hex digits are hex, whatever they are made of.
|
|
88
|
+
*
|
|
89
|
+
* Any other length is read as {@link peerHexOf} reads it, so an app
|
|
90
|
+
* may still hand back the decimal `peerNode` an event gave it. A
|
|
91
|
+
* decimal exactly sixteen digits long is the one spelling that cannot
|
|
92
|
+
* be told from the canonical hex, and it is read as hex.
|
|
93
|
+
*/
|
|
94
|
+
export function nodeHexOf(id) {
|
|
95
|
+
return /^[0-9a-f]{16}$/i.test(id) ? id.toLowerCase() : peerHexOf(id);
|
|
82
96
|
}
|
|
83
97
|
/**
|
|
84
98
|
* The store addresses live on each transport.
|
|
@@ -663,7 +677,7 @@ export function hostStore(options) {
|
|
|
663
677
|
owner: owner,
|
|
664
678
|
// The spelling `AccessRequest.peer` and `ActionContext.peer`
|
|
665
679
|
// promise: 16 lowercase hex, as a replica's frames are handed.
|
|
666
|
-
peer:
|
|
680
|
+
peer: nodeHexOf(authority) ?? authority,
|
|
667
681
|
maxEventBytes: options.maxEventBytes,
|
|
668
682
|
dispatched,
|
|
669
683
|
isClosed: () => closed,
|
package/dist/store/index.d.ts
CHANGED
|
@@ -48,3 +48,26 @@ export type { Hidden, Visibility, VisibilityPreset, VisibilityRule, VisibilityRu
|
|
|
48
48
|
export { cellKey, cellsAround, sameCells, stickyCells } from './interest.js';
|
|
49
49
|
export type { CellOptions } from './interest.js';
|
|
50
50
|
export { MAX_INTEREST_KEYS, MAX_INTEREST_KEY_BYTES } from './wire.js';
|
|
51
|
+
export { Assembly, ASSEMBLY_DEADLINE_MS, AssemblyTable, MAX_ASSEMBLY_BYTES_TOTAL, } from './assembly.js';
|
|
52
|
+
export type { AssemblyChunk, AssemblyManifest, AssemblyRefusal, AssemblyRejected, ChunkOutcome, } from './assembly.js';
|
|
53
|
+
export { assertChunkingFits, base64, chunkBytesFor, chunkSnapshot, fromBase64, jsonByteLength, LARGEST_FROZEN_ENVELOPE, MIN_CHUNK_BYTES, SNAP_ENVELOPE_RESERVE, } from './chunker.js';
|
|
54
|
+
export type { Chunked, } from './chunker.js';
|
|
55
|
+
export { isEntityMap, } from './definition.js';
|
|
56
|
+
export { isStaleStream, STORE_ERROR_CODES, } from './errors.js';
|
|
57
|
+
export { FAREWELL_DEADLINE_MS, hostInternals, nodeHexOf, peerHexOf, } from './host.js';
|
|
58
|
+
export type { HostInternals, } from './host.js';
|
|
59
|
+
export { TICK_INTERVAL_MS, TRANSITION_DEADLINE_MS, } from './join.js';
|
|
60
|
+
export { MAX_JSON_DEPTH, parseStoreJson, } from './json.js';
|
|
61
|
+
export type { JsonFailure, JsonObject, JsonRefusal, JsonResult, JsonValue, } from './json.js';
|
|
62
|
+
export { BINDING_INLINE_MAX_BYTES, canonicalBytes, canonicalRequest, digestBinding, HandleLedger, inlineBinding, InputSequences, LEDGER_MAX_AGE_MS, LEDGER_MAX_BYTES, LEDGER_MAX_ENTRIES, LedgerTable, MAX_SEQUENCE, needsDigest, } from './ledger.js';
|
|
63
|
+
export type { Disposition, Outcome, RequestBinding, } from './ledger.js';
|
|
64
|
+
export { HANDLE_LEASE_MS, MAX_HANDLES, MAX_PENDING_ACTIONS, StoreOwner, } from './owner.js';
|
|
65
|
+
export type { ActionHandlers, Dispatched, InputHandlers, Outbound, OwnerDeps, OwnerHandle, } from './owner.js';
|
|
66
|
+
export { applyPatch, FORBIDDEN_SEGMENTS, } from './patch.js';
|
|
67
|
+
export type { PatchApplied, PatchOutcome, PatchRefusal, PatchRejected, } from './patch.js';
|
|
68
|
+
export { MAX_JOIN_REASKS, StoreReplica, } from './replica.js';
|
|
69
|
+
export type { Received, ReplicaDeps, ReplicaState, Request, } from './replica.js';
|
|
70
|
+
export { applyVisibility, compileVisibility, } from './visibility.js';
|
|
71
|
+
export type { CompiledVisibility, } from './visibility.js';
|
|
72
|
+
export { CALLER_KINDS, decimalValue, decodeMessage, encodeMessage, HANDLE_HEX_LENGTH, inadmissibleValue, INCARNATION_HEX_LENGTH, isCanonicalBase64, isCanonicalDecimal, isCanonicalHex, MAX_AUDIENCE_LABEL_BYTES, MAX_AUDIENCE_LABELS, MAX_DECIMAL, MAX_DETAIL_BYTES, MAX_PATCH_OPS, MAX_PATH_SEGMENT_BYTES, MAX_PATH_SEGMENTS, MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_CHUNKS, OWNER_KINDS, REQUEST_HEX_LENGTH, utf8Length, WIRE_VERSION, } from './wire.js';
|
|
73
|
+
export type { ActMessage, AliveMessage, AudienceMessage, CallerKind, CallerMessage, Decimal, DecodeOptions, DecodeRefusal, DecodeResult, DecodeStage, DeltaMessage, Hex, InputMessage, InterestMessage, JoinMessage, LeaveMessage, ManifestMessage, MessageFor, MessageKind, NoMessage, OkMessage, OwnerKind, OwnerMessage, ResultMessage, ResumeMessage, ResyncMessage, Side, SnapMessage, StoreMessage, WireOp, } from './wire.js';
|
package/dist/store/index.js
CHANGED
|
@@ -38,3 +38,19 @@ export { addItems, countItems, emptyInventory, hasItems, InventoryError, invento
|
|
|
38
38
|
export { assertHidden, HIDDEN, hiddenOr, isHidden, projectVisible, } from './visibility.js';
|
|
39
39
|
export { cellKey, cellsAround, sameCells, stickyCells } from './interest.js';
|
|
40
40
|
export { MAX_INTEREST_KEYS, MAX_INTEREST_KEY_BYTES } from './wire.js';
|
|
41
|
+
// The store's lower-level building blocks — wire codec, ledgers,
|
|
42
|
+
// chunking, owner/replica cores — exported so callers can compose
|
|
43
|
+
// their own transports and tooling.
|
|
44
|
+
export { Assembly, ASSEMBLY_DEADLINE_MS, AssemblyTable, MAX_ASSEMBLY_BYTES_TOTAL, } from './assembly.js';
|
|
45
|
+
export { assertChunkingFits, base64, chunkBytesFor, chunkSnapshot, fromBase64, jsonByteLength, LARGEST_FROZEN_ENVELOPE, MIN_CHUNK_BYTES, SNAP_ENVELOPE_RESERVE, } from './chunker.js';
|
|
46
|
+
export { isEntityMap, } from './definition.js';
|
|
47
|
+
export { isStaleStream, STORE_ERROR_CODES, } from './errors.js';
|
|
48
|
+
export { FAREWELL_DEADLINE_MS, hostInternals, nodeHexOf, peerHexOf, } from './host.js';
|
|
49
|
+
export { TICK_INTERVAL_MS, TRANSITION_DEADLINE_MS, } from './join.js';
|
|
50
|
+
export { MAX_JSON_DEPTH, parseStoreJson, } from './json.js';
|
|
51
|
+
export { BINDING_INLINE_MAX_BYTES, canonicalBytes, canonicalRequest, digestBinding, HandleLedger, inlineBinding, InputSequences, LEDGER_MAX_AGE_MS, LEDGER_MAX_BYTES, LEDGER_MAX_ENTRIES, LedgerTable, MAX_SEQUENCE, needsDigest, } from './ledger.js';
|
|
52
|
+
export { HANDLE_LEASE_MS, MAX_HANDLES, MAX_PENDING_ACTIONS, StoreOwner, } from './owner.js';
|
|
53
|
+
export { applyPatch, FORBIDDEN_SEGMENTS, } from './patch.js';
|
|
54
|
+
export { MAX_JOIN_REASKS, StoreReplica, } from './replica.js';
|
|
55
|
+
export { applyVisibility, compileVisibility, } from './visibility.js';
|
|
56
|
+
export { CALLER_KINDS, decimalValue, decodeMessage, encodeMessage, HANDLE_HEX_LENGTH, inadmissibleValue, INCARNATION_HEX_LENGTH, isCanonicalBase64, isCanonicalDecimal, isCanonicalHex, MAX_AUDIENCE_LABEL_BYTES, MAX_AUDIENCE_LABELS, MAX_DECIMAL, MAX_DETAIL_BYTES, MAX_PATCH_OPS, MAX_PATH_SEGMENT_BYTES, MAX_PATH_SEGMENTS, MAX_SNAPSHOT_BYTES, MAX_SNAPSHOT_CHUNKS, OWNER_KINDS, REQUEST_HEX_LENGTH, utf8Length, WIRE_VERSION, } from './wire.js';
|
package/dist/store/join.d.ts
CHANGED
|
@@ -57,7 +57,12 @@ export declare const MAX_OUTSTANDING = 64;
|
|
|
57
57
|
export interface JoinStoreOptions<S extends object, A extends ActionSpec, I extends InputSpec> {
|
|
58
58
|
readonly definition: StoreDefinition<S, A, I>;
|
|
59
59
|
readonly transport: StoreTransport;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The node hosting the store: 16 hex digits, as `nodeIdHex()` and a
|
|
62
|
+
* descriptor's `peerIdHex` print it. The decimal `peerNode` of an
|
|
63
|
+
* event is accepted too, unless it is exactly sixteen digits long —
|
|
64
|
+
* that is the hex spelling, and it is read as hex.
|
|
65
|
+
*/
|
|
61
66
|
readonly host: string;
|
|
62
67
|
/** The audience this caller wants. */
|
|
63
68
|
readonly audience: readonly string[];
|
package/dist/store/join.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { StoreCore } from './core.js';
|
|
25
25
|
import { isStaleStream, StoreError } from './errors.js';
|
|
26
|
-
import {
|
|
26
|
+
import { nodeHexOf, peerHexOf } from './host.js';
|
|
27
27
|
import { StoreReplica } from './replica.js';
|
|
28
28
|
import { decodeMessage, encodeMessage, utf8Length } from './wire.js';
|
|
29
29
|
/** How often a live handle renews its lease (§1.6). */
|
|
@@ -83,7 +83,10 @@ export function joinStore(options) {
|
|
|
83
83
|
// had already accepted the subscription. Refused here instead,
|
|
84
84
|
// where the caller can read what it did wrong.
|
|
85
85
|
const selfId = options.transport.nodeIdHex();
|
|
86
|
-
|
|
86
|
+
// Read ONCE, as an id the caller holds: hex when it is the sixteen
|
|
87
|
+
// digits `nodeIdHex()` prints, even when none of them is a letter.
|
|
88
|
+
const hostHex = nodeHexOf(options.host);
|
|
89
|
+
if (selfId !== null && hostHex !== null && nodeHexOf(selfId) === hostHex) {
|
|
87
90
|
throw new StoreError('invalid-data', 'a replica cannot join the node it runs on: hold the hostStore handle instead');
|
|
88
91
|
}
|
|
89
92
|
const streamId = options.streamId ?? `store/${options.definition.id}`;
|
|
@@ -124,8 +127,7 @@ export function joinStore(options) {
|
|
|
124
127
|
return upstream;
|
|
125
128
|
if (opening !== null)
|
|
126
129
|
return opening;
|
|
127
|
-
|
|
128
|
-
if (addressable === null) {
|
|
130
|
+
if (hostHex === null) {
|
|
129
131
|
return Promise.reject(new StoreError('invalid-data', `the host id is not a node id: ${options.host}`));
|
|
130
132
|
}
|
|
131
133
|
// `label`, never `streamId`: see {@link StoreTransport.openStream}.
|
|
@@ -139,11 +141,11 @@ export function joinStore(options) {
|
|
|
139
141
|
// the relay is installed before the direct half is even tried, so
|
|
140
142
|
// the open below is the thing that decides, and its refusal is
|
|
141
143
|
// typed.
|
|
142
|
-
opening = Promise.resolve(options.transport.connectPeer?.(
|
|
144
|
+
opening = Promise.resolve(options.transport.connectPeer?.(hostHex))
|
|
143
145
|
.catch(() => undefined)
|
|
144
146
|
.then(() => options.transport.openStream({
|
|
145
147
|
reliability: 'reliable',
|
|
146
|
-
peer:
|
|
148
|
+
peer: hostHex,
|
|
147
149
|
label: streamId,
|
|
148
150
|
}))
|
|
149
151
|
.then(open => {
|
|
@@ -350,8 +352,12 @@ export function joinStore(options) {
|
|
|
350
352
|
// is what identifies this store's traffic.
|
|
351
353
|
// The host's identity, compared as a NUMBER: the event carries an
|
|
352
354
|
// exact decimal and `options.host` is hex, so `!==` on the strings
|
|
353
|
-
// rejected every frame the host ever sent.
|
|
354
|
-
|
|
355
|
+
// rejected every frame the host ever sent. Each side is read in
|
|
356
|
+
// its OWN spelling: one reader for both took an all-digit hex host
|
|
357
|
+
// for a decimal, and then no frame of that host's ever matched.
|
|
358
|
+
if (typeof event.peerNode !== 'string' || hostHex === null)
|
|
359
|
+
return;
|
|
360
|
+
if (peerHexOf(event.peerNode) !== hostHex)
|
|
355
361
|
return;
|
|
356
362
|
const payload = event.payload;
|
|
357
363
|
if (payload === undefined)
|
package/dist/world/link.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* handoffs.handoff('ship-7', 'r:4:8'); // when the ship crosses the border
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
import { peerHexOf } from '../store/host.js';
|
|
17
|
+
import { nodeHexOf, peerHexOf } from '../store/host.js';
|
|
18
18
|
import { ghostTargets, onForwardedAction, pruneActs, } from './border.js';
|
|
19
19
|
import { beginHandoff, locate, onHandoffOffer, onHandoffReply, pruneHandled, reofferHandoff, retryHandoffs, } from './handoff.js';
|
|
20
20
|
const encoder = new TextEncoder();
|
|
@@ -140,7 +140,7 @@ export function handoffLink(options) {
|
|
|
140
140
|
// handoff on another region's behalf.
|
|
141
141
|
const sender = typeof event.peerNode === 'string' ? peerHexOf(event.peerNode) : null;
|
|
142
142
|
const expected = options.peerOf(body.from);
|
|
143
|
-
if (sender === null || expected === null ||
|
|
143
|
+
if (sender === null || expected === null || nodeHexOf(expected) !== sender) {
|
|
144
144
|
drop('unauthenticated');
|
|
145
145
|
refresh(body.from);
|
|
146
146
|
return;
|
|
@@ -153,7 +153,7 @@ export function handoffLink(options) {
|
|
|
153
153
|
if (closed)
|
|
154
154
|
return;
|
|
155
155
|
const peer = options.peerOf(message.to);
|
|
156
|
-
const hex = peer === null ? null :
|
|
156
|
+
const hex = peer === null ? null : nodeHexOf(peer);
|
|
157
157
|
if (hex === null) {
|
|
158
158
|
drop('unknown-region');
|
|
159
159
|
refresh(message.to);
|
package/dist/world/regions.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* directory (`handoffLink({ peerOf: directory.peerOf })`) needs the list:
|
|
28
28
|
* otherwise a node that announces a region could also send offers as it.
|
|
29
29
|
*/
|
|
30
|
-
import {
|
|
30
|
+
import { nodeHexOf } from '../store/host.js';
|
|
31
31
|
import { joinStore } from '../store/join.js';
|
|
32
32
|
/** Region `r:<rx>:<rz>` for a position, with regions `size` units square. */
|
|
33
33
|
export function regionOf(x, z, size) {
|
|
@@ -77,7 +77,7 @@ export function announceRegions(node, world, regions, options = {}) {
|
|
|
77
77
|
export function regionDirectory(options) {
|
|
78
78
|
const trusted = options.trustedHosts === undefined
|
|
79
79
|
? null
|
|
80
|
-
: new Set(options.trustedHosts.map(id =>
|
|
80
|
+
: new Set(options.trustedHosts.map(id => nodeHexOf(id)).filter((id) => id !== null));
|
|
81
81
|
const cache = new Map();
|
|
82
82
|
return {
|
|
83
83
|
async lookup(region) {
|
|
@@ -90,7 +90,7 @@ export function regionDirectory(options) {
|
|
|
90
90
|
}
|
|
91
91
|
const hosts = [
|
|
92
92
|
...new Set(found
|
|
93
|
-
.map(descriptor =>
|
|
93
|
+
.map(descriptor => nodeHexOf(descriptor.peerIdHex))
|
|
94
94
|
.filter((id) => id !== null && (trusted === null || trusted.has(id)))),
|
|
95
95
|
].sort();
|
|
96
96
|
const answer = hosts.length === 0
|