@lakutata/nats 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [2.1.0](https://github.com/lakutata/lakutata-packages/compare/@lakutata/nats@2.0.1...@lakutata/nats@2.1.0) (2026-06-21)
7
+
8
+
9
+ ### Features
10
+
11
+ * **nats:** add bulk data transfer and service proxy retry ([107ad48](https://github.com/lakutata/lakutata-packages/commit/107ad48219857f7f7e13dcfd728d1f92438f10a9)), closes [#2](https://github.com/lakutata/lakutata-packages/issues/2) [#3](https://github.com/lakutata/lakutata-packages/issues/3) [#7](https://github.com/lakutata/lakutata-packages/issues/7)
12
+
13
+
14
+
15
+
16
+
6
17
  ## [2.0.1](https://github.com/lakutata/lakutata-packages/compare/@lakutata/nats@2.0.0...@lakutata/nats@2.0.1) (2026-03-02)
7
18
 
8
19
  **Note:** Version bump only for package @lakutata/nats
@@ -14,4 +14,6 @@ export { NatsInternalServerException } from './exceptions/NatsInternalServerExce
14
14
  export { NatsNoRespondersAvailableException } from './exceptions/NatsNoRespondersAvailableException';
15
15
  export { NatsNotFoundException } from './exceptions/NatsNotFoundException';
16
16
  export { NatsRequestTimeoutException } from './exceptions/NatsRequestTimeoutException';
17
+ export { ServiceInvokeException } from './exceptions/ServiceInvokeException';
18
+ export { NatsBulkException } from './exceptions/NatsBulkException';
17
19
  //# sourceMappingURL=CommonExports.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CommonExports.d.ts","sourceRoot":"","sources":["../src/CommonExports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,0BAA0B,EAAC,MAAM,0CAA0C,CAAA;AACnF,OAAO,EAAC,YAAY,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAC,MAAM,0BAA0B,CAAA;AACvG,OAAO,EAAC,eAAe,EAAC,MAAM,MAAM,CAAA;AACpC,OAAO,EAAC,IAAI,EAAE,sBAAsB,EAAC,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAA;AAC5C,OAAO,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAC,WAAW,EAAC,MAAM,MAAM,CAAA;AAChC,YAAY,EAAC,gBAAgB,EAAC,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAC,YAAY,EAAE,KAAK,EAAC,MAAM,MAAM,CAAA;AAC7C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAA;AACrE,OAAO,EAAC,uBAAuB,EAAC,MAAM,sCAAsC,CAAA;AAC5E,OAAO,EAAC,sBAAsB,EAAC,MAAM,qCAAqC,CAAA;AAC1E,OAAO,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAA;AACpF,OAAO,EAAC,kCAAkC,EAAC,MAAM,iDAAiD,CAAA;AAClG,OAAO,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAA"}
1
+ {"version":3,"file":"CommonExports.d.ts","sourceRoot":"","sources":["../src/CommonExports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,0BAA0B,EAAC,MAAM,0CAA0C,CAAA;AACnF,OAAO,EAAC,YAAY,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAC,MAAM,0BAA0B,CAAA;AACvG,OAAO,EAAC,eAAe,EAAC,MAAM,MAAM,CAAA;AACpC,OAAO,EAAC,IAAI,EAAE,sBAAsB,EAAC,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAA;AAC5C,OAAO,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAC,WAAW,EAAC,MAAM,MAAM,CAAA;AAChC,YAAY,EAAC,gBAAgB,EAAC,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAC,YAAY,EAAE,KAAK,EAAC,MAAM,MAAM,CAAA;AAC7C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAA;AACrE,OAAO,EAAC,uBAAuB,EAAC,MAAM,sCAAsC,CAAA;AAC5E,OAAO,EAAC,sBAAsB,EAAC,MAAM,qCAAqC,CAAA;AAC1E,OAAO,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAA;AACpF,OAAO,EAAC,kCAAkC,EAAC,MAAM,iDAAiD,CAAA;AAClG,OAAO,EAAC,qBAAqB,EAAC,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAA;AACpF,OAAO,EAAC,sBAAsB,EAAC,MAAM,qCAAqC,CAAA;AAC1E,OAAO,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAA"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NatsRequestTimeoutException = exports.NatsNotFoundException = exports.NatsNoRespondersAvailableException = exports.NatsInternalServerException = exports.NatsForbiddenException = exports.NatsBadRequestException = exports.StringCodec = exports.MessagePackCodec = exports.JSONCodec = exports.buildNatsClientOptions = exports.NATS = exports.RetentionPolicy = exports.BuildServiceProxy = exports.ServiceProxy = exports.SetupNatsServiceEntrypoint = void 0;
3
+ exports.NatsBulkException = exports.ServiceInvokeException = exports.NatsRequestTimeoutException = exports.NatsNotFoundException = exports.NatsNoRespondersAvailableException = exports.NatsInternalServerException = exports.NatsForbiddenException = exports.NatsBadRequestException = exports.StringCodec = exports.MessagePackCodec = exports.JSONCodec = exports.buildNatsClientOptions = exports.NATS = exports.RetentionPolicy = exports.BuildServiceProxy = exports.ServiceProxy = exports.SetupNatsServiceEntrypoint = void 0;
4
4
  var SetupNatsServiceEntrypoint_1 = require("./entrypoints/SetupNatsServiceEntrypoint");
5
5
  Object.defineProperty(exports, "SetupNatsServiceEntrypoint", { enumerable: true, get: function () { return SetupNatsServiceEntrypoint_1.SetupNatsServiceEntrypoint; } });
6
6
  var ServiceProxy_1 = require("./providers/ServiceProxy");
@@ -29,3 +29,7 @@ var NatsNotFoundException_1 = require("./exceptions/NatsNotFoundException");
29
29
  Object.defineProperty(exports, "NatsNotFoundException", { enumerable: true, get: function () { return NatsNotFoundException_1.NatsNotFoundException; } });
30
30
  var NatsRequestTimeoutException_1 = require("./exceptions/NatsRequestTimeoutException");
31
31
  Object.defineProperty(exports, "NatsRequestTimeoutException", { enumerable: true, get: function () { return NatsRequestTimeoutException_1.NatsRequestTimeoutException; } });
32
+ var ServiceInvokeException_1 = require("./exceptions/ServiceInvokeException");
33
+ Object.defineProperty(exports, "ServiceInvokeException", { enumerable: true, get: function () { return ServiceInvokeException_1.ServiceInvokeException; } });
34
+ var NatsBulkException_1 = require("./exceptions/NatsBulkException");
35
+ Object.defineProperty(exports, "NatsBulkException", { enumerable: true, get: function () { return NatsBulkException_1.NatsBulkException; } });
@@ -1,5 +1,5 @@
1
1
  import { Application, Component, ComponentOptionsBuilder } from 'lakutata';
2
- import { type Codec, Subscription } from 'nats';
2
+ import { type Codec, Subscription, type ObjectStoreStatus } from 'nats';
3
3
  import { SubscribeOptions } from '../types/SubscribeOptions';
4
4
  import { NatsClientOptions } from '../interfaces/NatsClientOptions';
5
5
  export declare const buildNatsClientOptions: ComponentOptionsBuilder<NatsClientOptions>;
@@ -94,6 +94,27 @@ export declare class NATS extends Component {
94
94
  * @protected
95
95
  */
96
96
  protected readonly reconnect?: boolean;
97
+ /**
98
+ * 是否启用 bulk 大数据旁路:启用后启动时建第二条独立连接 + 准备 Object Store。
99
+ * @default true
100
+ * @protected
101
+ */
102
+ protected readonly bulk?: boolean;
103
+ /**
104
+ * bulk Object Store bucket 名,默认 lkt-bulk-${name}。
105
+ * @protected
106
+ */
107
+ protected readonly bulkBucket?: string;
108
+ /**
109
+ * bulk 对象 TTL(毫秒),默认 5 分钟。
110
+ * @protected
111
+ */
112
+ protected readonly bulkTTL?: number;
113
+ /**
114
+ * bulk bucket 副本数,默认 1(中转数据用完即弃,不需要高可用副本)。
115
+ * @protected
116
+ */
117
+ protected readonly bulkReplicas?: number;
97
118
  /**
98
119
  * Service event subscription map
99
120
  * @protected
@@ -109,6 +130,13 @@ export declare class NATS extends Component {
109
130
  * @protected
110
131
  */
111
132
  protected destroy(): Promise<void>;
133
+ /**
134
+ * 优雅停机:drain 停止接收新消息、把 in-flight 处理完、flush 出站后再关闭,
135
+ * 这样 K8s 滚动升级/缩容时正在处理的请求不被硬切断;drain 失败兜底强制关闭。
136
+ * @param conn
137
+ * @private
138
+ */
139
+ private drainConnection;
112
140
  /**
113
141
  * Publishes the specified data to the specified subject.
114
142
  * @param subject
@@ -135,6 +163,59 @@ export declare class NATS extends Component {
135
163
  * @param subscribeOptions
136
164
  */
137
165
  subscribe(subject: string, callback: (data: any) => any | Promise<any>, subscribeOptions?: SubscribeOptions): Subscription;
166
+ /**
167
+ * 安全地发出 error 事件:仅在存在监听器时发出。
168
+ * Node EventEmitter 在没有 'error' 监听器时 emit('error') 会抛出,
169
+ * 在异步回调里那会变成 unhandled rejection,所以这里加监听器守卫。
170
+ * @param error
171
+ * @protected
172
+ */
173
+ protected emitError(error: any): void;
174
+ /**
175
+ * 是否启用了 bulk 旁路(连接 + Object Store 均就绪)
176
+ */
177
+ get bulkEnabled(): boolean;
178
+ /**
179
+ * 走 bulk 的字节阈值 = server max_payload × 0.9(从连接 info 自动读,跟随集群配置)
180
+ */
181
+ get bulkThreshold(): number;
182
+ /**
183
+ * server 是否支持消息 header(从连接 info)
184
+ */
185
+ get headersSupported(): boolean;
186
+ /**
187
+ * 把一块字节存入 Object Store,返回 objId(供引用中转)。
188
+ * @param bytes
189
+ */
190
+ putBulk(bytes: Uint8Array): Promise<string>;
191
+ /**
192
+ * 从 Object Store 取回一块字节。
193
+ * @param objId
194
+ */
195
+ getBulk(objId: string): Promise<Uint8Array>;
196
+ /**
197
+ * 删除 Object Store 中的对象(用完即清,失败由 TTL 兜底)。
198
+ * @param objId
199
+ */
200
+ deleteBulk(objId: string): Promise<void>;
201
+ /**
202
+ * 返回 bulk Object Store 的运行时状态(bucket/ttl/replicas/storage/size 等);未启用 bulk 返回 undefined。
203
+ */
204
+ bulkStatus(): Promise<ObjectStoreStatus | undefined>;
205
+ /**
206
+ * 打包待发送 payload:编码后若对端支持中转且超阈值,存入 Object Store 改发小引用。
207
+ * @param payload
208
+ * @param peerSupportsBulk 对端是否支持引用中转(不支持只能直传)
209
+ * @private
210
+ */
211
+ private packPayload;
212
+ /**
213
+ * 解包收到的字节:若是引用,从 Object Store 取回真实字节再解码(取回后即删,失败靠 TTL 兜底)。
214
+ * @param bytes
215
+ * @param isRef
216
+ * @private
217
+ */
218
+ private unpackPayload;
138
219
  /**
139
220
  * Emit service event
140
221
  * @param eventName
@@ -1 +1 @@
1
- {"version":3,"file":"NATS.d.ts","sourceRoot":"","sources":["../../src/components/NATS.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EACX,SAAS,EAET,uBAAuB,EAE1B,MAAM,UAAU,CAAA;AAEjB,OAAO,EAAC,KAAK,KAAK,EAAgC,YAAY,EAAC,MAAM,MAAM,CAAA;AAC3E,OAAO,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAA;AAO1D,OAAO,EAAC,iBAAiB,EAAC,MAAM,iCAAiC,CAAA;AAIjE,eAAO,MAAM,sBAAsB,EAAE,uBAAuB,CAAC,iBAAiB,CAkB7E,CAAA;AAED,qBAAa,IAAK,SAAQ,SAAS;;IAC/B;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;IAEnC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAE7C;;;OAGG;IAKH,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IAExC;;;;OAIG;IAEH,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IAEnC;;;;OAIG;IAEH,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IAEjC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IAEhC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IAEhC;;;;OAIG;IAEH,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAA;IAElC;;;;OAIG;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAEtC;;;;;;OAMG;IAEH,SAAS,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAEhD;;;;OAIG;IAEH,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IAEhC;;;;;OAKG;IAEH,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IAEnC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAExC;;;;;OAKG;IAEH,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAExC;;;;;OAKG;IAEH,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAEtC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,2BAA2B,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,YAAY,CAAC,CAAC,CAAY;IAQpH;;;OAGG;cACa,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBrC;;;OAGG;cACa,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACI,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,IAAI;IAInD;;;;;;;;;OASG;IACU,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IA4BpF;;;;;;;OAOG;IACI,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,YAAY;IAwBjI;;;;OAIG;IACI,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAMhE;;;;;;OAMG;IACI,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,wBAAwB,GAAE,OAAe,GAAG,IAAI;IAYhJ;;;;;OAKG;IACI,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;IAavG;;;;;OAKG;IACI,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;CAW1G"}
1
+ {"version":3,"file":"NATS.d.ts","sourceRoot":"","sources":["../../src/components/NATS.ts"],"names":[],"mappings":"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"}
@@ -19,8 +19,13 @@ const NatsNotFoundException_1 = require("../exceptions/NatsNotFoundException");
19
19
  const NatsRequestTimeoutException_1 = require("../exceptions/NatsRequestTimeoutException");
20
20
  const NatsNoRespondersAvailableException_1 = require("../exceptions/NatsNoRespondersAvailableException");
21
21
  const NatsInternalServerException_1 = require("../exceptions/NatsInternalServerException");
22
+ const NatsBulkException_1 = require("../exceptions/NatsBulkException");
22
23
  const ServiceEventCodec_1 = require("../lib/ServiceEventCodec");
23
24
  const MessagePackCodec_1 = require("../codecs/MessagePackCodec");
25
+ const node_crypto_1 = require("node:crypto");
26
+ // bulk 中转协议头(放 header,不污染 payload)
27
+ const HDR_BULK = 'X-Lkt-Bulk'; // 能力声明:本端支持 bulk 引用中转
28
+ const HDR_REF = 'X-Lkt-Ref'; // 标记:payload 是 Object Store 引用 {__ref}
24
29
  const buildNatsClientOptions = (options) => {
25
30
  return {
26
31
  class: NATS,
@@ -37,7 +42,11 @@ const buildNatsClientOptions = (options) => {
37
42
  noEcho: options.noEcho,
38
43
  noRandomize: options.noRandomize,
39
44
  pingInterval: options.pingInterval,
40
- reconnect: options.reconnect
45
+ reconnect: options.reconnect,
46
+ bulk: options.bulk,
47
+ bulkBucket: options.bulkBucket,
48
+ bulkTTL: options.bulkTTL,
49
+ bulkReplicas: options.bulkReplicas
41
50
  };
42
51
  };
43
52
  exports.buildNatsClientOptions = buildNatsClientOptions;
@@ -49,18 +58,36 @@ class NATS extends lakutata_1.Component {
49
58
  * @protected
50
59
  */
51
60
  this.serviceEventSubscriptionMap = new Map();
61
+ /**
62
+ * 缓存对端(按 subject)是否支持 bulk 中转,从对方响应/请求的能力声明 header 学到。
63
+ * 用于请求侧判断对端:已知支持才发引用,未知/不支持则直传(向后兼容)。
64
+ * @private
65
+ */
66
+ this.#peerCapabilityCache = new Map();
52
67
  }
53
68
  /**
54
69
  * NATS client instance
55
70
  * @private
56
71
  */
57
72
  #conn;
73
+ /**
74
+ * bulk 连接与 Object Store(仅 bulk 启用时存在)
75
+ * @private
76
+ */
77
+ #bulkConn;
78
+ #objectStore;
79
+ /**
80
+ * 缓存对端(按 subject)是否支持 bulk 中转,从对方响应/请求的能力声明 header 学到。
81
+ * 用于请求侧判断对端:已知支持才发引用,未知/不支持则直传(向后兼容)。
82
+ * @private
83
+ */
84
+ #peerCapabilityCache;
58
85
  /**
59
86
  * Initializer
60
87
  * @protected
61
88
  */
62
89
  async init() {
63
- this.#conn = await (0, nats_1.connect)({
90
+ const connectOptions = {
64
91
  servers: this.servers,
65
92
  user: this.user,
66
93
  pass: this.pass,
@@ -74,14 +101,50 @@ class NATS extends lakutata_1.Component {
74
101
  noRandomize: this.noRandomize,
75
102
  pingInterval: this.pingInterval,
76
103
  reconnect: this.reconnect
77
- });
104
+ };
105
+ this.#conn = await (0, nats_1.connect)(connectOptions);
106
+ if (this.bulk) {
107
+ // 用同一套配置再开一条【独立】连接专供大数据,避免大对象传输堵塞 core RPC 连接;
108
+ // 并 get-or-create 一个 Object Store bucket 做大对象中转(短 TTL 自动清理)。
109
+ this.#bulkConn = await (0, nats_1.connect)({
110
+ ...connectOptions,
111
+ name: this.name ? `${this.name}-bulk` : undefined
112
+ });
113
+ const bucket = this.bulkBucket ?? `lkt-bulk-${this.name ?? 'default'}`;
114
+ this.#objectStore = await this.#bulkConn.jetstream().views.os(bucket, {
115
+ storage: nats_1.StorageType.File,
116
+ replicas: this.bulkReplicas ?? 1,
117
+ ttl: (0, nats_1.nanos)(this.bulkTTL ?? 5 * 60 * 1000)
118
+ });
119
+ }
78
120
  }
79
121
  /**
80
122
  * Destroyer
81
123
  * @protected
82
124
  */
83
125
  async destroy() {
84
- await this.#conn.close();
126
+ await this.drainConnection(this.#conn);
127
+ if (this.#bulkConn)
128
+ await this.drainConnection(this.#bulkConn);
129
+ }
130
+ /**
131
+ * 优雅停机:drain 停止接收新消息、把 in-flight 处理完、flush 出站后再关闭,
132
+ * 这样 K8s 滚动升级/缩容时正在处理的请求不被硬切断;drain 失败兜底强制关闭。
133
+ * @param conn
134
+ * @private
135
+ */
136
+ async drainConnection(conn) {
137
+ try {
138
+ await conn.drain();
139
+ }
140
+ catch {
141
+ try {
142
+ await conn.close();
143
+ }
144
+ catch {
145
+ // 连接已不可用,忽略
146
+ }
147
+ }
85
148
  }
86
149
  /**
87
150
  * Publishes the specified data to the specified subject.
@@ -102,9 +165,19 @@ class NATS extends lakutata_1.Component {
102
165
  * @param timeout
103
166
  */
104
167
  async request(subject, payload, timeout) {
168
+ const reqHeaders = (0, nats_1.headers)();
169
+ if (this.bulkEnabled)
170
+ reqHeaders.set(HDR_BULK, '1'); // 仅本端启用 bulk 时声明能力
171
+ const peerSupports = this.#peerCapabilityCache.get(subject) === true;
172
+ const packed = await this.packPayload(payload, peerSupports);
173
+ if (packed.isRef)
174
+ reqHeaders.set(HDR_REF, '1');
105
175
  try {
106
- const response = await this.#conn.request(subject, this.codec.encode(payload), { timeout: timeout ? timeout : 0 });
107
- return this.codec.decode(response.data);
176
+ const response = await this.#conn.request(subject, packed.bytes, { timeout: timeout ? timeout : 0, headers: reqHeaders });
177
+ // 双向协商:从响应学习对端是否支持中转
178
+ this.#peerCapabilityCache.set(subject, response.headers?.get(HDR_BULK) === '1');
179
+ // 还原响应(可能是 Object Store 引用)
180
+ return await this.unpackPayload(response.data, response.headers?.get(HDR_REF) === '1');
108
181
  }
109
182
  catch (e) {
110
183
  if (e.code) {
@@ -116,6 +189,7 @@ class NATS extends lakutata_1.Component {
116
189
  case '404':
117
190
  throw new NatsNotFoundException_1.NatsNotFoundException();
118
191
  case '408':
192
+ case 'TIMEOUT':
119
193
  throw new NatsRequestTimeoutException_1.NatsRequestTimeoutException();
120
194
  case '503':
121
195
  throw new NatsNoRespondersAvailableException_1.NatsNoRespondersAvailableException();
@@ -129,6 +203,11 @@ class NATS extends lakutata_1.Component {
129
203
  throw e;
130
204
  }
131
205
  }
206
+ finally {
207
+ // 请求引用对象:服务端已取回(响应已返回),删除;失败靠 TTL 兜底
208
+ if (packed.objId)
209
+ this.deleteBulk(packed.objId).catch(() => undefined);
210
+ }
132
211
  }
133
212
  /**
134
213
  * Subscribe expresses interest in the specified subject. The subject may
@@ -143,26 +222,169 @@ class NATS extends lakutata_1.Component {
143
222
  queue: subscribeOptions?.queue,
144
223
  max: subscribeOptions?.max
145
224
  });
225
+ const handleMessage = async (msg) => {
226
+ try {
227
+ // 入站:若标记为引用,从 Object Store 取回真实 payload(对小消息/event 透明)
228
+ const data = await this.unpackPayload(msg.data, msg.headers?.get(HDR_REF) === '1');
229
+ const result = await callback(data);
230
+ // 仅当消息是请求(带 reply)时才回响应;publish/event 无 reply,跳过。
231
+ if (msg.reply) {
232
+ const respHeaders = (0, nats_1.headers)();
233
+ if (this.bulkEnabled)
234
+ respHeaders.set(HDR_BULK, '1'); // 仅本端启用 bulk 时声明能力
235
+ // 请求方是否支持中转 → 决定大响应能否走引用
236
+ const clientSupports = msg.headers?.get(HDR_BULK) === '1';
237
+ const packed = await this.packPayload(result, clientSupports);
238
+ if (packed.isRef)
239
+ respHeaders.set(HDR_REF, '1');
240
+ msg.respond(packed.bytes, { headers: respHeaders });
241
+ }
242
+ }
243
+ catch (e) {
244
+ this.emitError(e);
245
+ // 若是请求且上层提供了错误响应工厂,回标准失败响应(直传 + 能力声明),避免请求方挂到超时。
246
+ if (msg.reply && subscribeOptions?.errorResponse) {
247
+ try {
248
+ const errHeaders = (0, nats_1.headers)();
249
+ if (this.bulkEnabled)
250
+ errHeaders.set(HDR_BULK, '1');
251
+ const errorPayload = subscribeOptions.errorResponse(e);
252
+ if (errorPayload !== undefined)
253
+ msg.respond(this.codec.encode(errorPayload), { headers: errHeaders });
254
+ }
255
+ catch (encodeError) {
256
+ this.emitError(encodeError);
257
+ }
258
+ }
259
+ }
260
+ };
146
261
  if (subscribeOptions?.iterator) {
147
262
  setImmediate(async () => {
148
263
  for await (const msg of subscription) {
149
- const data = this.codec.decode(msg.data);
150
- const result = await callback(data);
151
- msg.respond(this.codec.encode(result));
264
+ await handleMessage(msg);
152
265
  }
153
266
  });
154
267
  }
155
268
  else {
156
269
  subscription.callback = async (err, msg) => {
157
- if (err)
158
- this.emit('error', err);
159
- const data = this.codec.decode(msg.data);
160
- const result = await callback(data);
161
- msg.respond(this.codec.encode(result));
270
+ // nats.js 在订阅级错误时以 (err, {}) 回调,此时 msg 为空对象无法处理,上报后返回。
271
+ if (err) {
272
+ this.emitError(err);
273
+ return;
274
+ }
275
+ await handleMessage(msg);
162
276
  };
163
277
  }
164
278
  return subscription;
165
279
  }
280
+ /**
281
+ * 安全地发出 error 事件:仅在存在监听器时发出。
282
+ * Node EventEmitter 在没有 'error' 监听器时 emit('error') 会抛出,
283
+ * 在异步回调里那会变成 unhandled rejection,所以这里加监听器守卫。
284
+ * @param error
285
+ * @protected
286
+ */
287
+ emitError(error) {
288
+ if (this.listenerCount('error') > 0)
289
+ this.emit('error', error);
290
+ }
291
+ /**
292
+ * 是否启用了 bulk 旁路(连接 + Object Store 均就绪)
293
+ */
294
+ get bulkEnabled() {
295
+ return !!this.bulk && !!this.#objectStore;
296
+ }
297
+ /**
298
+ * 走 bulk 的字节阈值 = server max_payload × 0.9(从连接 info 自动读,跟随集群配置)
299
+ */
300
+ get bulkThreshold() {
301
+ const maxPayload = this.#conn.info?.max_payload ?? 1024 * 1024;
302
+ return Math.floor(maxPayload * 0.9);
303
+ }
304
+ /**
305
+ * server 是否支持消息 header(从连接 info)
306
+ */
307
+ get headersSupported() {
308
+ return this.#conn.info?.headers === true;
309
+ }
310
+ /**
311
+ * 把一块字节存入 Object Store,返回 objId(供引用中转)。
312
+ * @param bytes
313
+ */
314
+ async putBulk(bytes) {
315
+ if (!this.#objectStore)
316
+ throw new NatsBulkException_1.NatsBulkException('bulk channel is not enabled');
317
+ const objId = (0, node_crypto_1.randomUUID)();
318
+ try {
319
+ await this.#objectStore.putBlob({ name: objId }, bytes);
320
+ }
321
+ catch (e) {
322
+ throw new NatsBulkException_1.NatsBulkException(`bulk put failed: ${e?.message ?? e}`);
323
+ }
324
+ return objId;
325
+ }
326
+ /**
327
+ * 从 Object Store 取回一块字节。
328
+ * @param objId
329
+ */
330
+ async getBulk(objId) {
331
+ if (!this.#objectStore)
332
+ throw new NatsBulkException_1.NatsBulkException('bulk channel is not enabled');
333
+ let data;
334
+ try {
335
+ data = await this.#objectStore.getBlob(objId);
336
+ }
337
+ catch (e) {
338
+ throw new NatsBulkException_1.NatsBulkException(`bulk get failed: ${e?.message ?? e}`);
339
+ }
340
+ if (data === null)
341
+ throw new NatsBulkException_1.NatsBulkException(`bulk object not found: ${objId}`);
342
+ return data;
343
+ }
344
+ /**
345
+ * 删除 Object Store 中的对象(用完即清,失败由 TTL 兜底)。
346
+ * @param objId
347
+ */
348
+ async deleteBulk(objId) {
349
+ if (!this.#objectStore)
350
+ return;
351
+ await this.#objectStore.delete(objId);
352
+ }
353
+ /**
354
+ * 返回 bulk Object Store 的运行时状态(bucket/ttl/replicas/storage/size 等);未启用 bulk 返回 undefined。
355
+ */
356
+ async bulkStatus() {
357
+ return this.#objectStore?.status();
358
+ }
359
+ /**
360
+ * 打包待发送 payload:编码后若对端支持中转且超阈值,存入 Object Store 改发小引用。
361
+ * @param payload
362
+ * @param peerSupportsBulk 对端是否支持引用中转(不支持只能直传)
363
+ * @private
364
+ */
365
+ async packPayload(payload, peerSupportsBulk) {
366
+ const bytes = this.codec.encode(payload);
367
+ if (peerSupportsBulk && this.bulkEnabled && bytes.length >= this.bulkThreshold) {
368
+ const objId = await this.putBulk(bytes);
369
+ return { bytes: this.codec.encode({ __ref: objId }), isRef: true, objId };
370
+ }
371
+ return { bytes, isRef: false };
372
+ }
373
+ /**
374
+ * 解包收到的字节:若是引用,从 Object Store 取回真实字节再解码(取回后即删,失败靠 TTL 兜底)。
375
+ * @param bytes
376
+ * @param isRef
377
+ * @private
378
+ */
379
+ async unpackPayload(bytes, isRef) {
380
+ if (isRef) {
381
+ const ref = this.codec.decode(bytes);
382
+ const realBytes = await this.getBulk(ref.__ref);
383
+ this.deleteBulk(ref.__ref).catch(() => undefined);
384
+ return this.codec.decode(realBytes);
385
+ }
386
+ return this.codec.decode(bytes);
387
+ }
166
388
  /**
167
389
  * Emit service event
168
390
  * @param eventName
@@ -299,3 +521,19 @@ __decorate([
299
521
  (0, di_1.Configurable)(lakutata_1.DTO.Boolean().optional().default(true)),
300
522
  __metadata("design:type", Boolean)
301
523
  ], NATS.prototype, "reconnect", void 0);
524
+ __decorate([
525
+ (0, di_1.Configurable)(lakutata_1.DTO.Boolean().optional().default(true)),
526
+ __metadata("design:type", Boolean)
527
+ ], NATS.prototype, "bulk", void 0);
528
+ __decorate([
529
+ (0, di_1.Configurable)(lakutata_1.DTO.String().optional()),
530
+ __metadata("design:type", String)
531
+ ], NATS.prototype, "bulkBucket", void 0);
532
+ __decorate([
533
+ (0, di_1.Configurable)(lakutata_1.DTO.Number().integer().optional().default(5 * 60 * 1000)),
534
+ __metadata("design:type", Number)
535
+ ], NATS.prototype, "bulkTTL", void 0);
536
+ __decorate([
537
+ (0, di_1.Configurable)(lakutata_1.DTO.Number().integer().optional().default(1)),
538
+ __metadata("design:type", Number)
539
+ ], NATS.prototype, "bulkReplicas", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"SetupNatsServiceEntrypoint.d.ts","sourceRoot":"","sources":["../../src/entrypoints/SetupNatsServiceEntrypoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,iBAAiB,EAEpB,MAAM,yBAAyB,CAAA;AAKhC;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,iBAAiB,EAAE,MAAM,GAAG,iBAAiB,CAevF"}
1
+ {"version":3,"file":"SetupNatsServiceEntrypoint.d.ts","sourceRoot":"","sources":["../../src/entrypoints/SetupNatsServiceEntrypoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,iBAAiB,EAEpB,MAAM,yBAAyB,CAAA;AAehC;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,iBAAiB,EAAE,MAAM,GAAG,iBAAiB,CAmBvF"}
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SetupNatsServiceEntrypoint = void 0;
4
4
  const entrypoint_1 = require("lakutata/com/entrypoint");
5
5
  const ServiceResponseCodec_1 = require("../lib/ServiceResponseCodec");
6
+ /**
7
+ * 把异常拷成可序列化的普通对象(保留 message/errno/statusCode 等自有属性)。
8
+ * @param error
9
+ */
10
+ function toErrorObject(error) {
11
+ const errorObject = {};
12
+ Object.getOwnPropertyNames(error).forEach((prop) => errorObject[prop] = error[prop]);
13
+ return errorObject;
14
+ }
6
15
  /**
7
16
  * Setup service based on NATS
8
17
  * @param natsComponentName
@@ -11,18 +20,22 @@ const ServiceResponseCodec_1 = require("../lib/ServiceResponseCodec");
11
20
  function SetupNatsServiceEntrypoint(natsComponentName) {
12
21
  return (0, entrypoint_1.BuildServiceEntrypoint)(async (module, handler) => {
13
22
  const nats = await module.getObject(natsComponentName);
14
- nats.subscribe(module.appId, async (incomeRequestPayload) => {
23
+ const appId = module.appId;
24
+ nats.subscribe(appId, async (incomeRequestPayload) => {
15
25
  try {
16
26
  return ServiceResponseCodec_1.ServiceResponseCodec.encode(await handler(new entrypoint_1.ServiceContext({
17
27
  data: incomeRequestPayload
18
28
  })), false);
19
29
  }
20
30
  catch (e) {
21
- const errorObject = {};
22
- Object.getOwnPropertyNames(e).forEach((prop) => errorObject[prop] = e[prop]);
23
- return ServiceResponseCodec_1.ServiceResponseCodec.encode(errorObject, true);
31
+ return ServiceResponseCodec_1.ServiceResponseCodec.encode(toErrorObject(e), true);
24
32
  }
25
- }, { queue: `${module.appId}.serviceQueue` });
33
+ }, {
34
+ queue: `${appId}.serviceQueue`,
35
+ // 入站 decode 失败 / 出站 encode 失败等发生在 handler 之外(subscribe 层)的错误,
36
+ // 由此工厂统一回成标准失败响应,避免请求方挂到超时。
37
+ errorResponse: (e) => ServiceResponseCodec_1.ServiceResponseCodec.encode(toErrorObject(e), true)
38
+ });
26
39
  });
27
40
  }
28
41
  exports.SetupNatsServiceEntrypoint = SetupNatsServiceEntrypoint;
@@ -0,0 +1,11 @@
1
+ import { Exception } from 'lakutata';
2
+ /**
3
+ * bulk 中转层故障(Object Store put/get 失败、引用过期取不到、bulk 未启用等)。
4
+ * 表示"大数据通道出问题",区别于业务错误;不应自动重试(响应取不到时对侧可能已处理)。
5
+ * message 由抛出处给出具体原因(不设默认值,以保留原因)。
6
+ */
7
+ export declare class NatsBulkException extends Exception {
8
+ errno: string | number;
9
+ statusCode: number;
10
+ }
11
+ //# sourceMappingURL=NatsBulkException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NatsBulkException.d.ts","sourceRoot":"","sources":["../../src/exceptions/NatsBulkException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,UAAU,CAAA;AAElC;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;IACrC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAgB;IACtC,UAAU,EAAE,MAAM,CAAM;CAClC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NatsBulkException = void 0;
4
+ const lakutata_1 = require("lakutata");
5
+ /**
6
+ * bulk 中转层故障(Object Store put/get 失败、引用过期取不到、bulk 未启用等)。
7
+ * 表示"大数据通道出问题",区别于业务错误;不应自动重试(响应取不到时对侧可能已处理)。
8
+ * message 由抛出处给出具体原因(不设默认值,以保留原因)。
9
+ */
10
+ class NatsBulkException extends lakutata_1.Exception {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.errno = 'E_NATS_BULK';
14
+ this.statusCode = 502;
15
+ }
16
+ }
17
+ exports.NatsBulkException = NatsBulkException;
@@ -14,5 +14,13 @@ export interface NatsClientOptions {
14
14
  noRandomize?: boolean;
15
15
  pingInterval?: number;
16
16
  reconnect?: boolean;
17
+ /** 是否启用 bulk 大数据旁路(启动建第二条连接 + Object Store),默认 true */
18
+ bulk?: boolean;
19
+ /** bulk Object Store bucket 名,默认 lkt-bulk-${name} */
20
+ bulkBucket?: string;
21
+ /** bulk 对象 TTL(毫秒),默认 5 分钟 */
22
+ bulkTTL?: number;
23
+ /** bulk bucket 副本数,默认 1(中转数据用完即弃) */
24
+ bulkReplicas?: number;
17
25
  }
18
26
  //# sourceMappingURL=NatsClientOptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NatsClientOptions.d.ts","sourceRoot":"","sources":["../../src/interfaces/NatsClientOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,EAAC,MAAM,MAAM,CAAA;AAE1B,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB"}
1
+ {"version":3,"file":"NatsClientOptions.d.ts","sourceRoot":"","sources":["../../src/interfaces/NatsClientOptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,EAAC,MAAM,MAAM,CAAA;AAE1B,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceResponseCodec.d.ts","sourceRoot":"","sources":["../../src/lib/ServiceResponseCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,UAAU,CAAA;AAGlC,UAAU,eAAe;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAA;IACxC,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAA;CAC/B;AAED,qBAAa,oBAAoB;IAC7B;;;;OAIG;WACW,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,eAAe;IAYlE;;;;OAIG;WACW,MAAM,CAAC,QAAQ,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG;CAS3E"}
1
+ {"version":3,"file":"ServiceResponseCodec.d.ts","sourceRoot":"","sources":["../../src/lib/ServiceResponseCodec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,UAAU,CAAA;AAGlC,UAAU,eAAe;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAA;IACxC,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAA;CAC/B;AAED,qBAAa,oBAAoB;IAC7B;;;;OAIG;WACW,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAG,eAAe;IAYlE;;;;OAIG;WACW,MAAM,CAAC,QAAQ,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG;CAoB3E"}
@@ -27,10 +27,22 @@ class ServiceResponseCodec {
27
27
  static decode(response, serviceId) {
28
28
  if (!response.success) {
29
29
  const remoteError = response.error;
30
- const serviceInvokeException = new ServiceInvokeException_1.ServiceInvokeException(remoteError.message || 'Unknown Error');
30
+ const serviceInvokeException = new ServiceInvokeException_1.ServiceInvokeException(remoteError?.message || 'Unknown Error');
31
31
  if (serviceId)
32
32
  serviceInvokeException.service = serviceId;
33
- throw Object.assign(serviceInvokeException, response.error);
33
+ // 安全拷贝远端错误的自有属性:逐个赋值并跳过只读属性( NatsError 的 name),
34
+ // 避免 Object.assign 碰到只读属性时整体抛 TypeError。
35
+ if (remoteError && typeof remoteError === 'object') {
36
+ for (const key of Object.keys(remoteError)) {
37
+ try {
38
+ serviceInvokeException[key] = remoteError[key];
39
+ }
40
+ catch {
41
+ // 只读属性,跳过
42
+ }
43
+ }
44
+ }
45
+ throw serviceInvokeException;
34
46
  }
35
47
  return response.payload;
36
48
  }
@@ -6,6 +6,10 @@ import { NATS } from '../components/NATS';
6
6
  export type BuildServiceProxyOptions = {
7
7
  readonly natsComponentName: string;
8
8
  readonly serviceId: string;
9
+ /** 仅对 NoResponders(对侧确定没处理)自动重试的次数,默认 3;超时/业务异常不重试 */
10
+ readonly maxRetries?: number;
11
+ /** 每次重试前等待的毫秒,默认 100 */
12
+ readonly retryDelay?: number;
9
13
  };
10
14
  /**
11
15
  * Build service proxy
@@ -32,6 +36,16 @@ export declare class ServiceProxy extends Provider {
32
36
  * @protected
33
37
  */
34
38
  protected readonly serviceId: string;
39
+ /**
40
+ * 仅对 NoResponders(对侧确定没处理)自动重试的次数;超时/业务异常不重试。
41
+ * @protected
42
+ */
43
+ protected readonly maxRetries: number;
44
+ /**
45
+ * 每次重试前等待的毫秒。
46
+ * @protected
47
+ */
48
+ protected readonly retryDelay: number;
35
49
  /**
36
50
  * NATS component instance
37
51
  * @protected
@@ -45,7 +59,7 @@ export declare class ServiceProxy extends Provider {
45
59
  /**
46
60
  * Invoke service registered method by pattern
47
61
  * @param input
48
- * @param timeout
62
+ * @param timeout 默认 10 分钟。服务不存在/无响应者由 NoResponders 立即失败,此超时仅兜底"已被接收但处理太慢"的情况;已知的慢操作可显式传更长值。
49
63
  */
50
64
  invoke<T extends Record<string, any>>(input: ActionPattern<T>, timeout?: number): Promise<any>;
51
65
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ServiceProxy.d.ts","sourceRoot":"","sources":["../../src/providers/ServiceProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAE,WAAW,EAAO,QAAQ,EAAE,sBAAsB,EAAC,MAAM,UAAU,CAAA;AAE1F,OAAO,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAA;AAGvC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACnC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,sBAAsB,CAAC,wBAAwB,CAM9E,CAAA;AAED;;GAEG;AACH,qBACa,YAAa,SAAQ,QAAQ;IACtC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;IAEnC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAE5C;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAEpC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,CAAA;IAEpB;;;OAGG;cACa,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;OAIG;IACU,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,MAAuB,GAAG,OAAO,CAAC,GAAG,CAAC;IAK3H;;;;;OAKG;IACI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,wBAAwB,GAAE,OAAe,GAAG,IAAI;IAKjH;;;;OAIG;IACI,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;IAKxE;;;;OAIG;IACI,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;CAI3E"}
1
+ {"version":3,"file":"ServiceProxy.d.ts","sourceRoot":"","sources":["../../src/providers/ServiceProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAE,WAAW,EAAO,QAAQ,EAAE,sBAAsB,EAAC,MAAM,UAAU,CAAA;AAG1F,OAAO,EAAC,IAAI,EAAC,MAAM,oBAAoB,CAAA;AAIvC;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACnC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,wBAAwB;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,sBAAsB,CAAC,wBAAwB,CAQ9E,CAAA;AAED;;GAEG;AACH,qBACa,YAAa,SAAQ,QAAQ;IACtC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;IAEnC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAE5C;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAEpC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAErC;;;OAGG;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAErC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,CAAA;IAEpB;;;OAGG;cACa,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;OAIG;IACU,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,MAAuB,GAAG,OAAO,CAAC,GAAG,CAAC;IAkB3H;;;;;OAKG;IACI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EAAE,wBAAwB,GAAE,OAAe,GAAG,IAAI;IAKjH;;;;OAIG;IACI,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;IAKxE;;;;OAIG;IACI,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,IAAI;CAI3E"}
@@ -12,7 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ServiceProxy = exports.BuildServiceProxy = void 0;
13
13
  const lakutata_1 = require("lakutata");
14
14
  const di_1 = require("lakutata/decorator/di");
15
+ const helper_1 = require("lakutata/helper");
15
16
  const ServiceResponseCodec_1 = require("../lib/ServiceResponseCodec");
17
+ const NatsNoRespondersAvailableException_1 = require("../exceptions/NatsNoRespondersAvailableException");
16
18
  /**
17
19
  * Build service proxy
18
20
  * @param options
@@ -22,7 +24,9 @@ const BuildServiceProxy = (options) => {
22
24
  return {
23
25
  class: ServiceProxy,
24
26
  natsComponentName: options.natsComponentName,
25
- serviceId: options.serviceId
27
+ serviceId: options.serviceId,
28
+ maxRetries: options.maxRetries,
29
+ retryDelay: options.retryDelay
26
30
  };
27
31
  };
28
32
  exports.BuildServiceProxy = BuildServiceProxy;
@@ -40,11 +44,25 @@ let ServiceProxy = class ServiceProxy extends lakutata_1.Provider {
40
44
  /**
41
45
  * Invoke service registered method by pattern
42
46
  * @param input
43
- * @param timeout
47
+ * @param timeout 默认 10 分钟。服务不存在/无响应者由 NoResponders 立即失败,此超时仅兜底"已被接收但处理太慢"的情况;已知的慢操作可显式传更长值。
44
48
  */
45
- async invoke(input, timeout = 60 * 60 * 1000) {
46
- const response = await this.nats.request(this.serviceId, input, timeout);
47
- return ServiceResponseCodec_1.ServiceResponseCodec.decode(response, this.serviceId);
49
+ async invoke(input, timeout = 10 * 60 * 1000) {
50
+ // 安全重试红线:只对 NoResponders 重试 —— NATS 明确告知"当时没人订阅",对侧【确定没处理】,
51
+ // 重试到其他副本是首次处理,不会重复。超时(可能已处理但响应丢失)、业务异常(对侧已处理并返回)
52
+ // 都【不】重试,直接抛给调用方,避免重复执行非幂等操作。
53
+ for (let attempt = 0;; attempt++) {
54
+ try {
55
+ const response = await this.nats.request(this.serviceId, input, timeout);
56
+ return ServiceResponseCodec_1.ServiceResponseCodec.decode(response, this.serviceId);
57
+ }
58
+ catch (e) {
59
+ if (e instanceof NatsNoRespondersAvailableException_1.NatsNoRespondersAvailableException && attempt < this.maxRetries) {
60
+ await (0, helper_1.Delay)(this.retryDelay);
61
+ continue;
62
+ }
63
+ throw e;
64
+ }
65
+ }
48
66
  }
49
67
  /**
50
68
  * On service event
@@ -93,6 +111,14 @@ __decorate([
93
111
  (0, di_1.Configurable)(lakutata_1.DTO.String().required()),
94
112
  __metadata("design:type", String)
95
113
  ], ServiceProxy.prototype, "serviceId", void 0);
114
+ __decorate([
115
+ (0, di_1.Configurable)(lakutata_1.DTO.Number().integer().optional().default(3)),
116
+ __metadata("design:type", Number)
117
+ ], ServiceProxy.prototype, "maxRetries", void 0);
118
+ __decorate([
119
+ (0, di_1.Configurable)(lakutata_1.DTO.Number().integer().optional().default(100)),
120
+ __metadata("design:type", Number)
121
+ ], ServiceProxy.prototype, "retryDelay", void 0);
96
122
  exports.ServiceProxy = ServiceProxy = __decorate([
97
123
  (0, di_1.Singleton)()
98
124
  ], ServiceProxy);
@@ -2,5 +2,11 @@ export type SubscribeOptions = {
2
2
  queue?: string;
3
3
  max?: number;
4
4
  iterator?: boolean;
5
+ /**
6
+ * 当消息处理出错且该消息是请求(带 reply 地址)时,用此工厂生成回传给请求方的 payload。
7
+ * subscribe 会用当前 codec 编码它并 respond;返回 undefined 则不回响应(请求方靠超时感知)。
8
+ * 用于让上层(如服务入口)注入自己的错误响应协议,而不让通用 subscribe 依赖该协议。
9
+ */
10
+ errorResponse?: (error: any) => any;
5
11
  };
6
12
  //# sourceMappingURL=SubscribeOptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SubscribeOptions.d.ts","sourceRoot":"","sources":["../../src/types/SubscribeOptions.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA"}
1
+ {"version":3,"file":"SubscribeOptions.d.ts","sourceRoot":"","sources":["../../src/types/SubscribeOptions.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;CACtC,CAAA"}
@@ -0,0 +1,199 @@
1
+ # 大消息传输方案设计(@lakutata/nats)
2
+
3
+ > 状态:**架构已定,工程细节待敲定**(见末尾 checklist)。
4
+ > 本文档是多轮设计讨论的沉淀,目标是让后续实现不必重推已排除的弯路。
5
+
6
+ ## 1. 背景与硬约束
7
+
8
+ 这个包用于 K8s 集群中、跨语言服务之间的 RPC 通讯(core NATS request-reply + pub/sub)。
9
+ 现实中存在大 payload(查大表、读文件等,8M+)。设计必须同时满足:
10
+
11
+ | 约束 | 说明 |
12
+ |---|---|
13
+ | **业务 API 不变** | 业务两端保持 `invoke(pattern)→完整对象`、`@ServiceAction return 完整对象`、`publish/subscribe`。中间件断面只在传输+调用层。 |
14
+ | **跨语言** | 有非 JS 服务,必须保证标准 JSON 子集语义(原生 codec 的 `JSON.parse(JSON.stringify())` 清洗必须保留,不能用 msgpack 富类型)。 |
15
+ | **不能 break 已上线服务** | 滚动升级新旧共存,必须向后兼容(能力协商 + 退化)。 |
16
+ | **不重复响应** | 多副本下一个请求只能一个副本处理(已由 queue group 保证)。 |
17
+
18
+ ## 2. 两个阻塞机制(问题根源)
19
+
20
+ | 机制 | 现象 | 本质 | 能否消除 |
21
+ |---|---|---|---|
22
+ | **机制1:TCP 队头阻塞** | 单条 16M 帧在连接上传输的 ~100ms 里,堵住该连接上后续所有消息 | 单 TCP 连接、协议帧不可交错 | 可隔离(独立连接) |
23
+ | **机制2:事件循环卡顿** | `codec.decode`/`encode` 是同步 CPU,16M 各 ~100ms 卡住**整个进程**的事件循环 | 完整对象 materialize 是物理成本 + **V8 对象只能在主线程构建** | **不能消除,只能隔离/分散** |
24
+
25
+ 实测(11KB→132µs encode;8.5M→50ms;16M→~100ms),decode/encode 随大小线性增长。
26
+
27
+ ## 3. 总体架构
28
+
29
+ > **一句话:小消息走现状 core RPC(零影响);大消息自动经 Object Store 中转(独立 bulk 连接 + 可选 bulk 副本双隔离);用 NATS header 协商保证灰度不破坏;业务两端 API 完全不变。**
30
+
31
+ ```
32
+ ┌─────────────────────────────────────────────────────────┐
33
+ │ 常规副本(处理高频小请求) │
34
+ │ 连接① nats(core) : invoke/event/publish 的小消息 │
35
+ │ 连接② natsBulk : 大数据 Object Store put/get(默认建) │
36
+ ├─────────────────────────────────────────────────────────┤
37
+ │ bulk 副本(可选,第二层,独立 deployment) │
38
+ │ 订阅 serviceId.bulk,用原生 codec,烧自己的事件循环 │
39
+ │ → 机制2 的处理隔离(没有它就退化到常规副本处理) │
40
+ ├─────────────────────────────────────────────────────────┤
41
+ │ NATS + JetStream Object Store(local-path 块存储) │
42
+ │ bucket:大对象中转,持久存储(get 不删),短 MaxAge 自动清 │
43
+ └─────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ **双隔离:**
47
+ - 独立连接(`natsBulk`)→ 解机制1(传输互不挤);
48
+ - 独立进程(bulk 副本)→ 解机制2(CPU 互不卡)。
49
+ - 两者是两件事,连接独立 ≠ 处理独立。
50
+
51
+ ## 4. bulk 连接策略(本次更新重点)
52
+
53
+ **`bulk` 默认 `true`:NATS 客户端启动时默认建立 bulk 连接 + 准备 Object Store 旁路。**
54
+
55
+ ```ts
56
+ nats: buildNatsClientOptions({
57
+ servers: '...',
58
+ bulk: true // 默认 true:启动即建 bulk 连接 + 准备 Object Store 旁路
59
+ })
60
+ ```
61
+
62
+ 设计要点:
63
+ - **预建立**(启动就连,无 cold start),符合 lakutata Component 的 init/destroy 生命周期;
64
+ - **连接配置自动复用 core**(servers/认证/codec 继承),**不用重复配**;但仍是**第二条独立 TCP**(隔离需要),不是共用一条;
65
+ - **连接与 Object Store 绑定**:默认开 = 连接 + Object Store 旁路一起就绪,**避免建一条空连接**;
66
+ - **可关**:`bulk: false` 给明确从不收发大数据的服务关掉省资源;
67
+ - **代价**:每副本 2 条连接,NATS server 端连接数翻倍,超大规模需评估;
68
+ - **bulk 副本是第二层可选**:没有它,大请求退化到常规副本处理(卡常规副本但功能正常);有它才完整隔离机制2。
69
+
70
+ `bulkNatsComponentName` 降级为**可选高级覆盖**:仅当想把 bulk 流量隔离到**另一个 NATS 集群**(不同 servers)时才用;默认同集群、自动复用配置,无需此项。
71
+
72
+ ## 5. 阈值:自动从 server 拿,零配置
73
+
74
+ 判断标准 = **encode 后的字节长度**(不是对象逻辑大小)。
75
+
76
+ ```ts
77
+ private get bulkThreshold(): number {
78
+ const maxPayload = this.#conn.info?.max_payload ?? 1024 * 1024 // fallback 1M
79
+ return Math.floor(maxPayload * 0.9) // 留 framing 余量
80
+ }
81
+ ```
82
+
83
+ - `max_payload` 由 NATS 握手的 `INFO` 消息提供,`nc.info.max_payload` 现成可读(已验证 `ServerInfo.max_payload` 存在);
84
+ - 阈值 = `max_payload × 0.9`,**跟随集群配置自动变化**,保证"core 发不出去的必走 bulk";
85
+ - 每次读、不缓存(应对 server 运行时重配,成本几乎为零);
86
+ - 可由用户配置向下覆盖(极低延迟场景想更激进隔离时)。
87
+
88
+ **诚实局限:判断需先 encode,而 encode 大对象本身就卡(机制2)。** 小消息 encode 微秒级无负担;大消息发送侧 encode 那一下卡躲不过(见 §11)。不能靠"预估大小"绕过(遍历估大小也是 O(size) 卡顿)。
89
+
90
+ ## 6. 数据流
91
+
92
+ ### (A) 小请求 RPC —— 完全是现状,零额外开销
93
+ ```
94
+ invoke(pattern) → encode < 阈值 → nats.request(serviceId, bytes) → 常规副本 → 响应
95
+ ```
96
+
97
+ ### (B) 大请求/大响应 RPC —— Object Store 传引用
98
+ ```
99
+ 客户端: encode ≥ 阈值 → natsBulk.put(TTL)→objId → nats.request(serviceId.bulk, {__ref:objId}, header)
100
+ bulk 副本: 收 {__ref} → natsBulk.get → decode → handler(完整对象)
101
+ → return 大对象 → put → 回 {__ref:objId2}
102
+ 客户端: 收 {__ref:objId2} → natsBulk.get → decode → resolve
103
+ ```
104
+ - core 连接上只跑**小引用消息**;大数据字节走 natsBulk + Object Store,离开 core;
105
+ - decode/encode 卡顿烧在 bulk 副本进程,常规副本无感;
106
+ - 业务两端只看到 `invoke(pattern)`/`return 对象`,看不到任何 ref。
107
+
108
+ ### (C) 大 publish/event —— Object Store 发引用(一对多更优)
109
+ ```
110
+ emitServiceEvent(evt, big) → encode ≥ 阈值 → put → publish(subject, {__ref})
111
+ 每个订阅方: 收 {__ref} → 按需 get → decode → listener(完整对象)
112
+ ```
113
+ - 广播的是小引用,所有订阅方 core 连接都不被砸;
114
+ - **一份存储多方按需取,没有 N× 流量放大**(优于 natsBulk 直传广播);
115
+ - 不关心的订阅方可以不下载。
116
+
117
+ ## 7. 协议:全部走 NATS header,payload 不动
118
+
119
+ | header 键 | 方向 | 含义 |
120
+ |---|---|---|
121
+ | `X-Lkt-Chunk: 1` | 请求 | 客户端能力声明:能处理引用中转 |
122
+
123
+ - payload 永远是业务数据或 `{__ref}`,协议元信息只在 header → 不污染、跨语言干净;
124
+ - `server` 是否支持 header:从 `nc.info.headers` 自动判断,不支持则自动退化。
125
+
126
+ ## 8. 向后兼容 + 灰度
127
+
128
+ 能力声明 header(`X-Lkt-Bulk`)可选,缺失即"对端不支持"→ 不给它发引用:
129
+
130
+ | 客户端 | 服务端 | 行为 |
131
+ |---|---|---|
132
+ | 旧 | 新 | 旧客户端不带 `X-Lkt-Bulk` → 新服务端响应直传退化 |
133
+ | 新 | 旧 | 旧服务端响应不带 `X-Lkt-Bulk` → 新客户端学到"不支持"→ 后续不发引用、直传 |
134
+ | 新 | 新 | 双向都声明 → 大数据走 Object Store 引用 |
135
+ | 旧 | 旧 | 完全不变 |
136
+
137
+ **已验证**:用裸 NATS 模拟真旧版(无 header、无中转),两个方向(旧→新、新→旧)wire 兼容均通过(`compat.test.ts`)。关键保证:**新版只在对端声明能力时才发引用,旧版不声明 → 自动退化直传**,新旧混部署 / 滚动升级不破坏。
138
+
139
+ **上线顺序(无 flag day):**
140
+ 1. 发新版(带能力,默认旁路可退化),`max_payload` 暂保持现值;
141
+ 2. 滚动升级所有服务;
142
+ 3. 部署 bulk 副本、开启旁路;
143
+ 4. 全升级确认后,把 `max_payload` 调回安全值(如 1M)。
144
+
145
+ ## 9. 清理 / 容量(Object Store)
146
+
147
+ - JetStream/Object Store **默认不会"消费即删"**(那是 WorkQueue retention);Object Store 是**持久存储,get 不删**;
148
+ - 必须配**短 `MaxAge`**(如 5min)兜底自动清;RPC 点对点可加"下载后显式 `delete`"双保险;publish 一对多只能靠 `MaxAge`;
149
+ - `max_file_store` 按 `峰值并发大对象数 × 大小 × TTL` 估算,别撑爆 local-path 磁盘。
150
+
151
+ ## 10. 分阶段落地
152
+
153
+ | 阶段 | 内容 | 状态 |
154
+ |---|---|---|
155
+ | **S1** | NATS 组件 bulk 基础能力:bulk 连接默认开(复用 core 配置)、Object Store `putBulk`/`getBulk`/`deleteBulk`/`bulkStatus`、阈值(`max_payload×0.9` 从 info)、`headersSupported` 探针、TTL/replicas/bucket 配置 | ✅ 已落地(`bulk.test.ts` 7 例) |
156
+ | **S2** | `request`/`subscribe` 透明中转:自适应阈值路由、双向能力协商(`X-Lkt-Bulk`/`X-Lkt-Ref`/`{__ref}`)、客户端缓存对端能力(`#peerCapabilityCache`)、退化兼容、引用用完即删 + TTL 兜底、`NatsBulkException` 错误语义化 | ✅ 已落地(`bulk-rpc.test.ts` 5 例 + `compat.test.ts` 真旧版兼容 2 例) |
157
+ | S3 | publish/event 的引用中转(一对多用 Object Store 发引用,不放大流量) | 待做 |
158
+ | S4 | bulk 副本(机制2 处理隔离)+ 部署/路由(没有 bulk 副本时退化常规副本) | 待做 |
159
+ | S5 | 背压(#1,bulk 副本尤其必要)+ 全集群升级后调回 `max_payload` | 待做 |
160
+
161
+ **S1+S2 = MVP 核心链路**:RPC 大请求/大响应透明走 Object Store、双向协商、退化兼容、API 完全不变,全部有测试。每阶段向后兼容、可单独测、不破坏现状。
162
+
163
+ ## 11. 已知边界 / 删不掉的代价(诚实记录)
164
+
165
+ - **调用方侧机制2躲不过**:调用方 `invoke` 大数据时,它自己 encode 请求 + decode 响应卡**它自己**的事件循环。自适应路由 + bulk 副本只隔离了**被调用方**侧;调用方侧的卡顿是物理成本,且调用方预知不了响应大小,无法提前隔离。**接受它。**
166
+ - 大数据请求**本身慢**(Object Store 多一跳 + encode/decode 物理成本)——但被关在 bulk 连接 + bulk 副本里,只烧自己,不连累高频小 RPC。
167
+ - bulk 副本需**单独部署**(运维多一份);Object Store 占磁盘 + 需 TTL 管理;依赖 JetStream(已开、底座已修为 local-path)。
168
+
169
+ ## 12. 待敲定的工程细节(实现前 checklist)
170
+
171
+ - [x] **objId 生命周期**:`randomUUID` 命名;取回后即删(请求 obj 在响应返回后删、响应 obj 客户端 get 后删),均 + 5min TTL 兜底 ✅
172
+ - [x] **配置 API**:`bulk`(默认 true)/`bulkBucket`/`bulkTTL`/`bulkReplicas`;`bulkNatsComponentName` 降级为可选高级覆盖 ✅
173
+ - [x] **测试策略**:容器加 `-js` 启 JetStream;`bulk.test.ts`(基础 7 例)+ `bulk-rpc.test.ts`(中转/协商/退化 4 例)✅
174
+ - [x] **超时/重试**:已在 §14 落地(超时 10min、NoResponders 安全重试)✅
175
+ - [x] **错误传播**:`NatsBulkException` 语义化(put/get 失败、引用取不到、未启用);服务端 bulk 失败 → errorResponse → 客户端收带 `E_NATS_BULK` 的 `ServiceInvokeException`;客户端 bulk 失败 → `NatsBulkException`(**不被 #7 重试**);`ServiceResponseCodec.decode` 只读属性 bug 已修。测试:`bulk.test.ts` / `bulk-rpc.test.ts`(端到端不同 bucket)/ 单元。Object Store 完全不可用靠 code review ✅
176
+ - [ ] **bulk 副本部署/路由**:常规 vs bulk 副本角色区分;没有 bulk 副本时大请求落哪 —— 留待 **S4**
177
+ - [ ] **背压(#1)**:bulk 副本尤其必要 —— 留待 **S5**
178
+
179
+ ## 13. 明确排除的弯路(不要重走)
180
+
181
+ | 方案 | 为什么不行 |
182
+ |---|---|
183
+ | 调大 max_payload 单条传 | 机制1 + 机制2 双重打击 |
184
+ | 自建 chunking + 流控 | 造轮子;Object Store 已内建分块/流控 |
185
+ | worker thread 放 codec | 对象跨边界结构化克隆 ≈ 序列化,还在主线程付账,多构建一次 |
186
+ | 异步 JSON 库 / stream-json | "异步"≠不占 CPU;JS 流式实现慢 5–10×;对象只能主线程构建 |
187
+ | 给 core RPC 开连接池 | 单线程瓶颈不变,多 socket ≠ 多处理能力 |
188
+
189
+ **根本原因**:只要"业务要完整对象 + API 不变",完整对象的 materialize(CPU + 内存)就删不掉、躲不出主线程。序列化层没有银弹,**解法只在架构隔离层**。
190
+
191
+ ## 14. 相关的独立改进项(与本方案正交,可先做)
192
+
193
+ - **✅ #3 优雅停机(drain)**:`destroy()` 已改为 `drain()` + `close()` 兜底,滚动升级不切断 in-flight(测试 `test/integration/drain.test.ts`)
194
+ - **✅ #2 超时默认值**:`invoke` 默认已从 1 小时改为 **10 分钟**(可显式覆盖;慢操作显式传更长值)
195
+ - **✅ #7 NoResponders 快速失败(已有)**:`request` 收到 503 立即抛 `NatsNoRespondersAvailableException`,不等超时——故障立即暴露,使 #2 的 10 分钟默认只兜底"处理慢"(测试 `errors.test.ts`)
196
+ - **✅ #7 NoResponders 安全重试**:`invoke` 只对 NoResponders(对侧确定没处理)自动重试(默认 3 次、间隔 100ms,均可配);**超时/业务异常一律不重试**,避免"对侧已处理但响应丢失"时重复执行非幂等操作(测试 `retry.test.ts`)
197
+ - **#1 背压(待做)**:服务端并发上限(bulk 副本尤其需要)
198
+
199
+ drain + 10 分钟超时 + NoResponders 立即失败 + NoResponders 安全重试 已齐,**滚动升级韧性完整**;仅剩 #1 背压作为增强。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakutata/nats",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Lakutata NATS Client",
5
5
  "keywords": [
6
6
  "lakutata",
@@ -23,7 +23,9 @@
23
23
  "clean": "shx rm -rf ./dist",
24
24
  "build": "tsc",
25
25
  "rebuild": "npm run clean && npm run build",
26
- "test": "bun src/tests/NatsTest.spec.ts"
26
+ "test": "npm run test:unit && npm run test:integration",
27
+ "test:unit": "bun test test/unit",
28
+ "test:integration": "bun test/run-integration.ts"
27
29
  },
28
30
  "bugs": {
29
31
  "url": "https://github.com/lakutata/lakutata-packages/issues"
@@ -32,5 +34,9 @@
32
34
  "lakutata": "^2.0.121",
33
35
  "nats": "^2.29.3"
34
36
  },
35
- "gitHead": "a6041bb579a295aed99b7f94301af373858b9624"
37
+ "gitHead": "d936b7289cccf1bf6d567fb99b7557d87da66f5f",
38
+ "devDependencies": {
39
+ "@types/bun": "^1.3.14",
40
+ "testcontainers": "^12.0.3"
41
+ }
36
42
  }
package/tsconfig.json CHANGED
@@ -3,5 +3,6 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "src",
5
5
  "outDir": "dist"
6
- }
6
+ },
7
+ "exclude": ["dist", "node_modules", "test"]
7
8
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=NatsTest.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"NatsTest.spec.d.ts","sourceRoot":"","sources":["../../src/tests/NatsTest.spec.ts"],"names":[],"mappings":""}
@@ -1,129 +0,0 @@
1
- "use strict";
2
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
- return c > 3 && r && Object.defineProperty(target, key, r), r;
7
- };
8
- var __metadata = (this && this.__metadata) || function (k, v) {
9
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- const lakutata_1 = require("lakutata");
13
- const entrypoint_1 = require("lakutata/com/entrypoint");
14
- const di_1 = require("lakutata/decorator/di");
15
- const CommonExports_1 = require("../CommonExports");
16
- const ctrl_1 = require("lakutata/decorator/ctrl");
17
- const ServiceProxy_1 = require("../providers/ServiceProxy");
18
- class TestComponent extends lakutata_1.Component {
19
- async init() {
20
- // this.nats.subscribe('test-invoke', (msg) => {
21
- // console.log('msg:', msg)
22
- // return Date.now()
23
- // })
24
- // this.nats.subscribe('test', (data: any) => {
25
- // console.log(data)
26
- // })
27
- //
28
- // setInterval(async () => {
29
- // this.nats.publish('test', 1234)
30
- // console.log('res:', await this.nats.request('test-invoke', JSON.stringify({haha: true})), 1000000)
31
- // }, 1)
32
- // console.log(await this.nats.request(this.app.appId, {test: true}))
33
- // this.nats.subscribe('test', async (inp) => {
34
- // console.log(inp)
35
- // await Delay(1000)
36
- // }, {iterator: true})
37
- //
38
- // for (let i = 0; i < 1000; i++) {
39
- // this.nats.publish('test', i)
40
- // }
41
- const handler1 = async (data1, data2) => {
42
- console.log('data1:', data1, data2);
43
- // this.nats.offServiceEvent(this.app.appId, 'testEvt', handler1)
44
- // this.self.off('testEvt', handler1)
45
- };
46
- const handler2 = async (data1, data2) => {
47
- console.log('data2:', data1, data2);
48
- // this.nats.offServiceEvent(this.app.appId, 'testEvt', handler1)
49
- // this.nats.offServiceEvent(this.app.appId, 'testEvt')
50
- };
51
- this.self.on('testEvt', handler1);
52
- this.self.on('testEvt', handler2);
53
- // this.nats.onServiceEvent(this.app.appId, 'testEvt', handler1)
54
- // this.nats.onServiceEvent(this.app.appId, 'testEvt', handler2)
55
- // setInterval(() => {
56
- // this.nats.emitServiceEvent('testEvt', 123, 456)
57
- // }, 1000)
58
- try {
59
- console.log(await this.self.invoke({ test: true, start: new Date() }));
60
- }
61
- catch (e) {
62
- // console.error(JSON.parse(JSON.stringify(e)))
63
- console.error(e);
64
- }
65
- }
66
- }
67
- __decorate([
68
- (0, di_1.Inject)(lakutata_1.Application),
69
- __metadata("design:type", lakutata_1.Application)
70
- ], TestComponent.prototype, "app", void 0);
71
- __decorate([
72
- (0, di_1.Inject)('nats'),
73
- __metadata("design:type", CommonExports_1.NATS)
74
- ], TestComponent.prototype, "nats", void 0);
75
- __decorate([
76
- (0, di_1.Inject)('self'),
77
- __metadata("design:type", ServiceProxy_1.ServiceProxy)
78
- ], TestComponent.prototype, "self", void 0);
79
- class TestController extends entrypoint_1.Controller {
80
- async test(inp) {
81
- console.log(inp);
82
- // throw new Error('fuck')
83
- // throw new NatsForbiddenException('fuck')
84
- // return 'hahahah'
85
- return {
86
- test: true,
87
- num: 1234,
88
- sub: {
89
- a: 1234,
90
- b: '1234234',
91
- c: [{ test: true }]
92
- }
93
- };
94
- // return ['hahahah']
95
- // return {
96
- // test: 123456
97
- // }
98
- }
99
- }
100
- __decorate([
101
- (0, ctrl_1.ServiceAction)({ test: true }),
102
- __metadata("design:type", Function),
103
- __metadata("design:paramtypes", [Object]),
104
- __metadata("design:returntype", Promise)
105
- ], TestController.prototype, "test", null);
106
- lakutata_1.Application.run({
107
- id: 'test.app',
108
- name: 'TestApp',
109
- components: {
110
- entrypoint: (0, entrypoint_1.BuildEntrypoints)({
111
- controllers: [TestController],
112
- service: (0, CommonExports_1.SetupNatsServiceEntrypoint)('nats')
113
- }),
114
- nats: (0, CommonExports_1.buildNatsClientOptions)({
115
- // servers: '127.0.0.1:4222'
116
- servers: '10.11.11.21:30422'
117
- }),
118
- test: {
119
- class: TestComponent
120
- }
121
- },
122
- providers: {
123
- self: (0, CommonExports_1.BuildServiceProxy)({ serviceId: 'test.app', natsComponentName: 'nats' })
124
- },
125
- bootstrap: [
126
- 'entrypoint',
127
- 'test'
128
- ]
129
- });