@deepseek-ai/dsh-remote-mock 0.1.6-alpha.2 → 0.1.7-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.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/test-support/remote-mock/README.md
5
- README.md: 82979e37052aecee6ec716702f634d25c969f5ad
6
- README.zh.md: 201ca011410dd5a4d65b6ebdfb0c47f23742b0d3
5
+ README.md: 7f76ce6217d7cc6f86923aa1ee4f7257ad30926e
6
+ README.zh.md: b79849fe3b23b5135feff7095468f65809b0d506
package/README.md CHANGED
@@ -73,7 +73,9 @@ mock.streams.fail('session/follow', new Error('gone'))
73
73
  await mock.streams.drained('session/follow')
74
74
  ```
75
75
 
76
- A failed stream rejects the consumer's next read with the given `Error`. Consumer cancellation (the opening signal or an early iterator `return()`) aborts `StreamHandle.signal`, ends the iteration without throwing, and logs the stream as `cancelled`.
76
+ A failed stream rejects the consumer's next read with the given `Error`. Consumer cancellation (the opening signal or an early iterator `return()`) aborts `StreamHandle.signal`, ends the iteration without throwing, and logs the stream as `cancelled`. `StreamHandle.uplink` is what the script reads as the Client's uplink: through `rpc.open` it is the carrier's iterable; for a direct `mock.remote.<namespace>.<method>(...)` call it is fed by the returned handle, which is the `RemoteStreamHandle` a generated method returns (iterate it for the downlink, `send()` and `end()` feed the uplink, `dispose()` cancels the stream); `send()` rejects a non-lossless JSON item as the real handle does, and the uplink it owns closes once the stream settles, is cancelled, or the consumer leaves, so a later `send()` throws. The mock function is called with the method's own arguments only, so the uplink never appears in call assertions or logged args.
77
+
78
+ A fake that stands in for a generated stream method returns the `RemoteStreamHandle` the generated method does. `streamHandle(source)` types an `AsyncIterable` as that handle with inert `send`, `end`, and `dispose`; `streamMethod<M>(generator)` lifts an async generator function written for the method's arguments into the method's own signature, for `vi.fn<M>()` and `mockImplementation`.
77
79
 
78
80
  ### Connect a client
79
81
 
@@ -108,7 +110,7 @@ A failed stream rejects the consumer's next read with the given `Error`. Consume
108
110
  | [`src/index.ts`](src/index.ts) | Public face re-exports |
109
111
  | [`src/remote-mock.ts`](src/remote-mock.ts) | `RemoteMock`: default responses, native mocks, Connection dispatch, controlled streams and missing-response checks; `ok` |
110
112
  | [`src/remote-proxy.ts`](src/remote-proxy.ts) | Namespace/method lookup and generated-map mock types |
111
- | [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` scripts and `MockStream` (handle + `AsyncIterable`) |
113
+ | [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` scripts, `streamHandle` / `streamMethod` fake typing, and `MockStream` (handle + `AsyncIterable`) |
112
114
  | [`src/log.ts`](src/log.ts) | Log store with the shared `seq` counter |
113
115
  | — | No runtime invariant companion is published; this test-support library owns no production event stream or mutable process state, and its behavior is exercised by its package tests. |
114
116
 
@@ -130,7 +132,7 @@ None; this package neither assembles nor sends a provider request.
130
132
  <a id="known-limitations-and-deferred-work"></a>
131
133
 
132
134
  - **In-process carrier only** — `rpc` serves a Connection instance in the same realm; no HTTP or WebSocket carrier for browser-lane specs is provided.
133
- - **Values cross by reference** — answers and stream items reach the client unserialized, so a non-JSON value that the real wire would reject passes through unchanged.
135
+ - **Values cross by reference** — answers and downlink items reach the client unserialized, so a non-JSON downlink value that the real wire would reject passes through unchanged; only the handle's `send()` applies the real uplink check.
134
136
  - **Values are not checked** — a unary answer must be the result the caller reads (`{ ok, value }` or `{ ok: false, error }`); the mock passes it through unchanged and does not check those fields.
135
137
  - **No payload matching** — rules match on endpoint only; discriminate on business arguments inside a handler.
136
138
  - **Native stream overrides own their iterable** — an override returning its own iterable bypasses scripted-stream logs, `requests`, `opened`, `drained`, and `push` / `end` / `fail`; the caller also owns cancellation. Native call assertions still work. Use a registered stream script when a scenario needs those controls.
package/README.zh.md CHANGED
@@ -73,7 +73,9 @@ mock.streams.fail('session/follow', new Error('gone'))
73
73
  await mock.streams.drained('session/follow')
74
74
  ```
75
75
 
76
- 失败的流让消费方的下一次读取以给定的 `Error` reject。消费方取消(打开时的 signal 或 iterator 提前 `return()`)会中止 `StreamHandle.signal`、结束迭代而不抛错,并把该流记为 `cancelled`。
76
+ 失败的流让消费方的下一次读取以给定的 `Error` reject。消费方取消(打开时的 signal 或 iterator 提前 `return()`)会中止 `StreamHandle.signal`、结束迭代而不抛错,并把该流记为 `cancelled`。`StreamHandle.uplink` 是脚本读到的 Client 上行:经 `rpc.open` 打开时它是载体传入的 iterable;直接调用 `mock.remote.<namespace>.<method>(...)` 时它由返回的句柄喂入——该句柄就是生成方法返回的 `RemoteStreamHandle`(迭代它读下行,`send()` 与 `end()` 喂上行,`dispose()` 取消流);`send()` 像真实句柄一样拒绝非无损 JSON 项,它拥有的上行在流结束、取消或消费方离开时关闭,之后的 `send()` 抛错。mock 函数只以方法自己的参数被调用,上行不会出现在调用断言或记录的 args 里。
77
+
78
+ 替代生成流方法的假实现返回生成方法所返回的 `RemoteStreamHandle`。`streamHandle(source)` 把一个 `AsyncIterable` 标注为该句柄,其 `send`、`end`、`dispose` 为空操作;`streamMethod<M>(generator)` 把按方法参数编写的 async generator 函数提升为方法自己的签名,供 `vi.fn<M>()` 与 `mockImplementation` 使用。
77
79
 
78
80
  ### 接上客户端
79
81
 
@@ -108,7 +110,7 @@ await mock.streams.drained('session/follow')
108
110
  | [`src/index.ts`](src/index.ts) | 公开面转出 |
109
111
  | [`src/remote-mock.ts`](src/remote-mock.ts) | `RemoteMock`:默认响应、原生 mock、Connection 分发、受控流与缺失响应检查;`ok` |
110
112
  | [`src/remote-proxy.ts`](src/remote-proxy.ts) | 命名空间/方法查找与生成映射的 mock 类型 |
111
- | [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` 脚本与 `MockStream`(句柄 + `AsyncIterable`) |
113
+ | [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` 脚本、`streamHandle` / `streamMethod` 假实现标注,以及 `MockStream`(句柄 + `AsyncIterable`) |
112
114
  | [`src/log.ts`](src/log.ts) | 带共享 `seq` 计数器的日志 |
113
115
  | — | 不发布运行时不变量伴生件;本测试支持库不拥有任何生产事件流或可变进程状态,其行为由本包测试覆盖。 |
114
116
 
@@ -130,7 +132,7 @@ await mock.streams.drained('session/follow')
130
132
  <a id="known-limitations-and-deferred-work"></a>
131
133
 
132
134
  - **仅进程内载体**——`rpc` 服务同一 realm 中的 Connection 实例;不提供给浏览器车道测试用的 HTTP 或 WebSocket 载体。
133
- - **值按引用传递**——应答与流项都未经序列化就到达客户端,真实线路会拒绝的非 JSON 值在这里原样通过。
135
+ - **值按引用传递**——应答与下行项都未经序列化就到达客户端,真实线路会拒绝的非 JSON 下行值在这里原样通过;只有句柄的 `send()` 施加真实的上行校验。
134
136
  - **不校验值**——一元应答必须是调用方读取的结果(`{ ok, value }` 或 `{ ok: false, error }`);mock 原样传递它,不检查这些字段。
135
137
  - **不做 payload 匹配**——规则只按端点匹配;在 handler 内按业务参数判别。
136
138
  - **原生流覆盖自行管理 iterable**——覆盖返回自有 iterable 时,不参与脚本流日志、`requests`、`opened`、`drained` 以及 `push` / `end` / `fail`;调用方也负责取消。原生调用断言仍然有效。需要这些控制能力的场景应使用已登记的流脚本。
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fn } from "@vitest/spy";
2
+ import { isRemoteUplinkItem } from "@deepseek-ai/dsh-typert-protocol";
2
3
  //#region lib/types/log.js
3
4
  /** What a `RemoteMock` saw: answered calls, opened streams, and requests without a rule. */
4
5
  /** The mock's log store. */
@@ -69,6 +70,30 @@ var MockLogStore = class {
69
70
  //#region lib/types/streams.js
70
71
  /** Stream scripts and the pushable, abort-aware stream a script drives. */
71
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
+ /**
72
97
  * Script yielding `items`, then ending.
73
98
  * @param items - items in order.
74
99
  * @returns the script.
@@ -93,6 +118,8 @@ function openStream(initial = []) {
93
118
  var MockStream = class {
94
119
  record;
95
120
  sourceSignal;
121
+ uplink;
122
+ release;
96
123
  queue = [];
97
124
  settled;
98
125
  waiting;
@@ -110,10 +137,14 @@ var MockStream = class {
110
137
  /**
111
138
  * @param record - log entry this stream updates.
112
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.
113
142
  */
114
- constructor(record, sourceSignal) {
143
+ constructor(record, sourceSignal, uplink, release) {
115
144
  this.record = record;
116
145
  this.sourceSignal = sourceSignal;
146
+ this.uplink = uplink;
147
+ this.release = release;
117
148
  if (sourceSignal.aborted) this.cancel(sourceSignal.reason);
118
149
  else sourceSignal.addEventListener("abort", this.onAbort, { once: true });
119
150
  }
@@ -140,6 +171,10 @@ var MockStream = class {
140
171
  error
141
172
  }, "failed");
142
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
+ }
143
178
  /**
144
179
  * Start `script` on this stream.
145
180
  * @param script - the registered script.
@@ -214,6 +249,7 @@ var MockStream = class {
214
249
  this.record.state = state;
215
250
  this.settled = settled;
216
251
  this.sourceSignal.removeEventListener("abort", this.onAbort);
252
+ this.release();
217
253
  this.wakeDrained();
218
254
  const waiting = this.waiting;
219
255
  if (waiting === void 0) return;
@@ -228,6 +264,7 @@ var MockStream = class {
228
264
  this.waiting = void 0;
229
265
  this.queue.length = 0;
230
266
  this.cancellation.abort(reason);
267
+ this.release();
231
268
  waiting?.resolve({
232
269
  value: void 0,
233
270
  done: true
@@ -247,6 +284,104 @@ var MockStream = class {
247
284
  }
248
285
  };
249
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
+ /**
250
385
  * The `Error` a thrown value stands for: itself, or a new Error carrying its string form.
251
386
  * @param reason - thrown value.
252
387
  * @returns the error.
@@ -318,6 +453,8 @@ var RemoteMock = class RemoteMock {
318
453
  logStore = new MockLogStore();
319
454
  unaryMocks = /* @__PURE__ */ new Map();
320
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;
321
458
  proxy = createRemoteProxy((endpoint) => this.modeOf(endpoint) === "stream" ? this.streamMock(endpoint) : this.unaryMock(endpoint));
322
459
  /**
323
460
  * Native mock functions for every namespace: configure answers and inspect calls without declaring method names.
@@ -361,7 +498,7 @@ var RemoteMock = class RemoteMock {
361
498
  const pending = this.dispatch(endpoint, argsOf(endpoint, payload));
362
499
  return await (signal === void 0 ? pending : settleOrAbort(pending, signal));
363
500
  },
364
- open: (_channel, endpoint, payload, signal) => this.open(endpoint, argsOf(endpoint, payload), signal)
501
+ open: (_channel, endpoint, payload, signal, uplink) => this.open(endpoint, argsOf(endpoint, payload), signal, uplink)
365
502
  };
366
503
  constructor() {}
367
504
  unary(endpoint, rule) {
@@ -455,16 +592,25 @@ var RemoteMock = class RemoteMock {
455
592
  return isRuleFn(rule) ? answerOf(rule, args) : rule;
456
593
  }
457
594
  /**
458
- * Open through the endpoint's native mock; its default runs the registered script as a controlled stream.
459
- * A native override returns its own iterable: the caller owns consumption and cancellation, outside `OpenStreams`.
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`.
460
599
  * @param endpoint - endpoint.
461
600
  * @param args - positional args.
462
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.
463
604
  * @returns the controlled script stream or the native override's caller-owned iterable.
464
605
  * @throws {Error} when the default runs without a registered script (logged as unmatched).
465
606
  */
466
- open(endpoint, args, signal) {
467
- return this.streamMock(endpoint)(...args, signal);
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
+ }
468
614
  }
469
615
  streamMock(endpoint) {
470
616
  let mock = this.streamMocks.get(endpoint);
@@ -472,23 +618,31 @@ var RemoteMock = class RemoteMock {
472
618
  mock = fn((...values) => {
473
619
  const args = [...values];
474
620
  const signal = args.at(-1) instanceof AbortSignal ? args.pop() : new AbortController().signal;
475
- return this.openScript(endpoint, args, signal);
621
+ return this.openScript(endpoint, args, signal, this.carrierUplink);
476
622
  });
477
623
  this.streamMocks.set(endpoint, mock);
478
624
  }
479
625
  return mock;
480
626
  }
481
- openScript(endpoint, args, signal) {
627
+ openScript(endpoint, args, signal, carrierUplink) {
482
628
  const script = this.scripts.get(endpoint);
483
629
  if (script === void 0) {
484
630
  this.logStore.miss(endpoint, "stream");
485
631
  throw new Error(this.noRuleMessage(endpoint));
486
632
  }
487
- const stream = new MockStream(this.logStore.stream(endpoint, args), signal);
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
+ });
488
642
  this.live.push(stream);
489
643
  this.wakeOpened(endpoint);
490
644
  stream.run(script, args);
491
- return stream;
645
+ return new MockClientStream(stream, owned);
492
646
  }
493
647
  /** Throw when any request found no rule, naming the endpoints and the registered ones. */
494
648
  assertNoUnmatched() {
@@ -554,4 +708,4 @@ function settleOrAbort(pending, signal) {
554
708
  });
555
709
  }
556
710
  //#endregion
557
- export { RemoteMock, frames, ok, openStream };
711
+ export { MockClientStream, RemoteMock, frames, ok, openStream, streamHandle, streamMethod };
@@ -10,8 +10,9 @@
10
10
  */
11
11
  export { RemoteMock, ok } from './remote-mock.ts';
12
12
  export type { OpenStreams, RemoteMockOptions, RemoteTable, StreamFilter, UnaryRuleFn } from './remote-mock.ts';
13
- export { frames, openStream } from './streams.ts';
13
+ export { frames, openStream, streamHandle, streamMethod } from './streams.ts';
14
14
  export type { StreamHandle, StreamScript } from './streams.ts';
15
+ export { MockClientStream } from './streams.ts';
15
16
  export type { LoggedCall, LoggedMiss, LoggedStream, MockLog } from './log.ts';
16
17
  export type { MockedRemote } from './remote-proxy.ts';
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -90,6 +90,8 @@ export declare class RemoteMock {
90
90
  private readonly logStore;
91
91
  private readonly unaryMocks;
92
92
  private readonly streamMocks;
93
+ /** The carrier's uplink for the open in progress through {@link open}; a direct call has none. */
94
+ private carrierUplink;
93
95
  private readonly proxy;
94
96
  /**
95
97
  * Native mock functions for every namespace: configure answers and inspect calls without declaring method names.
@@ -162,15 +164,19 @@ export declare class RemoteMock {
162
164
  private unaryMock;
163
165
  private unaryAnswer;
164
166
  /**
165
- * Open through the endpoint's native mock; its default runs the registered script as a controlled stream.
166
- * A native override returns its own iterable: the caller owns consumption and cancellation, outside `OpenStreams`.
167
+ * Open through the endpoint's native mock, which is called with the generated
168
+ * signature's arguments only; its default runs the registered script as a
169
+ * controlled stream. A native override returns its own iterable: the caller
170
+ * owns consumption and cancellation, outside `OpenStreams`.
167
171
  * @param endpoint - endpoint.
168
172
  * @param args - positional args.
169
173
  * @param signal - consumer cancellation.
174
+ * @param uplink - the carrier's uplink for this open, which the script reads as `StreamHandle.uplink`;
175
+ * absent for a direct call, whose returned handle owns the uplink.
170
176
  * @returns the controlled script stream or the native override's caller-owned iterable.
171
177
  * @throws {Error} when the default runs without a registered script (logged as unmatched).
172
178
  */
173
- open(endpoint: string, args: readonly unknown[], signal: AbortSignal): AsyncIterable<unknown>;
179
+ open(endpoint: string, args: readonly unknown[], signal: AbortSignal, uplink?: AsyncIterable<unknown>): AsyncIterable<unknown>;
174
180
  private streamMock;
175
181
  private openScript;
176
182
  /** Throw when any request found no rule, naming the endpoints and the registered ones. */
@@ -1,5 +1,22 @@
1
1
  /** Stream scripts and the pushable, abort-aware stream a script drives. */
2
+ import { type RemoteStreamHandle } from '@deepseek-ai/dsh-typert-protocol';
2
3
  import type { StreamRecord } from './log.ts';
4
+ /**
5
+ * Type a fake stream method's result as the handle a generated Client method
6
+ * returns. The uplink members are inert: a fake built on it drives the downlink only.
7
+ * @param source - downlink items the fake yields.
8
+ * @returns a handle that iterates `source`.
9
+ */
10
+ export declare function streamHandle<Out, In = never>(source: AsyncIterable<Out>): RemoteStreamHandle<Out, In>;
11
+ /** Downlink item type of one generated stream method. */
12
+ type StreamItem<Method> = Method extends (...args: never[]) => RemoteStreamHandle<infer Out, unknown> ? Out : never;
13
+ /**
14
+ * Lift a fake stream method written as an async generator function into the
15
+ * signature of the generated method it stands in for.
16
+ * @param method - fake taking the method's arguments and yielding its downlink items.
17
+ * @returns the fake returning {@link streamHandle} of each call.
18
+ */
19
+ export declare function streamMethod<Method extends (...args: never[]) => RemoteStreamHandle<unknown, unknown>>(method: (...args: Parameters<Method>) => AsyncIterable<StreamItem<Method>>): Method;
3
20
  /** Test-side controls over one open stream. */
4
21
  export interface StreamHandle {
5
22
  /** Queue one item for the consumer. */
@@ -10,6 +27,12 @@ export interface StreamHandle {
10
27
  fail(error: Error): void;
11
28
  /** Aborts when the opening signal aborts or the consumer returns early. */
12
29
  readonly signal: AbortSignal;
30
+ /**
31
+ * Uplink items for the script: the carrier's iterable when the open came
32
+ * through `rpc.open`, otherwise what the returned handle's `send()` and
33
+ * `end()` feed.
34
+ */
35
+ readonly uplink: AsyncIterable<unknown>;
13
36
  }
14
37
  /**
15
38
  * How a stream endpoint answers an open: receives the open args and the
@@ -33,6 +56,8 @@ export declare function openStream(initial?: readonly unknown[]): StreamScript;
33
56
  export declare class MockStream implements StreamHandle, AsyncIterable<unknown> {
34
57
  readonly record: StreamRecord;
35
58
  private readonly sourceSignal;
59
+ readonly uplink: AsyncIterable<unknown>;
60
+ private readonly release;
36
61
  private readonly queue;
37
62
  private settled;
38
63
  private waiting;
@@ -46,11 +71,15 @@ export declare class MockStream implements StreamHandle, AsyncIterable<unknown>
46
71
  /**
47
72
  * @param record - log entry this stream updates.
48
73
  * @param sourceSignal - cancellation from the caller that opened the stream.
74
+ * @param uplink - the uplink the script reads.
75
+ * @param release - closes the uplink this stream owns; called once, when the stream settles, is cancelled, or the consumer leaves.
49
76
  */
50
- constructor(record: StreamRecord, sourceSignal: AbortSignal);
77
+ constructor(record: StreamRecord, sourceSignal: AbortSignal, uplink: AsyncIterable<unknown>, release: () => void);
51
78
  push(item: unknown): void;
52
79
  end(): void;
53
80
  fail(error: Error): void;
81
+ /** The consumer left: cancel the stream and drop what it left. */
82
+ dispose(): void;
54
83
  /**
55
84
  * Start `script` on this stream.
56
85
  * @param script - the registered script.
@@ -72,10 +101,53 @@ export declare class MockStream implements StreamHandle, AsyncIterable<unknown>
72
101
  private isDrained;
73
102
  private wakeDrained;
74
103
  }
104
+ /**
105
+ * The uplink a direct handle owns: `send()` pushes, `end()` half-closes, and
106
+ * `close()` ends it and drops unread items once the stream settles, is
107
+ * cancelled, or the consumer leaves. The script reads it as `StreamHandle.uplink`.
108
+ */
109
+ export declare class HandleUplink implements AsyncIterable<unknown> {
110
+ private readonly items;
111
+ private done;
112
+ private waiter;
113
+ /**
114
+ * Queues one uplink item for the script, or completes the read waiting for it.
115
+ * @param item - the item the handle's `send()` carried.
116
+ * @throws {Error} after `end()`.
117
+ */
118
+ push(item: unknown): void;
119
+ /** Half-closes the uplink: pending and later reads complete as done; repeated calls are no-ops. */
120
+ end(): void;
121
+ /** Ends the uplink and drops unread items; the owning stream calls it on settle or cancel, the script's iterator on `return()`. */
122
+ close(): void;
123
+ [Symbol.asyncIterator](): AsyncIterator<unknown>;
124
+ }
125
+ /**
126
+ * The handle a direct `mock.remote` stream call returns, as a generated method
127
+ * would: iterate it for the script's downlink, `send()` and `end()` feed
128
+ * `StreamHandle.uplink`, `dispose()` cancels the stream. `send()` applies the
129
+ * real handle's check, so a non-lossless JSON item throws and a top-level
130
+ * `undefined` passes. A stream opened through `rpc.open` reads the carrier's
131
+ * uplink instead, and this handle owns none.
132
+ */
133
+ export declare class MockClientStream implements RemoteStreamHandle<unknown, unknown> {
134
+ private readonly stream;
135
+ private readonly uplink;
136
+ /**
137
+ * @param stream - the script-side stream.
138
+ * @param uplink - the uplink this handle feeds; absent when the carrier supplied one.
139
+ */
140
+ constructor(stream: MockStream, uplink: HandleUplink | undefined);
141
+ send(item: unknown): void;
142
+ end(): void;
143
+ dispose(): void;
144
+ [Symbol.asyncIterator](): AsyncIterator<unknown>;
145
+ }
75
146
  /**
76
147
  * The `Error` a thrown value stands for: itself, or a new Error carrying its string form.
77
148
  * @param reason - thrown value.
78
149
  * @returns the error.
79
150
  */
80
151
  export declare function toError(reason: unknown): Error;
152
+ export {};
81
153
  //# sourceMappingURL=streams.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-remote-mock",
3
3
  "description": "Endpoint-named mock for Typert Remote traffic: unary answers and stream scripts per <namespace>/<method>, live stream control, a log, and the Connection carrier face whole-client specs install",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,14 +26,15 @@
26
26
  "@vitest/spy": "^4.1.8"
27
27
  },
28
28
  "peerDependencies": {
29
- "@deepseek-ai/cordis": "^4.0.2"
29
+ "@deepseek-ai/cordis": "^4.0.3",
30
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1"
30
31
  },
31
32
  "devDependencies": {
32
33
  "typescript": "^6.0.3",
33
- "@deepseek-ai/dsh-api-settings-controller": "^0.1.6-alpha.2",
34
- "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.2",
35
- "@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.2",
36
- "@deepseek-ai/cordis": "^4.0.2"
34
+ "@deepseek-ai/cordis": "^4.0.3",
35
+ "@deepseek-ai/dsh-api-settings-controller": "^0.1.7-alpha.1",
36
+ "@deepseek-ai/dsh-client-connection": "^0.1.7-alpha.1",
37
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1"
37
38
  },
38
39
  "files": [
39
40
  "lib/index.js",