@agentplat/mesh 0.3.0-alpha.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.
@@ -0,0 +1,99 @@
1
+ import { type MeshProtocolOptions, type SignedMeshEnvelope } from '@agentplat/mesh-protocol';
2
+ import type { MeshCryptoPolicy, MeshEnvelopeSigner, MeshEnvelopeVerifier, MeshKeyResolver } from '@agentplat/mesh-crypto';
3
+ import type { MeshAdmissionPolicy, MeshLogicalTime, MeshPeerEffect, MeshPeerInput, MeshPeerState } from './contracts.js';
4
+ /** Exact transport route; peer IDs are never global lookup keys. */
5
+ export interface MeshLoopbackAddress {
6
+ readonly tenantId: string;
7
+ readonly meshId: string;
8
+ readonly peerId: string;
9
+ }
10
+ /** One coherent wall-clock and logical-time sample supplied by a driver. */
11
+ export interface MeshLoopbackTime {
12
+ readonly logicalTime: MeshLogicalTime;
13
+ readonly timestamp: string;
14
+ }
15
+ /** Explicit clock used by loopback and deterministic tests. */
16
+ export interface MeshLoopbackClock {
17
+ now(): MeshLoopbackTime;
18
+ }
19
+ /** Injectable canonical message-ID source. */
20
+ export interface MeshLoopbackMessageIdSource {
21
+ nextMessageId(): string;
22
+ }
23
+ /** Redacted queue entry returned by inspection. */
24
+ export interface MeshLoopbackQueueEntry {
25
+ readonly insertionSequence: number;
26
+ readonly messageId: string;
27
+ readonly target: MeshLoopbackAddress;
28
+ }
29
+ /** Delivery result at the in-memory transport boundary. */
30
+ export type MeshLoopbackReceipt = {
31
+ readonly accepted: true;
32
+ readonly messageId: string;
33
+ readonly target: MeshLoopbackAddress;
34
+ } | {
35
+ readonly accepted: false;
36
+ readonly messageId?: string;
37
+ readonly reasonCode: string;
38
+ readonly target?: MeshLoopbackAddress;
39
+ };
40
+ /** State and immediate effects returned after one local peer input. */
41
+ export interface MeshLoopbackDispatchResult {
42
+ readonly state: MeshPeerState;
43
+ readonly effects: readonly MeshPeerEffect[];
44
+ readonly receipts: readonly MeshLoopbackReceipt[];
45
+ }
46
+ /** External commands; interpreted effect results remain driver-owned. */
47
+ export type MeshLoopbackPeerCommand = Exclude<MeshPeerInput, {
48
+ readonly kind: 'effect.result';
49
+ }>;
50
+ /** Restart-safe snapshot owned by one registered loopback peer. */
51
+ export interface MeshLoopbackPeerSnapshot {
52
+ readonly state: MeshPeerState;
53
+ readonly outboundSequence: number;
54
+ }
55
+ /** Complete dependencies for one signed loopback peer. */
56
+ export interface MeshLoopbackPeerOptions {
57
+ readonly state: MeshPeerState;
58
+ readonly signer: MeshEnvelopeSigner;
59
+ readonly verifier: MeshEnvelopeVerifier;
60
+ readonly resolver: MeshKeyResolver;
61
+ readonly cryptoPolicy: MeshCryptoPolicy;
62
+ readonly privateKey: CryptoKey;
63
+ readonly clock: MeshLoopbackClock;
64
+ readonly messageIds?: MeshLoopbackMessageIdSource;
65
+ readonly admissionPolicy?: MeshAdmissionPolicy;
66
+ readonly outboundSequence?: number;
67
+ readonly crypto?: Crypto;
68
+ readonly protocolOptions?: MeshProtocolOptions;
69
+ readonly supportedCriticalExtensions?: readonly string[];
70
+ readonly onEffect?: (effect: MeshPeerEffect) => void;
71
+ }
72
+ /** Mutable driver handle around immutable peer snapshots. */
73
+ export interface MeshLoopbackPeer {
74
+ readonly address: MeshLoopbackAddress;
75
+ getState(): MeshPeerState;
76
+ snapshot(): MeshLoopbackPeerSnapshot;
77
+ dispatch(input: MeshLoopbackPeerCommand): Promise<MeshLoopbackDispatchResult>;
78
+ }
79
+ /** Construction bounds for one explicit in-memory transport. */
80
+ export interface MeshLoopbackTransportOptions {
81
+ readonly maximumPeers?: number;
82
+ readonly maximumQueueDepth?: number;
83
+ readonly maximumQueuedBytes?: number;
84
+ readonly maximumDuplicateCopies?: number;
85
+ readonly maximumInternalStepsPerTransition?: number;
86
+ }
87
+ /** Signed, bounded and explicitly scoped local transport. */
88
+ export interface MeshLoopbackTransport {
89
+ register(options: MeshLoopbackPeerOptions): MeshLoopbackPeer;
90
+ unregister(address: MeshLoopbackAddress): void;
91
+ deliver(envelope: SignedMeshEnvelope): Promise<MeshLoopbackReceipt>;
92
+ injectDuplicate(envelope: SignedMeshEnvelope, additionalDeliveries?: number): Promise<readonly MeshLoopbackReceipt[]>;
93
+ inspectQueue(): readonly MeshLoopbackQueueEntry[];
94
+ idle(): Promise<void>;
95
+ close(): Promise<void>;
96
+ }
97
+ /** Creates an isolated loopback bus with no process-global registry. */
98
+ export declare function createMeshLoopbackTransport(options?: MeshLoopbackTransportOptions): MeshLoopbackTransport;
99
+ //# sourceMappingURL=loopback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../src/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,580 @@
1
+ import { MESH_PROTOCOL, MESH_SIGNATURE_ALGORITHM, MESH_WIRE_VERSION, } from '@agentplat/mesh-protocol';
2
+ import { ALLOW_PREPROVISIONED_MESH_ADMISSION, processMeshEnvelope, } from './inbound.js';
3
+ import { reduceMeshPeer } from './reducer.js';
4
+ const defaultMaximumQueueDepth = 1_024;
5
+ const defaultMaximumQueuedBytes = 16 * 1_024 * 1_024;
6
+ /** Creates an isolated loopback bus with no process-global registry. */
7
+ export function createMeshLoopbackTransport(options = {}) {
8
+ return new InMemoryMeshLoopbackTransport(options);
9
+ }
10
+ class InMemoryMeshLoopbackTransport {
11
+ #maximumQueueDepth;
12
+ #maximumPeers;
13
+ #maximumQueuedBytes;
14
+ #maximumDuplicateCopies;
15
+ #maximumInternalStepsPerTransition;
16
+ #peers = new Map();
17
+ #seenInstances = new Set();
18
+ #queue = [];
19
+ #idleWaiters = [];
20
+ #insertionSequence = 0;
21
+ #queuedBytes = 0;
22
+ #inFlight = 0;
23
+ #inFlightBytes = 0;
24
+ #draining = false;
25
+ #activeDispatches = 0;
26
+ #closed = false;
27
+ #closePromise;
28
+ #resolveClose;
29
+ constructor(options) {
30
+ const maximumQueueDepth = options.maximumQueueDepth ?? defaultMaximumQueueDepth;
31
+ const limits = {
32
+ maximumPeers: options.maximumPeers ?? 256,
33
+ maximumQueueDepth,
34
+ maximumQueuedBytes: options.maximumQueuedBytes ?? defaultMaximumQueuedBytes,
35
+ maximumDuplicateCopies: options.maximumDuplicateCopies ?? 16,
36
+ maximumInternalStepsPerTransition: options.maximumInternalStepsPerTransition ?? 1_024,
37
+ };
38
+ for (const [name, value] of Object.entries(limits)) {
39
+ if (!Number.isSafeInteger(value) || value < 1) {
40
+ throw new RangeError(`Mesh loopback ${name} must be a positive safe integer`);
41
+ }
42
+ }
43
+ this.#maximumQueueDepth = maximumQueueDepth;
44
+ this.#maximumPeers = limits.maximumPeers;
45
+ this.#maximumQueuedBytes = limits.maximumQueuedBytes;
46
+ this.#maximumDuplicateCopies = limits.maximumDuplicateCopies;
47
+ this.#maximumInternalStepsPerTransition =
48
+ limits.maximumInternalStepsPerTransition;
49
+ }
50
+ register(options) {
51
+ if (this.#closed)
52
+ throw new Error('Mesh loopback transport is closed');
53
+ if (this.#peers.size >= this.#maximumPeers) {
54
+ throw new RangeError('Mesh loopback peer capacity exceeded');
55
+ }
56
+ const peer = new LoopbackPeer(this, options);
57
+ const key = addressKey(peer.address);
58
+ if (this.#peers.has(key)) {
59
+ throw new TypeError('Duplicate Mesh loopback peer registration');
60
+ }
61
+ const instanceKey = JSON.stringify([
62
+ ...JSON.parse(key),
63
+ options.state.identity.instanceId,
64
+ ]);
65
+ if (this.#seenInstances.has(instanceKey)) {
66
+ throw new TypeError('Mesh loopback peer instance cannot be re-registered');
67
+ }
68
+ this.#seenInstances.add(instanceKey);
69
+ this.#peers.set(key, peer);
70
+ return peer;
71
+ }
72
+ unregister(address) {
73
+ const key = addressKey(freezeAddress(address));
74
+ this.#peers.get(key)?.dispose();
75
+ this.#peers.delete(key);
76
+ }
77
+ deliver(envelope) {
78
+ const target = directTarget(envelope);
79
+ if (!target) {
80
+ return Promise.resolve(Object.freeze({
81
+ accepted: false,
82
+ messageId: envelope?.messageId,
83
+ reasonCode: 'audience_not_direct',
84
+ }));
85
+ }
86
+ return this.#enqueue(envelope, target);
87
+ }
88
+ injectDuplicate(envelope, additionalDeliveries = 1) {
89
+ if (!Number.isSafeInteger(additionalDeliveries) ||
90
+ additionalDeliveries < 1 ||
91
+ additionalDeliveries > this.#maximumDuplicateCopies) {
92
+ throw new RangeError('Mesh loopback additionalDeliveries must be a positive safe integer');
93
+ }
94
+ const target = directTarget(envelope);
95
+ if (!target) {
96
+ return Promise.resolve(Object.freeze([
97
+ Object.freeze({
98
+ accepted: false,
99
+ messageId: envelope?.messageId,
100
+ reasonCode: 'audience_not_direct',
101
+ }),
102
+ ]));
103
+ }
104
+ const byteLength = envelopeByteLength(envelope);
105
+ const batchSize = additionalDeliveries + 1;
106
+ if (this.#closed ||
107
+ this.#queue.length + this.#inFlight + batchSize >
108
+ this.#maximumQueueDepth ||
109
+ this.#queuedBytes +
110
+ this.#inFlightBytes +
111
+ byteLength * batchSize >
112
+ this.#maximumQueuedBytes) {
113
+ return Promise.resolve(Object.freeze([
114
+ Object.freeze({
115
+ accepted: false,
116
+ messageId: envelope.messageId,
117
+ reasonCode: this.#closed
118
+ ? 'transport_closed'
119
+ : 'queue_capacity_exceeded',
120
+ target,
121
+ }),
122
+ ]));
123
+ }
124
+ return Promise.all(Array.from({ length: additionalDeliveries + 1 }, () => this.#enqueue(envelope, target))).then((receipts) => Object.freeze(receipts));
125
+ }
126
+ inspectQueue() {
127
+ return Object.freeze(this.#queue.map((job) => Object.freeze({
128
+ insertionSequence: job.insertionSequence,
129
+ messageId: job.envelope.messageId,
130
+ target: job.target,
131
+ })));
132
+ }
133
+ idle() {
134
+ if (!this.#draining && this.#queue.length === 0)
135
+ return Promise.resolve();
136
+ return new Promise((resolve) => this.#idleWaiters.push(resolve));
137
+ }
138
+ close() {
139
+ if (this.#closePromise)
140
+ return this.#closePromise;
141
+ this.#closed = true;
142
+ this.#closePromise = new Promise((resolve) => {
143
+ this.#resolveClose = resolve;
144
+ }).then(() => {
145
+ for (const peer of this.#peers.values())
146
+ peer.dispose();
147
+ this.#peers.clear();
148
+ });
149
+ this.#resolveCloseIfQuiescent();
150
+ return this.#closePromise;
151
+ }
152
+ async dispatchFrom(peer, input) {
153
+ if (this.#closed)
154
+ throw new Error('Mesh loopback transport is closed');
155
+ this.#activeDispatches += 1;
156
+ try {
157
+ const result = await peer.applyLocal(input);
158
+ const receipts = await Promise.all(result.outgoing.map((envelope) => {
159
+ const target = directTarget(envelope);
160
+ return target
161
+ ? this.#enqueue(envelope, target, true)
162
+ : Promise.resolve(Object.freeze({
163
+ accepted: false,
164
+ messageId: envelope.messageId,
165
+ reasonCode: 'audience_not_direct',
166
+ }));
167
+ }));
168
+ return Object.freeze({
169
+ state: result.state,
170
+ effects: result.effects,
171
+ receipts: Object.freeze(receipts),
172
+ });
173
+ }
174
+ finally {
175
+ this.#activeDispatches -= 1;
176
+ this.#resolveCloseIfQuiescent();
177
+ }
178
+ }
179
+ #enqueue(envelope, target, internal = false) {
180
+ if (this.#closed && !internal) {
181
+ return Promise.resolve(Object.freeze({
182
+ accepted: false,
183
+ messageId: envelope.messageId,
184
+ reasonCode: 'transport_closed',
185
+ target,
186
+ }));
187
+ }
188
+ const byteLength = envelopeByteLength(envelope);
189
+ if (this.#queue.length + this.#inFlight >= this.#maximumQueueDepth ||
190
+ this.#queuedBytes + this.#inFlightBytes + byteLength >
191
+ this.#maximumQueuedBytes) {
192
+ return Promise.resolve(Object.freeze({
193
+ accepted: false,
194
+ messageId: envelope.messageId,
195
+ reasonCode: 'queue_capacity_exceeded',
196
+ target,
197
+ }));
198
+ }
199
+ return new Promise((resolve) => {
200
+ this.#insertionSequence += 1;
201
+ this.#queue.push({
202
+ insertionSequence: this.#insertionSequence,
203
+ envelope,
204
+ target,
205
+ byteLength,
206
+ resolve,
207
+ });
208
+ this.#queuedBytes += byteLength;
209
+ this.#scheduleDrain();
210
+ });
211
+ }
212
+ #scheduleDrain() {
213
+ if (this.#draining)
214
+ return;
215
+ this.#draining = true;
216
+ void Promise.resolve().then(() => this.#drain());
217
+ }
218
+ async #drain() {
219
+ try {
220
+ while (this.#queue.length > 0) {
221
+ const job = this.#queue.shift();
222
+ if (!job)
223
+ break;
224
+ this.#queuedBytes -= job.byteLength;
225
+ this.#inFlight = 1;
226
+ this.#inFlightBytes = job.byteLength;
227
+ const peer = this.#peers.get(addressKey(job.target));
228
+ if (!peer) {
229
+ job.resolve(Object.freeze({
230
+ accepted: false,
231
+ messageId: job.envelope.messageId,
232
+ reasonCode: 'endpoint_not_found',
233
+ target: job.target,
234
+ }));
235
+ this.#inFlight = 0;
236
+ this.#inFlightBytes = 0;
237
+ continue;
238
+ }
239
+ let result;
240
+ try {
241
+ result = await peer.accept(job.envelope);
242
+ }
243
+ catch {
244
+ result = {
245
+ receipt: Object.freeze({
246
+ accepted: false,
247
+ messageId: job.envelope.messageId,
248
+ reasonCode: 'endpoint_failed',
249
+ target: job.target,
250
+ }),
251
+ outgoing: Object.freeze([]),
252
+ };
253
+ }
254
+ // Acceptance has completed, so its bounded processing slot can be
255
+ // handed directly to any causal descendant produced by that
256
+ // transition (for example, a ping acknowledgement).
257
+ this.#inFlight = 0;
258
+ this.#inFlightBytes = 0;
259
+ job.resolve(result.receipt);
260
+ for (const outgoing of result.outgoing) {
261
+ const target = directTarget(outgoing);
262
+ if (target)
263
+ void this.#enqueue(outgoing, target, true);
264
+ }
265
+ }
266
+ }
267
+ finally {
268
+ this.#draining = false;
269
+ if (this.#queue.length > 0) {
270
+ this.#scheduleDrain();
271
+ }
272
+ else {
273
+ for (const resolve of this.#idleWaiters.splice(0))
274
+ resolve();
275
+ }
276
+ this.#resolveCloseIfQuiescent();
277
+ }
278
+ }
279
+ get maximumInternalStepsPerTransition() {
280
+ return this.#maximumInternalStepsPerTransition;
281
+ }
282
+ #resolveCloseIfQuiescent() {
283
+ if (this.#closed &&
284
+ this.#activeDispatches === 0 &&
285
+ !this.#draining &&
286
+ this.#queue.length === 0) {
287
+ this.#resolveClose?.();
288
+ this.#resolveClose = undefined;
289
+ }
290
+ }
291
+ }
292
+ class LoopbackPeer {
293
+ address;
294
+ #transport;
295
+ #options;
296
+ #prepared = new Map();
297
+ #state;
298
+ #outboundSequence;
299
+ #tail = Promise.resolve();
300
+ constructor(transport, options) {
301
+ if (!options || typeof options !== 'object') {
302
+ throw new TypeError('Mesh loopback peer options are required');
303
+ }
304
+ this.#transport = transport;
305
+ this.#options = options;
306
+ this.#state = options.state;
307
+ this.address = freezeAddress(options.state.identity);
308
+ this.#outboundSequence = options.outboundSequence ?? 0;
309
+ if (!Number.isSafeInteger(this.#outboundSequence) ||
310
+ this.#outboundSequence < 0) {
311
+ throw new RangeError('Invalid Mesh loopback outbound sequence');
312
+ }
313
+ }
314
+ getState() {
315
+ return this.#state;
316
+ }
317
+ snapshot() {
318
+ return Object.freeze({
319
+ state: this.#state,
320
+ outboundSequence: this.#outboundSequence,
321
+ });
322
+ }
323
+ dispatch(input) {
324
+ return this.#transport.dispatchFrom(this, input);
325
+ }
326
+ applyLocal(input) {
327
+ return this.#exclusive(async () => {
328
+ const time = freezeTime(this.#options.clock.now());
329
+ const transition = reduceMeshPeer(this.#state, input, time.logicalTime);
330
+ this.#state = transition.state;
331
+ const interpreted = await this.#interpret(transition.effects, time);
332
+ return Object.freeze({
333
+ state: this.#state,
334
+ effects: transition.effects,
335
+ outgoing: interpreted,
336
+ });
337
+ });
338
+ }
339
+ accept(envelope) {
340
+ return this.#exclusive(async () => {
341
+ const time = freezeTime(this.#options.clock.now());
342
+ const decision = await processMeshEnvelope(this.#state, {
343
+ envelope,
344
+ verifiedAt: time.timestamp,
345
+ receivedAt: time.logicalTime,
346
+ verifier: this.#options.verifier,
347
+ resolver: this.#options.resolver,
348
+ cryptoPolicy: this.#options.cryptoPolicy,
349
+ admissionPolicy: this.#options.admissionPolicy ??
350
+ ALLOW_PREPROVISIONED_MESH_ADMISSION,
351
+ crypto: this.#options.crypto,
352
+ protocolOptions: this.#options.protocolOptions,
353
+ supportedCriticalExtensions: this.#options.supportedCriticalExtensions,
354
+ });
355
+ if (!decision.accepted) {
356
+ return Object.freeze({
357
+ receipt: Object.freeze({
358
+ accepted: false,
359
+ messageId: envelope.messageId,
360
+ reasonCode: decision.code,
361
+ target: this.address,
362
+ }),
363
+ outgoing: Object.freeze([]),
364
+ });
365
+ }
366
+ this.#state = decision.state;
367
+ const outgoing = await this.#interpret(decision.effects, time);
368
+ return Object.freeze({
369
+ receipt: Object.freeze({
370
+ accepted: true,
371
+ messageId: envelope.messageId,
372
+ target: this.address,
373
+ }),
374
+ outgoing,
375
+ });
376
+ });
377
+ }
378
+ async #interpret(initialEffects, time) {
379
+ const effects = [...initialEffects];
380
+ const outgoing = [];
381
+ for (let index = 0; index < effects.length; index += 1) {
382
+ if (index >= this.#transport.maximumInternalStepsPerTransition) {
383
+ throw new RangeError('Mesh loopback internal step limit exceeded');
384
+ }
385
+ const effect = effects[index];
386
+ this.#observe(effect);
387
+ if (effect.kind === 'message.prepare') {
388
+ let transition;
389
+ try {
390
+ const envelope = await this.#prepare(effect, time);
391
+ this.#prepared.set(effect.effectId, envelope);
392
+ transition = reduceMeshPeer(this.#state, Object.freeze({
393
+ kind: 'effect.result',
394
+ effectId: effect.effectId,
395
+ status: 'succeeded',
396
+ preparedMessage: Object.freeze({
397
+ messageId: envelope.messageId,
398
+ type: envelope.type,
399
+ audiencePeerId: effect.intent.audiencePeerId,
400
+ expiresAt: time.logicalTime + effect.intent.maximumLifetimeMs,
401
+ }),
402
+ }), time.logicalTime);
403
+ }
404
+ catch {
405
+ transition = reduceMeshPeer(this.#state, Object.freeze({
406
+ kind: 'effect.result',
407
+ effectId: effect.effectId,
408
+ status: 'failed',
409
+ errorCode: 'preparation_failed',
410
+ }), time.logicalTime);
411
+ }
412
+ this.#state = transition.state;
413
+ effects.push(...transition.effects);
414
+ }
415
+ else if (effect.kind === 'message.deliver') {
416
+ const envelope = this.#prepared.get(effect.preparationEffectId);
417
+ if (!envelope ||
418
+ envelope.messageId !== effect.messageId ||
419
+ envelope.audience.kind !== 'peer' ||
420
+ envelope.audience.peerId !== effect.audiencePeerId) {
421
+ throw new TypeError('Mesh loopback delivery lacks prepared bytes');
422
+ }
423
+ this.#prepared.delete(effect.preparationEffectId);
424
+ outgoing.push(envelope);
425
+ }
426
+ }
427
+ return Object.freeze(outgoing);
428
+ }
429
+ dispose() {
430
+ this.#prepared.clear();
431
+ }
432
+ async #prepare(effect, time) {
433
+ if (this.#outboundSequence >= Number.MAX_SAFE_INTEGER) {
434
+ throw new RangeError('Mesh loopback outbound sequence exhausted');
435
+ }
436
+ if (time.logicalTime >
437
+ Number.MAX_SAFE_INTEGER - effect.intent.maximumLifetimeMs) {
438
+ throw new RangeError('Mesh loopback logical expiry overflow');
439
+ }
440
+ this.#outboundSequence += 1;
441
+ const messageId = (this.#options.messageIds ?? defaultMessageIdSource(this.#options.crypto)).nextMessageId();
442
+ const sentAtMilliseconds = Date.parse(time.timestamp);
443
+ if (!Number.isFinite(sentAtMilliseconds)) {
444
+ throw new TypeError('Invalid Mesh loopback timestamp');
445
+ }
446
+ const expiresAt = new Date(sentAtMilliseconds + effect.intent.maximumLifetimeMs).toISOString();
447
+ const intent = effect.intent;
448
+ const unsigned = {
449
+ protocol: MESH_PROTOCOL,
450
+ wireVersion: MESH_WIRE_VERSION,
451
+ messageId,
452
+ tenantId: this.#state.identity.tenantId,
453
+ meshId: this.#state.identity.meshId,
454
+ type: intent.type,
455
+ sender: Object.freeze({
456
+ peerId: this.#state.identity.peerId,
457
+ instanceId: this.#state.identity.instanceId,
458
+ }),
459
+ audience: Object.freeze({
460
+ kind: 'peer',
461
+ peerId: intent.audiencePeerId,
462
+ }),
463
+ sequence: this.#outboundSequence,
464
+ sentAt: time.timestamp,
465
+ expiresAt,
466
+ payload: intent.payload,
467
+ proof: Object.freeze({
468
+ algorithm: MESH_SIGNATURE_ALGORITHM,
469
+ keyId: this.#state.identity.keyId,
470
+ }),
471
+ ...(intent.objectiveId === undefined
472
+ ? {}
473
+ : { objectiveId: intent.objectiveId }),
474
+ ...(intent.causationId === undefined
475
+ ? {}
476
+ : { causationId: intent.causationId }),
477
+ ...(intent.correlationId === undefined
478
+ ? {}
479
+ : { correlationId: intent.correlationId }),
480
+ };
481
+ return this.#options.signer.sign({
482
+ envelope: unsigned,
483
+ privateKey: this.#options.privateKey,
484
+ crypto: this.#options.crypto,
485
+ protocolOptions: this.#options.protocolOptions,
486
+ });
487
+ }
488
+ #observe(effect) {
489
+ try {
490
+ this.#options.onEffect?.(effect);
491
+ }
492
+ catch {
493
+ // Observability cannot reverse a committed peer decision.
494
+ }
495
+ }
496
+ #exclusive(operation) {
497
+ const result = this.#tail.then(operation, operation);
498
+ this.#tail = result.then(() => undefined, () => undefined);
499
+ return result;
500
+ }
501
+ }
502
+ function directTarget(envelope) {
503
+ if (!envelope ||
504
+ typeof envelope !== 'object' ||
505
+ envelope.audience?.kind !== 'peer') {
506
+ return undefined;
507
+ }
508
+ return Object.freeze({
509
+ tenantId: envelope.tenantId,
510
+ meshId: envelope.meshId,
511
+ peerId: envelope.audience.peerId,
512
+ });
513
+ }
514
+ function freezeAddress(value) {
515
+ if (!value ||
516
+ typeof value.tenantId !== 'string' ||
517
+ typeof value.meshId !== 'string' ||
518
+ typeof value.peerId !== 'string') {
519
+ throw new TypeError('Invalid Mesh loopback address');
520
+ }
521
+ return Object.freeze({
522
+ tenantId: value.tenantId,
523
+ meshId: value.meshId,
524
+ peerId: value.peerId,
525
+ });
526
+ }
527
+ function freezeTime(value) {
528
+ if (!value ||
529
+ !Number.isSafeInteger(value.logicalTime) ||
530
+ value.logicalTime < 0 ||
531
+ typeof value.timestamp !== 'string') {
532
+ throw new TypeError('Invalid Mesh loopback time sample');
533
+ }
534
+ return Object.freeze({
535
+ logicalTime: value.logicalTime,
536
+ timestamp: value.timestamp,
537
+ });
538
+ }
539
+ function addressKey(address) {
540
+ return JSON.stringify([address.tenantId, address.meshId, address.peerId]);
541
+ }
542
+ function defaultMessageIdSource(cryptoOverride) {
543
+ const cryptoProvider = cryptoOverride ?? globalThis.crypto;
544
+ return {
545
+ nextMessageId() {
546
+ if (!cryptoProvider?.getRandomValues) {
547
+ throw new Error('Mesh loopback message ID source is unavailable');
548
+ }
549
+ const bytes = cryptoProvider.getRandomValues(new Uint8Array(16));
550
+ return encodeBase64Url(bytes);
551
+ },
552
+ };
553
+ }
554
+ function encodeBase64Url(bytes) {
555
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
556
+ let output = '';
557
+ for (let index = 0; index < bytes.length; index += 3) {
558
+ const first = bytes[index];
559
+ const second = bytes[index + 1];
560
+ const third = bytes[index + 2];
561
+ const combined = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
562
+ output += alphabet[(combined >>> 18) & 63];
563
+ output += alphabet[(combined >>> 12) & 63];
564
+ if (second !== undefined)
565
+ output += alphabet[(combined >>> 6) & 63];
566
+ if (third !== undefined)
567
+ output += alphabet[combined & 63];
568
+ }
569
+ return output;
570
+ }
571
+ const envelopeEncoder = new TextEncoder();
572
+ function envelopeByteLength(envelope) {
573
+ try {
574
+ return envelopeEncoder.encode(JSON.stringify(envelope)).byteLength;
575
+ }
576
+ catch {
577
+ return Number.MAX_SAFE_INTEGER;
578
+ }
579
+ }
580
+ //# sourceMappingURL=loopback.js.map