@api.global/typedsocket 6.3.0 → 7.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.
Files changed (35) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.typedsocket.d.ts +24 -52
  3. package/dist_ts/classes.typedsocket.js +277 -358
  4. package/dist_ts/classes.typedsockettagpolicymanager.d.ts +1 -0
  5. package/dist_ts/classes.typedsockettagpolicymanager.js +26 -2
  6. package/dist_ts/classes.virtualstreammanager.d.ts +274 -0
  7. package/dist_ts/classes.virtualstreammanager.js +2115 -0
  8. package/dist_ts/constants.virtualstream.d.ts +58 -0
  9. package/dist_ts/constants.virtualstream.js +58 -0
  10. package/dist_ts/helpers.virtualstreamcodec.d.ts +14 -0
  11. package/dist_ts/helpers.virtualstreamcodec.js +349 -0
  12. package/dist_ts/index.d.ts +5 -9
  13. package/dist_ts/index.js +3 -6
  14. package/dist_ts/interfaces.diagnostics.d.ts +6 -18
  15. package/dist_ts/interfaces.virtualstream.d.ts +131 -0
  16. package/dist_ts/interfaces.virtualstream.js +2 -0
  17. package/package.json +5 -5
  18. package/readme.hints.md +11 -59
  19. package/readme.md +306 -840
  20. package/dist_ts/classes.nativebyteerror.d.ts +0 -26
  21. package/dist_ts/classes.nativebyteerror.js +0 -51
  22. package/dist_ts/classes.nativebytemanager.d.ts +0 -505
  23. package/dist_ts/classes.nativebytemanager.js +0 -3691
  24. package/dist_ts/constants.nativebytes.d.ts +0 -59
  25. package/dist_ts/constants.nativebytes.js +0 -58
  26. package/dist_ts/constants.nativemessages.d.ts +0 -45
  27. package/dist_ts/constants.nativemessages.js +0 -44
  28. package/dist_ts/helpers.nativebytecodec.d.ts +0 -8
  29. package/dist_ts/helpers.nativebytecodec.js +0 -328
  30. package/dist_ts/helpers.nativemessagecodec.d.ts +0 -6
  31. package/dist_ts/helpers.nativemessagecodec.js +0 -267
  32. package/dist_ts/interfaces.nativebytes.d.ts +0 -187
  33. package/dist_ts/interfaces.nativebytes.js +0 -2
  34. package/dist_ts/interfaces.nativemessages.d.ts +0 -144
  35. package/dist_ts/interfaces.nativemessages.js +0 -2
package/readme.md CHANGED
@@ -1,999 +1,465 @@
1
1
  # @api.global/typedsocket
2
2
 
3
- A TypeScript library for creating typed WebSocket connections with bi-directional communication support. Extends `@api.global/typedrequest` to bring type-safe request/response patterns to WebSocket connections.
3
+ Typed request/response communication over WebSockets with one peer-scoped transport for JSON RPC and ordered `virtual-stream-v1` byte streams. TypedSocket 7 integrates TypedRequest 7 with SmartServe 5.1.1, enforces an exact package-major handshake, and binds every server operation to the physical peer and routing surface selected during upgrade.
4
4
 
5
5
  ## Issue Reporting and Security
6
6
 
7
7
  For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
8
8
 
9
- ## Features
9
+ ## Install
10
10
 
11
- - 🔒 **Full Type Safety** - Leverages TypeScript for compile-time checking of all request/response payloads
12
- - 🔄 **Bi-directional Communication** - Both server and client can initiate requests
13
- - 🔌 **Auto-reconnect** - Client automatically reconnects on connection loss
14
- - 🏷️ **Policy-Gated Connection Tagging** - Default-deny client tags and protected server metadata
15
- - 🌐 **Browser Compatible** - Works in both Node.js and browser environments
16
- - 🚀 **SmartServe Integration** - Native support for SmartServe's WebSocket handling
17
- - **Ephemeral Binary Messages** - Bounded, acknowledged binary messages up to 4 MiB without JSON/base64 expansion
11
+ ```bash
12
+ pnpm add @api.global/typedsocket @api.global/typedrequest @api.global/typedrequest-interfaces
13
+ ```
18
14
 
19
- ## Install
15
+ Server applications also need SmartServe:
20
16
 
21
17
  ```bash
22
- pnpm add @api.global/typedsocket
18
+ pnpm add @push.rocks/smartserve
23
19
  ```
24
20
 
25
- ## Usage
21
+ TypedSocket 7 requires `@api.global/typedrequest` 7, `@api.global/typedrequest-interfaces` 7, and `@push.rocks/smartserve` 5.1.1 or newer within major 5. These package majors form one transport contract and must not be mixed with earlier router or stream APIs.
22
+
23
+ ## Version 7 transport model
26
24
 
27
- ### Prerequisites
25
+ Each physical WebSocket peer has one always-on TypedSocket transport:
28
26
 
29
- - TypeScript project setup
30
- - Basic understanding of async/await patterns
31
- - Familiarity with `@api.global/typedrequest` concepts
32
- - `@api.global/typedrequest` 5 and `@api.global/typedrequest-interfaces` 5
33
- - `@push.rocks/smartserve` 4.2.1 or newer within major 4 for server integrations
27
+ - text frames carry bidirectional TypedRequest envelopes;
28
+ - binary frames carry the same peer's `virtual-stream-v1` streams;
29
+ - SmartServe fixes the peer's `routingSurface` and `transportOwner` during upgrade;
30
+ - the client and server must complete the exact TypedSocket package-major handshake before application requests or streams are admitted;
31
+ - client connection restoration runs after the handshake and before desired tags and the `connected` state are published.
34
32
 
35
- TypedSocket 6, SmartServe 4.2.1, and TypedRequest 5.2.1 share one `TypedRouter` contract. Do not
36
- mix older router contracts or bridge the mismatch with casts.
33
+ There are no optional native-byte or native-message capability modes in version 7. The v6 `nativeBytes`, `native-byte-v1`, `native-message-v1`, binary-message channel, and capability-mode APIs are not part of the v7 public surface. There is also no `TypedSocket.fromSmartServe()` attachment shortcut: server composition must happen before SmartServe is constructed.
37
34
 
38
- ### Define Your Request Interface
35
+ ## Define shared contracts
39
36
 
40
- First, define the typed request interface that both client and server will use:
37
+ TypedSocket uses ordinary TypedRequest interfaces. VirtualStreams use the transport-neutral TypedRequest 7 types:
41
38
 
42
39
  ```typescript
43
- import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
40
+ import type {
41
+ ITypedRequest,
42
+ TVirtualStream,
43
+ implementsTR,
44
+ } from '@api.global/typedrequest-interfaces';
44
45
 
45
- interface IGreetingRequest extends typedrequestInterfaces.implementsTR<
46
- typedrequestInterfaces.ITypedRequest,
47
- IGreetingRequest
48
- > {
46
+ export interface IGreetRequest extends implementsTR<ITypedRequest, IGreetRequest> {
49
47
  method: 'greet';
48
+ request: { name: string };
49
+ response: { message: string };
50
+ }
51
+
52
+ export interface IUploadRequest extends implementsTR<ITypedRequest, IUploadRequest> {
53
+ method: 'upload';
50
54
  request: {
51
- name: string;
55
+ stream: TVirtualStream<'send'>;
52
56
  };
53
57
  response: {
54
- message: string;
58
+ storedBytes: number;
55
59
  };
56
60
  }
57
- ```
58
-
59
- ### Server Setup
60
61
 
61
- TypedSocket composes its private protocol handlers into the application router
62
- before that router is given to SmartServe:
63
-
64
- ```typescript
65
- import { TypedSocket } from '@api.global/typedsocket';
66
- import * as typedrequest from '@api.global/typedrequest';
67
- import { SmartServe } from '@push.rocks/smartserve';
62
+ export interface IDownloadRequest extends implementsTR<ITypedRequest, IDownloadRequest> {
63
+ method: 'download';
64
+ request: { objectId: string };
65
+ response: {
66
+ // Direction is local to the requester. The server handler sees 'send'.
67
+ stream: TVirtualStream<'receive'>;
68
+ };
69
+ }
68
70
 
69
- // Create the router and add handlers
70
- const typedRouter = new typedrequest.TypedRouter();
71
+ export interface IRestoreSessionRequest
72
+ extends implementsTR<ITypedRequest, IRestoreSessionRequest> {
73
+ method: 'restoreSession';
74
+ request: { token: string };
75
+ response: { restored: true };
76
+ }
77
+ ```
71
78
 
72
- typedRouter.addTypedHandler<IGreetingRequest>(
73
- new typedrequest.TypedHandler('greet', async (requestData) => {
74
- return {
75
- message: `Hello, ${requestData.name}! 👋`,
76
- };
77
- })
78
- );
79
+ `TypedHandler` reverses stream directions at the handler boundary. An upload declared as requester-local `send` reaches the server handler as local `receive`; a download declared as requester-local `receive` is created by the handler as local `send`.
79
80
 
80
- const server = TypedSocket.createServer(typedRouter);
81
- const smartServe = new SmartServe({
82
- port: 3000,
83
- websocket: {
84
- typedRouter,
85
- transportOwner: server.webSocketTransportOwner,
86
- },
87
- });
88
- server.attachSmartServe(smartServe);
89
- await smartServe.start();
90
- ```
81
+ ## Server setup with SmartServe 5.1.1
91
82
 
92
- #### Integration with SmartServe
83
+ Construction order is part of the transport contract:
93
84
 
94
- For SmartServe-based applications, compose the protocol router synchronously,
95
- attach the transport, and then start listening:
85
+ 1. Create and populate the application `TypedRouter`.
86
+ 2. Call `TypedSocket.createServer()`.
87
+ 3. Obtain the generated transport routing surface with `getServerRoutingSurface()`.
88
+ 4. Construct SmartServe with that routing surface and the exact `webSocketTransportOwner` object.
89
+ 5. Call `attachSmartServe()`.
90
+ 6. Start SmartServe.
96
91
 
97
92
  ```typescript
98
93
  import { TypedSocket } from '@api.global/typedsocket';
94
+ import { TypedHandler, TypedRouter } from '@api.global/typedrequest';
99
95
  import { SmartServe } from '@push.rocks/smartserve';
100
- import * as typedrequest from '@api.global/typedrequest';
101
96
 
102
- const typedRouter = new typedrequest.TypedRouter();
97
+ const applicationRouter = new TypedRouter();
103
98
 
104
- // Add handlers for client-to-server requests
105
- typedRouter.addTypedHandler<IGreetingRequest>(
106
- new typedrequest.TypedHandler('greet', async (requestData) => {
107
- return { message: `Hello, ${requestData.name}!` };
108
- })
99
+ applicationRouter.addTypedHandler(
100
+ new TypedHandler<IGreetRequest>('greet', async ({ name }) => ({
101
+ message: `Hello, ${name}!`,
102
+ })),
109
103
  );
110
104
 
111
- const typedSocket = TypedSocket.createServer(typedRouter);
105
+ const typedSocket = TypedSocket.createServer(applicationRouter, {
106
+ onServerConnectionReady: (connection) => {
107
+ typedSocket.setServerTag(connection, 'application-client');
108
+ return undefined;
109
+ },
110
+ });
112
111
 
113
- // Create SmartServe with the composed application router
114
112
  const smartServe = new SmartServe({
115
113
  port: 3000,
116
114
  websocket: {
117
- typedRouter,
115
+ typedRouter: typedSocket.getServerRoutingSurface(applicationRouter),
118
116
  transportOwner: typedSocket.webSocketTransportOwner,
119
- onConnectionOpen: (peer) => {
120
- // Server metadata is protected from client overwrite/removal.
121
- typedSocket.setServerTag(peer, 'client');
122
- }
123
- }
117
+ },
124
118
  });
125
119
 
126
120
  typedSocket.attachSmartServe(smartServe);
127
121
  await smartServe.start();
128
-
129
- // Push notifications to tagged clients
130
- const clients = await typedSocket.findAllTargetConnectionsByTag('client');
131
- for (const client of clients) {
132
- const request = typedSocket.createTypedRequest<IGreetingRequest>('greet', client);
133
- await request.fire({ name: 'server' });
134
- }
135
122
  ```
136
123
 
137
- > **Note:** When using SmartServe, the WebSocket transport is managed by SmartServe. TypedSocket acts as a convenience layer for finding connections and sending server-initiated requests.
124
+ Do not pass `applicationRouter` directly to `websocket.typedRouter`. `createServer()` creates a distinct routing surface that composes the private TypedSocket protocol before the application router. SmartServe must bind that returned surface and the exact transport-owner identity to the peer.
125
+
126
+ `onServerConnectionReady(connection)` may synchronously assign protected tags or
127
+ other connection-local state after the exact handshake response has been
128
+ settled. It must return `undefined`; returning any other value, including a
129
+ Promise or custom thenable, or throwing closes the connection before readiness
130
+ is published.
131
+
132
+ ### Multiple isolated routing surfaces
138
133
 
139
- Multiple isolated application routers can share one transport without becoming
140
- reachable from each other:
134
+ One TypedSocket can compose multiple application routers without making them reachable from one another. Resolve the corresponding generated surface during upgrade:
141
135
 
142
136
  ```typescript
137
+ const publicRouter = new TypedRouter();
138
+ const adminRouter = new TypedRouter();
143
139
  const typedSocket = TypedSocket.createServer([publicRouter, adminRouter]);
140
+
144
141
  const smartServe = new SmartServe({
145
142
  port: 3000,
146
143
  authorityValidation: 'strict',
147
144
  websocket: {
148
145
  resolveTypedRouter: (context) => {
149
- if (context.url.hostname === 'example.com') return publicRouter;
150
- if (context.url.hostname === 'admin.example.com') return adminRouter;
146
+ if (context.url.hostname === 'api.example.com') {
147
+ return typedSocket.getServerRoutingSurface(publicRouter);
148
+ }
149
+ if (context.url.hostname === 'admin.example.com') {
150
+ return typedSocket.getServerRoutingSurface(adminRouter);
151
+ }
151
152
  return undefined;
152
153
  },
153
154
  transportOwner: typedSocket.webSocketTransportOwner,
154
155
  },
155
156
  });
157
+
156
158
  typedSocket.attachSmartServe(smartServe);
157
159
  await smartServe.start();
158
160
  ```
159
161
 
160
- TypedSocket adds a private, one-way fallback router containing its protocol
161
- handlers to every application router. Duplicate protocol method names are
162
- rejected during composition and on later router mutations; `stop()` releases
163
- the owned fallback edges. `fromSmartServe()` remains available as a JSON-only
164
- attachment helper that delegates to `createServer()` and `attachSmartServe()`.
165
-
166
- Connection lookup via `findTargetConnection()`/`findAllTargetConnections()`
167
- spans every peer attached to this server's routing surfaces. Deprecated
168
- implicit single-peer targeting is narrower: multi-router servers throw
169
- `'TypedSocket multi-router servers require an explicit targetConnection'`
170
- before any lookup, and single-router servers only consider peers bound to that
171
- router's surface. Multi-surface servers should tag or filter peers by surface
172
- and pass an explicit target to `createTypedRequest()`.
173
-
174
- ### Native Byte Streams
175
-
176
- TypedSocket 6 negotiates `native-byte-v1` on each physical WebSocket before
177
- restoring tags or publishing the client as connected. Native bytes require the
178
- SmartServe 4.2.1 raw-frame owner to be selected when SmartServe is constructed. The
179
- server construction order is strict:
180
-
181
- 1. Create the application `TypedRouter` instances.
182
- 2. Call `TypedSocket.createServer(routerOrRouters, options?)`.
183
- 3. Construct `SmartServe` with the composed router and
184
- `transportOwner: typedSocket.webSocketTransportOwner`.
185
- 4. Call `typedSocket.attachSmartServe(smartServe)`.
186
- 5. Start SmartServe.
187
-
188
- `fromSmartServe()` supports JSON-only attachment to an existing server. It cannot add a
189
- raw-frame owner to a SmartServe instance that has already selected transports,
190
- so native negotiation succeeds only for peers already bound to the exact
191
- `webSocketTransportOwner` object.
192
-
193
- Client and server options accept `nativeByteCapabilityMode`:
194
-
195
- - `optional` is the default. JSON RPC remains available if native negotiation is unavailable.
196
- - `required` fails client startup when negotiation does not return `native-byte-v1`. On servers it closes peers that do not negotiate the capability.
197
- - `disabled` advertises no native capability and keeps JSON RPC available.
198
-
199
- ### Ephemeral Binary Message Channels
200
-
201
- TypedSocket separately negotiates `native-message-v1` with
202
- `__typedsocket_negotiateNativeMessages` after the existing primary capability
203
- negotiation. Client and server options accept an independent
204
- `nativeMessageCapabilityMode` (`disabled`, `optional`, or `required`). Optional
205
- clients tolerate servers that do not know the new private method. Disabled
206
- clients still call it and advertise an empty capability list when it exists.
207
- Required servers close peers that do not complete this negotiation within 10
208
- seconds.
209
-
210
- Message channels reuse the one `NativeByteManager`, SmartServe raw-frame owner,
211
- raw queue, control-first scheduler, exact outbound settlement identity, and
212
- aggregate connection/server accounting. They use a distinct `TSM1` codec and
213
- do not alter `native-byte-v1` frame encoding or byte-stream protocol semantics.
214
-
215
- The preferred server API is `createBinaryMessageChannel()`. It synchronously
216
- binds application authorization and returns a receive grant with an ordinary
217
- JSON descriptor. Applications explicitly carry that descriptor in their own
218
- typed DTO; TypedRequest and `VirtualStream` do not serialize it automatically.
162
+ SmartServe rejects an upgrade when `resolveTypedRouter()` returns `undefined`. `typedRouter` and `resolveTypedRouter` are mutually exclusive, as are `transportOwner` and `resolveTransportOwner`.
219
163
 
220
- ```typescript
221
- import {
222
- TypedSocket,
223
- type INativeMessageDescriptor,
224
- } from '@api.global/typedsocket';
225
-
226
- interface ICameraAuthorization {
227
- principalId: string;
228
- credentialRevision: string;
229
- configRevision: string;
230
- bindingRevision: string;
231
- }
164
+ ## Client setup
232
165
 
233
- declare const cameraAuthorization: ICameraAuthorization;
234
- declare function isCameraAuthorizationCurrent(
235
- authorizationArg: ICameraAuthorization,
236
- operationArg: 'open' | 'message' | 'close' | 'reject',
237
- ): boolean | Promise<boolean>;
238
-
239
- const server = TypedSocket.createServer(typedRouter, {
240
- nativeMessageCapabilityMode: 'required',
241
- nativeMessageAuthorizationAdapter: {
242
- bind: (authorizationArg, contextArg) => {
243
- const authorization = authorizationArg as ICameraAuthorization;
244
- return {
245
- ...authorization,
246
- revalidate: async (revalidationArg) =>
247
- revalidationArg.connection.side === 'server'
248
- && revalidationArg.connection.peer === contextArg.target
249
- && await isCameraAuthorizationCurrent(
250
- authorization,
251
- revalidationArg.operation,
252
- ),
253
- };
254
- },
255
- },
256
- });
166
+ The client router handles server-initiated requests. `createClient()` resolves only after the package-major handshake, optional connection restoration, and desired-tag reconciliation succeed.
257
167
 
258
- // Inside a server TypedHandler:
259
- const target = server.getServerConnectionForRequest(typedToolsArg);
260
- const grant = server.createBinaryMessageChannel({
261
- direction: 'receive',
262
- target,
263
- authorization: cameraAuthorization,
264
- maxMessageBytes: 4 * 1024 * 1024,
265
- });
266
-
267
- // Return grant.descriptor in an application-defined response DTO.
268
- const descriptor: INativeMessageDescriptor = grant.descriptor;
269
-
270
- void (async () => {
271
- while (true) {
272
- const message = await grant.receiver.receive();
273
- if (message === undefined) break;
274
- // Process one complete logical message.
275
- }
276
- await grant.receiver.closed;
277
- })().catch((errorArg) => grant.receiver.reject(errorArg));
278
-
279
- // On the descriptor recipient:
280
- const sender = await client.nativeBytes.messages.openSender(descriptor);
281
- await sender.send(jpegBytes);
282
- await sender.close();
283
- await sender.closed;
284
- ```
168
+ ```typescript
169
+ import { TypedHandler, TypedRouter } from '@api.global/typedrequest';
170
+ import { TypedSocket } from '@api.global/typedsocket';
285
171
 
286
- `INativeMessageReceiver` deliberately exposes only `receive()`, `closed`, and
287
- `reject()`. It has no `ReadableStream`. A complete message occupies one
288
- application slot, and `MESSAGE_ACK` is queued only when `receive()` dequeues it.
289
- `send()` snapshots its `Uint8Array` at admission and remains pending until the
290
- exact logical sequence is acknowledged. Sends serialize without fragment
291
- interleaving; `close()` queues after admitted sends and waits for `CLOSE_ACK`.
292
- Empty messages are valid.
293
-
294
- The receive grant also exposes `opened`, `revoke(reason?)`, and `dispose()`.
295
- `opened` settles when the peer successfully opens the channel. Use `revoke()`
296
- to reject an advertised grant, and use `dispose()` to release a descriptor that
297
- was never published or opened.
298
-
299
- Descriptors are single-use, expire after 10 seconds, and are bound to one exact
300
- physical peer or client generation. They are recreated after reconnect. The
301
- protocol has no durability claim, content hash, FIN, reconnect resume, or
302
- automatic business idempotency. `RESET` normally closes only its channel;
303
- malformed binary framing closes the physical connection because the routing
304
- identity cannot be trusted.
305
-
306
- | Message-channel limit | Exported constant | Value |
307
- |---|---|---:|
308
- | Complete raw frame | `NATIVE_MESSAGE_MAX_FRAME_BYTES` | 32 KiB |
309
- | Fragment payload | `NATIVE_MESSAGE_MAX_FRAGMENT_PAYLOAD_BYTES` | 32,720 bytes |
310
- | Logical message package ceiling/default | `NATIVE_MESSAGE_MAX_LOGICAL_MESSAGE_BYTES` / `NATIVE_MESSAGE_DEFAULT_MAX_LOGICAL_MESSAGE_BYTES` | 4 MiB |
311
- | Pending messages / bytes per channel | `NATIVE_MESSAGE_MAX_PENDING_MESSAGES_PER_CHANNEL` / `NATIVE_MESSAGE_MAX_PENDING_BYTES_PER_CHANNEL` | 8 / 8 MiB |
312
- | Message channels per connection | `NATIVE_MESSAGE_MAX_CHANNELS_PER_CONNECTION` | 32, also subject to the shared 32 stream+channel slots |
313
- | Closed-channel tombstones | `NATIVE_MESSAGE_MAX_TOMBSTONES_PER_CONNECTION` / `NATIVE_MESSAGE_TOMBSTONE_RETENTION_MS` | 64 / 60 seconds |
314
- | Negotiation and OPEN deadlines | `NATIVE_MESSAGE_NEGOTIATION_TIMEOUT_MS` / `NATIVE_MESSAGE_OPEN_TIMEOUT_MS` | 10 seconds |
315
- | Fragment, acknowledgement, receive, close, and frame progress | `NATIVE_MESSAGE_PROGRESS_TIMEOUT_MS` | 30 seconds |
316
-
317
- Pending outbound messages, inbound reassembly/completed slots, native-byte
318
- stream queues, the raw queue, and server-retained frame/authentication state are
319
- charged to shared bounded connection and server budgets. Message authorization
320
- reuses the native transport's principal, three authority revisions, callback
321
- deadline, and per-peer/principal/server retained-revalidation caps. Revalidation
322
- callbacks receive operation and authority metadata but never message payloads or
323
- capability secrets. `nativeBytes.messages.getCapability()` and `getStats()`
324
- likewise expose only bounded counters and status.
325
-
326
- #### Preferred VirtualStream Facade
327
-
328
- For bounded byte payloads, application code uses
329
- `TypedSocket.createVirtualStream()` and passes the returned
330
- `VirtualStream<Uint8Array>` facade in its typed DTO. TypedRequest transfers the
331
- opaque transport descriptor automatically. The receiving application drains
332
- and durably commits the stream before confirmation; the sender sees only the
333
- reversed `send` facade and its durable completion receipt.
172
+ const clientRouter = new TypedRouter();
334
173
 
335
- ```typescript
336
- import * as typedrequest from '@api.global/typedrequest';
337
- import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
338
- import { SmartServe } from '@push.rocks/smartserve';
339
- import { createSha256Hasher } from '@push.rocks/smarthash/web';
340
- import {
341
- TypedSocket,
342
- type TNativeByteAuthorityOperation,
343
- } from '@api.global/typedsocket';
344
-
345
- interface IUploadRequest extends typedrequestInterfaces.implementsTR<
346
- typedrequestInterfaces.ITypedRequest,
347
- IUploadRequest
348
- > {
349
- method: 'uploadBytes';
350
- request: {
351
- uploadId: string;
352
- byteLength: number;
353
- sha256: string;
354
- contentType: string;
355
- };
356
- response: {
357
- stream: typedrequestInterfaces.INativeByteVirtualStream<Uint8Array>;
358
- };
359
- }
174
+ clientRouter.addTypedHandler(
175
+ new TypedHandler<IGreetRequest>('greet', async ({ name }) => ({
176
+ message: `Hello from the client, ${name}!`,
177
+ })),
178
+ );
360
179
 
361
- interface IUploadAuthorization {
362
- uploadId: string;
363
- principalId: string;
364
- credentialRevision: string;
365
- configRevision: string;
366
- bindingRevision: string;
367
- }
180
+ const client = await TypedSocket.createClient(
181
+ clientRouter,
182
+ 'https://api.example.com',
183
+ {
184
+ autoReconnect: true,
185
+ maxRetries: 20,
186
+ initialBackoffMs: 1_000,
187
+ maxBackoffMs: 30_000,
188
+ },
189
+ );
368
190
 
369
- interface IDurableUpload {
370
- writable: WritableStream<Uint8Array>;
371
- commit(): Promise<void>;
372
- abort(reasonArg: unknown): Promise<void>;
373
- }
191
+ const response = await client
192
+ .createTypedRequest<IGreetRequest>('greet')
193
+ .fire({ name: 'Ada' });
194
+ ```
374
195
 
375
- // These hooks belong to the application and its durable storage layer.
376
- declare function bindUploadAuthorization(uploadIdArg: string): IUploadAuthorization;
377
- declare function revalidateUpload(
378
- authorizationArg: IUploadAuthorization,
379
- manifestArg: typedrequestInterfaces.INativeByteStreamManifest,
380
- operationArg: TNativeByteAuthorityOperation,
381
- ): Promise<boolean>;
382
- declare function openDurableUpload(uploadIdArg: string): Promise<IDurableUpload>;
383
-
384
- function parseUploadAuthorization(valueArg: unknown): IUploadAuthorization {
385
- if (typeof valueArg !== 'object' || valueArg === null) {
386
- throw new Error('Upload authorization is invalid');
387
- }
388
- const uploadId = Reflect.get(valueArg, 'uploadId');
389
- const principalId = Reflect.get(valueArg, 'principalId');
390
- const credentialRevision = Reflect.get(valueArg, 'credentialRevision');
391
- const configRevision = Reflect.get(valueArg, 'configRevision');
392
- const bindingRevision = Reflect.get(valueArg, 'bindingRevision');
393
- if (
394
- typeof uploadId !== 'string'
395
- || typeof principalId !== 'string'
396
- || typeof credentialRevision !== 'string'
397
- || typeof configRevision !== 'string'
398
- || typeof bindingRevision !== 'string'
399
- ) {
400
- throw new Error('Upload authorization is invalid');
401
- }
402
- return { uploadId, principalId, credentialRevision, configRevision, bindingRevision };
403
- }
196
+ Use `TypedSocket.useWindowLocationOriginUrl()` for same-origin browser connections. Remote connections must use `https:` or `wss:`. Plain `http:` and `ws:` are restricted to loopback hosts. URLs containing credentials or fragments are rejected, and lifecycle logs redact paths and query strings.
404
197
 
405
- const serverRouter = new typedrequest.TypedRouter();
406
- let typedSocketServer!: TypedSocket;
407
-
408
- serverRouter.addTypedHandler(new typedrequest.TypedHandler<IUploadRequest>(
409
- 'uploadBytes',
410
- async (requestArg, metaArg) => {
411
- const authorization = bindUploadAuthorization(requestArg.uploadId);
412
- const connection = typedSocketServer.getServerConnectionForRequest(metaArg);
413
- const stream = typedSocketServer.createVirtualStream({
414
- protocol: 'native-byte-v1',
415
- direction: 'receive',
416
- target: connection,
417
- byteLength: requestArg.byteLength,
418
- sha256: requestArg.sha256,
419
- contentType: requestArg.contentType,
420
- authorization,
421
- });
198
+ ### Restoring authenticated connection state
422
199
 
423
- void (async () => {
424
- let durableUpload: IDurableUpload | undefined;
425
- try {
426
- durableUpload = await openDurableUpload(authorization.uploadId);
427
- await stream.writeToWebstream(durableUpload.writable);
428
- await durableUpload.commit();
429
- await stream.confirmDurable();
430
- } catch (errorArg) {
431
- await durableUpload?.abort(errorArg).catch(() => {});
432
- await stream.reject(errorArg).catch(() => {});
433
- }
434
- })();
200
+ `restoreConnection` runs after the version handshake and before tags or readiness. Its request factory is deadline-bound and becomes invalid when the callback finishes:
435
201
 
436
- return { stream };
437
- },
438
- ));
202
+ ```typescript
203
+ declare const serverUrl: string;
204
+ declare const currentSessionToken: string;
439
205
 
440
- typedSocketServer = TypedSocket.createServer(serverRouter, {
441
- nativeByteCapabilityMode: 'required',
442
- nativeByteAuthorizationAdapter: {
443
- bind: (authorizationArg, contextArg) => {
444
- const authorization = parseUploadAuthorization(authorizationArg);
445
- return {
446
- principalId: authorization.principalId,
447
- credentialRevision: authorization.credentialRevision,
448
- configRevision: authorization.configRevision,
449
- bindingRevision: authorization.bindingRevision,
450
- revalidate: async (revalidationArg) =>
451
- revalidationArg.connection.side === 'server'
452
- && revalidationArg.connection.peer === contextArg.target
453
- && await revalidateUpload(
454
- authorization,
455
- contextArg.manifest,
456
- revalidationArg.operation,
457
- ),
458
- };
459
- },
206
+ const client = await TypedSocket.createClient(clientRouter, serverUrl, {
207
+ restoreConnection: async ({ createTypedRequest, abortSignal }) => {
208
+ if (abortSignal.aborted) return;
209
+ await createTypedRequest<IRestoreSessionRequest>('restoreSession').fire(
210
+ { token: currentSessionToken },
211
+ );
460
212
  },
461
213
  });
214
+ ```
462
215
 
463
- const smartServe = new SmartServe({
464
- port: 3000,
465
- websocket: {
466
- typedRouter: serverRouter,
467
- transportOwner: typedSocketServer.webSocketTransportOwner,
468
- },
216
+ A `TypedSocketHandshakeError` is terminal for that client startup. A package-major mismatch, malformed handshake envelope, handshake timeout, or binary frame before handshake completion closes the connection instead of falling back to a reduced transport.
217
+
218
+ ## Explicit server targets
219
+
220
+ Client requests target their server implicitly because the client owns one current physical connection. Server-initiated requests always require an explicit `ISmartServeConnectionWrapper`:
221
+
222
+ ```typescript
223
+ const target = await typedSocket.findTargetConnectionByTag('account', {
224
+ accountId: 'account-123',
469
225
  });
470
- typedSocketServer.attachSmartServe(smartServe);
471
- await smartServe.start();
472
226
 
473
- const client = await TypedSocket.createClient(
474
- new typedrequest.TypedRouter(),
475
- 'http://127.0.0.1:3000',
476
- { nativeByteCapabilityMode: 'required', autoReconnect: false },
477
- );
478
- try {
479
- const payload = new TextEncoder().encode('authorized payload');
480
- const sha256 = `sha256:${createSha256Hasher().update(payload).digest()}`;
481
- const response = await client.createTypedRequest<IUploadRequest>('uploadBytes').fire({
482
- uploadId: 'upload-1',
483
- byteLength: payload.byteLength,
484
- sha256,
485
- contentType: 'text/plain',
486
- });
487
- await response.stream.readFromWebstream(new ReadableStream<Uint8Array>({
488
- start: (controllerArg) => {
489
- controllerArg.enqueue(payload);
490
- controllerArg.close();
491
- },
492
- }));
493
- const receipt = await response.stream.completion;
494
- if (receipt.sha256 !== sha256 || !receipt.durable) {
495
- throw new Error('Upload receipt did not match the authorized manifest');
496
- }
497
- } finally {
498
- await client.stop();
499
- await typedSocketServer.stop();
500
- await smartServe.stop();
227
+ if (target) {
228
+ const response = await typedSocket
229
+ .createTypedRequest<IGreetRequest>('greet', target, {
230
+ timeoutMs: 15_000,
231
+ })
232
+ .fire({ name: 'server push' });
501
233
  }
502
234
  ```
503
235
 
504
- `nativeByteAuthorizationAdapter.bind()` is synchronous so descriptor publication
505
- cannot race authority capture. Its `revalidate()` callback may be asynchronous;
506
- OPEN, DATA, FIN, and durable confirmation are followed by an exact
507
- peer/generation ownership check. Rejection attempts one final best-effort
508
- revalidation before local cleanup and RESET.
509
-
510
- #### Advanced Transport API
511
-
512
- `nativeBytes.createReceiveGrant()`, `nativeBytes.openSender()`, and opaque native
513
- descriptors are protocol-integration APIs. Normal application DTOs should carry
514
- the `VirtualStream` facade shown above, not a descriptor. Transport integrations
515
- that cannot use the facade must still provide a non-negative safe-integer
516
- `byteLength`, exact `sha256:<64 lowercase hex>`, and a normalized media-type
517
- `contentType` of at most 255 UTF-8 bytes (`NATIVE_BYTE_MAX_CONTENT_TYPE_BYTES`).
518
- They must also provide all three
519
- nonempty authority revisions and mandatory `revalidate(context)`.
520
-
521
- The application must drain the stream, commit it durably, and only then call
522
- `confirmDurable()`. The transport can enforce validated FIN, complete drain, and
523
- explicit confirmation, but the application owns the storage durability claim.
524
- `FIN_ACK` is not emitted before confirmation. The sender's `close()` and
525
- `completion` remain pending until then.
526
- Accepted nonempty DATA calls are copied and admitted against the authorized
527
- length and queue limits before asynchronous execution, so caller mutation,
528
- large backing buffers, and non-awaited calls cannot escape transport accounting.
529
-
530
- Native byte transport deliberately provides no business idempotency, reconnect
531
- resume, HTTP fallback, or legacy-byte fallback. A disconnect or generation
532
- change fails all affected grants and streams. Capabilities and stream IDs are
533
- one-use on one exact physical peer.
534
-
535
- #### Legacy VirtualStream Removal
536
-
537
- TypedSocket 6 unconditionally rejects generic `##VirtualStream##` descriptors and
538
- control requests. There is no client option, server option, fallback, or migration
539
- opt-in. Byte DTOs must use the exact native facade returned by
540
- `TypedSocket.createVirtualStream()`.
541
-
542
- #### Native Limits
543
-
544
- | Limit | Exported constant | Value |
545
- |---|---|---:|
546
- | Complete binary message | `NATIVE_BYTE_MAX_FRAME_BYTES` | 32 KiB |
547
- | DATA payload per message | `NATIVE_BYTE_MAX_DATA_PAYLOAD_BYTES` | 32,720 bytes |
548
- | Maximum stream length | `NATIVE_BYTE_MAX_STREAM_BYTES` | 140,531,329,925,120 bytes (127.8125 TiB) |
549
- | Default receive window | `NATIVE_BYTE_DEFAULT_INITIAL_WINDOW_BYTES` | 256 KiB |
550
- | Maximum receive window / queued payload per stream | `NATIVE_BYTE_MAX_WINDOW_BYTES` / `NATIVE_BYTE_MAX_QUEUED_PAYLOAD_BYTES_PER_STREAM` | 1 MiB |
551
- | Queued payload per connection | `NATIVE_BYTE_MAX_QUEUED_PAYLOAD_BYTES_PER_CONNECTION` | 8 MiB |
552
- | Queued receive chunks per stream | `NATIVE_BYTE_MAX_RECEIVE_QUEUE_CHUNKS` | 4,096 |
553
- | Native-byte grants/streams plus native-message grants/channels per connection | `NATIVE_BYTE_MAX_STREAMS_PER_CONNECTION` | 32 shared slots |
554
- | Pending admitted DATA operations per connection | `NATIVE_BYTE_MAX_PENDING_DATA_OPERATIONS_PER_CONNECTION` | 64 |
555
- | Raw inbound queue | `NATIVE_BYTE_MAX_RAW_QUEUE_FRAMES` / `NATIVE_BYTE_MAX_RAW_QUEUE_BYTES` | 64 frames / 2 MiB |
556
- | Grant and OPEN timeout | `NATIVE_BYTE_GRANT_OPEN_TIMEOUT_MS` | 10 seconds |
557
- | Progress, ACK, frame settlement, FIN_ACK, durable confirmation | `NATIVE_BYTE_PROGRESS_TIMEOUT_MS` | 30 seconds |
558
- | Closed-stream tombstones | `NATIVE_BYTE_MAX_TOMBSTONES` / `NATIVE_BYTE_TOMBSTONE_RETENTION_MS` | 64, oldest-first, 60 seconds |
559
- | Principal and each authority revision | `NATIVE_BYTE_MAX_PRINCIPAL_ID_BYTES` / `NATIVE_BYTE_MAX_AUTHORITY_REVISION_BYTES` | 256 UTF-8 bytes |
560
- | Retained revalidation callbacks | `NATIVE_BYTE_MAX_REVALIDATIONS_PER_PEER` / `_PER_PRINCIPAL` / `_PER_SERVER` | 4 per peer / 16 per principal / 128 per server |
561
- | Server connections / native-byte and native-message endpoints | `NATIVE_BYTE_MAX_CONNECTIONS_PER_SERVER` / `NATIVE_BYTE_MAX_STREAMS_PER_SERVER` | 1,024 / 1,024 shared slots |
562
- | Server retained bytes / receive reservations | `NATIVE_BYTE_MAX_RETAINED_BYTES_PER_SERVER` | 64 MiB / 64 MiB |
563
-
564
- SmartServe owner sends prioritize control frames over DATA and pull one binary
565
- frame per requested turn. Browser clients likewise send one binary frame per
566
- macrotask, allowing direct JSON text traffic to run before the next binary turn.
567
- Server frame accounting remains retained after stream or connection cleanup
568
- until SmartServe settles the exact frame object returned by `pullBinaryFrame()`;
569
- late, cloned, or duplicate callbacks cannot settle newer work. Tombstone
570
- admission evicts oldest entries first and closes the connection if the fixed
571
- retained-byte budget still cannot hold the required replay fence.
572
- `getStats()` exposes connection/negotiation status, counts, and byte totals;
573
- descriptors, capability tokens, digests, and payloads are not included.
574
-
575
- ### Client Setup
576
-
577
- Connect to the WebSocket server from a client:
236
+ Inside a server handler, bind follow-up work to the request's exact trusted peer:
578
237
 
579
238
  ```typescript
580
- import { TypedSocket } from '@api.global/typedsocket';
581
- import * as typedrequest from '@api.global/typedrequest';
582
-
583
- // Create a router for handling server-initiated requests (if needed)
584
- const clientRouter = new typedrequest.TypedRouter();
585
-
586
- // Connect to the server
587
- const client = await TypedSocket.createClient(
588
- clientRouter,
589
- 'http://localhost:3000'
239
+ applicationRouter.addTypedHandler(
240
+ new TypedHandler<IGreetRequest>('greet', async ({ name }, tools) => {
241
+ const target = typedSocket.getServerConnectionForRequest(tools);
242
+ typedSocket.setServerTag(target, 'authenticated', { subject: 'user-123' });
243
+ return { message: `Hello, ${name}!` };
244
+ }),
590
245
  );
591
246
  ```
592
247
 
593
- Remote connections must use `https:` or `wss:`. Plain `http:` and `ws:` are
594
- accepted only for loopback hosts. Credentials and URL fragments are rejected;
595
- connection logs include only the protocol and authority, never paths or queries.
248
+ `findTargetConnection()`, `findAllTargetConnections()`, and their tag variants return only live peers attached to this TypedSocket's generated routing surfaces. There is no implicit single-peer server fallback in v7.
596
249
 
597
- Client options can lower, but never raise, package ceilings for text-frame bytes,
598
- queued text frames/bytes, concurrent handlers, retained callbacks, pending
599
- requests, outbound WebSocket buffering, request timeouts, and connection
600
- restoration timeouts. `restoreConnection(context)` runs after capability
601
- negotiation and before desired tags are reconciled or `connected` is published.
602
- Its `context.createTypedRequest<T>(method)` uses the new physical connection and
603
- inherits the restoration abort signal and remaining deadline. The factory is
604
- invalidated when restoration succeeds, fails, times out, or is aborted, so it
605
- must not be retained for later application traffic.
250
+ ## VirtualStreams
251
+
252
+ TypedSocket 7 supplies TypedRequest 7's `IVirtualStreamTransport` for each handshake-ready physical peer. TypedRequest serializes only the JSON-compatible descriptor in the parent envelope; ordered `Uint8Array` chunks travel as bounded binary frames on that exact peer.
253
+
254
+ All stream facades expose `protocol`, `direction`, `streamId`, optional `contentType` and `integrity`, `opened`, `completion`, `closed`, and `abort()`. Senders add `send()`, `writable`, and `close()`. Receivers add `receive()`, `readable`, `accept()`, and `reject()`.
255
+
256
+ `receive()` returns one complete logical chunk at a time and `undefined` at graceful EOF. The receiver must call `accept()` after draining EOF. `completion` resolves with the shared acceptance receipt; abnormal termination rejects it. Direct `receive()` and `readable` consumption are mutually exclusive.
257
+
258
+ ### Client-created streams with manager registrations
259
+
260
+ Application-level client streams use the advanced manager registration API, then bind the registration to TypedRequest's public facade:
606
261
 
607
262
  ```typescript
608
- import * as typedrequestInterfaces from '@api.global/typedrequest-interfaces';
263
+ import { VirtualStream } from '@api.global/typedrequest';
609
264
 
610
- interface IRestoreSessionRequest extends typedrequestInterfaces.implementsTR<
611
- typedrequestInterfaces.ITypedRequest,
612
- IRestoreSessionRequest
613
- > {
614
- method: 'restoreSession';
615
- request: { token: string; connectionId: string };
616
- response: { restored: true };
265
+ const transport = client.virtualStreams.getClientTransport();
266
+ if (!transport) {
267
+ throw new Error('TypedSocket client transport is not connected');
617
268
  }
618
269
 
619
- const client = await TypedSocket.createClient(clientRouter, serverUrl, {
620
- autoReconnect: true,
621
- restoreConnection: async ({
622
- connectionId,
623
- abortSignal,
624
- deadline,
625
- createTypedRequest,
626
- }) => {
627
- if (abortSignal.aborted || Date.now() >= deadline) {
628
- throw new Error('Connection restoration expired');
629
- }
630
- await createTypedRequest<IRestoreSessionRequest>('restoreSession').fire({
631
- token: sessionToken,
632
- connectionId,
633
- });
634
- },
270
+ const registration = client.virtualStreams.createRegistration({
271
+ creatorDirection: 'send',
272
+ contentType: 'application/octet-stream',
635
273
  });
636
- ```
637
274
 
638
- | Client limit | Exported constant | Package ceiling |
639
- |---|---|---:|
640
- | Complete text frame | `TYPEDSOCKET_MAX_TEXT_FRAME_BYTES` | 16 MiB |
641
- | Queued text frames / bytes | `TYPEDSOCKET_MAX_QUEUED_TEXT_FRAMES` / `TYPEDSOCKET_MAX_QUEUED_TEXT_BYTES` | 64 / 32 MiB |
642
- | Concurrent handlers / retained callbacks | `TYPEDSOCKET_MAX_CONCURRENT_CLIENT_HANDLERS` / `TYPEDSOCKET_MAX_RETAINED_CLIENT_CALLBACKS` | 16 / 64 |
643
- | Pending client requests | `TYPEDSOCKET_MAX_PENDING_CLIENT_REQUESTS` | 1,024 |
644
- | Outbound WebSocket buffered bytes | `TYPEDSOCKET_MAX_OUTBOUND_BUFFERED_BYTES` | 32 MiB |
645
- | Method name / correlation ID | `TYPEDSOCKET_MAX_METHOD_NAME_BYTES` / `TYPEDSOCKET_MAX_CORRELATION_ID_BYTES` | 256 UTF-8 bytes each |
646
- | Request timeout | `TYPEDSOCKET_MAX_REQUEST_TIMEOUT_MS` | 5 minutes |
647
- | Connection restoration timeout | `TYPEDSOCKET_MAX_CONNECTION_RESTORE_TIMEOUT_MS` | 10 seconds |
648
- | Reconnect attempts | `TYPEDSOCKET_MAX_RECONNECT_RETRIES` | 100 |
649
- | Initial / maximum reconnect backoff | `TYPEDSOCKET_MAX_RECONNECT_BACKOFF_MS` | 60 seconds |
650
-
651
- `maxRetries` is a non-negative safe integer no greater than 100.
652
- `initialBackoffMs` and `maxBackoffMs` are positive safe integers no greater
653
- than 60,000, and the initial value cannot exceed the maximum.
654
-
655
- Every ceiling in this document is exported as a named constant from the package
656
- root, so consumers can compare against the canonical value instead of
657
- hardcoding numbers. The limits philosophy is uniform and deliberate:
658
- configuration may lower a ceiling, but nothing may raise one — there is no
659
- unsafe override option. When a legitimate use case outgrows a ceiling, the
660
- constant is raised in a reviewed package release, not by per-deployment
661
- configuration.
662
-
663
- #### Abortable Startup
664
-
665
- Pass an `AbortSignal` when startup or reconnect attempts must be cancellable. Aborting stops the in-flight WebSocket and prevents queued reconnect attempts from continuing.
275
+ const stream = VirtualStream.fromRegistration({
276
+ transport,
277
+ registration,
278
+ });
666
279
 
667
- ```typescript
668
- const abortController = new AbortController();
280
+ const request = client.createTypedRequest<IUploadRequest>('upload');
281
+ const responsePromise = request.fire({ stream });
669
282
 
670
- const clientPromise = TypedSocket.createClient(
671
- clientRouter,
672
- 'http://localhost:3000',
673
- {
674
- abortSignal: abortController.signal,
675
- initialBackoffMs: 1000,
676
- maxRetries: 10,
677
- }
678
- );
283
+ await stream.opened;
284
+ await stream.send(new Uint8Array([1, 2, 3]));
285
+ await stream.close();
679
286
 
680
- // Later, if the connection attempt should no longer continue:
681
- abortController.abort();
682
-
683
- try {
684
- const client = await clientPromise;
685
- } catch (error) {
686
- // Startup was aborted before a stable connection was established.
687
- }
287
+ const response = await responsePromise;
688
288
  ```
689
289
 
690
- #### Using Window Location (Browser)
290
+ Client registrations do not take a peer target: the manager binds them to the current handshake-ready client generation. Registration is synchronous and silent. Its descriptor capability expires if it is not consumed, and TypedRequest owns disposal after the facade is created. Do not hand-build descriptors or reuse them across connections.
691
291
 
692
- In browser environments, you can automatically use the current page's origin:
292
+ The matching server handler receives a requester-local `send` stream as local `receive`:
693
293
 
694
294
  ```typescript
695
- const client = await TypedSocket.createClient(
696
- clientRouter,
697
- TypedSocket.useWindowLocationOriginUrl()
295
+ applicationRouter.addTypedHandler(
296
+ new TypedHandler<IUploadRequest>('upload', async ({ stream }) => {
297
+ let storedBytes = 0;
298
+ while (true) {
299
+ const chunk = await stream.receive();
300
+ if (chunk === undefined) break;
301
+ storedBytes += chunk.byteLength;
302
+ }
303
+ await stream.accept();
304
+ return { storedBytes };
305
+ }),
698
306
  );
699
307
  ```
700
308
 
701
- ### Sending Requests
309
+ ### Server-created streams and the authorization facade
702
310
 
703
- #### Client to Server
311
+ Server application code should create streams through `TypedSocket.createVirtualStream()`. This facade requires an exact attached target and a configured `virtualStreamAuthorizationAdapter`; it synchronously binds application authorization before publishing a descriptor.
704
312
 
705
313
  ```typescript
706
- const request = client.createTypedRequest<IGreetingRequest>('greet');
707
- const response = await request.fire({
708
- name: 'World',
709
- });
710
-
711
- console.log(response.message); // "Hello, World! 👋"
712
- ```
314
+ interface IStreamAuthorization {
315
+ subject: string;
316
+ objectId: string;
317
+ revision: string;
318
+ }
713
319
 
714
- #### Server to Client
320
+ declare function isStreamAuthorityCurrent(
321
+ authority: IStreamAuthorization,
322
+ operation: 'open' | 'chunk' | 'accept' | 'reject',
323
+ ): Promise<boolean>;
715
324
 
716
- The server can also initiate requests to connected clients. Always pass the
717
- target connection explicitly:
325
+ const typedSocket = TypedSocket.createServer(applicationRouter, {
326
+ virtualStreamAuthorizationAdapter: {
327
+ bind: (authorization, context) => {
328
+ const authority = authorization as IStreamAuthorization;
329
+ if (!authority.subject || !authority.objectId || !authority.revision) {
330
+ throw new Error('Invalid stream authorization');
331
+ }
332
+ const target = context.target;
718
333
 
719
- ```typescript
720
- const connection = await server.findTargetConnection(async (conn) => {
721
- // Your filter logic here
722
- return true;
334
+ return {
335
+ revalidate: async ({ operation, connection, abortSignal }) => {
336
+ if (
337
+ abortSignal.aborted
338
+ || connection.side !== 'server'
339
+ || connection.peer !== target
340
+ ) return false;
341
+ return await isStreamAuthorityCurrent(authority, operation);
342
+ },
343
+ };
344
+ },
345
+ },
723
346
  });
724
- const targetedRequest = server.createTypedRequest<IGreetingRequest>('greet', connection);
725
- const response = await targetedRequest.fire({ name: 'Client' });
726
347
  ```
727
348
 
728
- > **Deprecated:** omitting the target on a server-side `createTypedRequest()`
729
- > auto-selects the connection only while exactly one client is attached, and
730
- > throws as soon as a second client connects. It also silently disables
731
- > native-byte transport for that request, because no peer was known when the
732
- > request object was created. Each implicit resolution emits an
733
- > `implicitTargetingUsed` diagnostic (once per request object); the implicit
734
- > path will be removed in the next major version.
735
-
736
- #### Request Deadlines and Cancellation
737
-
738
- TypedSocket forwards both configured request cancellation and per-`fire()` deadlines to its client
739
- and SmartServe server transports. The first timeout or abort to occur cancels the transport work and
740
- cleans the pending request state.
349
+ `bind()` must return synchronously and must provide `revalidate(context)`. Revalidation runs with the exact connection binding, operation (`open`, `chunk`, `accept`, or `reject`), deadline, and abort signal. Return literal `true` only while the application authority remains current.
741
350
 
742
351
  ```typescript
743
- const requestAbort = new AbortController();
744
- const request = client.createTypedRequest<IGreetingRequest>(
745
- 'greet',
746
- undefined,
747
- {
748
- timeoutMs: 10_000,
749
- abortSignal: requestAbort.signal,
750
- }
751
- );
352
+ declare function loadBoundedObjectChunks(
353
+ objectId: string,
354
+ ): AsyncIterable<Uint8Array>;
355
+
356
+ applicationRouter.addTypedHandler(
357
+ new TypedHandler<IDownloadRequest>('download', async ({ objectId }, tools) => {
358
+ const target = typedSocket.getServerConnectionForRequest(tools);
359
+ const stream = typedSocket.createVirtualStream({
360
+ target,
361
+ creatorDirection: 'send',
362
+ contentType: 'application/octet-stream',
363
+ authorization: {
364
+ subject: 'user-123',
365
+ objectId,
366
+ revision: 'revision-7',
367
+ } satisfies IStreamAuthorization,
368
+ });
752
369
 
753
- const responsePromise = request.fire(
754
- { name: 'World' },
755
- { timeoutMs: 3_000 }
756
- );
370
+ const production = (async () => {
371
+ await stream.opened;
372
+ for await (const chunk of loadBoundedObjectChunks(objectId)) {
373
+ await stream.send(chunk);
374
+ }
375
+ await stream.close();
376
+ })();
377
+ void production.catch((error) => stream.abort(error).catch(() => undefined));
757
378
 
758
- // A lifecycle owner can independently cancel before either timeout:
759
- // requestAbort.abort();
760
- const response = await responsePromise;
379
+ return { stream };
380
+ }),
381
+ );
761
382
  ```
762
383
 
763
- Server-initiated requests retain at most 64 pending requests per peer and 1,024
764
- per TypedSocket server (`TYPEDSOCKET_MAX_PENDING_SERVER_REQUESTS_PER_PEER`,
765
- `TYPEDSOCKET_MAX_PENDING_SERVER_REQUESTS`). Asynchronous `addInterest()`
766
- registration retains at most 8 operations per peer and 64 per server
767
- (`TYPEDSOCKET_MAX_RETAINED_SERVER_INTERESTS_PER_PEER`,
768
- `TYPEDSOCKET_MAX_RETAINED_SERVER_INTERESTS`). Cancellation removes the pending
769
- request immediately, while a non-settling registration remains charged until
770
- its underlying promise actually settles.
384
+ Finite streams may include `{ algorithm: 'sha256', byteLength, digest }` integrity metadata. Open-ended streams omit integrity. Capabilities are opaque, single-use, peer-scoped, generation-scoped, and short-lived.
771
385
 
772
- ### Connection Tagging
386
+ ## Connection tags
773
387
 
774
- Client tag mutation is disabled by default. A server must opt in each exact name, choose whether
775
- an accepted proposal remains client-owned or becomes server-owned, and validate both payload shape
776
- and connection authorization inside the private protocol boundary.
388
+ Client tag mutation is default-deny. Declare exact rules on the server:
777
389
 
778
390
  ```typescript
779
- interface IProgressTag extends typedrequestInterfaces.ITag {
780
- name: 'progressSubscription';
781
- payload: { channel: 'scan-progress' };
782
- }
783
-
784
- declare const authenticatedAuthorities: ReadonlySet<string>;
785
-
786
- const server = TypedSocket.createServer(typedRouter, {
391
+ const typedSocket = TypedSocket.createServer(applicationRouter, {
787
392
  clientTagPolicy: {
393
+ authorizationTimeoutMs: 2_000,
788
394
  rules: [{
789
- name: 'progressSubscription',
395
+ name: 'workspace',
790
396
  owner: 'client',
791
- validateAndAuthorize: ({ operation, payload, authority, abortSignal }) => {
792
- // Both IDs are opaque exact-object identities. The callback receives no
793
- // mutable peer.tags, peer.data, peer, or router object access.
794
- const authorityKey = `${authority.connectionId}:${authority.routingSurfaceId}`;
795
- if (abortSignal.aborted || !authenticatedAuthorities.has(authorityKey)) return false;
796
- return operation === 'remove'
797
- || (
798
- typeof payload === 'object'
799
- && payload !== null
800
- && Reflect.get(payload, 'channel') === 'scan-progress'
801
- );
397
+ validateAndAuthorize: ({ payload, operation, abortSignal }) => {
398
+ if (abortSignal.aborted) return false;
399
+ if (operation === 'remove') return true;
400
+ return typeof payload === 'object'
401
+ && payload !== null
402
+ && typeof Reflect.get(payload, 'workspaceId') === 'string';
802
403
  },
803
404
  }],
804
405
  },
805
406
  });
806
-
807
- await client.setTag<IProgressTag>(
808
- 'progressSubscription',
809
- { channel: 'scan-progress' },
810
- );
811
-
812
- // On reconnect, desired tags are reconciled before statusSubject emits
813
- // "connected". Acknowledgements are scoped to one physical generation.
814
-
815
- // A policy denial discards only that FIFO intent and recomputes desired state
816
- // from any later pending intent or the stable baseline established by earlier
817
- // settlements. Transport failure retains the canonical desired removal tombstone;
818
- // accepted removal, including successful reconnect replay, clears it.
819
- await client.removeTag('progressSubscription');
820
407
  ```
821
408
 
822
409
  ```typescript
823
- const progressConnections = await server.findAllTargetConnectionsByTag<IProgressTag>(
824
- 'progressSubscription',
825
- { channel: 'scan-progress' }
826
- );
827
-
828
- for (const conn of progressConnections) {
829
- const request = server.createTypedRequest<IGreetingRequest>('greet', conn);
830
- await request.fire({ name: 'subscriber' });
831
- }
410
+ await client.setTag('workspace', { workspaceId: 'workspace-123' });
411
+ await client.removeTag('workspace');
832
412
  ```
833
413
 
834
- Connection wrappers deliberately expose the underlying transport peer as
835
- `connection.peer` — this is the supported escape hatch for admission-time
836
- metadata in selection predicates, for example reading
837
- `connection.peer.context.headers` or server-owned `connection.peer.context.state`
838
- inside `findAllTargetConnections()`. Reading through `peer` is fine; mutating
839
- `peer.tags` or TypedSocket's prefixed `peer.data` entries is not — ownership of
840
- those is reconciled by the protected server tag methods.
414
+ Use `setServerTag()` and `removeServerTag()` for authentication, roles, registration state, and other server-owned metadata. A server-owned name remains protected from client overwrite after removal. Desired client tags are reconciled after reconnect only after `restoreConnection` succeeds.
841
415
 
842
- Authentication, roles, service registration, and other authoritative metadata must be assigned by
843
- the server after application-level verification:
416
+ Do not use a universal `allClients` broadcast tag. Assign a dedicated application tag and target only clients that implement the corresponding server-initiated method.
844
417
 
845
- ```typescript
846
- const connection = server.getServerConnectionForRequest(typedToolsArg);
847
- server.setServerTag(connection, 'authenticatedUser', { userId });
848
-
849
- // Clients cannot set, overwrite, or remove this name. Removal remains server-owned.
850
- server.removeServerTag(connection, 'authenticatedUser');
851
- ```
418
+ ## Lifecycle, limits, and diagnostics
852
419
 
853
- Do not mutate `peer.tags` or TypedSocket's prefixed `peer.data` entries directly. The protected
854
- server methods reconcile ownership with in-flight client proposals.
420
+ - `statusSubject` publishes `new`, `connecting`, `connected`, `disconnected`, and `reconnecting` transitions.
421
+ - `diagnosticsSubject` publishes bounded structured events for invariant closes, peer rejection, reconnect scheduling or exhaustion, and tag denial. Subscribers own unsubscription; the subject does not complete.
422
+ - `stop()` disables client reconnect, rejects pending work, closes streams, and releases router registrations. Server `stop()` detaches TypedSocket state and composition but does not stop SmartServe.
423
+ - Request `timeoutMs` and `abortSignal` are supported on both sides. Server requests are cancelled on target disconnect or server stop.
424
+ - Client `limits` may lower package ceilings but cannot raise them. Untrusted network deployments should lower text-frame and queue ceilings to match the application protocol.
425
+ - The stream transport bounds connections, active streams, logical chunk size, queued chunks and bytes, raw frames, outbound frames, revalidations, arrival accounting, tombstones, capability lifetime, progress time, and cleanup time.
426
+ - Invalid framing, overflow, integrity failure, authority revocation, handshake failure, and timeout fail closed. Physical-peer identity and raw-frame settlement identity are never inferred from caller-controlled payloads.
855
427
 
856
- Fixed limits cannot be raised by policy (each is exported by name from the
857
- package root):
428
+ Selected stream defaults are 32 KiB physical frames, 4 MiB logical chunks, 32 active streams per connection, a 10-second handshake and capability deadline, a 30-second progress deadline, and a 5-second revalidation deadline. Root exports provide the principal package ceilings and timeout constants.
858
429
 
859
- - Tag name: 128 UTF-8 bytes (`TYPEDSOCKET_MAX_TAG_NAME_BYTES`).
860
- - SmartJSON payload envelope: 4,096 bytes (`TYPEDSOCKET_MAX_TAG_PAYLOAD_BYTES`).
861
- - Client-originated retained tags: 16 per peer (`TYPEDSOCKET_MAX_CLIENT_TAGS_PER_PEER`).
862
- - Client-originated cumulative name and payload storage: 16,384 bytes per peer (`TYPEDSOCKET_MAX_CLIENT_TAG_RETAINED_BYTES_PER_PEER`).
863
- - Client-side retained mutation work: 8 per exact name and 64 per client (`TYPEDSOCKET_MAX_RETAINED_CLIENT_TAG_MUTATIONS_PER_NAME`, `TYPEDSOCKET_MAX_RETAINED_CLIENT_TAG_MUTATIONS`).
864
- - Retained mutation/authorization work: 8 per peer, 32 per exact rule, and 256 per TypedSocket server (`TYPEDSOCKET_MAX_RETAINED_TAG_MUTATIONS_PER_PEER`, `_PER_RULE`, `_PER_SERVER`).
865
- - Protected server tag state: 64 names and 65,536 cumulative name/payload bytes per peer (`TYPEDSOCKET_MAX_SERVER_TAG_NAMES_PER_PEER`, `TYPEDSOCKET_MAX_SERVER_TAG_RETAINED_BYTES_PER_PEER`).
866
- - Authorization callback deadline: at most 5 seconds; policy may only lower it (`TYPEDSOCKET_MAX_TAG_AUTHORIZATION_TIMEOUT_MS`).
430
+ ## Public API summary
867
431
 
868
- Same-name mutations execute FIFO. Timed-out callbacks receive an aborted signal and remain charged
869
- against authorization budgets until they actually settle. Disconnect and server stop abort active
870
- callbacks and prevent late commits.
432
+ ### `TypedSocket`
871
433
 
872
- #### TypedServer Integration Contract
434
+ | API | Side | Purpose |
435
+ | --- | --- | --- |
436
+ | `TypedSocket.createClient(router, url, options?)` | client | Connects, handshakes, restores connection state, and reconciles tags. |
437
+ | `TypedSocket.createServer(routerOrRouters, options?)` | server | Composes private protocol and application routers before SmartServe construction. |
438
+ | `getServerRoutingSurface(applicationRouter?)` | server | Returns the exact generated router SmartServe must bind during upgrade. |
439
+ | `attachSmartServe(smartServe)` | server | Attaches lifecycle, authority guards, and peer-scoped stream resolvers. |
440
+ | `createTypedRequest(method, target?, options?)` | both | Creates a TypedRequest; server calls require an explicit target. |
441
+ | `createVirtualStream(options)` | server | Creates an exact authorized stream facade for one attached peer. |
442
+ | `getServerConnectionForRequest(tools)` | server | Resolves the exact trusted physical peer for an incoming request. |
443
+ | `setTag()` / `removeTag()` | client | Mutates an explicitly allowed client-owned tag. |
444
+ | `setServerTag()` / `removeServerTag()` | server | Maintains protected server-owned peer metadata. |
445
+ | `findTargetConnection*()` / `findAllTargetConnections*()` | server | Finds live attached targets by predicate or tag. |
446
+ | `getStatus()` | both | Returns the current connection status. |
447
+ | `stop()` | both | Releases all TypedSocket-owned lifecycle state. |
873
448
 
874
- TypedServer 9 passes `clientTagPolicy` through, exposes
875
- `webSocketTransportOwner`, resolves handler-local peers through
876
- `getServerConnectionForRequest(typedTools)`, and assigns infrastructure tags
877
- through `setServerTag()`. Authentication, roles, frontend registration, and
878
- service-worker registration are server-owned application state; v6 provides no
879
- legacy tag migration mode.
449
+ ### `virtualStreams`
880
450
 
881
- ### Event Handling
451
+ `VirtualStreamManager` is the peer-scoped transport manager. Client applications may use `getClientTransport()` and `createRegistration()` for explicit creator registrations. `getStats()` exposes bounded transport accounting. Server registration is not exposed on the manager; server applications must use the authorization-enforcing `TypedSocket.createVirtualStream()` facade.
882
452
 
883
- Client instances publish connection status events:
884
-
885
- ```typescript
886
- client.statusSubject.subscribe((status) => {
887
- console.log('Connection status:', status);
888
- });
889
- ```
890
-
891
- ### Diagnostics
892
-
893
- Both sides publish structured diagnostics on `diagnosticsSubject` — the *why*
894
- channel next to `statusSubject` (state transitions) and `nativeBytes.getStats()`
895
- (counters). Events carry package-defined static strings, bounded identifiers
896
- (tag names, method names), and bounded codes only; payloads, URLs beyond
897
- protocol//host, and free-form error messages never appear. Client-received
898
- denial codes are validated against `TYPEDSOCKET_TAG_DENIAL_CODES` before they
899
- reach the channel; anything unknown collapses to `MALFORMED_TAG_RESPONSE`.
900
-
901
- ```typescript
902
- import type { TTypedSocketDiagnosticEvent } from '@api.global/typedsocket';
903
-
904
- client.diagnosticsSubject.subscribe((event: TTypedSocketDiagnosticEvent) => {
905
- switch (event.kind) {
906
- case 'connectionClosed': // an invariant close: scope, closeCode, reason
907
- case 'peerRejected': // server-only: peer rejected before state existed
908
- case 'reconnectScheduled': // attempt, maxRetries, delayMs, endpoint
909
- case 'reconnectExhausted': // at most once per exhausted sequence
910
- case 'tagMutationDenied': // operation, tag name, denial code, side
911
- case 'implicitTargetingUsed': // server-only: deprecated implicit targeting fired
912
- console.log(event);
913
- }
914
- });
915
- ```
916
-
917
- `diagnosticsSubject` never completes, mirroring `statusSubject`; subscribers
918
- own their unsubscription. A `NativeByteManager` used standalone accepts the
919
- same sink via the `onDiagnostic` option, typed to the narrower
920
- `TNativeByteDiagnosticEvent` subset.
921
-
922
- Close codes in `connectionClosed` events are always the semantic protocol
923
- codes (1002, 1003, 1008, 1009, 1011, 1013). On the wire, client-initiated invariant
924
- closes mirror them into the application range the WebSocket `close()` API
925
- permits — 1009 becomes 4009 (`toClientWebSocketCloseCode()`); server-initiated
926
- closes keep the protocol codes.
927
-
928
- ### Cleanup
929
-
930
- Properly close connections when done:
931
-
932
- ```typescript
933
- // Client
934
- await client.stop();
935
-
936
- // Server integration
937
- await typedSocket.stop();
938
- await smartServe.stop();
939
- ```
453
+ ## Migration from version 6
940
454
 
941
- ## API Reference
942
-
943
- ### TypedSocket
944
-
945
- #### Static Methods
946
-
947
- | Method | Description |
948
- |--------|-------------|
949
- | `createClient(router, serverUrl, options?)` | Creates a WebSocket client. Options include reconnect controls, `abortSignal`, independent `nativeByteCapabilityMode` and `nativeMessageCapabilityMode`, lowering-only `limits`, and `restoreConnection`. |
950
- | `createServer(routerOrRouters, options?)` | Synchronously composes protocol handling. Options include independent native byte/message capability modes and authorization adapters, plus the default-deny `clientTagPolicy`. |
951
- | `fromSmartServe(smartServe, routerOrRouters, options?)` | Creates and attaches a JSON-only server-side TypedSocket to an existing SmartServe instance. |
952
- | `useWindowLocationOriginUrl()` | Returns the current window location origin (browser only). |
953
-
954
- #### Instance Properties
955
-
956
- | Property | Description |
957
- |----------|-------------|
958
- | `side` | Whether this instance is a `'server'` or `'client'`. |
959
- | `typedrouter` | The TypedRouter instance handling requests. |
960
- | `nativeBytes` | Native transport API. Existing stream methods stay on this object; ephemeral message channels are under `nativeBytes.messages`. |
961
- | `webSocketTransportOwner` | Stable SmartServe 4 raw-frame owner selected during SmartServe construction. |
962
- | `statusSubject` | RxJS Subject for client connection status events. Server instances do not publish lifecycle transitions here. |
963
- | `diagnosticsSubject` | RxJS Subject of structured `TTypedSocketDiagnosticEvent` values: invariant closes (including `scope: 'nativeMessage'`), peer rejections, reconnect scheduling/exhaustion, tag denials, and deprecated implicit targeting. Never completes. |
964
-
965
- #### Instance Methods
966
-
967
- | Method | Description |
968
- |--------|-------------|
969
- | `attachSmartServe(smartServe)` | Attaches one SmartServe transport to a composed server-side TypedSocket before listening. |
970
- | `createVirtualStream(options)` | Creates the preferred exact, authorized native-byte receive facade for one server peer. TypedRequest transfers its descriptor automatically. |
971
- | `createBinaryMessageChannel(options)` | Creates an authorized ephemeral binary-message receive grant and ordinary descriptor for one exact server peer. |
972
- | `createTypedRequest(method, targetConnection?, options?)` | Creates a typed request. Options include the transport `timeoutMs` and `abortSignal`; per-call `fire()` deadlines are also forwarded to the transport. |
973
- | `getServerConnectionForRequest(typedTools)` | Resolves the exact transport connection for an incoming server handler without assertions. |
974
- | `getStatus()` | Returns the client connection lifecycle status. Server instances remain in the initial `new` state. |
975
- | `setTag(name, payload)` | Requests one exact policy-authorized tag and records reconnect state only after an ordered server acknowledgement. |
976
- | `removeTag(name)` | Requests removal of a client-owned tag. Policy denial discards that FIFO intent and recomputes from any later pending intent or the stable settled baseline. Transport failure retains the canonical desired removal tombstone; accepted removal, including successful reconnect replay, clears it. |
977
- | `setServerTag(connection, name, payload?)` | Assigns protected server-owned metadata after application verification. |
978
- | `removeServerTag(connection, name)` | Removes server-owned metadata while keeping the name protected from client mutation for that peer. |
979
- | `findAllTargetConnections(filterFn)` | Finds all connections matching the filter (server-side only). |
980
- | `findTargetConnection(filterFn)` | Finds the first connection matching the filter (server-side only). |
981
- | `findAllTargetConnectionsByTag(key, payload?)` | Finds all connections with the specified tag. |
982
- | `findTargetConnectionByTag(key, payload?)` | Finds the first connection with the specified tag. |
983
- | `stop()` | On clients, closes the WebSocket and rejects pending requests. On servers, cancels pending requests, cleans their interests, unsubscribes from SmartServe, and releases protocol-router composition without stopping SmartServe itself. |
984
-
985
- #### Advanced Native Methods
986
-
987
- | Method | Description |
988
- |--------|-------------|
989
- | `nativeBytes.createReceiveGrant(options)` | Creates an explicit exact-manifest receive grant for a protocol integration. |
990
- | `nativeBytes.openSender(descriptor, options?)` | Opens an explicit sender for an opaque descriptor on the exact target connection. |
991
- | `nativeBytes.getCapability(target?)` | Reports negotiated native-byte capability without exposing tokens or descriptors. |
992
- | `nativeBytes.getStats(target?)` | Reports bounded connection, stream, queue, and tombstone counts. |
993
- | `nativeBytes.messages.createReceiveGrant(options)` | Advanced explicit receive-grant API for one ephemeral message channel. The grant exposes `opened`, `revoke(reason?)`, and `dispose()` in addition to its descriptor and receiver. |
994
- | `nativeBytes.messages.openSender(descriptor, options?)` | Opens the descriptor on the exact physical client generation or required server peer target. |
995
- | `nativeBytes.messages.getCapability(target?)` | Reports separately negotiated `native-message-v1` status. |
996
- | `nativeBytes.messages.getStats(target?)` | Reports bounded channel, pending message, reassembly, queue, and aggregate accounting. |
455
+ - Replace TypedRequest 5 and SmartServe 4 with TypedRequest 7 and SmartServe 5.1.1.
456
+ - Remove `nativeByteCapabilityMode`, `nativeMessageCapabilityMode`, `nativeBytes`, message-channel APIs, and native-specific authorization adapters.
457
+ - Replace native stream DTOs with `TVirtualStream<'send' | 'receive'>` from `@api.global/typedrequest-interfaces`.
458
+ - Replace `fromSmartServe()` with the required `createServer()` → SmartServe construction → `attachSmartServe()` order.
459
+ - Pass `getServerRoutingSurface(applicationRouter)` to SmartServe, not the application router itself.
460
+ - Always pass an explicit server target to `createTypedRequest()`.
461
+ - Configure `virtualStreamAuthorizationAdapter` and use `createVirtualStream()` for server-created streams.
462
+ - Treat a package-major handshake failure as terminal; there is no JSON-only or capability-disabled fallback.
997
463
 
998
464
  ## License and Legal Information
999
465