@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 +1 -1
- package/package.json +2 -2
- package/src/liveBinding.ts +9 -30
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.
|
|
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.
|
|
37
|
+
"@greendrake/util": "^0.8.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@greendrake/dev-config": "0.7.0",
|
package/src/liveBinding.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|