@api.global/typedsocket 6.2.0 → 7.0.0

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