angelia.io 3.4.84 → 4.0.85
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/client/index.js +132 -373
- package/client/worker.js +217 -0
- package/package.json +22 -16
- package/readme.md +81 -247
- package/server/Server.js +207 -389
- package/server/Socket.js +69 -81
- package/server/index.js +1 -3
- package/server/rooms/Manager.js +3 -3
- package/server/rooms/Room.js +8 -8
- package/server/utils.js +47 -0
- package/tsconfig.json +23 -0
- package/types/client/index.d.ts +48 -0
- package/types/client/worker.d.ts +1 -0
- package/types/index.d.ts +1 -0
- package/types/server/Server.d.ts +81 -0
- package/types/server/Socket.d.ts +49 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/rooms/Manager.d.ts +9 -0
- package/types/server/rooms/Room.d.ts +17 -0
- package/types/server/rooms/Rooms.d.ts +11 -0
- package/types/server/rooms/constants.d.ts +5 -0
- package/types/server/utils.d.ts +26 -0
- package/publish.bat +0 -1
- package/server/Listeners.js +0 -199
package/client/index.js
CHANGED
|
@@ -1,429 +1,187 @@
|
|
|
1
|
-
|
|
1
|
+
import { ClientWebWorker } from './worker.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
constructor() {
|
|
5
|
-
Blob.prototype.toJSON = function () {
|
|
6
|
-
return {
|
|
7
|
-
b: new FileReaderSync().readAsDataURL(this),
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
ArrayBuffer.prototype.toJSON = function () {
|
|
11
|
-
return {
|
|
12
|
-
a: new FileReaderSync().readAsDataURL(this),
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
Object.assign(this, {
|
|
17
|
-
connid: this.generateId(),
|
|
18
|
-
|
|
19
|
-
connected: true,
|
|
20
|
-
reconnect: true,
|
|
21
|
-
isReconnect: false,
|
|
22
|
-
|
|
23
|
-
messages: [],
|
|
24
|
-
buffered: [],
|
|
25
|
-
|
|
26
|
-
onopen: this.onopen.bind(this),
|
|
27
|
-
onclose: this.onclose.bind(this),
|
|
28
|
-
onerror: this.onerror.bind(this),
|
|
29
|
-
onmessage: this.onmessage.bind(this),
|
|
30
|
-
|
|
31
|
-
postMessage: self.postMessage.bind(self),
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
self.onmessage = function (e) {
|
|
35
|
-
switch (e.data[0]) {
|
|
36
|
-
case 'emit': {
|
|
37
|
-
this.emit(e.data[1])
|
|
38
|
-
break
|
|
39
|
-
}
|
|
40
|
-
case 'connect': {
|
|
41
|
-
this.connect(e.data[1])
|
|
42
|
-
break
|
|
43
|
-
}
|
|
44
|
-
case 'disconnect': {
|
|
45
|
-
this.disconnect(e.data[1])
|
|
46
|
-
break
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}.bind(this)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
connect(options) {
|
|
53
|
-
if (
|
|
54
|
-
this.reconnect &&
|
|
55
|
-
(!this.io || this.io.readyState === WebSocket.CLOSED)
|
|
56
|
-
) {
|
|
57
|
-
let url = new URL(options.url)
|
|
58
|
-
|
|
59
|
-
// parms creation
|
|
60
|
-
let params = options.params
|
|
61
|
-
if (!params.connid) {
|
|
62
|
-
params.connid = this.connid
|
|
63
|
-
}
|
|
64
|
-
for (let [k, v] of url.searchParams.entries()) {
|
|
65
|
-
if (!params.hasOwnProperty(k)) {
|
|
66
|
-
params[k] = v
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
url.search = Object.entries(params)
|
|
70
|
-
.filter(([k, v]) => {
|
|
71
|
-
return !(
|
|
72
|
-
k === undefined ||
|
|
73
|
-
k === null ||
|
|
74
|
-
v === undefined ||
|
|
75
|
-
v === null
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
.map(
|
|
79
|
-
([k, v]) =>
|
|
80
|
-
encodeURIComponent(k) + '=' + encodeURIComponent(v),
|
|
81
|
-
)
|
|
82
|
-
.join('&')
|
|
83
|
-
|
|
84
|
-
url = url.toString()
|
|
85
|
-
|
|
86
|
-
// append buffered messages
|
|
87
|
-
let oURL = url
|
|
88
|
-
if (this.messages.length && !this.isReconnect) {
|
|
89
|
-
url =
|
|
90
|
-
(url.indexOf('?') === -1 ? url + '?' : url + '&') +
|
|
91
|
-
'angelia.io=' +
|
|
92
|
-
encodeURIComponent(JSON.stringify(this.messages))
|
|
93
|
-
if (url.length < 2048) {
|
|
94
|
-
this.messages = []
|
|
95
|
-
} else {
|
|
96
|
-
url = oURL
|
|
97
|
-
}
|
|
98
|
-
} else if (this.messages.length && this.isReconnect) {
|
|
99
|
-
for (let m of this.messages) {
|
|
100
|
-
// dont buffer callbacks, they are gone for new connections
|
|
101
|
-
if (m[0] !== '') this.buffered.push(m)
|
|
102
|
-
}
|
|
103
|
-
this.messages = []
|
|
104
|
-
}
|
|
105
|
-
this.io = new WebSocket(url)
|
|
106
|
-
Object.assign(this.io, {
|
|
107
|
-
onopen: this.onopen,
|
|
108
|
-
onclose: this.onclose,
|
|
109
|
-
onerror: this.onerror,
|
|
110
|
-
onmessage: this.onmessage,
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
disconnect(noReconnect) {
|
|
116
|
-
if (noReconnect) this.reconnect = false
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
this.io &&
|
|
120
|
-
this.io.readyState !== WebSocket.CLOSING &&
|
|
121
|
-
this.io.readyState !== WebSocket.CLOSED
|
|
122
|
-
) {
|
|
123
|
-
this.io.close()
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
onopen() {
|
|
128
|
-
this.connected = true
|
|
129
|
-
this.postMessage(['connected', true])
|
|
130
|
-
|
|
131
|
-
if (this.isReconnect) {
|
|
132
|
-
this.postMessage(['dispatch', 'reconnect'])
|
|
133
|
-
} else {
|
|
134
|
-
this.isReconnect = true
|
|
135
|
-
this.postMessage(['dispatch', 'connect'])
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
this.send()
|
|
139
|
-
}
|
|
140
|
-
onclose(event) {
|
|
141
|
-
switch (event.code) {
|
|
142
|
-
// normal close
|
|
143
|
-
case 1000:
|
|
144
|
-
/*console.log('ws - normal close', event.code, event.reason)*/
|
|
145
|
-
break
|
|
146
|
-
// closed by client
|
|
147
|
-
case 1005:
|
|
148
|
-
/*console.log(
|
|
149
|
-
'ws - we called socket.disconnect()',
|
|
150
|
-
event.code,
|
|
151
|
-
event.reason,
|
|
152
|
-
)*/
|
|
153
|
-
break
|
|
154
|
-
// closed by server or
|
|
155
|
-
// connection never opened and failed to connect
|
|
156
|
-
case 1006: {
|
|
157
|
-
/*console.log(
|
|
158
|
-
'ws - server killed the connection, or we failed to connect to server',
|
|
159
|
-
event.code,
|
|
160
|
-
event.reason,
|
|
161
|
-
)*/
|
|
162
|
-
break
|
|
163
|
-
}
|
|
164
|
-
default: {
|
|
165
|
-
console.log('ws - unknown close', event.code, event.reason)
|
|
166
|
-
break
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (this.connected) {
|
|
171
|
-
this.connected = false
|
|
172
|
-
this.postMessage(['connected', false])
|
|
173
|
-
this.postMessage(['dispatch', 'disconnect'])
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (this.reconnect) this.postMessage(['connect'])
|
|
177
|
-
}
|
|
178
|
-
// this happens when trying to connect while the server or
|
|
179
|
-
// the internet connection is down
|
|
180
|
-
onerror() {
|
|
181
|
-
if (this.connected) {
|
|
182
|
-
this.connected = false
|
|
183
|
-
this.postMessage(['connected', false])
|
|
184
|
-
this.postMessage(['dispatch', 'disconnect'])
|
|
185
|
-
}
|
|
3
|
+
const isFunction = v => typeof v === 'function'
|
|
186
4
|
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
onmessage(e) {
|
|
190
|
-
if (e.data === '') {
|
|
191
|
-
this.pong()
|
|
192
|
-
} else {
|
|
193
|
-
this.postMessage(['messages', JSON.parse(e.data)])
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
emit(messages) {
|
|
197
|
-
for (let m of messages) {
|
|
198
|
-
this.messages.push(m)
|
|
199
|
-
}
|
|
200
|
-
this.send()
|
|
201
|
-
}
|
|
202
|
-
send() {
|
|
203
|
-
if (this.io && this.io.readyState === WebSocket.OPEN) {
|
|
204
|
-
if (this.messages.length) {
|
|
205
|
-
let messages = this.messages
|
|
206
|
-
this.messages = []
|
|
207
|
-
this.io.send(JSON.stringify(messages))
|
|
208
|
-
}
|
|
209
|
-
if (this.buffered.length) {
|
|
210
|
-
let buffered = this.buffered
|
|
211
|
-
this.buffered = []
|
|
212
|
-
this.io.send(JSON.stringify(buffered))
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
pong() {
|
|
217
|
-
if (this.io && this.io.readyState === WebSocket.OPEN) {
|
|
218
|
-
this.io.send('')
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
generateId() {
|
|
222
|
-
var id = ''
|
|
223
|
-
while (!id) {
|
|
224
|
-
id = Math.random().toString(36).substr(2, 10)
|
|
225
|
-
}
|
|
226
|
-
return id
|
|
227
|
-
}
|
|
228
|
-
}
|
|
5
|
+
const error = console.error
|
|
229
6
|
|
|
230
7
|
export default class Client {
|
|
8
|
+
#callbacks
|
|
9
|
+
#listeners
|
|
10
|
+
#messages
|
|
11
|
+
#postMessage
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a Socket Client Instance
|
|
15
|
+
*
|
|
16
|
+
* @param options {string | {url:string, params?:Function,
|
|
17
|
+
* noConnect?:boolean }}
|
|
18
|
+
*/
|
|
231
19
|
constructor(options) {
|
|
232
|
-
if (
|
|
20
|
+
if (typeof options === 'string') {
|
|
233
21
|
options = {
|
|
234
22
|
url: options,
|
|
235
23
|
}
|
|
236
24
|
}
|
|
237
25
|
|
|
238
|
-
|
|
239
|
-
if (!options.url) {
|
|
240
|
-
options.url = protocol + '://' + location.hostname + ':3001'
|
|
241
|
-
} else if (options.url.indexOf('://') === -1) {
|
|
242
|
-
options.url = protocol + '://' + options.url
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const io = new Worker(
|
|
246
|
-
URL.createObjectURL(
|
|
247
|
-
new Blob(
|
|
248
|
-
["'use strict';new (" + ClientWebWorker.toString() + ')'],
|
|
249
|
-
{
|
|
250
|
-
type: 'application/javascript; charset=utf-8',
|
|
251
|
-
},
|
|
252
|
-
),
|
|
253
|
-
),
|
|
254
|
-
)
|
|
255
|
-
let shouldConnect =
|
|
256
|
-
options.dontConnect !== undefined ? !options.dontConnect : true
|
|
257
|
-
|
|
258
|
-
Object.assign(this, {
|
|
259
|
-
url: options.url,
|
|
260
|
-
params: options.params,
|
|
261
|
-
longLiveFlash: options.longLiveFlash,
|
|
262
|
-
shouldConnect: shouldConnect,
|
|
263
|
-
connected: shouldConnect,
|
|
264
|
-
messages: [],
|
|
265
|
-
listeners: {
|
|
266
|
-
'': [this.oncallback.bind(this)],
|
|
267
|
-
},
|
|
268
|
-
callbacks: [() => {}],
|
|
269
|
-
|
|
270
|
-
onworkermessage: this.onworkermessage.bind(this),
|
|
271
|
-
postMessage: io.postMessage.bind(io),
|
|
272
|
-
disconnect: this.disconnect.bind(this),
|
|
26
|
+
this.options = options
|
|
273
27
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
28
|
+
this.#callbacks = [null]
|
|
29
|
+
this.#listeners = {
|
|
30
|
+
'': [this.#oncallback],
|
|
31
|
+
}
|
|
32
|
+
this.#messages = []
|
|
277
33
|
|
|
278
|
-
|
|
34
|
+
this.connected = false
|
|
279
35
|
|
|
280
36
|
// to force disconnect
|
|
281
37
|
this.on('disconnect', this.disconnect)
|
|
282
38
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
39
|
+
const workerURL = URL.createObjectURL(
|
|
40
|
+
new Blob(['(' + ClientWebWorker + ')()']),
|
|
41
|
+
)
|
|
42
|
+
const io = new Worker(workerURL)
|
|
43
|
+
URL.revokeObjectURL(workerURL)
|
|
44
|
+
|
|
45
|
+
io.onmessage = this.#onworkermessage
|
|
46
|
+
this.#postMessage = io.postMessage.bind(io)
|
|
286
47
|
|
|
287
|
-
|
|
48
|
+
options.noConnect === undefined && this.connect()
|
|
49
|
+
}
|
|
288
50
|
|
|
51
|
+
/** Connect to server */
|
|
289
52
|
connect() {
|
|
290
|
-
this
|
|
53
|
+
this.#postMessage([
|
|
291
54
|
'connect',
|
|
292
55
|
{
|
|
293
|
-
url: this.url,
|
|
56
|
+
url: this.options.url,
|
|
294
57
|
params:
|
|
295
|
-
|
|
296
|
-
? this.params()
|
|
297
|
-
: {},
|
|
58
|
+
(isFunction(this.options.params)
|
|
59
|
+
? this.options.params()
|
|
60
|
+
: this.options.params) || {},
|
|
298
61
|
},
|
|
299
62
|
])
|
|
300
63
|
}
|
|
301
64
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Disconnect from server
|
|
67
|
+
*
|
|
68
|
+
* @param {boolean} noReconnect - To prevent reconnections
|
|
69
|
+
*/
|
|
70
|
+
disconnect = noReconnect => {
|
|
71
|
+
this.#postMessage(['disconnect', noReconnect])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Listen for data sent by the server
|
|
76
|
+
*
|
|
77
|
+
* @param {string} key
|
|
78
|
+
* @param {any | Function} [valueOrCallback]
|
|
79
|
+
* @param {Function} [callback]
|
|
80
|
+
* @returns {Promise<any> | undefined} When callback is provided it
|
|
81
|
+
* returns a promise
|
|
82
|
+
*/
|
|
83
|
+
emit(key, valueOrCallback, callback) {
|
|
84
|
+
// empty `key` is reserved
|
|
85
|
+
if (key == '') {
|
|
86
|
+
error('`key` cannot be empty')
|
|
320
87
|
} else {
|
|
321
|
-
this
|
|
88
|
+
if (!this.#messages.length) {
|
|
89
|
+
queueMicrotask(() => queueMicrotask(this.#send))
|
|
90
|
+
}
|
|
91
|
+
if (callback) {
|
|
92
|
+
const [i, promise] = this.#callback(callback)
|
|
93
|
+
this.#messages.push([key, valueOrCallback, i])
|
|
94
|
+
return promise
|
|
95
|
+
} else if (isFunction(valueOrCallback)) {
|
|
96
|
+
const [i, promise] = this.#callback(valueOrCallback)
|
|
97
|
+
this.#messages.push([key, 0, i])
|
|
98
|
+
return promise
|
|
99
|
+
} else if (valueOrCallback !== undefined) {
|
|
100
|
+
this.#messages.push([key, valueOrCallback])
|
|
101
|
+
} else {
|
|
102
|
+
this.#messages.push([key])
|
|
103
|
+
}
|
|
322
104
|
}
|
|
323
105
|
}
|
|
324
106
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
off(k, v) {
|
|
339
|
-
if (!this.listeners[k]) {
|
|
340
|
-
console.error(
|
|
341
|
-
'ws - socket.off("' + k + '", callback)',
|
|
342
|
-
k,
|
|
343
|
-
'key not found',
|
|
344
|
-
)
|
|
107
|
+
/**
|
|
108
|
+
* Listen for data sent by the server
|
|
109
|
+
*
|
|
110
|
+
* @param {string} key
|
|
111
|
+
* @param {Function} value
|
|
112
|
+
* @returns () => void | undefined - `off` function to remove the
|
|
113
|
+
* listener
|
|
114
|
+
*/
|
|
115
|
+
on(key, value) {
|
|
116
|
+
// empty `key` is reserved
|
|
117
|
+
if (key == '') {
|
|
118
|
+
error('`key` cannot be empty')
|
|
345
119
|
} else {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
'ws - socket.off("' + k + '", callback)',
|
|
350
|
-
v,
|
|
351
|
-
'callback not found',
|
|
352
|
-
)
|
|
353
|
-
} else {
|
|
354
|
-
this.listeners[k].splice(i, 1)
|
|
355
|
-
}
|
|
120
|
+
this.#listeners[key] = this.#listeners[key] || []
|
|
121
|
+
this.#listeners[key].push(value)
|
|
122
|
+
return () => this.#off(key, value)
|
|
356
123
|
}
|
|
357
124
|
}
|
|
358
125
|
|
|
359
126
|
// PRIVATE API
|
|
360
127
|
|
|
361
|
-
|
|
128
|
+
#off(k, v) {
|
|
129
|
+
const i = this.#listeners[k].indexOf(v)
|
|
130
|
+
if (i !== -1) {
|
|
131
|
+
this.#listeners[k].splice(i, 1)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @returns {[number, Promise<any>]} */
|
|
136
|
+
#callback(c) {
|
|
362
137
|
let resolve
|
|
363
138
|
const promise = new Promise(r => (resolve = r))
|
|
364
|
-
|
|
365
|
-
this
|
|
366
|
-
return
|
|
367
|
-
}
|
|
368
|
-
oncallback
|
|
369
|
-
this
|
|
370
|
-
this
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
})
|
|
390
|
-
} else {
|
|
391
|
-
for (let e of d) {
|
|
392
|
-
if (this.listeners[e[0]]) {
|
|
393
|
-
for (let fn of this.listeners[e[0]]) {
|
|
394
|
-
fn(
|
|
395
|
-
e[1],
|
|
396
|
-
e[2]
|
|
397
|
-
? (...d) => {
|
|
398
|
-
this.emit('', [e[2], d])
|
|
399
|
-
}
|
|
400
|
-
: null,
|
|
401
|
-
)
|
|
402
|
-
}
|
|
139
|
+
const i = this.#callbacks.length
|
|
140
|
+
this.#callbacks[i] = args => resolve(c(...args))
|
|
141
|
+
return [i, promise]
|
|
142
|
+
}
|
|
143
|
+
#oncallback = d => {
|
|
144
|
+
this.#callbacks[d[0]](d[1])
|
|
145
|
+
this.#callbacks[d[0]] = null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#dispatch(d) {
|
|
149
|
+
for (const e of d) {
|
|
150
|
+
if (this.#listeners[e[0]]) {
|
|
151
|
+
for (const fn of this.#listeners[e[0]]) {
|
|
152
|
+
fn(
|
|
153
|
+
e[1],
|
|
154
|
+
e[2]
|
|
155
|
+
? (...d) => {
|
|
156
|
+
this.emit('', [e[2], d])
|
|
157
|
+
}
|
|
158
|
+
: undefined,
|
|
159
|
+
)
|
|
403
160
|
}
|
|
404
161
|
}
|
|
405
162
|
}
|
|
406
163
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this
|
|
164
|
+
|
|
165
|
+
#send = () => {
|
|
166
|
+
if (this.#messages.length) {
|
|
167
|
+
const messages = this.#messages
|
|
168
|
+
this.#messages = []
|
|
169
|
+
this.#postMessage(['emit', messages])
|
|
412
170
|
}
|
|
413
171
|
}
|
|
414
172
|
|
|
415
|
-
onworkermessage
|
|
173
|
+
#onworkermessage = e => {
|
|
416
174
|
switch (e.data[0]) {
|
|
417
175
|
case 'messages': {
|
|
418
|
-
this
|
|
176
|
+
this.#dispatch(e.data[1])
|
|
419
177
|
break
|
|
420
178
|
}
|
|
421
179
|
case 'dispatch': {
|
|
422
|
-
this
|
|
180
|
+
this.#dispatch([[e.data[1], e.data[2]]])
|
|
423
181
|
break
|
|
424
182
|
}
|
|
425
183
|
case 'connect': {
|
|
426
|
-
|
|
184
|
+
this.connect()
|
|
427
185
|
break
|
|
428
186
|
}
|
|
429
187
|
case 'connected': {
|
|
@@ -432,6 +190,7 @@ export default class Client {
|
|
|
432
190
|
}
|
|
433
191
|
}
|
|
434
192
|
}
|
|
193
|
+
|
|
435
194
|
decode(s) {
|
|
436
195
|
if (s.b) {
|
|
437
196
|
return fetch(s.b).then(r => r.blob())
|