@sigx/lynx-websocket 0.1.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,429 @@
1
+ /**
2
+ * Browser-standard `WebSocket` client backed by the `@sigx/lynx-websocket`
3
+ * native module (URLSessionWebSocketTask on iOS, OkHttp WebSocket on
4
+ * Android).
5
+ *
6
+ * Public surface mirrors the WHATWG WebSocket interface:
7
+ *
8
+ * new WebSocket(url, protocols?)
9
+ * .readyState / .url / .protocol / .extensions / .bufferedAmount
10
+ * .binaryType ('arraybuffer' only — 'blob' is not supported)
11
+ * .onopen / .onmessage / .onerror / .onclose
12
+ * .addEventListener / .removeEventListener / .dispatchEvent
13
+ * .send(string | ArrayBuffer | ArrayBufferView)
14
+ * .close(code?, reason?)
15
+ *
16
+ * Multi-socket dispatch: each instance is assigned a monotonic numeric id.
17
+ * The native side emits a single `__sigxWebSocketEvent` global event
18
+ * carrying `{ id, type, ... }`; the JS shim demultiplexes by id and fires
19
+ * the matching instance's listeners.
20
+ */
21
+ import { callAsync, guardModule, isModuleAvailable } from '@sigx/lynx-core';
22
+ const MODULE = 'WebSocket';
23
+ const EVENT_NAME = '__sigxWebSocketEvent';
24
+ function lynxObj() {
25
+ return typeof lynx !== 'undefined' ? lynx : undefined;
26
+ }
27
+ const CONNECTING = 0;
28
+ const OPEN = 1;
29
+ const CLOSING = 2;
30
+ const CLOSED = 3;
31
+ /**
32
+ * Decode a base64 string into an `ArrayBuffer`. Lynx's BTS runtime has
33
+ * `atob` per the platform docs, but fall back to a manual decoder to keep
34
+ * this shim portable across hosts where it might be absent.
35
+ */
36
+ function base64ToArrayBuffer(b64) {
37
+ if (typeof atob === 'function') {
38
+ const bin = atob(b64);
39
+ const buf = new ArrayBuffer(bin.length);
40
+ const view = new Uint8Array(buf);
41
+ for (let i = 0; i < bin.length; i++)
42
+ view[i] = bin.charCodeAt(i);
43
+ return buf;
44
+ }
45
+ // Pure-JS fallback. Not the fastest, but executed at most for binary
46
+ // frames on hosts that ship no atob (vanishingly rare).
47
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
48
+ const lookup = new Int8Array(256).fill(-1);
49
+ for (let i = 0; i < chars.length; i++)
50
+ lookup[chars.charCodeAt(i)] = i;
51
+ const clean = b64.replace(/=+$/, '');
52
+ const out = new Uint8Array((clean.length * 3) >> 2);
53
+ let p = 0;
54
+ let buf = 0;
55
+ let bits = 0;
56
+ for (let i = 0; i < clean.length; i++) {
57
+ const v = lookup[clean.charCodeAt(i)];
58
+ if (v < 0)
59
+ continue;
60
+ buf = (buf << 6) | v;
61
+ bits += 6;
62
+ if (bits >= 8) {
63
+ bits -= 8;
64
+ out[p++] = (buf >> bits) & 0xff;
65
+ }
66
+ }
67
+ return out.buffer;
68
+ }
69
+ /**
70
+ * Encode an `ArrayBuffer` / view to base64 for transport to native.
71
+ * Native side base64-decodes back to raw bytes before sending on the wire.
72
+ */
73
+ function arrayBufferToBase64(buf) {
74
+ const bytes = new Uint8Array(buf);
75
+ if (typeof btoa === 'function') {
76
+ // Build the binary string in chunks to dodge call-stack limits on
77
+ // large frames (String.fromCharCode.apply blows up around ~64k args).
78
+ let bin = '';
79
+ const CHUNK = 0x8000;
80
+ for (let i = 0; i < bytes.length; i += CHUNK) {
81
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK)));
82
+ }
83
+ return btoa(bin);
84
+ }
85
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
86
+ let out = '';
87
+ let i = 0;
88
+ for (; i + 2 < bytes.length; i += 3) {
89
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
90
+ out += chars[(n >> 18) & 63] + chars[(n >> 12) & 63] + chars[(n >> 6) & 63] + chars[n & 63];
91
+ }
92
+ if (i < bytes.length) {
93
+ const rem = bytes.length - i;
94
+ const n = rem === 1 ? bytes[i] << 16 : (bytes[i] << 16) | (bytes[i + 1] << 8);
95
+ out += chars[(n >> 18) & 63] + chars[(n >> 12) & 63];
96
+ out += rem === 2 ? chars[(n >> 6) & 63] + '=' : '==';
97
+ }
98
+ return out;
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Shared event dispatch — one global lynx listener that demuxes by socket id.
102
+ const sockets = new Map();
103
+ let nextId = 1;
104
+ let subscribed = false;
105
+ let cachedEmitter = null;
106
+ function ensureSubscribed() {
107
+ if (subscribed)
108
+ return;
109
+ const emitter = lynxObj()?.getJSModule?.('GlobalEventEmitter');
110
+ if (!emitter)
111
+ return; // web/SSR/test — events simply won't arrive
112
+ cachedEmitter = emitter;
113
+ emitter.addListener(EVENT_NAME, (raw) => {
114
+ // Lynx ships event params as a single JSON-shaped object or as the
115
+ // first arg of the listener. Tolerate both shapes.
116
+ const evt = typeof raw === 'string' ? safeParse(raw) : raw;
117
+ if (!evt || typeof evt.id !== 'number')
118
+ return;
119
+ const ws = sockets.get(evt.id);
120
+ if (!ws)
121
+ return;
122
+ // Internal dispatch lives on the instance so it can mutate state.
123
+ ws._dispatch(evt);
124
+ });
125
+ subscribed = true;
126
+ }
127
+ function safeParse(s) {
128
+ try {
129
+ return JSON.parse(s);
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ /**
137
+ * WHATWG-compatible WebSocket. Drop-in for browser code.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const ws = new WebSocket('wss://ws.postman-echo.com/raw');
142
+ * ws.onopen = () => ws.send('hello');
143
+ * ws.onmessage = e => console.log(e.data);
144
+ * ```
145
+ */
146
+ export class WebSocket {
147
+ get readyState() {
148
+ return this._readyState;
149
+ }
150
+ constructor(url, protocols) {
151
+ this.CONNECTING = CONNECTING;
152
+ this.OPEN = OPEN;
153
+ this.CLOSING = CLOSING;
154
+ this.CLOSED = CLOSED;
155
+ this.protocol = '';
156
+ this.extensions = '';
157
+ this.bufferedAmount = 0;
158
+ this.binaryType = 'arraybuffer';
159
+ this.onopen = null;
160
+ this.onmessage = null;
161
+ this.onerror = null;
162
+ this.onclose = null;
163
+ this._readyState = CONNECTING;
164
+ this._listeners = Object.create(null);
165
+ if (typeof url !== 'string' || url.length === 0) {
166
+ throw new TypeError(`WebSocket: invalid URL`);
167
+ }
168
+ // Match browsers: only ws:/wss: are valid. We accept http:/https: too
169
+ // and let the native side reject — some debug proxies normalise.
170
+ const colon = url.indexOf(':');
171
+ if (colon <= 0) {
172
+ throw new SyntaxError(`WebSocket: invalid URL "${url}"`);
173
+ }
174
+ const scheme = url.slice(0, colon).toLowerCase();
175
+ if (scheme !== 'ws' && scheme !== 'wss' && scheme !== 'http' && scheme !== 'https') {
176
+ throw new SyntaxError(`WebSocket: unsupported URL scheme "${scheme}"`);
177
+ }
178
+ guardModule(MODULE);
179
+ this.url = url;
180
+ this._id = nextId++;
181
+ sockets.set(this._id, this);
182
+ ensureSubscribed();
183
+ const protoList = Array.isArray(protocols)
184
+ ? protocols
185
+ : typeof protocols === 'string' && protocols.length > 0
186
+ ? [protocols]
187
+ : [];
188
+ // Fire-and-forget — open/error are delivered through the event
189
+ // channel, not the callback. We still surface synchronous bridge
190
+ // failures (e.g. module not registered) as an async error event.
191
+ callAsync(MODULE, 'create', this._id, url, protoList).catch(err => {
192
+ this._dispatch({
193
+ id: this._id,
194
+ type: 'error',
195
+ data: err instanceof Error ? err.message : String(err),
196
+ });
197
+ this._dispatch({
198
+ id: this._id,
199
+ type: 'close',
200
+ code: 1006,
201
+ reason: '',
202
+ wasClean: false,
203
+ });
204
+ });
205
+ }
206
+ send(data) {
207
+ if (this._readyState === CONNECTING) {
208
+ // Browsers throw InvalidStateError here.
209
+ throw new Error("InvalidStateError: WebSocket is still in CONNECTING state.");
210
+ }
211
+ if (this._readyState !== OPEN) {
212
+ // Browsers silently drop on CLOSING/CLOSED but warn in devtools.
213
+ return;
214
+ }
215
+ let isBinary = false;
216
+ let payload;
217
+ if (typeof data === 'string') {
218
+ payload = data;
219
+ }
220
+ else if (data instanceof ArrayBuffer) {
221
+ isBinary = true;
222
+ payload = arrayBufferToBase64(data);
223
+ }
224
+ else if (ArrayBuffer.isView(data)) {
225
+ isBinary = true;
226
+ const view = data;
227
+ // Copy the active range out of the underlying buffer so we don't
228
+ // accidentally send bytes outside the view.
229
+ const slice = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
230
+ payload = arrayBufferToBase64(slice);
231
+ }
232
+ else {
233
+ throw new TypeError('WebSocket.send: unsupported data type');
234
+ }
235
+ // bufferedAmount is approximated as the byte length the JS side has
236
+ // handed off — the native side acks via 'flushed' frames in a future
237
+ // version; for now this is a write-through counter.
238
+ this.bufferedAmount += isBinary ? base64ByteLength(payload) : utf8ByteLength(payload);
239
+ callAsync(MODULE, 'send', this._id, payload, isBinary).catch(err => {
240
+ this._dispatch({
241
+ id: this._id,
242
+ type: 'error',
243
+ data: err instanceof Error ? err.message : String(err),
244
+ });
245
+ });
246
+ }
247
+ close(code, reason) {
248
+ if (this._readyState === CLOSING || this._readyState === CLOSED)
249
+ return;
250
+ // WHATWG: code must be 1000 or 3000–4999. Validate to mirror browsers.
251
+ if (code !== undefined) {
252
+ if (code !== 1000 && (code < 3000 || code > 4999)) {
253
+ throw new Error(`InvalidAccessError: close code ${code} must be 1000 or in the 3000-4999 range.`);
254
+ }
255
+ }
256
+ if (reason !== undefined && utf8ByteLength(reason) > 123) {
257
+ throw new SyntaxError('SyntaxError: close reason must be ≤123 UTF-8 bytes.');
258
+ }
259
+ this._readyState = CLOSING;
260
+ callAsync(MODULE, 'close', this._id, code ?? 1000, reason ?? '').catch(err => {
261
+ // Bridge call itself failed (e.g. module missing). Synthesize an
262
+ // abnormal close so the instance doesn't get stuck in CLOSING and
263
+ // we still clean up sockets state.
264
+ this._dispatch({
265
+ id: this._id,
266
+ type: 'error',
267
+ data: err instanceof Error ? err.message : String(err),
268
+ });
269
+ this._dispatch({
270
+ id: this._id,
271
+ type: 'close',
272
+ code: 1006,
273
+ reason: '',
274
+ wasClean: false,
275
+ });
276
+ });
277
+ }
278
+ // -- EventTarget ---------------------------------------------------------
279
+ addEventListener(type, listener) {
280
+ var _a;
281
+ if (!listener)
282
+ return;
283
+ ((_a = this._listeners)[type] ?? (_a[type] = new Set())).add(listener);
284
+ }
285
+ removeEventListener(type, listener) {
286
+ this._listeners[type]?.delete(listener);
287
+ }
288
+ dispatchEvent(event) {
289
+ this._invoke(event.type, event);
290
+ return true;
291
+ }
292
+ // -- Internal ------------------------------------------------------------
293
+ /** @internal — called by the shared global-event subscriber. */
294
+ _dispatch(evt) {
295
+ switch (evt.type) {
296
+ case 'open': {
297
+ this._readyState = OPEN;
298
+ if (typeof evt.protocol === 'string')
299
+ this.protocol = evt.protocol;
300
+ if (typeof evt.extensions === 'string')
301
+ this.extensions = evt.extensions;
302
+ this._invoke('open', {
303
+ type: 'open',
304
+ target: this,
305
+ currentTarget: this,
306
+ });
307
+ break;
308
+ }
309
+ case 'message': {
310
+ if (this._readyState !== OPEN)
311
+ return;
312
+ let data;
313
+ if (evt.isBinary && typeof evt.binary === 'string') {
314
+ data = base64ToArrayBuffer(evt.binary);
315
+ }
316
+ else {
317
+ data = evt.data ?? '';
318
+ }
319
+ this._invoke('message', {
320
+ type: 'message',
321
+ target: this,
322
+ currentTarget: this,
323
+ data,
324
+ });
325
+ break;
326
+ }
327
+ case 'error': {
328
+ this._invoke('error', {
329
+ type: 'error',
330
+ target: this,
331
+ currentTarget: this,
332
+ message: evt.data,
333
+ });
334
+ break;
335
+ }
336
+ case 'close': {
337
+ if (this._readyState === CLOSED)
338
+ return;
339
+ this._readyState = CLOSED;
340
+ sockets.delete(this._id);
341
+ this._invoke('close', {
342
+ type: 'close',
343
+ target: this,
344
+ currentTarget: this,
345
+ code: evt.code ?? 1006,
346
+ reason: evt.reason ?? '',
347
+ wasClean: evt.wasClean ?? false,
348
+ });
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ _invoke(type, event) {
354
+ const handler = this[`on${type}`];
355
+ if (typeof handler === 'function') {
356
+ try {
357
+ handler.call(this, event);
358
+ }
359
+ catch (e) {
360
+ console.warn(`[WebSocket] on${type} handler threw:`, e);
361
+ }
362
+ }
363
+ const set = this._listeners[type];
364
+ if (set) {
365
+ for (const listener of set) {
366
+ try {
367
+ if (typeof listener === 'function')
368
+ listener.call(this, event);
369
+ else
370
+ listener.handleEvent(event);
371
+ }
372
+ catch (e) {
373
+ console.warn(`[WebSocket] '${type}' listener threw:`, e);
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ WebSocket.CONNECTING = CONNECTING;
380
+ WebSocket.OPEN = OPEN;
381
+ WebSocket.CLOSING = CLOSING;
382
+ WebSocket.CLOSED = CLOSED;
383
+ /** Whether the native WebSocket module is registered in this build. */
384
+ export function isWebSocketAvailable() {
385
+ return isModuleAvailable(MODULE);
386
+ }
387
+ // ---------------------------------------------------------------------------
388
+ // Byte-length helpers (kept local to avoid pulling a dep).
389
+ function utf8ByteLength(s) {
390
+ let n = 0;
391
+ for (let i = 0; i < s.length; i++) {
392
+ const c = s.charCodeAt(i);
393
+ if (c < 0x80)
394
+ n += 1;
395
+ else if (c < 0x800)
396
+ n += 2;
397
+ else if (c >= 0xd800 && c <= 0xdbff) {
398
+ n += 4;
399
+ i++; // surrogate pair
400
+ }
401
+ else
402
+ n += 3;
403
+ }
404
+ return n;
405
+ }
406
+ function base64ByteLength(b64) {
407
+ const padding = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0;
408
+ return ((b64.length * 3) >> 2) - padding;
409
+ }
410
+ // Test-only escape hatch: exported under a stable name with a leading
411
+ // underscore so it's tree-shakeable but reachable from unit tests.
412
+ /** @internal */
413
+ export const __internal = {
414
+ deliver(evt) {
415
+ const ws = sockets.get(evt.id);
416
+ if (ws)
417
+ ws._dispatch(evt);
418
+ },
419
+ reset() {
420
+ sockets.clear();
421
+ nextId = 1;
422
+ subscribed = false;
423
+ cachedEmitter = null;
424
+ },
425
+ get cachedEmitter() {
426
+ return cachedEmitter;
427
+ },
428
+ };
429
+ //# sourceMappingURL=websocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ import Foundation
2
+
3
+ /// Process-wide pub/sub bus that owns the conversion of native WebSocket
4
+ /// callbacks into JS-side event payloads. Sockets, sessions, and OS
5
+ /// delegates write here; per-LynxView `WebSocketPublisher` instances read.
6
+ ///
7
+ /// Mirrors the publisher pattern used elsewhere in the repo
8
+ /// (`LinkingState`, `SafeAreaPublisher`, …): native lifecycle is decoupled
9
+ /// from any specific LynxView so events survive view recreation and
10
+ /// per-view subscribers can fan out without holding onto the WS task.
11
+ final class WebSocketEventBus {
12
+
13
+ static let shared = WebSocketEventBus()
14
+
15
+ /// JSON-encodable payload pushed to JS as the single
16
+ /// `__sigxWebSocketEvent` global event. Keys match the `NativeEvent`
17
+ /// interface in `src/websocket.ts`.
18
+ typealias Payload = [String: Any]
19
+ typealias Listener = (Payload) -> Void
20
+
21
+ private let queue = DispatchQueue(label: "com.sigx.websocket.bus")
22
+ private var listeners: [(token: UUID, fn: Listener)] = []
23
+
24
+ @discardableResult
25
+ func addListener(_ fn: @escaping Listener) -> UUID {
26
+ let token = UUID()
27
+ queue.sync { listeners.append((token, fn)) }
28
+ return token
29
+ }
30
+
31
+ func removeListener(_ token: UUID) {
32
+ queue.sync { listeners.removeAll { $0.token == token } }
33
+ }
34
+
35
+ // MARK: - Publish
36
+
37
+ func publish(open protocolStr: String, extensions: String, id: Int) {
38
+ emit([
39
+ "id": id,
40
+ "type": "open",
41
+ "protocol": protocolStr,
42
+ "extensions": extensions,
43
+ ])
44
+ }
45
+
46
+ func publish(messageText text: String, id: Int) {
47
+ emit([
48
+ "id": id,
49
+ "type": "message",
50
+ "data": text,
51
+ "isBinary": false,
52
+ ])
53
+ }
54
+
55
+ func publish(messageBinary base64: String, id: Int) {
56
+ emit([
57
+ "id": id,
58
+ "type": "message",
59
+ "binary": base64,
60
+ "isBinary": true,
61
+ ])
62
+ }
63
+
64
+ func publish(error message: String, id: Int) {
65
+ emit([
66
+ "id": id,
67
+ "type": "error",
68
+ "data": message,
69
+ ])
70
+ }
71
+
72
+ func publish(close code: Int, reason: String, wasClean: Bool, id: Int) {
73
+ emit([
74
+ "id": id,
75
+ "type": "close",
76
+ "code": code,
77
+ "reason": reason,
78
+ "wasClean": wasClean,
79
+ ])
80
+ }
81
+
82
+ private func emit(_ payload: Payload) {
83
+ let snapshot = queue.sync { listeners }
84
+ for (_, fn) in snapshot { fn(payload) }
85
+ }
86
+ }
@@ -0,0 +1,89 @@
1
+ import Foundation
2
+ import Lynx
3
+
4
+ /// Native WebSocket bridge — JS-callable side.
5
+ ///
6
+ /// JS usage (via the `@sigx/lynx-websocket` shim, not directly):
7
+ ///
8
+ /// NativeModules.WebSocket.create(id, url, protocols, cb)
9
+ /// NativeModules.WebSocket.send(id, payload, isBinary, cb)
10
+ /// NativeModules.WebSocket.close(id, code, reason, cb)
11
+ ///
12
+ /// Async lifecycle events (`open`, `message`, `error`, `close`) are pushed
13
+ /// back via `WebSocketEventBus`, which a per-LynxView `WebSocketPublisher`
14
+ /// forwards to JS through `LynxView.sendGlobalEvent("__sigxWebSocketEvent",
15
+ /// [...])`.
16
+ ///
17
+ /// Implemented with `URLSessionWebSocketTask` (iOS 13+). Each socket is
18
+ /// stored in `tasks` keyed by the JS-supplied numeric id; the same id is
19
+ /// echoed back in every event so the JS shim can demultiplex.
20
+ @objc class WebSocketModule: NSObject, LynxModule {
21
+
22
+ @objc static var name: String { "WebSocket" }
23
+
24
+ @objc static var methodLookup: [String: String] {
25
+ [
26
+ "create": NSStringFromSelector(#selector(create(_:url:protocols:callback:))),
27
+ "send": NSStringFromSelector(#selector(send(_:payload:isBinary:callback:))),
28
+ "close": NSStringFromSelector(#selector(close(_:code:reason:callback:))),
29
+ ]
30
+ }
31
+
32
+ required override init() { super.init() }
33
+ required init(param: Any) { super.init() }
34
+
35
+ // MARK: - JS-callable methods
36
+
37
+ @objc func create(_ id: NSNumber, url: String?, protocols: [Any]?, callback: LynxCallbackBlock?) {
38
+ guard let urlString = url, let parsed = URL(string: urlString) else {
39
+ WebSocketEventBus.shared.publish(error: "Invalid URL", id: id.intValue)
40
+ WebSocketEventBus.shared.publish(close: 1006, reason: "Invalid URL", wasClean: false, id: id.intValue)
41
+ callback?(NSNull())
42
+ return
43
+ }
44
+
45
+ var request = URLRequest(url: parsed)
46
+ if let protos = protocols as? [String], !protos.isEmpty {
47
+ request.setValue(protos.joined(separator: ", "), forHTTPHeaderField: "Sec-WebSocket-Protocol")
48
+ }
49
+
50
+ WebSocketTaskStore.shared.create(id: id.intValue, request: request)
51
+ callback?(NSNull())
52
+ }
53
+
54
+ @objc func send(_ id: NSNumber, payload: String?, isBinary: NSNumber?, callback: LynxCallbackBlock?) {
55
+ let binary = (isBinary?.boolValue ?? false)
56
+ guard let task = WebSocketTaskStore.shared.task(forId: id.intValue) else {
57
+ callback?(["error": "WebSocket \(id) not found"])
58
+ return
59
+ }
60
+
61
+ let message: URLSessionWebSocketTask.Message
62
+ if binary {
63
+ // Payload is base64-encoded on the JS side — decode to raw bytes.
64
+ guard let data = Data(base64Encoded: payload ?? "") else {
65
+ callback?(["error": "Invalid base64 payload"])
66
+ return
67
+ }
68
+ message = .data(data)
69
+ } else {
70
+ message = .string(payload ?? "")
71
+ }
72
+
73
+ task.send(message) { error in
74
+ if let error = error {
75
+ WebSocketEventBus.shared.publish(error: error.localizedDescription, id: id.intValue)
76
+ }
77
+ }
78
+ callback?(NSNull())
79
+ }
80
+
81
+ @objc func close(_ id: NSNumber, code: NSNumber?, reason: String?, callback: LynxCallbackBlock?) {
82
+ WebSocketTaskStore.shared.close(
83
+ id: id.intValue,
84
+ code: code?.intValue ?? 1000,
85
+ reason: reason ?? ""
86
+ )
87
+ callback?(NSNull())
88
+ }
89
+ }
@@ -0,0 +1,34 @@
1
+ import Foundation
2
+ import Lynx
3
+
4
+ /// Per-`LynxView` publisher that pumps `WebSocketEventBus` payloads into JS
5
+ /// via `LynxView.sendGlobalEvent("__sigxWebSocketEvent", [...])`.
6
+ ///
7
+ /// One instance per LynxView; instantiated by the generated
8
+ /// `GeneratedLifecyclePublishers.attachAll(to:)` and retained for the
9
+ /// LynxView's lifetime. The bus is global so opening a socket from one
10
+ /// LynxView and reading it from another (via the JS shim) works, but in
11
+ /// practice each LynxView holds its own JS heap so events are delivered to
12
+ /// the matching view only.
13
+ final class WebSocketPublisher {
14
+
15
+ private weak var lynxView: LynxView?
16
+ private var token: UUID?
17
+
18
+ init(lynxView: LynxView) {
19
+ self.lynxView = lynxView
20
+ self.token = WebSocketEventBus.shared.addListener { [weak self] payload in
21
+ guard let view = self?.lynxView else { return }
22
+ // sendGlobalEvent expects an array of params; pass a single
23
+ // dictionary as the only param, matching what the JS shim
24
+ // expects (it reads the first arg as `NativeEvent`).
25
+ view.sendGlobalEvent("__sigxWebSocketEvent", withParams: [payload])
26
+ }
27
+ }
28
+
29
+ deinit {
30
+ if let token = token {
31
+ WebSocketEventBus.shared.removeListener(token)
32
+ }
33
+ }
34
+ }