@tetherto/mdk-app-runtime 0.0.0 → 0.12.1

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,184 @@
1
+ 'use strict'
2
+
3
+ // Wire protocol between an app runtime (server) and its callers (the Gateway,
4
+ // tests) -- see lib/runtime.js and lib/client.js.
5
+ //
6
+ // Transport: @hyperswarm/rpc (HyperDHT + protomux-rpc), one app runtime = one
7
+ // RPC server key. Everything on the wire is JSON in Buffers.
8
+ //
9
+ // Unary routes -- `app.invoke`
10
+ // A regular @hyperswarm/rpc request/response.
11
+ // request : { v:1, routeId, req:{ params, query, body, headers }, identity }
12
+ // response: { v:1, ok:true, result }
13
+ // or { v:1, ok:false, status, code, message }
14
+ // The responder never throws: every failure is an `ok:false` response, so a
15
+ // rejected `rpc.request()` always means a transport failure.
16
+ //
17
+ // Stream routes -- `app.stream`
18
+ // @hyperswarm/rpc 3.x has no streaming request API:
19
+ // `request()` / `event()` are strictly one message in, one message out. So
20
+ // streaming rides on the *same* authenticated HyperDHT connection through a
21
+ // protomux channel (the mux the RPC layer already uses, reachable as
22
+ // `rpc.mux` on both sides). Each stream uses a dedicated connection with a
23
+ // single channel (protocol `app.stream`, null id), so closing either side
24
+ // tears the whole thing down and there is no multiplexing state to leak.
25
+ // The firewall that guards `app.invoke` guards the connection, hence the
26
+ // stream, too.
27
+ //
28
+ // Channel messages (fixed order, index = message type, raw buffers):
29
+ // 0 REQUEST client -> server same JSON as an `app.invoke` request
30
+ // 1 HEAD server -> client JSON { statusCode, headers }
31
+ // 2 CHUNK server -> client raw body bytes (any number)
32
+ // 3 END server -> client empty; body complete
33
+ // 4 ERROR server -> client JSON { v:1, ok:false, status, code, message }
34
+ // protocol-level failure before any HEAD
35
+ // (unknown route, version mismatch, ...)
36
+ // Either side closing the channel (or the connection dropping) aborts the
37
+ // stream: the server's `raw` emits 'close', the client's Readable errors out
38
+ // unless END was already received.
39
+ //
40
+ // Backpressure: there is no credit or pause message, and the transport under
41
+ // protomux (UDX) does not push back on a reader that stops reading, so the
42
+ // client cannot slow the server down. Instead:
43
+ // - client: each stream's unread bytes are capped (`maxStreamBufferBytes` on
44
+ // createRuntimeClient, default 16 MiB). Past the cap the Readable is
45
+ // destroyed with ERR_APP_STREAM_OVERFLOW and the channel and its connection
46
+ // are closed, so the runtime stops sending (the handler's `raw` emits
47
+ // 'close'). A Gateway piping to a slow HTTP client therefore holds at most
48
+ // that much per stream.
49
+ // - server: `raw.write()` returns false while the connection's outgoing buffer
50
+ // is full and `raw` emits 'drain' once it has flushed, so a handler that
51
+ // honours write()'s return value (or uses stream.pipeline) does not queue
52
+ // unbounded data in the runtime process.
53
+ //
54
+ // Errors: a message starting with `ERR_` is safe to return as-is; anything else
55
+ // is masked with a generic status text. Status is `err.statusCode` when it is
56
+ // an integer >= 400, else 400 for an `ERR_` error and 500 for anything else.
57
+
58
+ const http = require('http')
59
+
60
+ const PROTOCOL_VERSION = 1
61
+
62
+ const METHODS = Object.freeze({
63
+ INVOKE: 'app.invoke',
64
+ STREAM: 'app.stream'
65
+ })
66
+
67
+ // Index of each message on the `app.stream` channel.
68
+ const STREAM_MESSAGES = Object.freeze({
69
+ REQUEST: 0,
70
+ HEAD: 1,
71
+ CHUNK: 2,
72
+ END: 3,
73
+ ERROR: 4
74
+ })
75
+
76
+ const ERR = Object.freeze({
77
+ ROUTE_NOT_FOUND: 'ERR_APP_ROUTE_NOT_FOUND',
78
+ PROTOCOL_VERSION: 'ERR_APP_PROTOCOL_VERSION',
79
+ BAD_REQUEST: 'ERR_APP_BAD_REQUEST',
80
+ ROUTE_IS_STREAM: 'ERR_APP_ROUTE_IS_STREAM',
81
+ ROUTE_NOT_STREAM: 'ERR_APP_ROUTE_NOT_STREAM',
82
+ REQUEST_FAILED: 'ERR_APP_REQUEST_FAILED',
83
+ RUNTIME_UNAVAILABLE: 'ERR_APP_RUNTIME_UNAVAILABLE',
84
+ RUNTIME_TIMEOUT: 'ERR_APP_RUNTIME_TIMEOUT',
85
+ REQUEST_ABORTED: 'ERR_APP_REQUEST_ABORTED',
86
+ STREAM_OVERFLOW: 'ERR_APP_STREAM_OVERFLOW'
87
+ })
88
+
89
+ function encode (obj) {
90
+ return Buffer.from(JSON.stringify(obj))
91
+ }
92
+
93
+ function decode (buf) {
94
+ return JSON.parse(Buffer.from(buf).toString('utf8'))
95
+ }
96
+
97
+ function buildRequest (routeId, req, identity) {
98
+ const r = req || {}
99
+ return {
100
+ v: PROTOCOL_VERSION,
101
+ routeId,
102
+ req: { params: r.params, query: r.query, body: r.body, headers: r.headers },
103
+ identity: identity === undefined ? null : identity
104
+ }
105
+ }
106
+
107
+ function okResponse (result) {
108
+ return { v: PROTOCOL_VERSION, ok: true, result }
109
+ }
110
+
111
+ function errorResponse (status, code, message) {
112
+ return { v: PROTOCOL_VERSION, ok: false, status, code, message }
113
+ }
114
+
115
+ function isSafeError (err) {
116
+ return !!(err && typeof err.message === 'string' && err.message.startsWith('ERR_'))
117
+ }
118
+
119
+ function _statusOf (err) {
120
+ if (Number.isInteger(err && err.statusCode) && err.statusCode >= 400) return err.statusCode
121
+ return isSafeError(err) ? 400 : 500
122
+ }
123
+
124
+ // What a handler/protocol error looks like on the wire (the `ok:false` shape).
125
+ // Safe (`ERR_*`) messages pass through; everything else becomes a generic
126
+ // status text so internals never leak to a caller.
127
+ function toErrorResponse (err) {
128
+ const status = _statusOf(err)
129
+ if (isSafeError(err)) {
130
+ const m = /^ERR_[A-Z0-9_]+/.exec(err.message)
131
+ return errorResponse(status, m ? m[0] : ERR.REQUEST_FAILED, err.message)
132
+ }
133
+ return errorResponse(status, ERR.REQUEST_FAILED, http.STATUS_CODES[status] || 'Bad Request')
134
+ }
135
+
136
+ // An error carrying an explicit status + code, for protocol-level failures.
137
+ function protocolError (status, code, detail) {
138
+ const err = new Error(detail ? `${code}: ${detail}` : code)
139
+ err.statusCode = status
140
+ err.code = code
141
+ return err
142
+ }
143
+
144
+ // Rebuild an Error from an `ok:false` response (client side).
145
+ // `remote` marks an error the runtime already handled (and logged, if it was a
146
+ // bug): the caller only relays it.
147
+ function fromErrorResponse (res) {
148
+ const err = new Error(res.message || res.code || 'Bad Request')
149
+ err.statusCode = Number.isInteger(res.status) ? res.status : 400
150
+ err.code = res.code || ERR.REQUEST_FAILED
151
+ err.remote = true
152
+ return err
153
+ }
154
+
155
+ // Validates a decoded request envelope; returns null when fine, else an Error.
156
+ function checkRequest (msg) {
157
+ if (!msg || typeof msg !== 'object') {
158
+ return protocolError(400, ERR.BAD_REQUEST, 'request must be an object')
159
+ }
160
+ if (msg.v !== PROTOCOL_VERSION) {
161
+ return protocolError(400, ERR.PROTOCOL_VERSION, `expected ${PROTOCOL_VERSION}, got ${msg.v}`)
162
+ }
163
+ if (typeof msg.routeId !== 'string' || !msg.routeId) {
164
+ return protocolError(400, ERR.BAD_REQUEST, 'routeId is required')
165
+ }
166
+ return null
167
+ }
168
+
169
+ module.exports = {
170
+ PROTOCOL_VERSION,
171
+ METHODS,
172
+ STREAM_MESSAGES,
173
+ ERR,
174
+ encode,
175
+ decode,
176
+ buildRequest,
177
+ okResponse,
178
+ errorResponse,
179
+ toErrorResponse,
180
+ fromErrorResponse,
181
+ isSafeError,
182
+ protocolError,
183
+ checkRequest
184
+ }