entangld 2.4.1 → 2.7.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.
@@ -0,0 +1,484 @@
1
+ # Entangld protocol
2
+
3
+ > **Draft.** This describes what `lib/` does today, and it's incomplete. Where this document and the code disagree, the
4
+ > code wins. The format has no version number yet, and nothing here is a compatibility promise. Open questions are marked
5
+ > **TODO**. The [preferred direction](#proposed-handshakes-and-versioning) and other
6
+ > [changes we're considering](#changes-under-consideration) are at the end.
7
+
8
+ ## Overview
9
+
10
+ Stores talk to each other by passing messages. A store knows only its immediate neighbours, so a path which crosses
11
+ several stores is handled one link at a time.
12
+
13
+ On any link, the store which attached the other is **upstream**, and the attached store is **downstream**. `get`, `set`,
14
+ `push`, `subscribe` and `unsubscribe` messages travel downstream. `value` and `event` messages travel upstream.
15
+
16
+ The library doesn't provide a transport. `transmit()` supplies the function which sends a message, and `receive()` takes
17
+ the messages which arrive.
18
+
19
+ **TODO:** state what a transport must guarantee. Today, a lost `value` leaves a request waiting unless the caller set a
20
+ limit, and nothing says whether messages must arrive in order.
21
+
22
+ ## Messages
23
+
24
+ Messages are objects with these fields. A field which is `undefined` is left out when the message is serialized as JSON.
25
+
26
+ | Field | Used by | Meaning |
27
+ | --- | --- | --- |
28
+ | `type` | All | One of the types below |
29
+ | `path` | All but `unsubscribe` | A path relative to the downstream store |
30
+ | `value` | `get`, `value`, `set`, `push`, `event` | The payload |
31
+ | `uuid` | `get`, `value`, `subscribe`, `event`, `unsubscribe` | Links messages together, see [uuids](#uuids) |
32
+ | `params` | `set`, `push` | Options. `limit` is the maximum length of the array after a `push` |
33
+ | `every` | `subscribe` | Send one event per this many changes. `null` means every change |
34
+
35
+ ### `get`
36
+
37
+ Reads a path or calls a function. `value` is the optional parameter: the argument for a function, or a maximum depth for
38
+ data. It's left out if there is none.
39
+
40
+ ```js
41
+ { "type": "get", "path": "system.voltage", "uuid": "fe9921dd-a48a-4fac-9f4e-2fa882b2948d" }
42
+ { "type": "get", "path": "double.me", "value": [ 4 ], "uuid": "1023077d-683f-4ff1-8900-bb01b298e0ca" }
43
+ ```
44
+
45
+ ### `value`
46
+
47
+ Answers a `get`, with the same `path` and `uuid`. A path with no data has no `value` field.
48
+
49
+ ```js
50
+ { "type": "value", "path": "system.voltage", "value": 12, "uuid": "fe9921dd-a48a-4fac-9f4e-2fa882b2948d" }
51
+ ```
52
+
53
+ RPCs use this message too. `set_rpc` wraps a function so that its reply is `[ true, result ]`, or `[ false, error ]` if
54
+ it failed. `call_rpc` unwraps that, so to the protocol an RPC is an ordinary `get`.
55
+
56
+ ### `set` and `push`
57
+
58
+ Write to a path. There is no reply. A `set` with no `value` removes the path. A `push` adds to an array, and fails on the
59
+ receiving store if the path isn't one.
60
+
61
+ ```js
62
+ { "type": "set", "path": "system.voltage", "value": 2, "params": {} }
63
+ { "type": "push", "path": "log", "value": 1, "params": { "limit": 5 } }
64
+ ```
65
+
66
+ ### `subscribe`
67
+
68
+ Asks to be told about changes to a path. There is no reply. The path may use `*` as a wildcard.
69
+
70
+ ```js
71
+ { "type": "subscribe", "path": "system.*", "uuid": "e6c079db-a6d6-4b5b-9baa-b6bb4f8f3a49", "every": 2 }
72
+ ```
73
+
74
+ ### `event`
75
+
76
+ Reports a change for a subscription. `path` is the path which changed, relative to the sender. The receiver adds its
77
+ namespace for that link. `every` is applied by the store which owns the data, so skipped changes never cross a link.
78
+
79
+ ```js
80
+ { "type": "event", "path": "system.voltage", "value": 12, "uuid": "e6c079db-a6d6-4b5b-9baa-b6bb4f8f3a49" }
81
+ ```
82
+
83
+ ### `unsubscribe`
84
+
85
+ Ends a subscription. It has no `path`. A store also sends one in reply to an `event` which matches none of its
86
+ subscriptions.
87
+
88
+ ```js
89
+ { "type": "unsubscribe", "uuid": "e6c079db-a6d6-4b5b-9baa-b6bb4f8f3a49" }
90
+ ```
91
+
92
+ ## Behaviours
93
+
94
+ ### uuids
95
+
96
+ - **`get` and `value`:** a uuid belongs to one link. A store forwarding a `get` makes a new one, so a chain of three
97
+ stores uses two uuids for one request.
98
+ - **Subscriptions:** one uuid is used along the whole chain, by `subscribe`, `event` and `unsubscribe`.
99
+
100
+ ### Chains
101
+
102
+ A store forwarding a request holds its own pending entry, and answers upstream when its downstream answers.
103
+ Subscriptions are daisy-chained in the same way. A store in the middle holds a `pass through` subscription, which only
104
+ sends events upstream.
105
+
106
+ ### Reattaching
107
+
108
+ `attach()` removes any subscriptions beneath the new namespace and subscribes them again, using the same uuids. A store
109
+ replaces an existing subscription which has the same uuid and path, so that a client which reconnects doesn't get
110
+ duplicate events.
111
+
112
+ ### Failures
113
+
114
+ - **Nothing reports an error across a link.** There is no error message.
115
+ - **Functions:** if a function behind a `get` fails, no `value` is sent. Functions which can fail should use `set_rpc`.
116
+ - **Unknown message types:** `receive()` throws an `EntangldError`.
117
+ - **Late replies:** a `value` with an unknown uuid is ignored, because the request may have timed out.
118
+ - **Failed writes:** if a `set` or `push` fails, the error stays on the receiving store. The sender isn't told.
119
+
120
+ ### What stays local
121
+
122
+ `timeout_ms` and `signal` never appear in a message. They stop the wait or end the subscription on the calling store,
123
+ and aborting a subscription sends an ordinary `unsubscribe`.
124
+
125
+ **TODO:** cover `attach`, `detach`, and what happens to pending requests when a link drops. `detach()` leaves them in
126
+ `_requests` today.
127
+
128
+ ## Proposed: handshakes and versioning
129
+
130
+ **Not built or adopted.** This is the preferred direction, not a description of what the library does.
131
+
132
+ Today a store can't tell what a peer supports, and a new message type raises an error on any store which hasn't upgraded.
133
+ This proposal lets stores find out, and sets rules so that later changes need as few stores upgraded as possible.
134
+
135
+ ### Negotiating a link
136
+
137
+ The application calls `negotiate()` when a link comes up, because only it knows when that happens. `attach()` can't do
138
+ this: some stores call it once at startup before any socket connects, and others call it per connection.
139
+
140
+ ```js
141
+ // A client which reconnects by itself
142
+ this.cortex.on("connect", () => {
143
+ this.datastore.negotiate(this.cortex, { timeout_ms: 5000 });
144
+ });
145
+
146
+ // A server, with a new socket per connection
147
+ _serverOnConnect(socket) {
148
+ this.datastore.attach(`chips.${id}`, socket);
149
+ this.datastore.negotiate(socket, { timeout_ms: 5000 });
150
+ }
151
+ ```
152
+
153
+ Asking lazily, on first need, was rejected. `set` and `subscribe` are synchronous so they can't wait for an answer, and a
154
+ lost reply would stall every caller waiting on it. It would also still need a hook, to forget a peer on disconnect.
155
+
156
+ Negotiation is per link, because messages already work hop by hop. It's an ordinary `get` of `@entangld.protocol` on the
157
+ peer, and the answer is a protocol number and a list of features, both separate from the npm version:
158
+
159
+ ```js
160
+ { "protocol": 1, "features": [ "cancel", "error_reply" ] }
161
+ ```
162
+
163
+ - **Old stores:** a `get` of an unknown path returns `undefined` on 2.6.0, with no error, so an old store can be asked
164
+ safely. No new message type is needed.
165
+ - **Unknown means old:** a link counts as protocol 0 until it has an answer. A lost reply leaves it there, so nothing
166
+ new is sent to a store which may not understand it.
167
+ - **Reconnects:** a peer can change across a reconnect, so `negotiate()` runs on every connect. A stale answer's worst
168
+ case should be an ignored message, once unknown messages are ignored.
169
+
170
+ ### Features upgrade existing calls
171
+
172
+ Applications don't check feature lists or build messages. Once a link is negotiated, the library takes over. **A feature
173
+ upgrades an existing call, and never needs its own code path**, so the same code works on old and new links.
174
+
175
+ | Application code | With an old peer | With a peer which has the feature |
176
+ | --- | --- | --- |
177
+ | `controller.abort()` | Stops waiting | Also cancels the remote work |
178
+ | `{ timeout_ms: 5000 }` | Local timer | Also sends the deadline along the chain |
179
+ | `subscribe(path, callback)` | Today's events | Compact events |
180
+ | `get` on a function which fails | Waits for its limit | Rejects with the real error |
181
+ | `get(path, undefined, { strict: true })` | Behaves as `get` does today | Rejects if the path isn't found |
182
+
183
+ For the times an application wants to know, there's one checker. It takes a path, so the caller doesn't need to know
184
+ which link that means:
185
+
186
+ ```js
187
+ store.supports("b.slow_job", "cancel");
188
+ ```
189
+
190
+ It can only answer for the immediate neighbour, so `true` doesn't promise that a store further along the chain has the
191
+ feature. Avoid methods per feature, like `can_cancel()`.
192
+
193
+ ### Reserved names
194
+
195
+ Any path segment starting with `@` is reserved, at any level. New stores refuse to `set` or `attach` these names, so they
196
+ never appear in the data, in `get("")`, or in wildcards. `$` was rejected because shells expand it, so
197
+ `--get bots.x.$entangld.protocol` silently loses the name.
198
+
199
+ Only one reserved namespace is used to start, `@entangld`, at the root of a store:
200
+
201
+ ```js
202
+ store.get("b.@entangld.protocol");
203
+ ```
204
+
205
+ Suffixes like `a.b.c.@schema` are reserved but not used. Asked this on 2.6.0, a store runs any function at `a.b.c`, fails
206
+ navigating the result, and never replies. Negotiation doesn't make that safe, because it only describes the neighbour,
207
+ and a path may be owned several stores away. They could become a shorthand once old stores are gone.
208
+
209
+ ### Queries
210
+
211
+ A root query has to reach the store which owns a path, and callers usually don't know where one store ends and the next
212
+ begins. So the library does it, and each store peels off one hop:
213
+
214
+ ```js
215
+ store.describe("b.c.motor.start");
216
+ ```
217
+
218
+ ```js
219
+ { "type": "get", "path": "@entangld.describe", "value": { "path": "c.motor.start" }, "uuid": "..." }
220
+ ```
221
+
222
+ B sees that `c` is an attached store, and forwards the query to C with `motor.start`. An old store in the middle answers
223
+ `undefined`, so the query stops there, safely. This first upgrade needs every store on the way to be new. The rules below
224
+ are there so that later ones don't.
225
+
226
+ **A query a store understands never answers `undefined`.** That leaves `undefined` to mean only that a store on the way
227
+ doesn't support the query, so a caller can tell the two apart.
228
+
229
+ `describe` looks at a path without calling it:
230
+
231
+ ```js
232
+ await store.describe("b.c.motor.start");
233
+
234
+ // { exists: true, kind: "rpc" }
235
+ // { exists: false }
236
+ // { exists: null, kind: "function" } Beneath a function, so it can't tell without calling it
237
+ ```
238
+
239
+ `store.exists(path)` is a thin wrapper around this, for the common case. It returns `null` when it can't tell, including
240
+ for a store which doesn't support the query.
241
+
242
+ ### Strict reads
243
+
244
+ `get` returns `undefined` for a missing path, a removed path, and a function which returned `undefined`, and the replies
245
+ are identical. A `value` reply gains a field saying whether the path was found:
246
+
247
+ ```js
248
+ { "type": "value", "path": "nope", "found": false, "uuid": "..." }
249
+ ```
250
+
251
+ ```js
252
+ await store.get("b.nope"); // undefined, as it is today
253
+ await store.get("b.nope", undefined, { strict: true }); // Rejects with an EntangldNotFoundError
254
+ await store.get("b.empty_fn", undefined, { strict: true }); // undefined, because it was found
255
+ ```
256
+
257
+ This takes one round trip and can't go stale, unlike calling `exists` before `get`, so `exists` is for asking about a
258
+ path and `strict` is for reading one. An old store sends no `found`, so `strict` can't tell, and behaves as `get` does
259
+ today.
260
+
261
+ Wrinkle: a store forwarding a `get` rebuilds the reply from the value alone, because `_requests` only passes on
262
+ `msg.value`, so `found` would be lost at each hop. Stores need to carry the whole reply, which is also what keeping
263
+ unrecognised fields needs.
264
+
265
+ `subscribe` doesn't take `strict`. Subscribing to a path before it exists is normal, and a subscription whose path
266
+ appears later still works. It's also synchronous with no reply, so it has nothing to reject. To check first, call
267
+ `describe`. Knowing that a subscription arrived is a different question, see
268
+ [acknowledgements](#acknowledgements).
269
+
270
+ ### Forward compatibility
271
+
272
+ **Only the stores which care about a change should need upgrading.** This release has to touch every store, so it should
273
+ make that the last time where it can. Each store follows these rules from protocol 1 on:
274
+
275
+ - **Route without interpreting.** A store forwards an `@entangld` query by its `path`, whether or not it recognises the
276
+ name. Only the store which answers needs to understand it.
277
+ - **Don't answer what you don't know.** The store which owns the path answers `undefined` for an unknown name. Queries
278
+ it does know never answer `undefined`, so a caller can tell that it isn't supported.
279
+ - **Keep what you don't recognise.** A store forwarding a message or a reply passes on fields it doesn't know.
280
+ `receive()` drops them today, by calling `get(msg.path, msg.value)`, and `_requests` only passes on `msg.value`.
281
+ - **Ignore unknown message types.** `receive()` throws today. Ignoring them makes a stale negotiation harmless, at the
282
+ cost of hiding bugs which are loud today, so it may be worth emitting an event.
283
+ - **Degrade, don't fail.** A feature a store lacks should make a call weaker, not break it.
284
+
285
+ For example, a later `who_owns` query, where a store says whether it owns an endpoint or only passes it through, would
286
+ need only the answering store to upgrade. Every store between would forward it without knowing what it means.
287
+
288
+ **The limit:** this covers anything a store only needs to pass along. A feature which needs a store to act still needs
289
+ that store upgraded. Cancelling and deadlines have to clear each store's pending request, and compact events change the
290
+ encoding of one link, so those stop at the first old store, or only apply where both ends support them.
291
+
292
+ **TODO:** decide what the protocol number means once features exist. It may only need to change for a breaking change.
293
+
294
+ ## Changes under consideration
295
+
296
+ Nothing here is decided or built. A change to the protocol is a good moment to commit to this document, and to make
297
+ several of these at once. Most depend on the [proposal above](#proposed-handshakes-and-versioning).
298
+
299
+ ### Error replies
300
+
301
+ A `value` can't carry an error, which is why `call_rpc` puts success or failure inside the value. An error reply would
302
+ end the hang behind a raw `get`, and would let a store report that it gave up. A raw `get` can't be fixed without this.
303
+
304
+ Wrinkle: `set_rpc` already has its own error format, so we'd need to decide whether the two merge.
305
+
306
+ ### Cancelling remote work
307
+
308
+ Applications would use this through `abort()`, not a new method, as [proposed](#features-upgrade-existing-calls).
309
+
310
+ Today an RPC which timed out or was aborted may still finish, so a retry can run it twice. A new message could carry the
311
+ abort down the chain, and let the function at the far end stop:
312
+
313
+ ```js
314
+ { type: "cancel", uuid }
315
+
316
+ store.set("slow.job", async (params, { signal }) => { /* can stop early */ });
317
+ ```
318
+
319
+ Each store on the way clears its pending request, which fixes the leak described under deadlines when the link works.
320
+
321
+ Wrinkles:
322
+
323
+ - **Middle stores need a new map.** A `get` has a different uuid on each link, so `cancel` can't simply be forwarded. A
324
+ store has to remember which incoming request (by uuid and sender) became which outgoing request, and clean that up on
325
+ every outcome.
326
+ - **Function signatures.** Functions are called as `data(params)`, so a second argument is possible, but any function
327
+ which already has an optional second parameter would start receiving the context by accident. It may need to be
328
+ opt-in. `_deref_mode` also calls functions, with no request at all.
329
+ - **`set_rpc` has no obvious place for it.** It calls `func(...params)`, so every position belongs to the caller's
330
+ arguments. Appending the context would collide with optional parameters, and `set_rpc` prints usage from the
331
+ function's parameters, so those would need filtering. This probably needs an opt-in on `set_rpc` itself.
332
+ - **Functions have to cooperate.** A signal only asks a function to stop. One which ignores it keeps running, so a
333
+ timeout still can't promise the work stopped.
334
+ - **It can't cross a dead link.** If the link is down, `cancel` is lost just as the reply was.
335
+
336
+ ### Deadlines which travel
337
+
338
+ In a chain A → B → C, if a reply is lost after B, A gives up but B keeps its pending entry forever. Sending the remaining
339
+ time with the request would let each store enforce it, even if the link dies. Relative time avoids clock skew between
340
+ stores. This is how gRPC deadlines work.
341
+
342
+ ```js
343
+ { type: "get", path, value, uuid, timeout_ms: 8500 }
344
+ ```
345
+
346
+ Wrinkles:
347
+
348
+ - **Coverage stops at old stores, silently.** `receive()` only reads the fields it knows, so an old peer should ignore
349
+ the extra field, but it won't enforce it or pass it on. Confirm this before relying on it.
350
+ - **A store can't report that it gave up,** without error replies. It can only clean up and stay quiet, and the caller
351
+ gets the timeout from its own timer.
352
+ - **Should the far end get it?** Passing the deadline to a function is the same signature problem as cancelling.
353
+ - **Stores need a little margin.** If every store uses the same remaining time, they all expire together and race. It
354
+ may be worth subtracting a small allowance per link.
355
+
356
+ ### Acknowledgements
357
+
358
+ `set`, `push` and `subscribe` have no reply, so a lost message goes unnoticed. A lost `subscribe` is the worst case,
359
+ because the events simply never arrive. Acks would need an answer for what to do when one doesn't come, and they would
360
+ add traffic to what is often a high-rate path, so they should probably be opt-in.
361
+
362
+ For `subscribe`, what a caller wants to know is that the subscription arrived, not that the path exists yet. That's a
363
+ better fit than a strict mode, and [checking a subscription](#checking-a-subscription) may answer it without acks at all.
364
+
365
+ ### Checking a subscription
366
+
367
+ Because a subscription keeps one uuid along its whole chain, the store which owns the path can confirm that it has it.
368
+ This would be another `@entangld` query:
369
+
370
+ ```js
371
+ const uuid = store.subscribe("b.c.motor.speed", callback);
372
+
373
+ await store.subscription_active(uuid);
374
+ // true The owning store has it
375
+ // false It doesn't, so it was lost or hasn't arrived yet
376
+ // null A store on the way doesn't support the check
377
+ ```
378
+
379
+ Unlike a per-message ack, this costs nothing unless it's called, it works at any time, such as after a reconnect, and only
380
+ the owning store needs to understand it. The library already knows the path from the uuid.
381
+
382
+ Wrinkle: `false` can mean a subscription is still on its way, so checking straight after subscribing isn't useful.
383
+
384
+ ### Describing endpoints
385
+
386
+ `set_rpc` already returns usage text, which is a small version of this. `describe` would also return what a store has been
387
+ told about a path:
388
+
389
+ ```js
390
+ store.set_description("motor.speed", {
391
+ description: "Wheel speed",
392
+ encoding: speed_encoding.to_json(),
393
+ user_data: { unit: "m/s", precision: 0.001 }
394
+ });
395
+
396
+ store.set_rpc("motor.stop", stop, { description: "Stop the motor", user_data: { safe_to_retry: true } });
397
+
398
+ await store.describe("b.motor.speed");
399
+ // { exists: true, kind: "data", description: "Wheel speed", encoding: { ... }, user_data: { ... } }
400
+ ```
401
+
402
+ - **`description` is for people.** It's a short summary of the path.
403
+ - **`encoding` is for the library.** It says how a value is packed, and only matters if the path uses a codec.
404
+ - **`user_data` is for the application.** The library stores it and returns it unchanged, and never reads it. It can hold
405
+ anything which fits in JSON, like a unit, the precision of a sensor, or a JSON Schema. The library doesn't define any of
406
+ those, so an application can use whatever suits it.
407
+
408
+ All three are optional, and nothing is enforced, because a store accepts any JSON. They live beside the data, not in it, so
409
+ they don't appear in `get("")`, and removing a value doesn't lose them. Only the owning store has them, for exact paths at
410
+ first.
411
+
412
+ Wrinkles:
413
+
414
+ - **Size.** `describe` returns `user_data` every time, so large values cost traffic on slow links. It may be worth a way
415
+ to ask for it only when needed.
416
+ - **Drift.** Nothing checks `user_data` against the real value, or against `encoding`. A `Float32` might not hold a
417
+ claimed precision, for example. That's accepted, because the library doesn't read it.
418
+
419
+ ### Compact events
420
+
421
+ Today's events repeat the `path` and `uuid` in every message. For high-rate paths this can be replaced in stages, each
422
+ useful by itself. Sizes are the message alone, before framing, and were worked out rather than measured on a link:
423
+
424
+ | Encoding | One number | 8-field pose |
425
+ | --- | --- | --- |
426
+ | Today's JSON | 115 | 209 |
427
+ | Short subscription id, in JSON | 15 | 114 |
428
+ | Id, with an `encoding` for the keys, in JSON | 15 | 69 |
429
+ | Id, with binary values as base64 in JSON | about 18 | about 54 |
430
+ | Id, with raw binary values | about 7 | about 35 |
431
+
432
+ The owner agrees the id when the subscription starts, and sends the `encoding` then, so both ends build the same codec
433
+ and can't disagree. This reply also confirms that the subscription arrived:
434
+
435
+ ```js
436
+ { type: "subscribed", uuid, id: 12, encoding: { /* ... */ } }
437
+ ```
438
+
439
+ Wrinkles:
440
+
441
+ - **It's per link.** Each link agrees its own ids, so a store in the middle decodes and encodes again, and a link with an
442
+ old store stays on today's events.
443
+ - **Values which don't fit.** A value which doesn't match its `encoding` goes as a normal event.
444
+ - **Lossless by default.** Dropping the `path` and `uuid` loses nothing, so that can happen automatically. An `encoding`
445
+ which narrows a value, like `Float32`, is lossy, so the owner chooses it for a path.
446
+ - **Rate often matters more.** The existing `every` option can cut a high-rate path by more than any encoding, with no
447
+ protocol change.
448
+
449
+ ### Codecs
450
+
451
+ The library keeps no runtime dependencies, so packing is pluggable. A codec only provides ways to load an encoding, and
452
+ to encode and decode a value with it, and links opt in:
453
+
454
+ ```js
455
+ store.negotiate(link, { codec: entangld_obj2buf });
456
+ ```
457
+
458
+ [obj2buf](https://github.com/Nova-Dynamics/obj2buf-node) is the intended reference. It's pure JavaScript with no
459
+ dependencies. `Schema.to_json()` and `Schema.from_json()` give us an `encoding` which can be sent over a link, it can tell
460
+ `Float32` from `Float64`, and `schema.validate()` gives us the fallback for values which don't fit. Fields which can be
461
+ `null` need `OptionalType`. It hasn't been tried with this library yet.
462
+
463
+ The two ends need to use the same codec, so the codec's name should be part of negotiation.
464
+
465
+ ### Binary frames
466
+
467
+ The aim is raw binary on the wire, but `sockhop` can't carry it yet. It splits messages on a terminator and parses each one
468
+ as JSON, so binary data needs a change there, like length-prefixed frames. That would also remove most of the framing
469
+ around each message.
470
+
471
+ Until then, the library supports both, and negotiation decides which a link uses:
472
+
473
+ 1. **Base64 inside JSON.** This works on any JSON transport today, for about a third more bytes than raw binary.
474
+ 2. **Raw binary frames.** This needs a transport which can carry them, so a transport has to be able to say that it can.
475
+
476
+ The library doesn't own the transport, so the change itself belongs in `sockhop`.
477
+
478
+ ### Smaller improvements
479
+
480
+ - **`value` size:** `value` repeats the `path` the requester already knows, and `set` sends an empty `params`.
481
+ - **`every: null`:** `subscribe` sends this explicitly, where other optional fields are left out.
482
+ - **Transport guidance:** a recommended framing and example transport, so that a new port doesn't start from `lib/`. What
483
+ a transport must guarantee is an open question [above](#overview).
484
+ - **Conformance cases:** a shared set of message examples which any port can run, so that ports notice when they drift.
package/generate_docs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const fs=require("fs");
2
2
  const jsdoc2md=require("jsdoc-to-markdown");
3
3
 
4
- const templatefile=fs.readFileSync("README.hbs", "utf8");
4
+ const template="# API reference\n\n{{>main}}\n";
5
5
 
6
- jsdoc2md.render({files: "lib/*.js", template:templatefile})
7
- .then(output=> fs.writeFileSync("README.md", output));
6
+ jsdoc2md.render({files: "lib/*.js", template})
7
+ .then(output=> fs.writeFileSync("API.md", output));