@hile/message-ws 4.0.2 → 4.0.4
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/AI.md +150 -27
- package/README.md +15 -6
- package/dist/codec.js +15 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/AI.md
CHANGED
|
@@ -32,13 +32,14 @@ Message handler file:
|
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
34
|
// src/messages/ping.msg.ts
|
|
35
|
-
import {
|
|
35
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
36
36
|
|
|
37
|
-
export default
|
|
37
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
38
38
|
return {
|
|
39
39
|
type: 'pong',
|
|
40
40
|
data,
|
|
41
41
|
params,
|
|
42
|
+
requestId: invocation.context.values.requestId,
|
|
42
43
|
timestamp: Date.now(),
|
|
43
44
|
}
|
|
44
45
|
})
|
|
@@ -71,7 +72,11 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
71
72
|
Caller:
|
|
72
73
|
|
|
73
74
|
```ts
|
|
74
|
-
|
|
75
|
+
import { randomUUID } from 'node:crypto'
|
|
76
|
+
import { createExecutionContext } from '@hile/context'
|
|
77
|
+
|
|
78
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
79
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
## More Examples
|
|
@@ -80,11 +85,11 @@ Streaming handler:
|
|
|
80
85
|
|
|
81
86
|
```ts
|
|
82
87
|
// src/messages/events.msg.ts
|
|
83
|
-
import {
|
|
88
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
84
89
|
|
|
85
|
-
export default
|
|
90
|
+
export default defineMicroMessage(async function* ({ invocation }) {
|
|
86
91
|
for (let i = 0; i < 3; i++) {
|
|
87
|
-
yield { seq: i }
|
|
92
|
+
yield { seq: i, requestId: invocation.context.values.requestId }
|
|
88
93
|
}
|
|
89
94
|
})
|
|
90
95
|
```
|
|
@@ -92,12 +97,48 @@ export default defineMessage(async function* () {
|
|
|
92
97
|
Streaming caller:
|
|
93
98
|
|
|
94
99
|
```ts
|
|
95
|
-
const stream = await app.stream('example.service', '/events', {})
|
|
100
|
+
const stream = await app.stream('example.service', '/events', {}, { context })
|
|
96
101
|
for await (const chunk of stream) {
|
|
97
102
|
console.log(chunk)
|
|
98
103
|
}
|
|
99
104
|
```
|
|
100
105
|
|
|
106
|
+
Streaming request body with a normal response:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// src/messages/upload.msg.ts
|
|
110
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
111
|
+
|
|
112
|
+
export default defineMicroMessage(async ({ data, input, invocation }) => {
|
|
113
|
+
if (!input) throw new Error('upload body is required')
|
|
114
|
+
|
|
115
|
+
let bytes = 0
|
|
116
|
+
for await (const chunk of input) bytes += Buffer.byteLength(chunk)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
filename: data.filename,
|
|
120
|
+
bytes,
|
|
121
|
+
requestId: invocation.context.values.requestId,
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { createReadStream } from 'node:fs'
|
|
128
|
+
|
|
129
|
+
const result = await app.call(
|
|
130
|
+
'example.service',
|
|
131
|
+
'/upload',
|
|
132
|
+
{ filename: 'archive.tar' },
|
|
133
|
+
{ context, input: createReadStream('/tmp/archive.tar') },
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
If no structured metadata is needed, pass an `AsyncIterable`, `Uint8Array`, or
|
|
138
|
+
`ArrayBuffer` directly as `data`; Hile sends it as the request input stream and
|
|
139
|
+
the handler receives `data === undefined`. `app.stream()` accepts the same
|
|
140
|
+
request input forms when both request and response need to stream.
|
|
141
|
+
|
|
101
142
|
Custom WebSocket modem:
|
|
102
143
|
|
|
103
144
|
```ts
|
|
@@ -121,6 +162,68 @@ class RpcWs extends MessageWs {
|
|
|
121
162
|
|
|
122
163
|
Notice that `request()` returns a `Promise<T>`. Await it directly.
|
|
123
164
|
|
|
165
|
+
## Stream Wire Model
|
|
166
|
+
|
|
167
|
+
One request ID owns the structured request and both optional stream directions:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
type RequestFrame = {
|
|
171
|
+
id: number
|
|
172
|
+
mode: MESSAGE_MODEM_TYPE.REQUEST
|
|
173
|
+
twoway: boolean
|
|
174
|
+
data?: unknown
|
|
175
|
+
streams?: {
|
|
176
|
+
input?: true
|
|
177
|
+
output?: { window?: number }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type StreamDataFrame = {
|
|
182
|
+
id: number
|
|
183
|
+
mode: MESSAGE_MODEM_TYPE.STREAM_DATA
|
|
184
|
+
twoway: false
|
|
185
|
+
data: {
|
|
186
|
+
direction: 'input' | 'output'
|
|
187
|
+
seq: number
|
|
188
|
+
payload?: unknown
|
|
189
|
+
final: boolean
|
|
190
|
+
status?: string | number
|
|
191
|
+
message?: string
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type StreamCreditFrame = {
|
|
196
|
+
id: number
|
|
197
|
+
mode: MESSAGE_MODEM_TYPE.STREAM_CREDIT
|
|
198
|
+
twoway: false
|
|
199
|
+
data: {
|
|
200
|
+
direction: 'input' | 'output'
|
|
201
|
+
seq: number
|
|
202
|
+
window?: number
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type StreamCancelFrame = {
|
|
207
|
+
id: number
|
|
208
|
+
mode: MESSAGE_MODEM_TYPE.STREAM_CANCEL
|
|
209
|
+
twoway: false
|
|
210
|
+
data: {
|
|
211
|
+
direction: 'input' | 'output'
|
|
212
|
+
status?: string | number
|
|
213
|
+
message?: string
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`data` is the structured message payload; stream chunks never get embedded in
|
|
219
|
+
it. The request frame declares the active directions, and every later stream
|
|
220
|
+
frame reuses the request ID. Input and output sequence numbers, credits, and
|
|
221
|
+
cancellation are independent.
|
|
222
|
+
|
|
223
|
+
A non-final stream frame must carry a payload other than `null` or `undefined`.
|
|
224
|
+
Node `Readable` reserves those values for its own end/no-op semantics, so Hile
|
|
225
|
+
rejects them instead of silently losing a credit or ending a stream early.
|
|
226
|
+
|
|
124
227
|
## Use When
|
|
125
228
|
|
|
126
229
|
Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.
|
|
@@ -128,14 +231,15 @@ Use the message packages for request/response messaging over WebSocket, process
|
|
|
128
231
|
## Do Not Use When
|
|
129
232
|
|
|
130
233
|
- Do not use `stream()` for normal single-result calls.
|
|
234
|
+
- Do not enable retries for a streamed request input. Input streams are consumed once and cannot be replayed safely.
|
|
131
235
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
132
|
-
-
|
|
236
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
133
237
|
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
134
238
|
|
|
135
239
|
## Install
|
|
136
240
|
|
|
137
241
|
```bash
|
|
138
|
-
pnpm add @hile/micro @hile/message-loader @hile/message-ws
|
|
242
|
+
pnpm add @hile/context @hile/micro @hile/message-loader @hile/message-ws
|
|
139
243
|
```
|
|
140
244
|
|
|
141
245
|
Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
|
|
@@ -144,7 +248,8 @@ Use transport-specific packages only when you need to build custom IPC or worker
|
|
|
144
248
|
|
|
145
249
|
```ts
|
|
146
250
|
import { defineMessage, MessageLoader } from '@hile/message-loader'
|
|
147
|
-
import {
|
|
251
|
+
import { createExecutionContext } from '@hile/context'
|
|
252
|
+
import { Application, defineMicroMessage, Registry, Server } from '@hile/micro'
|
|
148
253
|
import { MessageWs } from '@hile/message-ws'
|
|
149
254
|
import { MessageIpc } from '@hile/message-ipc'
|
|
150
255
|
import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
@@ -152,7 +257,7 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
152
257
|
|
|
153
258
|
## Compose With
|
|
154
259
|
|
|
155
|
-
-
|
|
260
|
+
- Pass `ExecutionContext` explicitly in every business call or stream option; the receiver gets it in `invocation.context`.
|
|
156
261
|
- `@hile/redis-idempotency` protects retryable side effects in message handlers.
|
|
157
262
|
- `@hile/redis-stream-queue` is better for durable background jobs.
|
|
158
263
|
|
|
@@ -161,14 +266,21 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
161
266
|
- `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
|
|
162
267
|
- `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
|
|
163
268
|
- `MessageModem._send()` returns a `Promise`.
|
|
269
|
+
- `_send()` and `_stream()` accept `options.input` as an `AsyncIterable`, `Uint8Array`, or `ArrayBuffer`. Passing one of those values directly as `data` automatically selects request streaming and leaves the handler's structured `data` undefined.
|
|
164
270
|
- `MessageModem._send()` and `_push()` use a `30_000` ms timeout when none is provided. An explicit timeout must be a safe integer from `1` through `2_147_483_647`; invalid values throw `TypeError` before a message is sent.
|
|
165
271
|
- `MessageModem._stream()` returns a Node `Readable` in object mode.
|
|
166
|
-
- Stream `timeout` and `idleTimeout` values use the same `1` through `2_147_483_647` ms range. The stream `window` must be a safe integer from `1` through `64` and defaults to `1`.
|
|
272
|
+
- Stream `timeout` and `idleTimeout` values use the same `1` through `2_147_483_647` ms range. Idle timeout is refreshed by valid input or output stream activity. The response stream `window` must be a safe integer from `1` through `64` and defaults to `1`; request input uses receiver-issued credit with a window of `1`.
|
|
167
273
|
- Each modem schedules request, total-stream, and idle-stream deadlines through one internal deadline scheduler. This reduces active Node.js timers without changing timeout, cancellation, ordering, or error semantics.
|
|
168
|
-
-
|
|
274
|
+
- Request and response chunks share `STREAM_DATA` frames and carry an explicit `direction: 'input' | 'output'`; each direction has independent sequence and credit state. `STREAM_CANCEL` stops one direction. A cancel carrying `status` fails the owning request immediately; `ABORT` cancels the whole invocation.
|
|
275
|
+
- This frame model replaces the former response-only `stream`, `streamVersion`, and `streamWindow` fields. All peers on one transport connection must use compatible `@hile/message-modem` and transport package versions; do not mix old and new peers during a rolling deployment.
|
|
276
|
+
- `@hile/message-ws` sends `Uint8Array` and `ArrayBuffer` request and response chunks as native binary frames rather than JSON/base64. Public `decodeMessageFrame()` payloads remain isolated from caller-owned input by default; the owned WebSocket `RawData` path uses a zero-copy binary view internally.
|
|
277
|
+
- `@hile/message-ipc` transparently Base64-wraps only binary `STREAM_DATA` payloads so request and response streams survive Node child-process IPC's default JSON serialization. Other IPC frames keep their ordinary object representation.
|
|
169
278
|
- A stream request requires `exec()` to return an async iterable.
|
|
170
|
-
- `
|
|
171
|
-
- `Application.
|
|
279
|
+
- `defineMicroMessage()` handlers receive request streams as `input: Readable | undefined`, separately from structured `data` and `invocation`.
|
|
280
|
+
- `Application.call(namespace, url, data, options)` requires `options.context` and returns a promise. It accepts `options.input` for a streamed request with a normal response.
|
|
281
|
+
- `Application.stream(namespace, url, data, options)` requires `options.context` and returns a readable response stream; it can carry a request input stream at the same time.
|
|
282
|
+
- `Application.call()` and `Application.stream()` default retries to `0` when request input is streamed. Explicit nonzero retries fail before service discovery because streamed input is non-replayable.
|
|
283
|
+
- A caller-owned request input source failure is surfaced as `MessageInputError`, aborts the peer invocation, and is not counted against that peer's circuit-breaker health.
|
|
172
284
|
- `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
|
|
173
285
|
- `Application.subscribe(topic, callback)` returns an unsubscribe function.
|
|
174
286
|
- `Registry` stores service addresses and retained config/topic state under `~/.registry`.
|
|
@@ -177,14 +289,17 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
177
289
|
|
|
178
290
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
179
291
|
- Returning a plain object from a handler called through `stream()`.
|
|
292
|
+
- Retrying a consumed request stream or hiding it inside a replay-unsafe factory.
|
|
180
293
|
- Using pub/sub as a durable queue.
|
|
181
294
|
- Forgetting to register `shutdown(await app.listen(...))`.
|
|
182
295
|
|
|
183
296
|
## Verification Checklist
|
|
184
297
|
|
|
185
|
-
-
|
|
186
|
-
- RPC callers use `await app.call(
|
|
298
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
299
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
187
300
|
- Streaming handlers are async generators.
|
|
301
|
+
- Request-stream handlers consume `input` and callers either pass `options.input` alongside metadata or pass a stream directly as `data`.
|
|
302
|
+
- Streamed request calls do not configure nonzero retries.
|
|
188
303
|
- Custom modem timeout values use the documented safe-integer range.
|
|
189
304
|
- Registry is started before application nodes need discovery.
|
|
190
305
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
@@ -203,10 +318,10 @@ Provider handler:
|
|
|
203
318
|
|
|
204
319
|
```ts
|
|
205
320
|
// src/messages/charge.msg.ts
|
|
206
|
-
import {
|
|
321
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
207
322
|
|
|
208
|
-
export default
|
|
209
|
-
return { charged: true, input: data }
|
|
323
|
+
export default defineMicroMessage(async ({ data, invocation }) => {
|
|
324
|
+
return { charged: true, input: data, requestId: invocation.context.values.requestId }
|
|
210
325
|
})
|
|
211
326
|
```
|
|
212
327
|
|
|
@@ -234,10 +349,14 @@ export default defineService('billing.micro', async (shutdown) => {
|
|
|
234
349
|
Consumer:
|
|
235
350
|
|
|
236
351
|
```ts
|
|
352
|
+
import { randomUUID } from 'node:crypto'
|
|
353
|
+
import { createExecutionContext } from '@hile/context'
|
|
354
|
+
|
|
355
|
+
const context = createExecutionContext({ requestId: randomUUID(), tenantId: 't1' })
|
|
237
356
|
const result = await app.call('billing', '/charge', {
|
|
238
357
|
tenantId: 't1',
|
|
239
358
|
amount: 100,
|
|
240
|
-
})
|
|
359
|
+
}, { context })
|
|
241
360
|
```
|
|
242
361
|
|
|
243
362
|
## File Layout
|
|
@@ -257,21 +376,23 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
257
376
|
## Packages To Use
|
|
258
377
|
|
|
259
378
|
- `@hile/micro`
|
|
260
|
-
- `@hile/
|
|
261
|
-
- `@hile/context` when context must cross service boundaries
|
|
379
|
+
- `@hile/context` for the required explicit execution context carrier
|
|
262
380
|
- `@hile/redis-idempotency` for retryable side effects
|
|
263
381
|
|
|
264
382
|
## Implementation Steps
|
|
265
383
|
|
|
266
384
|
1. Start a Registry with `hile registry`.
|
|
267
385
|
2. Start providers with stable namespaces.
|
|
268
|
-
3.
|
|
269
|
-
4.
|
|
386
|
+
3. Default-export `defineMicroMessage()` handlers and load them through `app.load()`.
|
|
387
|
+
4. Create context at ingress and call providers with `await app.call(namespace, url, data, { context })`.
|
|
270
388
|
5. Use `app.stream()` only for async-generator handlers.
|
|
389
|
+
6. For streamed request bodies, consume `input` in the `defineMicroMessage()` handler and pass the source through `options.input`; pass the stream as `data` only when no structured metadata is needed.
|
|
271
390
|
|
|
272
391
|
## Failure And Cleanup Behavior
|
|
273
392
|
|
|
274
393
|
- `Application.call()` may retry; side-effecting handlers need idempotency.
|
|
394
|
+
- A streamed request body is non-replayable. Its retry default is `0`, and an explicit nonzero retry count is rejected before discovery.
|
|
395
|
+
- Request input and response output have independent credit-based backpressure and may be active together.
|
|
275
396
|
- Registry disconnect triggers reconnect; apps re-declare topics and subscriptions.
|
|
276
397
|
- Circuit breaker excludes failing nodes for cooldown.
|
|
277
398
|
|
|
@@ -279,8 +400,9 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
279
400
|
|
|
280
401
|
- Registry is reachable.
|
|
281
402
|
- Provider namespace matches consumer call.
|
|
282
|
-
- Handlers default-export `
|
|
283
|
-
- Consumer code awaits `app.call(
|
|
403
|
+
- Handlers default-export `defineMicroMessage()` and consume explicit invocation context when needed.
|
|
404
|
+
- Consumer code awaits `app.call(..., { context })` directly.
|
|
405
|
+
- Streamed request handlers consume `input: Readable`, preserve structured metadata in `data`, and do not enable retries.
|
|
284
406
|
|
|
285
407
|
|
|
286
408
|
|
|
@@ -296,6 +418,7 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
296
418
|
- Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
|
|
297
419
|
- Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
|
|
298
420
|
- Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
|
|
421
|
+
- Do not invent service-specific HTTP-in-Micro envelopes or Base64 file bodies; use `@hile/http-over-micro` and its request/response streams.
|
|
299
422
|
- Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
|
|
300
423
|
- Do not use queue `jobId` as the only side-effect idempotency boundary.
|
|
301
424
|
- Do not log the entire async context by default.
|
package/README.md
CHANGED
|
@@ -22,13 +22,14 @@ Message handler file:
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
// src/messages/ping.msg.ts
|
|
25
|
-
import {
|
|
25
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
26
26
|
|
|
27
|
-
export default
|
|
27
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
28
28
|
return {
|
|
29
29
|
type: 'pong',
|
|
30
30
|
data,
|
|
31
31
|
params,
|
|
32
|
+
requestId: invocation.context.values.requestId,
|
|
32
33
|
timestamp: Date.now(),
|
|
33
34
|
}
|
|
34
35
|
})
|
|
@@ -61,26 +62,34 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
61
62
|
Caller:
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
|
-
|
|
65
|
+
import { randomUUID } from 'node:crypto'
|
|
66
|
+
import { createExecutionContext } from '@hile/context'
|
|
67
|
+
|
|
68
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
69
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
## Boundaries
|
|
68
73
|
|
|
69
74
|
- Do not use `stream()` for normal single-result calls.
|
|
75
|
+
- Do not enable retries for a streamed request input. Input streams are consumed once and cannot be replayed safely.
|
|
70
76
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
71
|
-
-
|
|
77
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
72
78
|
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
73
79
|
|
|
74
80
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
75
81
|
- Returning a plain object from a handler called through `stream()`.
|
|
82
|
+
- Retrying a consumed request stream or hiding it inside a replay-unsafe factory.
|
|
76
83
|
- Using pub/sub as a durable queue.
|
|
77
84
|
- Forgetting to register `shutdown(await app.listen(...))`.
|
|
78
85
|
|
|
79
86
|
## Verify
|
|
80
87
|
|
|
81
|
-
-
|
|
82
|
-
- RPC callers use `await app.call(
|
|
88
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
89
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
83
90
|
- Streaming handlers are async generators.
|
|
91
|
+
- Request-stream handlers consume `input` and callers either pass `options.input` alongside metadata or pass a stream directly as `data`.
|
|
92
|
+
- Streamed request calls do not configure nonzero retries.
|
|
84
93
|
- Custom modem timeout values use the documented safe-integer range.
|
|
85
94
|
- Registry is started before application nodes need discovery.
|
|
86
95
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
package/dist/codec.js
CHANGED
|
@@ -19,7 +19,7 @@ function isRecord(value) {
|
|
|
19
19
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
20
|
}
|
|
21
21
|
function isBinaryPayload(value) {
|
|
22
|
-
return Buffer.isBuffer(value) || value instanceof Uint8Array;
|
|
22
|
+
return Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
23
23
|
}
|
|
24
24
|
function parseJson(value) {
|
|
25
25
|
try {
|
|
@@ -40,11 +40,10 @@ function toBuffer(raw) {
|
|
|
40
40
|
return Buffer.from(raw);
|
|
41
41
|
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
42
42
|
}
|
|
43
|
-
function
|
|
44
|
-
return message.mode === MESSAGE_MODEM_TYPE.
|
|
45
|
-
&& message.stream === true
|
|
46
|
-
&& message.streamVersion === 1
|
|
43
|
+
function isBinaryStreamData(message) {
|
|
44
|
+
return message.mode === MESSAGE_MODEM_TYPE.STREAM_DATA
|
|
47
45
|
&& isRecord(message.data)
|
|
46
|
+
&& (message.data.direction === 'input' || message.data.direction === 'output')
|
|
48
47
|
&& isBinaryPayload(message.data.payload);
|
|
49
48
|
}
|
|
50
49
|
function validateBinaryEnvelope(value) {
|
|
@@ -53,12 +52,10 @@ function validateBinaryEnvelope(value) {
|
|
|
53
52
|
}
|
|
54
53
|
if (!Number.isSafeInteger(value.id)
|
|
55
54
|
|| value.id < 0
|
|
56
|
-
|| value.mode !== MESSAGE_MODEM_TYPE.
|
|
55
|
+
|| value.mode !== MESSAGE_MODEM_TYPE.STREAM_DATA
|
|
57
56
|
|| value.twoway !== false
|
|
58
|
-
|| value.stream !== true
|
|
59
|
-
|| value.streamVersion !== 1
|
|
60
57
|
|| !isRecord(value.data)) {
|
|
61
|
-
fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header is not
|
|
58
|
+
fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header is not stream data');
|
|
62
59
|
}
|
|
63
60
|
if (Object.prototype.hasOwnProperty.call(value.data, 'payload')) {
|
|
64
61
|
fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header must not contain an inline payload');
|
|
@@ -66,15 +63,20 @@ function validateBinaryEnvelope(value) {
|
|
|
66
63
|
if (!Number.isSafeInteger(value.data.seq)
|
|
67
64
|
|| value.data.seq < 0
|
|
68
65
|
|| typeof value.data.final !== 'boolean'
|
|
69
|
-
||
|
|
70
|
-
|
|
66
|
+
|| (value.data.direction !== 'input' && value.data.direction !== 'output')
|
|
67
|
+
|| (value.data.direction === 'output'
|
|
68
|
+
&& typeof value.data.status !== 'string'
|
|
69
|
+
&& typeof value.data.status !== 'number')) {
|
|
71
70
|
fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame chunk metadata is invalid');
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
export function encodeMessageFrame(message) {
|
|
75
|
-
if (!
|
|
74
|
+
if (!isBinaryStreamData(message))
|
|
76
75
|
return JSON.stringify(message);
|
|
77
|
-
const
|
|
76
|
+
const source = message.data.payload;
|
|
77
|
+
const payload = source instanceof ArrayBuffer
|
|
78
|
+
? Buffer.from(source)
|
|
79
|
+
: Buffer.from(source.buffer, source.byteOffset, source.byteLength);
|
|
78
80
|
const { payload: _payload, ...chunkHeader } = message.data;
|
|
79
81
|
const header = Buffer.from(JSON.stringify({
|
|
80
82
|
...message,
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * from './codec.js';
|
|
|
6
6
|
* exec 方法由子类实现,本类不做实现。
|
|
7
7
|
*
|
|
8
8
|
* 构造时传入已连接的 WebSocket 实例,自动绑定 message 事件。
|
|
9
|
-
* 普通消息通过 JSON 传输;二进制 stream
|
|
9
|
+
* 普通消息通过 JSON 传输;二进制 stream input/output 使用 Hile 二进制帧,避免 Base64 开销。
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* class MyWs extends MessageWs {
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export * from './codec.js';
|
|
|
6
6
|
* exec 方法由子类实现,本类不做实现。
|
|
7
7
|
*
|
|
8
8
|
* 构造时传入已连接的 WebSocket 实例,自动绑定 message 事件。
|
|
9
|
-
* 普通消息通过 JSON 传输;二进制 stream
|
|
9
|
+
* 普通消息通过 JSON 传输;二进制 stream input/output 使用 Hile 二进制帧,避免 Base64 开销。
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* class MyWs extends MessageWs {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/message-ws",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"vitest": "^4.0.18"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@hile/message-modem": "^4.0.
|
|
27
|
+
"@hile/message-modem": "^4.0.4",
|
|
28
28
|
"ws": "^8.21.0"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "c57b2c5a3c017ae56d4a862a089b2af01368c3ba"
|
|
31
31
|
}
|