@polytric/openws-spec 0.0.1 → 0.0.2

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,125 +1,453 @@
1
1
  # OpenWS Specification <!-- omit in toc -->
2
2
 
3
- OpenWS is a specification for describing `WebSocket-based systems` in the same way OpenAPI describes HTTP APIs.
3
+ OpenWS is a specification for describing **WebSocket-based systems** in the same way OpenAPI describes HTTP APIs. It models WebSocket communication as an **asymmetrical 2-way request-response mesh**: multiple components exchange named messages, and "responses" are simply messages sent back through the same network.
4
4
 
5
5
  An OpenWS document describes:
6
6
 
7
- - Networks
8
- - Participants (w/ named roles)
9
- - Handlers (message names)
10
- - Payload schemas
11
- - Endpoints
7
+ - **Networks**: logical WebSocket systems where `message`s are exchanged
8
+ - **Roles**: `role`-based API surfaces with statically defined `message` contracts
9
+ - **Messages**: named `message` definitions that contain a `payload`
10
+ - **Payload**: the shape of application data transmitted through WebSocket messages, defined with JSON Schema
11
+ - **Metadata & connection hints**: document metadata (title, version, description) and optional connection hints (endpoints)
12
12
 
13
- It _does not_ describe behavior or execution.
13
+ Conceptually (at runtime), we also use the following terms throughout this document:
14
14
 
15
+ - **Participants**: runtime instances/connections that assume a `role` on a `network`
16
+ - **Handlers**: runtime message-processing functions invoked when a participant receives a `message`
17
+
18
+ Participants and handlers are **not serialized** in the OpenWS JSON document. They are explanatory terms used to describe how role/message contracts are typically implemented.
19
+
20
+ This spec intentionally does **not** describe behavior or execution.
21
+
22
+ - [Conventions](#conventions)
23
+ - [Document Structure](#document-structure)
24
+ - [Ecosystem](#ecosystem)
15
25
  - [Core Concepts](#core-concepts)
16
- - [Network](#network)
17
- - [Participants](#participants)
18
- - [Endpoints](#endpoints)
19
- - [Handlers](#handlers)
20
- - [Payloads](#payloads)
21
- - [What the Spec Does NOT Do](#what-the-spec-does-not-do)
22
- - [Intended Use Cases](#intended-use-cases)
23
- - [Summary](#summary)
26
+ - [Networks](#networks)
27
+ - [Roles](#roles)
28
+ - [Messages](#messages)
29
+ - [Request/response modeling](#requestresponse-modeling)
30
+ - [Payload](#payload)
31
+ - [Metadata \& Connection Hints](#metadata--connection-hints)
32
+ - [Document metadata](#document-metadata)
33
+ - [Connection hints](#connection-hints)
34
+ - [Custom metadata and extensions](#custom-metadata-and-extensions)
35
+ - [Complete Example](#complete-example)
36
+
37
+ # Conventions
38
+
39
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in RFC 2119.
40
+
41
+ Unless otherwise noted:
42
+
43
+ - OpenWS documents are JSON objects.
44
+ - Examples are written as valid JSON (no comments, no trailing commas).
45
+ - `payload` schemas use **JSON Schema**. A specific JSON Schema dialect may be chosen by downstream tooling; OpenWS itself describes shapes rather than enforcing a single validator implementation.
46
+
47
+ > Note: The OpenWS document describes _contracts_. Runtime details such as routing, correlation, authentication, and connection lifecycle are intentionally left to runtime layers.
48
+
49
+ # Document Structure
50
+
51
+ An OpenWS document is a JSON object with the following top-level fields:
52
+
53
+ - `openws` (**REQUIRED**): the OpenWS specification version as a string.
54
+ - `title` (**OPTIONAL**): a human-readable name for the document.
55
+ - `version` (**OPTIONAL**): the version of the described system/API (not the OpenWS spec version).
56
+ - `description` (**OPTIONAL**): a longer explanation of the document.
57
+ - `networks` (**REQUIRED**): a map of network names to network definitions.
58
+
59
+ An implementation:
60
+
61
+ - **MUST** ignore unknown fields (to allow forward compatibility and extensions).
62
+ - **SHOULD** preserve unknown fields when round-tripping documents (when feasible).
63
+ - **MAY** support custom extension fields anywhere in the document (see [Metadata & Connection Hints](#metadata--connection-hints)).
64
+
65
+ Minimal skeleton:
66
+
67
+ ```json
68
+ {
69
+ "openws": "0.0.2",
70
+ "title": "My WebSocket System",
71
+ "version": "1.0.0",
72
+ "description": "Optional, human-readable description.",
73
+ "networks": {
74
+ "example": {
75
+ "roles": {
76
+ "server": {
77
+ "messages": {}
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ # Ecosystem
86
+
87
+ OpenWS is designed as a **tooling ecosystem**, similar in spirit to OpenAPI: a spec, plus runtimes, adapters, generators, and framework integrations.
88
+
89
+ This repository/spec is intended to support a full "fleet" of packages, including (but not limited to):
90
+
91
+ - **Spec & validation**
92
+ - [`@polytric/openws-spec`](https://www.npmjs.com/package/@polytric/openws-spec) - This library
93
+ - [`@polytric/fastify-openws-spec`](https://www.npmjs.com/package/@polytric/fastify-openws-spec) -- Fastify plugin to generate OpenWS spec from code
94
+
95
+ - **Runtime framework**
96
+ - [`@polytric/ws`](https://www.npmjs.com/package/@polytric/ws) -- Polytric WS framework for JavaScript.
97
+ - [`@polytric/fastify-openws`](https://www.npmjs.com/package/@polytric/fastify-openws) -- Fastify OpenWS SDK for JavaScript
24
98
 
99
+ - **SDK generation**
100
+ - [`@polytric/openws-sdkgen`](https://www.npmjs.com/package/@polytric/openws-sdkgen) -- OpenWS code generation tool with multi-language support
101
+
102
+ > The spec intentionally stays framework-agnostic; framework integrations and SDKs belong to the ecosystem layer.
25
103
 
26
104
  # Core Concepts
27
105
 
28
- ## Network
106
+ ## Networks
29
107
 
30
- A network is a logical WebSocket system, and a single server may define multiple networks.
108
+ A network is a logical WebSocket system, with multiple participants on the network forming a mesh. It is common to run multiple isolated features over WebSocket on a single host. For example, a chess game client may have a chat room, a tournament entry, and the actual chess game. They are logically separated like this in the spec:
31
109
 
32
110
  ```json
33
- "networks": {
34
- "chat": { ... },
35
- "chessGame": { ... },
36
- "ticketing": { ... },
111
+ {
112
+ "networks": {
113
+ "chat": {},
114
+ "tournament": {},
115
+ "chess": {}
116
+ }
37
117
  }
38
118
  ```
39
119
 
40
- ## Participants
120
+ > [!NOTE]
121
+ > Examples in the core concepts sections have omitted details for brevity; they are not valid standalone.
122
+ > Consult the complete example towards the bottom of the doc for a fully working example.
123
+
124
+ On the backend side, there may be a dedicated service for each of these components, so each service would typically define just one network:
41
125
 
42
- Participants take on roles in a network.
126
+ Chat service:
43
127
 
44
128
  ```json
45
- "participants": {
46
- "server": { ... },
47
- "webapp": { ... },
48
- "mobile": { ... },
49
- "admin": { ... }
129
+ {
130
+ "networks": {
131
+ "chat": {}
132
+ }
50
133
  }
51
134
  ```
52
135
 
53
- - A participant's role may represent a server, client, tool, or service
54
- - Multiple instances of a participant may exist at runtime
55
- - Participants are purely declarative
136
+ Tournament service:
56
137
 
57
- ## Endpoints
138
+ ```json
139
+ {
140
+ "networks": {
141
+ "tournament": {}
142
+ }
143
+ }
144
+ ```
58
145
 
59
- Participants in a network typically have some that sits there and listens to incoming connections requests, and the rest proactively request to connect to the aforementioned participants. The listening participants may optionally provide one or more endpoints statically, to help clients determine how to connect.
146
+ Chess service:
60
147
 
61
148
  ```json
62
- "endpoints": [
63
- { "host": "localhost", "url": "/abc", "port": 8082 }
64
- ]
149
+ {
150
+ "networks": {
151
+ "chess": {}
152
+ }
153
+ }
65
154
  ```
66
155
 
67
- ## Handlers
156
+ A network definition:
157
+
158
+ - **MUST** define `roles`.
159
+ - **MAY** include metadata such as `description`.
160
+ - **MAY** include custom extension fields.
68
161
 
69
- A handler is a named message type.
162
+ ## Roles
163
+
164
+ In the OpenWS document, we define **roles**. At runtime, a **participant** is a connected instance that assumes a `role` on a `network`.
165
+
166
+ Multiple participants of the same role may exist on the same network. For example, in a chat service, a `server` is normally connected to many `client`s. OpenWS defers modeling one-to-one, one-to-many, or many-to-many relationships to the application/runtime.
167
+
168
+ ```json
169
+ {
170
+ "networks": {
171
+ "chat": {
172
+ "roles": {
173
+ "server": {},
174
+ "client": {}
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ It is common for different components of a system to expose different APIs and provide different behaviors. This is naturally modeled by participants on the same network having different roles. Continuing the chat example, imagine we now have a mobile client, a customer web portal, and an admin console. To serve both customer and admin needs, the server is split into two separate roles, while a single backend may implement both roles. Distinct roles can be set up like this:
70
182
 
71
183
  ```json
72
- "message": {
73
- "payload": { ... },
74
- "description": { ... }
184
+ {
185
+ "roles": {
186
+ "adminServer": {},
187
+ "customerServer": {},
188
+ "mobile": {},
189
+ "portal": {},
190
+ "console": {}
191
+ }
75
192
  }
76
193
  ```
77
194
 
78
- Handlers:
195
+ IMPORTANT: this is not a role-based access control system, but the clean API boundary is intended to make layering an auth system trivial.
196
+
197
+ A role definition:
79
198
 
80
- - Are invokable by other participants
81
- - Define payload shape only
82
- - Do not define behavior
199
+ - **MUST** define `messages` (it MAY be empty).
200
+ - **MAY** include metadata such as `description` or connection hints such as `endpoints`.
201
+ - **MAY** include custom extension fields.
83
202
 
84
- ## Payloads
203
+ ## Messages
85
204
 
86
- Payloads are defined using `JSON Schema`.
205
+ In the OpenWS document, roles declare the data shapes they accept as messages. Messages are indexed by name.
206
+
207
+ Message definitions are **scoped to the receiving role**. At runtime:
208
+
209
+ - A participant receives a message by name, and dispatches it to a handler.
210
+ - A participant sends a message by targeting some other participant(s) whose role defines that message name and payload shape.
211
+
212
+ OpenWS intentionally describes **contracts**, not delivery semantics. Routing (broadcast, directed, room-based, etc.) is runtime-specific.
213
+
214
+ For example, a chat server may accept `message`, `join`, `createRoom`, while the customer portal may accept `channelStats`. The meaning of messages differs depending on perspective: the chat server accepts `join` as an incoming message and handles it to update its state, while the portal can send `join` to the server. Similarly, the portal accepts `channelStats` as an incoming message, while the server can send `channelStats` to the portal.
87
215
 
88
216
  ```json
89
- "payload": {
90
- "type": "string"
217
+ {
218
+ "roles": {
219
+ "server": {
220
+ "messages": {
221
+ "message": {},
222
+ "join": {},
223
+ "createRoom": {}
224
+ }
225
+ },
226
+ "portal": {
227
+ "messages": {
228
+ "channelStats": {}
229
+ }
230
+ }
231
+ }
91
232
  }
92
233
  ```
93
234
 
94
- Implementations must:
235
+ A message definition:
95
236
 
96
- - Validate payloads
97
- - Encode/decode according to content type
237
+ - **MUST** define `payload`.
238
+ - **MAY** include metadata such as `description`.
239
+ - **MUST** be uniquely named within its role (message names under the same role **MUST NOT** collide).
240
+ - **MAY** include custom extension fields.
98
241
 
99
- # What the Spec Does NOT Do
242
+ ### Request/response modeling
100
243
 
101
- The OpenWS spec intentionally does not define:
244
+ OpenWS supports request/response patterns, but does not mandate a single correlation mechanism. If your system uses request/response semantics, runtimes **SHOULD** define a correlation convention (for example, an ID field inside the payload or an envelope-level correlation ID).
102
245
 
103
- - Message handling logic
104
- - Connection state
105
- - Lifecycle hooks
106
- - Transport implementation details
246
+ ## Payload
107
247
 
108
- Those belong to runtime layers.
248
+ Each message declares its payload shape using JSON Schema:
109
249
 
110
- # Intended Use Cases
250
+ ```json
251
+ {
252
+ "message": {
253
+ "payload": {
254
+ "type": "string",
255
+ "minLength": 1,
256
+ "maxLength": 2048
257
+ }
258
+ }
259
+ }
260
+ ```
111
261
 
112
- - Runtime enforcement
113
- - SDK / client generation
114
- - Documentation
115
- - Cross-language interoperability
116
- - Static analysis
262
+ Payload schemas:
117
263
 
118
- # Summary
264
+ - **MUST** be valid JSON Schema objects (as interpreted by the runtime/tooling).
265
+ - **SHOULD** be compatible with your chosen JSON Schema dialect across toolchains.
266
+ - **MAY** use common JSON Schema keywords such as `type`, `properties`, `required`, `enum`, and `format`.
119
267
 
120
- If OpenWS were HTTP:
268
+ A role's handlers are the runtime functions that process incoming messages whose payloads match the message definitions in the spec.
121
269
 
122
- - This package is OpenAPI
123
- - Not Express
124
- - Not Fastify
125
- - Not application code
270
+ ## Metadata & Connection Hints
271
+
272
+ OpenWS includes a small set of **well-known metadata fields** for product-grade tooling, while still allowing custom metadata anywhere in the document.
273
+
274
+ ### Document metadata
275
+
276
+ OpenWS supports a small set of well-known metadata fields at the root of the document:
277
+
278
+ - `title` (**SHOULD**): a human-readable name for the document.
279
+ - `version` (**SHOULD**): the version of the described system/API (not the OpenWS spec version).
280
+ - `description` (**MAY**): a longer explanation of the document.
281
+
282
+ Tooling **MAY** use these fields for documentation, generation, and display.
283
+
284
+ ### Connection hints
285
+
286
+ Endpoints are **optional connection hints** that help participants determine how to establish connections. They do not prescribe deployment topology, discovery, authentication, or load-balancing.
287
+
288
+ An endpoint is typically attached to a role definition:
289
+
290
+ ```json
291
+ {
292
+ "networks": {
293
+ "chat": {
294
+ "roles": {
295
+ "server": {
296
+ "endpoints": [
297
+ { "scheme": "wss", "host": "localhost", "port": 8082, "path": "/abc" }
298
+ ]
299
+ }
300
+ }
301
+ }
302
+ }
303
+ }
304
+ ```
305
+
306
+ Endpoint objects:
307
+
308
+ - **MAY** include `scheme` (`ws` or `wss`), `host`, `port`, and `path`.
309
+ - **SHOULD** be treated as hints. Implementations **MAY** connect using other configuration (service discovery, environment variables, user choice, etc.).
310
+ - Dynamic endpoint resolution is outside the scope of this spec.
311
+
312
+ ### Custom metadata and extensions
313
+
314
+ Custom metadata fields **MAY** be added anywhere in the JSON structure. Implementations:
315
+
316
+ - **MUST** ignore fields they do not understand.
317
+ - **SHOULD** encourage a consistent prefix such as `x-...` for extension fields to reduce the chance of collisions with future spec fields.
318
+
319
+ ## Complete Example
320
+
321
+ The following example is a minimal but complete OpenWS document modeling a chat system. It uses:
322
+
323
+ - a single `chat` network
324
+ - three roles (`server`, `client`, `portal`)
325
+ - message contracts scoped to the receiving role
326
+ - connection hints via `endpoints`
327
+ - one extension field (`x-routingNotes`) to document intended flows
328
+
329
+ ```<!-- embed:./test/chat-spec.json:scope:{ -->
330
+ {
331
+ "openws": "0.0.2",
332
+ "title": "Example Chat Service",
333
+ "version": "1.0.0",
334
+ "description": "A minimal OpenWS document modeling a chat network.",
335
+ "networks": {
336
+ "chat": {
337
+ "description": "Realtime chat network.",
338
+ "roles": {
339
+ "server": {
340
+ "description": "Backend that hosts rooms and broadcasts messages.",
341
+ "endpoints": [
342
+ {
343
+ "scheme": "wss",
344
+ "host": "chat.example.com",
345
+ "port": 443,
346
+ "path": "/ws/chat"
347
+ }
348
+ ],
349
+ "messages": {
350
+ "join": {
351
+ "description": "Request to join a room.",
352
+ "payload": {
353
+ "type": "object",
354
+ "additionalProperties": false,
355
+ "required": ["userId", "roomId"],
356
+ "properties": {
357
+ "userId": { "type": "string", "minLength": 1 },
358
+ "roomId": { "type": "string", "minLength": 1 }
359
+ }
360
+ }
361
+ },
362
+ "message": {
363
+ "description": "Send a chat message to a room.",
364
+ "payload": {
365
+ "type": "object",
366
+ "additionalProperties": false,
367
+ "required": ["userId", "roomId", "text"],
368
+ "properties": {
369
+ "userId": { "type": "string", "minLength": 1 },
370
+ "roomId": { "type": "string", "minLength": 1 },
371
+ "text": { "type": "string", "minLength": 1, "maxLength": 2048 }
372
+ }
373
+ }
374
+ },
375
+ "createRoom": {
376
+ "description": "Create a new room.",
377
+ "payload": {
378
+ "type": "object",
379
+ "additionalProperties": false,
380
+ "required": ["userId", "name"],
381
+ "properties": {
382
+ "userId": { "type": "string", "minLength": 1 },
383
+ "name": { "type": "string", "minLength": 1, "maxLength": 128 }
384
+ }
385
+ }
386
+ },
387
+ "requestStats": {
388
+ "description": "Request channel statistics.",
389
+ "payload": {
390
+ "type": "object",
391
+ "additionalProperties": false,
392
+ "required": ["roomId"],
393
+ "properties": {
394
+ "roomId": { "type": "string", "minLength": 1 }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ },
400
+ "client": {
401
+ "description": "End-user client that receives events from the server.",
402
+ "messages": {
403
+ "roomJoined": {
404
+ "description": "Emitted after a join succeeds.",
405
+ "payload": {
406
+ "type": "object",
407
+ "additionalProperties": false,
408
+ "required": ["roomId"],
409
+ "properties": {
410
+ "roomId": { "type": "string", "minLength": 1 }
411
+ }
412
+ }
413
+ },
414
+ "messageReceived": {
415
+ "description": "Broadcast of a message to participants in a room.",
416
+ "payload": {
417
+ "type": "object",
418
+ "additionalProperties": false,
419
+ "required": ["roomId", "text", "senderId", "sentAt"],
420
+ "properties": {
421
+ "roomId": { "type": "string", "minLength": 1 },
422
+ "text": { "type": "string", "minLength": 1, "maxLength": 2048 },
423
+ "senderId": { "type": "string", "minLength": 1 },
424
+ "sentAt": { "type": "integer" }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ },
430
+ "portal": {
431
+ "description": "Internal web portal that receives aggregated stats.",
432
+ "messages": {
433
+ "channelStats": {
434
+ "description": "Room-level metrics snapshot.",
435
+ "payload": {
436
+ "type": "object",
437
+ "additionalProperties": false,
438
+ "required": ["roomId", "members", "messagesLastMinute"],
439
+ "properties": {
440
+ "roomId": { "type": "string", "minLength": 1 },
441
+ "members": { "type": "integer", "minimum": 0 },
442
+ "messagesLastMinute": { "type": "integer", "minimum": 0 }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ },
449
+ "x-routingNotes": "Clients send server.join/server.message. Server emits client.roomJoined/client.messageReceived and portal.channelStats."
450
+ }
451
+ }
452
+ }
453
+ ```