@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.
@@ -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
- return drive();
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
- /** Run `drive` as this peer's answer, unless one is already under way. */
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
  *
@@ -115,7 +115,8 @@ export interface StoreTransport {
115
115
  connectPeer?(peerHex: string): Promise<unknown>;
116
116
  }
117
117
  /**
118
- * A peer id in the spelling `openStream` takes: 16 lowercase hex.
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
- /** Whether two peer spellings name the same node. */
130
- export declare function samePeer(left: string, right: string): boolean;
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
  *
@@ -50,7 +50,8 @@ export const HOST_SWEEP_MS = 5_000;
50
50
  */
51
51
  export const FAREWELL_DEADLINE_MS = 500;
52
52
  /**
53
- * A peer id in the spelling `openStream` takes: 16 lowercase hex.
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
- /** Whether two peer spellings name the same node. */
78
- export function samePeer(left, right) {
79
- const a = peerHexOf(left);
80
- const b = peerHexOf(right);
81
- return a !== null && a === b;
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: peerHexOf(authority) ?? authority,
680
+ peer: nodeHexOf(authority) ?? authority,
667
681
  maxEventBytes: options.maxEventBytes,
668
682
  dispatched,
669
683
  isClosed: () => closed,
@@ -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';
@@ -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';
@@ -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
- /** The node hosting the store. */
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[];
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { StoreCore } from './core.js';
25
25
  import { isStaleStream, StoreError } from './errors.js';
26
- import { peerHexOf, samePeer } from './host.js';
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
- if (selfId !== null && samePeer(selfId, options.host)) {
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
- const addressable = peerHexOf(options.host);
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?.(addressable))
144
+ opening = Promise.resolve(options.transport.connectPeer?.(hostHex))
143
145
  .catch(() => undefined)
144
146
  .then(() => options.transport.openStream({
145
147
  reliability: 'reliable',
146
- peer: addressable,
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
- if (typeof event.peerNode !== 'string' || !samePeer(event.peerNode, options.host))
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)
@@ -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 || peerHexOf(expected) !== sender) {
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 : peerHexOf(peer);
156
+ const hex = peer === null ? null : nodeHexOf(peer);
157
157
  if (hex === null) {
158
158
  drop('unknown-region');
159
159
  refresh(message.to);
@@ -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 { peerHexOf } from '../store/host.js';
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 => peerHexOf(id)).filter((id) => id !== null));
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 => peerHexOf(descriptor.peerIdHex))
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/browser",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
4
4
  "description": "Browser-native Net mesh leaf \u2014 idiomatic TypeScript over the net-mesh-leaf wasm node",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",