@greendrake/rpc 0.7.0 → 0.8.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
@@ -177,7 +177,7 @@ The contract:
177
177
  - **Relevance.** `when` decides whether a push concerns what the binding shows; omitted, every push does.
178
178
  - **Apply or resync.** `apply` folds the push in and nothing else happens. Without it the push runs `resync` — so `{}` means "every such push re-reads". A handler with neither `apply` nor a `resync` to fall back on throws at binding time.
179
179
  - **Reconnect.** `resync` also runs when the socket returns from a drop the binding witnessed: a connection has no memory, so what it missed is only recoverable by asking. The first connect after the binding is created is not a return from a drop. A binding without `resync` subscribes to nothing connection-related.
180
- - **Coalescing.** One `resync` at a time; pushes arriving during a run collapse into a single follow-up after it, however many there are — they ask the same question.
180
+ - **Coalescing.** One `resync` at a time; pushes arriving during a run collapse into a single follow-up after it, however many there are — they ask the same question. This is `@greendrake/util`'s `coalesced`, which a server building what it pushes can use the same way.
181
181
  - **No error policy.** The binding awaits `resync` only to know when the slot is free, and interprets neither outcome: `resync` owns its errors. It must also not be served by a response cache the missed pushes would have invalidated — invalidate the entries its read would hit, then read.
182
182
  - **Release.** The returned function detaches every subscription and drops any pending follow-up; calling it twice is harmless.
183
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greendrake/rpc",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "RPC client over JSON: typed method-map API, HTTP and reconnecting WebSocket transports, pluggable codec and headers, TTL response cache",
5
5
  "license": "MIT",
6
6
  "author": "Eugene Greendrake <eugene@greendrake.nz>",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "prettier": "@greendrake/dev-config/prettier",
36
36
  "dependencies": {
37
- "@greendrake/util": "0.7.0"
37
+ "@greendrake/util": "^0.8.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@greendrake/dev-config": "0.7.0",
@@ -1,3 +1,4 @@
1
+ import { coalesced } from '@greendrake/util'
1
2
  import type { PushMapConstraint, WsTransport } from './WsTransport'
2
3
 
3
4
  // What one push means to the thing the binding keeps current.
@@ -36,38 +37,17 @@ export interface LiveOptions<PEvents, PData> {
36
37
  export const liveBinding = <PEvents extends PushMapConstraint<PEvents>, PData extends PushMapConstraint<PData>>(transport: WsTransport<PEvents, PData>, options: LiveOptions<PEvents, PData>): (() => void) => {
37
38
  const { resync } = options
38
39
  const releases: (() => void)[] = []
39
- let running = false
40
- let queued = false
41
40
  let released = false
42
41
 
42
+ // One re-read at a time, and at most one more behind it however many
43
+ // pushes land while it runs: they all ask the same question, and the answer
44
+ // to the last one is the answer to all of them. One still queued when the
45
+ // binding is released has nobody left to answer, and does nothing.
46
+ const reread = coalesced(() => (released ? undefined : resync?.()))
47
+ // The outcome belongs to resync — swallowed here rather than left
48
+ // unhandled because this runs from a subscription callback nobody awaits.
43
49
  const runResync = (): void => {
44
- if (!resync || released) return
45
- // One re-read at a time, and at most one more behind it however many
46
- // pushes land while it runs: they all ask the same question, and the
47
- // answer to the last one is the answer to all of them.
48
- if (running) {
49
- queued = true
50
- return
51
- }
52
- running = true
53
- // `new Promise(resolve => resolve(…))` rather than Promise.resolve(…):
54
- // it puts the call inside the promise, so a resync that throws
55
- // synchronously becomes a rejection like any other failure instead of
56
- // escaping with the slot still held — which would wedge this binding for
57
- // good, and abort the transport's dispatch to every other subscriber of
58
- // the same frame.
59
- void new Promise(resolve => resolve(resync()))
60
- // Whatever the outcome, the slot is free and a push that arrived
61
- // during the run gets its re-read. The outcome itself belongs to
62
- // resync — swallowed here rather than left unhandled because this
63
- // runs from a subscription callback nobody awaits.
64
- .catch(() => {})
65
- .then(() => {
66
- running = false
67
- if (!queued) return
68
- queued = false
69
- runResync()
70
- })
50
+ void reread().catch(() => {})
71
51
  }
72
52
 
73
53
  const attach = <P>(handlers: { [K in keyof P]?: LiveHandler<P[K]> } | undefined, subscribe: <K extends keyof P & string>(key: K, handler: (data: P[K]) => void) => () => void): (() => () => void)[] =>
@@ -109,7 +89,6 @@ export const liveBinding = <PEvents extends PushMapConstraint<PEvents>, PData ex
109
89
 
110
90
  return () => {
111
91
  released = true
112
- queued = false
113
92
  releases.forEach(release => release())
114
93
  releases.length = 0
115
94
  }