@qilin-agent/remote-mock 0.0.0-stage → 3.1.3

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/lib/index.js ADDED
@@ -0,0 +1,711 @@
1
+ import { fn } from "@vitest/spy";
2
+ import { isRemoteUplinkItem } from "@qilin-agent/typert-protocol";
3
+ //#region lib/types/log.js
4
+ /** What a `RemoteMock` saw: answered calls, opened streams, and requests without a rule. */
5
+ /** The mock's log store. */
6
+ var MockLogStore = class {
7
+ callEntries = [];
8
+ streamEntries = [];
9
+ missEntries = [];
10
+ seq = 0;
11
+ calls(endpoint) {
12
+ return this.callEntries.filter((entry) => endpoint === void 0 || entry.endpoint === endpoint);
13
+ }
14
+ streams(endpoint) {
15
+ return this.streamEntries.filter((entry) => endpoint === void 0 || entry.endpoint === endpoint);
16
+ }
17
+ requests(endpoint) {
18
+ return [...this.calls(endpoint), ...this.streams(endpoint)].filter((entry) => endpoint !== void 0 || !entry.endpoint.startsWith("$")).sort((a, b) => a.seq - b.seq).map((entry) => entry.args[0]);
19
+ }
20
+ unmatched() {
21
+ return [...this.missEntries];
22
+ }
23
+ /**
24
+ * Record one call whose rule was selected.
25
+ * @param endpoint - endpoint.
26
+ * @param args - positional args.
27
+ * @returns the mutable entry the dispatcher settles.
28
+ */
29
+ call(endpoint, args) {
30
+ const record = {
31
+ endpoint,
32
+ args,
33
+ state: "pending",
34
+ result: void 0,
35
+ seq: ++this.seq
36
+ };
37
+ this.callEntries.push(record);
38
+ return record;
39
+ }
40
+ /**
41
+ * Record one stream open.
42
+ * @param endpoint - endpoint.
43
+ * @param args - positional args.
44
+ * @returns the mutable entry the stream updates.
45
+ */
46
+ stream(endpoint, args) {
47
+ const record = {
48
+ endpoint,
49
+ args,
50
+ state: "open",
51
+ pushed: 0,
52
+ seq: ++this.seq
53
+ };
54
+ this.streamEntries.push(record);
55
+ return record;
56
+ }
57
+ /**
58
+ * Record one request without a rule.
59
+ * @param endpoint - endpoint.
60
+ * @param mode - how it was requested.
61
+ */
62
+ miss(endpoint, mode) {
63
+ this.missEntries.push({
64
+ endpoint,
65
+ mode
66
+ });
67
+ }
68
+ };
69
+ //#endregion
70
+ //#region lib/types/streams.js
71
+ /** Stream scripts and the pushable, abort-aware stream a script drives. */
72
+ /**
73
+ * Type a fake stream method's result as the handle a generated Client method
74
+ * returns. The uplink members are inert: a fake built on it drives the downlink only.
75
+ * @param source - downlink items the fake yields.
76
+ * @returns a handle that iterates `source`.
77
+ */
78
+ function streamHandle(source) {
79
+ return {
80
+ [Symbol.asyncIterator]: () => source[Symbol.asyncIterator](),
81
+ send: () => void 0,
82
+ end: () => void 0,
83
+ dispose: () => void 0
84
+ };
85
+ }
86
+ /**
87
+ * Lift a fake stream method written as an async generator function into the
88
+ * signature of the generated method it stands in for.
89
+ * @param method - fake taking the method's arguments and yielding its downlink items.
90
+ * @returns the fake returning {@link streamHandle} of each call.
91
+ */
92
+ function streamMethod(method) {
93
+ const lifted = (...args) => streamHandle(method(...args));
94
+ return lifted;
95
+ }
96
+ /**
97
+ * Script yielding `items`, then ending.
98
+ * @param items - items in order.
99
+ * @returns the script.
100
+ */
101
+ function frames(items) {
102
+ return (_args, stream) => {
103
+ for (const item of items) stream.push(item);
104
+ stream.end();
105
+ };
106
+ }
107
+ /**
108
+ * Script yielding `initial`, then staying open for `streams.push`.
109
+ * @param initial - items yielded on open.
110
+ * @returns the script.
111
+ */
112
+ function openStream(initial = []) {
113
+ return (_args, stream) => {
114
+ for (const item of initial) stream.push(item);
115
+ };
116
+ }
117
+ /** One open stream: a queue the script pushes into and a single consumer reads from; the log entry tracks its state. */
118
+ var MockStream = class {
119
+ record;
120
+ sourceSignal;
121
+ uplink;
122
+ release;
123
+ queue = [];
124
+ settled;
125
+ waiting;
126
+ drainWaiters = [];
127
+ cancellation = new AbortController();
128
+ signal = this.cancellation.signal;
129
+ /** Consumer cancellation listener; attached while the stream is open, removed when it settles or cancels. */
130
+ onAbort = () => {
131
+ this.cancel(this.sourceSignal.reason);
132
+ };
133
+ /** Items pushed but not yet pulled by the consumer. */
134
+ get queued() {
135
+ return this.queue.length;
136
+ }
137
+ /**
138
+ * @param record - log entry this stream updates.
139
+ * @param sourceSignal - cancellation from the caller that opened the stream.
140
+ * @param uplink - the uplink the script reads.
141
+ * @param release - closes the uplink this stream owns; called once, when the stream settles, is cancelled, or the consumer leaves.
142
+ */
143
+ constructor(record, sourceSignal, uplink, release) {
144
+ this.record = record;
145
+ this.sourceSignal = sourceSignal;
146
+ this.uplink = uplink;
147
+ this.release = release;
148
+ if (sourceSignal.aborted) this.cancel(sourceSignal.reason);
149
+ else sourceSignal.addEventListener("abort", this.onAbort, { once: true });
150
+ }
151
+ push(item) {
152
+ if (this.record.state !== "open") return;
153
+ this.record.pushed += 1;
154
+ if (this.waiting !== void 0) {
155
+ const { resolve } = this.waiting;
156
+ this.waiting = void 0;
157
+ resolve({
158
+ value: item,
159
+ done: false
160
+ });
161
+ return;
162
+ }
163
+ this.queue.push(item);
164
+ }
165
+ end() {
166
+ this.settle({ kind: "end" }, "ended");
167
+ }
168
+ fail(error) {
169
+ this.settle({
170
+ kind: "fail",
171
+ error
172
+ }, "failed");
173
+ }
174
+ /** The consumer left: cancel the stream and drop what it left. */
175
+ dispose() {
176
+ this.cancel(/* @__PURE__ */ new Error(`remote-mock: ${this.record.endpoint} stream disposed`));
177
+ }
178
+ /**
179
+ * Start `script` on this stream.
180
+ * @param script - the registered script.
181
+ * @param args - open args.
182
+ */
183
+ run(script, args) {
184
+ let outcome;
185
+ try {
186
+ outcome = script(args, this);
187
+ } catch (error) {
188
+ this.fail(toError(error));
189
+ return;
190
+ }
191
+ if (outcome instanceof Promise) outcome.catch((error) => {
192
+ this.fail(toError(error));
193
+ });
194
+ }
195
+ /**
196
+ * Resolve once the consumer has pulled every queued item and waits for the next one, or the stream is no longer
197
+ * open and holds nothing the consumer could still pull (a consumer that returns discards what it left).
198
+ * @returns settles when drained.
199
+ */
200
+ drained() {
201
+ if (this.isDrained()) return Promise.resolve();
202
+ return new Promise((resolve) => {
203
+ this.drainWaiters.push(resolve);
204
+ });
205
+ }
206
+ [Symbol.asyncIterator]() {
207
+ return {
208
+ next: () => this.next(),
209
+ return: () => {
210
+ this.cancel();
211
+ return Promise.resolve({
212
+ value: void 0,
213
+ done: true
214
+ });
215
+ }
216
+ };
217
+ }
218
+ next() {
219
+ const result = this.pull();
220
+ this.wakeDrained();
221
+ return result;
222
+ }
223
+ pull() {
224
+ if (this.queue.length > 0) return Promise.resolve({
225
+ value: this.queue.shift(),
226
+ done: false
227
+ });
228
+ if (this.settled !== void 0) return this.finish(this.settled);
229
+ if (this.record.state === "cancelled") return Promise.resolve({
230
+ value: void 0,
231
+ done: true
232
+ });
233
+ if (this.waiting !== void 0) return Promise.reject(/* @__PURE__ */ new Error(`remote-mock: ${this.record.endpoint} stream has one consumer`));
234
+ return new Promise((resolve, reject) => {
235
+ this.waiting = {
236
+ resolve,
237
+ reject
238
+ };
239
+ });
240
+ }
241
+ finish(settled) {
242
+ return settled.kind === "end" ? Promise.resolve({
243
+ value: void 0,
244
+ done: true
245
+ }) : Promise.reject(settled.error);
246
+ }
247
+ settle(settled, state) {
248
+ if (this.record.state !== "open") return;
249
+ this.record.state = state;
250
+ this.settled = settled;
251
+ this.sourceSignal.removeEventListener("abort", this.onAbort);
252
+ this.release();
253
+ this.wakeDrained();
254
+ const waiting = this.waiting;
255
+ if (waiting === void 0) return;
256
+ this.waiting = void 0;
257
+ this.finish(settled).then(waiting.resolve, waiting.reject);
258
+ }
259
+ cancel(reason) {
260
+ if (this.record.state === "open") {
261
+ this.record.state = "cancelled";
262
+ this.sourceSignal.removeEventListener("abort", this.onAbort);
263
+ const waiting = this.waiting;
264
+ this.waiting = void 0;
265
+ this.queue.length = 0;
266
+ this.cancellation.abort(reason);
267
+ this.release();
268
+ waiting?.resolve({
269
+ value: void 0,
270
+ done: true
271
+ });
272
+ }
273
+ this.queue.length = 0;
274
+ this.wakeDrained();
275
+ }
276
+ isDrained() {
277
+ return this.queue.length === 0 && (this.waiting !== void 0 || this.record.state !== "open");
278
+ }
279
+ wakeDrained() {
280
+ if (!this.isDrained()) return;
281
+ const waiters = this.drainWaiters;
282
+ this.drainWaiters = [];
283
+ for (const resolve of waiters) resolve();
284
+ }
285
+ };
286
+ /**
287
+ * The uplink a direct handle owns: `send()` pushes, `end()` half-closes, and
288
+ * `close()` ends it and drops unread items once the stream settles, is
289
+ * cancelled, or the consumer leaves. The script reads it as `StreamHandle.uplink`.
290
+ */
291
+ var HandleUplink = class {
292
+ items = [];
293
+ done = false;
294
+ waiter;
295
+ /**
296
+ * Queues one uplink item for the script, or completes the read waiting for it.
297
+ * @param item - the item the handle's `send()` carried.
298
+ * @throws {Error} after `end()`.
299
+ */
300
+ push(item) {
301
+ if (this.done) throw new Error("remote-mock: uplink was ended");
302
+ const waiter = this.waiter;
303
+ this.waiter = void 0;
304
+ if (waiter === void 0) this.items.push(item);
305
+ else waiter({
306
+ value: item,
307
+ done: false
308
+ });
309
+ }
310
+ /** Half-closes the uplink: pending and later reads complete as done; repeated calls are no-ops. */
311
+ end() {
312
+ if (this.done) return;
313
+ this.done = true;
314
+ this.waiter?.({
315
+ value: void 0,
316
+ done: true
317
+ });
318
+ this.waiter = void 0;
319
+ }
320
+ /** Ends the uplink and drops unread items; the owning stream calls it on settle or cancel, the script's iterator on `return()`. */
321
+ close() {
322
+ this.items.length = 0;
323
+ this.end();
324
+ }
325
+ [Symbol.asyncIterator]() {
326
+ return {
327
+ next: () => {
328
+ if (this.items.length > 0) return Promise.resolve({
329
+ value: this.items.shift(),
330
+ done: false
331
+ });
332
+ if (this.done) return Promise.resolve({
333
+ value: void 0,
334
+ done: true
335
+ });
336
+ return new Promise((resolve) => {
337
+ this.waiter = resolve;
338
+ });
339
+ },
340
+ return: () => {
341
+ this.close();
342
+ return Promise.resolve({
343
+ value: void 0,
344
+ done: true
345
+ });
346
+ }
347
+ };
348
+ }
349
+ };
350
+ /**
351
+ * The handle a direct `mock.remote` stream call returns, as a generated method
352
+ * would: iterate it for the script's downlink, `send()` and `end()` feed
353
+ * `StreamHandle.uplink`, `dispose()` cancels the stream. `send()` applies the
354
+ * real handle's check, so a non-lossless JSON item throws and a top-level
355
+ * `undefined` passes. A stream opened through `rpc.open` reads the carrier's
356
+ * uplink instead, and this handle owns none.
357
+ */
358
+ var MockClientStream = class {
359
+ stream;
360
+ uplink;
361
+ /**
362
+ * @param stream - the script-side stream.
363
+ * @param uplink - the uplink this handle feeds; absent when the carrier supplied one.
364
+ */
365
+ constructor(stream, uplink) {
366
+ this.stream = stream;
367
+ this.uplink = uplink;
368
+ }
369
+ send(item) {
370
+ if (this.uplink === void 0) throw new Error(`remote-mock: ${this.stream.record.endpoint} uplink belongs to the carrier that opened the stream`);
371
+ if (!isRemoteUplinkItem(item)) throw new Error(`remote-mock: ${this.stream.record.endpoint} uplink item is not a lossless JSON value`);
372
+ this.uplink.push(item);
373
+ }
374
+ end() {
375
+ this.uplink?.end();
376
+ }
377
+ dispose() {
378
+ this.stream.dispose();
379
+ }
380
+ [Symbol.asyncIterator]() {
381
+ return this.stream[Symbol.asyncIterator]();
382
+ }
383
+ };
384
+ /**
385
+ * The `Error` a thrown value stands for: itself, or a new Error carrying its string form.
386
+ * @param reason - thrown value.
387
+ * @returns the error.
388
+ */
389
+ function toError(reason) {
390
+ return reason instanceof Error ? reason : new Error(String(reason));
391
+ }
392
+ //#endregion
393
+ //#region lib/types/remote-proxy.js
394
+ /**
395
+ * Cache accessed namespaces and expose the current native mock for each method.
396
+ * @param method - selects a stable endpoint mock for its configured invocation mode.
397
+ * @returns enumerable accessed namespaces and methods; symbols and thenable probes remain inert.
398
+ */
399
+ function createRemoteProxy(method) {
400
+ return new Proxy(Object.create(null), { get(namespaces, namespace) {
401
+ if (typeof namespace !== "string" || namespace === "then") return void 0;
402
+ return namespaces[namespace] ??= new Proxy(Object.create(null), { get(methods, name) {
403
+ if (typeof name !== "string" || name === "then") return void 0;
404
+ return methods[name] = method(`${namespace}/${name}`);
405
+ } });
406
+ } });
407
+ }
408
+ //#endregion
409
+ //#region lib/types/remote-mock.js
410
+ /** `RemoteMock`: an endpoint table (unary answers or stream scripts), live stream control, a log, and the Connection carrier face. */
411
+ const EVENTS_ENDPOINT = "$events";
412
+ /**
413
+ * The success envelope the Client's Remote callers read: `{ ok: true, value }`.
414
+ * @param value - success value.
415
+ * @returns the envelope.
416
+ */
417
+ function ok(value) {
418
+ return {
419
+ ok: true,
420
+ value
421
+ };
422
+ }
423
+ /** A missing default answer is an unmatched request, not a selected rule that failed. */
424
+ var MissingUnaryRule = class extends Error {};
425
+ /**
426
+ * Endpoint-named Remote mock. `dispatch` / `open` are the core; `rpc` is the
427
+ * same core as the decoded carrier accepted by the Connection installer.
428
+ */
429
+ var RemoteMock = class RemoteMock {
430
+ /**
431
+ * Create a mock whose `$events` stream answers the Gateway client's opening
432
+ * with one ready frame and then stays open, so the assembled client connects.
433
+ * @param options - host facts for the ready frame.
434
+ * @returns the mock.
435
+ */
436
+ static create(options = {}) {
437
+ const mock = new RemoteMock();
438
+ const host = options.host ?? { home: "/home/mock" };
439
+ let generation = 0;
440
+ return mock.stream(EVENTS_ENDPOINT, (_args, stream) => {
441
+ generation += 1;
442
+ stream.push({
443
+ type: "ready",
444
+ clientId: `mock-client-${String(generation)}`,
445
+ host
446
+ });
447
+ });
448
+ }
449
+ unaryDefaults = /* @__PURE__ */ new Map();
450
+ scripts = /* @__PURE__ */ new Map();
451
+ live = [];
452
+ openWaiters = [];
453
+ logStore = new MockLogStore();
454
+ unaryMocks = /* @__PURE__ */ new Map();
455
+ streamMocks = /* @__PURE__ */ new Map();
456
+ /** The carrier's uplink for the open in progress through {@link open}; a direct call has none. */
457
+ carrierUplink;
458
+ proxy = createRemoteProxy((endpoint) => this.modeOf(endpoint) === "stream" ? this.streamMock(endpoint) : this.unaryMock(endpoint));
459
+ /**
460
+ * Native mock functions for every namespace: configure answers and inspect calls without declaring method names.
461
+ * Explicit stream registrations select stream mocks; all other methods use unary mocks.
462
+ * Native overrides belong to this instance; `mockReset()` restores the implementation reading current defaults.
463
+ * Generated declarations provide exact signatures; absent declarations weaken only this test-owned proxy.
464
+ */
465
+ remote = this.proxy;
466
+ /** Everything observed so far. */
467
+ log = this.logStore;
468
+ /** Control over streams opened by registered scripts. */
469
+ streams = {
470
+ push: (endpoint, item, where) => this.forEachOpen(endpoint, where, (stream) => {
471
+ stream.push(item);
472
+ }),
473
+ end: (endpoint, where) => this.forEachOpen(endpoint, where, (stream) => {
474
+ stream.end();
475
+ }),
476
+ fail: (endpoint, error, where) => this.forEachOpen(endpoint, where, (stream) => {
477
+ stream.fail(error);
478
+ }),
479
+ opened: (endpoint, count) => {
480
+ if (this.logStore.streams(endpoint).length >= count) return Promise.resolve();
481
+ return new Promise((resolve) => {
482
+ this.openWaiters.push({
483
+ endpoint,
484
+ count,
485
+ resolve
486
+ });
487
+ });
488
+ },
489
+ drained: (endpoint, where) => Promise.all(this.matching(endpoint, where).map((stream) => stream.drained())).then(() => void 0)
490
+ };
491
+ /**
492
+ * The Connection carrier face: `call` dispatches, `open` opens; payloads carry
493
+ * `{ args }` as the whole-client proxies (an array) or the Gateway's own
494
+ * endpoints (one object) send them, and a call aborted by its signal rejects.
495
+ */
496
+ rpc = {
497
+ call: async (_channel, endpoint, payload, signal) => {
498
+ const pending = this.dispatch(endpoint, argsOf(endpoint, payload));
499
+ return await (signal === void 0 ? pending : settleOrAbort(pending, signal));
500
+ },
501
+ open: (_channel, endpoint, payload, signal, uplink) => this.open(endpoint, argsOf(endpoint, payload), signal, uplink)
502
+ };
503
+ constructor() {}
504
+ unary(endpoint, rule) {
505
+ this.unaryDefaults.set(endpoint, rule);
506
+ return this;
507
+ }
508
+ /**
509
+ * Declare `endpoint` as a stream and replace its default script when supplied. A declaration without a script
510
+ * preserves any existing script; an endpoint declared without one fails an open as a stream miss.
511
+ * @param endpoint - `<namespace>/<method>`.
512
+ * @param script - script (`frames` / `openStream` build the common ones).
513
+ * @returns this.
514
+ */
515
+ stream(endpoint, script) {
516
+ if (script !== void 0 || !this.scripts.has(endpoint)) this.scripts.set(endpoint, script);
517
+ return this;
518
+ }
519
+ /**
520
+ * Register endpoint defaults without changing native mock overrides.
521
+ * @param table - unary answers and stream scripts.
522
+ * @returns this.
523
+ */
524
+ load(table) {
525
+ for (const [endpoint, rule] of Object.entries(table.unary ?? {})) this.unary(endpoint, rule);
526
+ for (const endpoint of table.streams ?? []) this.stream(endpoint);
527
+ for (const [endpoint, script] of Object.entries(table.stream ?? {})) this.stream(endpoint, script);
528
+ return this;
529
+ }
530
+ /**
531
+ * Whether `endpoint` is declared a stream (with or without a script) or has a unary rule — the one declaration
532
+ * the whole-client proxies need; an endpoint neither declared nor ruled is dispatched as a unary call.
533
+ * @param endpoint - endpoint.
534
+ * @returns the mode, or undefined when nothing is registered.
535
+ */
536
+ modeOf(endpoint) {
537
+ if (this.scripts.has(endpoint)) return "stream";
538
+ if (this.unaryDefaults.has(endpoint)) return "unary";
539
+ }
540
+ /**
541
+ * Registered endpoints and accessed proxy methods, sorted.
542
+ * @returns endpoint names.
543
+ */
544
+ endpoints() {
545
+ const names = new Set(this.scripts.keys());
546
+ for (const endpoint of this.unaryDefaults.keys()) names.add(endpoint);
547
+ for (const [namespace, methods] of Object.entries(this.proxy)) for (const method of Object.keys(methods)) names.add(`${namespace}/${method}`);
548
+ return [...names].sort();
549
+ }
550
+ /**
551
+ * Answer one unary call with the registered rule's value, verbatim. The call
552
+ * is logged as soon as its rule is selected and settles with the rule: a
553
+ * rule that throws or rejects fails the call with that error.
554
+ * @param endpoint - endpoint.
555
+ * @param args - positional args.
556
+ * @returns the answer.
557
+ * @throws {Error} when no rule is registered (logged as unmatched).
558
+ */
559
+ async dispatch(endpoint, args) {
560
+ let answer;
561
+ try {
562
+ answer = this.unaryMock(endpoint)(...args);
563
+ } catch (error) {
564
+ if (error instanceof MissingUnaryRule) throw error;
565
+ answer = Promise.reject(toError(error));
566
+ }
567
+ const record = this.logStore.call(endpoint, args);
568
+ try {
569
+ record.result = await answer;
570
+ } catch (error) {
571
+ record.state = "failed";
572
+ record.result = error;
573
+ throw error;
574
+ }
575
+ record.state = "answered";
576
+ return record.result;
577
+ }
578
+ unaryMock(endpoint) {
579
+ let mock = this.unaryMocks.get(endpoint);
580
+ if (mock === void 0) {
581
+ mock = fn((...args) => Promise.resolve(this.unaryAnswer(endpoint, args)));
582
+ this.unaryMocks.set(endpoint, mock);
583
+ }
584
+ return mock;
585
+ }
586
+ unaryAnswer(endpoint, args) {
587
+ if (!this.unaryDefaults.has(endpoint)) {
588
+ this.logStore.miss(endpoint, "unary");
589
+ throw new MissingUnaryRule(this.noRuleMessage(endpoint));
590
+ }
591
+ const rule = this.unaryDefaults.get(endpoint);
592
+ return isRuleFn(rule) ? answerOf(rule, args) : rule;
593
+ }
594
+ /**
595
+ * Open through the endpoint's native mock, which is called with the generated
596
+ * signature's arguments only; its default runs the registered script as a
597
+ * controlled stream. A native override returns its own iterable: the caller
598
+ * owns consumption and cancellation, outside `OpenStreams`.
599
+ * @param endpoint - endpoint.
600
+ * @param args - positional args.
601
+ * @param signal - consumer cancellation.
602
+ * @param uplink - the carrier's uplink for this open, which the script reads as `StreamHandle.uplink`;
603
+ * absent for a direct call, whose returned handle owns the uplink.
604
+ * @returns the controlled script stream or the native override's caller-owned iterable.
605
+ * @throws {Error} when the default runs without a registered script (logged as unmatched).
606
+ */
607
+ open(endpoint, args, signal, uplink) {
608
+ this.carrierUplink = uplink;
609
+ try {
610
+ return this.streamMock(endpoint)(...args, signal);
611
+ } finally {
612
+ this.carrierUplink = void 0;
613
+ }
614
+ }
615
+ streamMock(endpoint) {
616
+ let mock = this.streamMocks.get(endpoint);
617
+ if (mock === void 0) {
618
+ mock = fn((...values) => {
619
+ const args = [...values];
620
+ const signal = args.at(-1) instanceof AbortSignal ? args.pop() : new AbortController().signal;
621
+ return this.openScript(endpoint, args, signal, this.carrierUplink);
622
+ });
623
+ this.streamMocks.set(endpoint, mock);
624
+ }
625
+ return mock;
626
+ }
627
+ openScript(endpoint, args, signal, carrierUplink) {
628
+ const script = this.scripts.get(endpoint);
629
+ if (script === void 0) {
630
+ this.logStore.miss(endpoint, "stream");
631
+ throw new Error(this.noRuleMessage(endpoint));
632
+ }
633
+ let uplink;
634
+ let owned;
635
+ if (carrierUplink === void 0) {
636
+ owned = new HandleUplink();
637
+ uplink = owned;
638
+ } else uplink = carrierUplink;
639
+ const stream = new MockStream(this.logStore.stream(endpoint, args), signal, uplink, () => {
640
+ owned?.close();
641
+ });
642
+ this.live.push(stream);
643
+ this.wakeOpened(endpoint);
644
+ stream.run(script, args);
645
+ return new MockClientStream(stream, owned);
646
+ }
647
+ /** Throw when any request found no rule, naming the endpoints and the registered ones. */
648
+ assertNoUnmatched() {
649
+ const unmatched = this.logStore.unmatched();
650
+ if (unmatched.length === 0) return;
651
+ const lines = unmatched.map((entry) => ` ${entry.endpoint} (${entry.mode})`);
652
+ throw new Error(`remote-mock: ${String(unmatched.length)} unmatched request(s):\n${lines.join("\n")}\nregistered: ${this.endpoints().join(", ")}`);
653
+ }
654
+ noRuleMessage(endpoint) {
655
+ return `remote-mock: no rule for ${endpoint}; registered: ${this.endpoints().join(", ")}`;
656
+ }
657
+ /** Streams on `endpoint` still open or still holding items their consumer has not pulled; the rest are forgotten. */
658
+ matching(endpoint, where) {
659
+ this.live = this.live.filter((stream) => stream.record.state === "open" || stream.queued > 0);
660
+ return this.live.filter((stream) => stream.record.endpoint === endpoint && (where === void 0 || where(stream.record.args)));
661
+ }
662
+ forEachOpen(endpoint, where, action) {
663
+ const targets = this.matching(endpoint, where).filter((stream) => stream.record.state === "open");
664
+ for (const stream of targets) action(stream);
665
+ return targets.length;
666
+ }
667
+ wakeOpened(endpoint) {
668
+ const opened = this.logStore.streams(endpoint).length;
669
+ const ready = this.openWaiters.filter((waiter) => waiter.endpoint === endpoint && opened >= waiter.count);
670
+ this.openWaiters = this.openWaiters.filter((waiter) => !ready.includes(waiter));
671
+ for (const waiter of ready) waiter.resolve();
672
+ }
673
+ };
674
+ function isRuleFn(rule) {
675
+ return typeof rule === "function";
676
+ }
677
+ /** A rule's synchronous throw becomes a rejection so the call settles through one path. */
678
+ function answerOf(rule, args) {
679
+ try {
680
+ return rule(...args);
681
+ } catch (error) {
682
+ return Promise.reject(toError(error));
683
+ }
684
+ }
685
+ /** Positional args from a carrier payload: the array the whole-client proxies send, or the one object the Gateway's own endpoints send. */
686
+ function argsOf(endpoint, payload) {
687
+ if (typeof payload === "object" && payload !== null && "args" in payload) {
688
+ const { args } = payload;
689
+ if (Array.isArray(args)) return args;
690
+ if (typeof args === "object" && args !== null) return [args];
691
+ }
692
+ throw new TypeError(`remote-mock: payload of ${endpoint} must be { args: unknown[] | object }`);
693
+ }
694
+ /**
695
+ * Settle with the call, or reject with the abort reason first; the call itself
696
+ * always keeps a handler so its own outcome is never an unhandled rejection.
697
+ */
698
+ function settleOrAbort(pending, signal) {
699
+ return new Promise((resolve, reject) => {
700
+ const abort = () => {
701
+ reject(signal.reason instanceof Error ? signal.reason : new Error("remote-mock: call aborted", { cause: signal.reason }));
702
+ };
703
+ signal.addEventListener("abort", abort, { once: true });
704
+ pending.then(resolve, reject).finally(() => {
705
+ signal.removeEventListener("abort", abort);
706
+ });
707
+ if (signal.aborted) abort();
708
+ });
709
+ }
710
+ //#endregion
711
+ export { MockClientStream, RemoteMock, frames, ok, openStream, streamHandle, streamMethod };