webspresso 0.0.90 → 0.0.93

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.
Files changed (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Redis Pub/Sub Distributed Adapter for Webspresso Realtime
3
+ * Enables cross-instance realtime broadcasting across PM2 clusters, Docker containers, and Kubernetes pods.
4
+ * @module core/realtime/adapters/redis
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+
9
+ /**
10
+ * Default Redis message envelope serializer
11
+ */
12
+ function defaultSerialize(envelope) {
13
+ return JSON.stringify(envelope);
14
+ }
15
+
16
+ /**
17
+ * Default Redis message envelope deserializer
18
+ */
19
+ function defaultDeserialize(raw) {
20
+ try {
21
+ return JSON.parse(raw);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * In-memory distributed bus singleton for fallback / local multi-client simulation
29
+ */
30
+ class InMemoryDistributedBus {
31
+ constructor() {
32
+ this._channels = new Map();
33
+ }
34
+
35
+ subscribe(channel, callback) {
36
+ if (!this._channels.has(channel)) {
37
+ this._channels.set(channel, new Set());
38
+ }
39
+ this._channels.get(channel).add(callback);
40
+ return () => {
41
+ const set = this._channels.get(channel);
42
+ if (set) {
43
+ set.delete(callback);
44
+ if (set.size === 0) {
45
+ this._channels.delete(channel);
46
+ }
47
+ }
48
+ };
49
+ }
50
+
51
+ publish(channel, rawMessage) {
52
+ const set = this._channels.get(channel);
53
+ if (set) {
54
+ for (const cb of Array.from(set)) {
55
+ try {
56
+ cb(channel, rawMessage);
57
+ } catch {
58
+ // ignore receiver errors
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ clear() {
65
+ this._channels.clear();
66
+ }
67
+ }
68
+
69
+ const globalInMemoryBus = new InMemoryDistributedBus();
70
+
71
+ class RedisAdapter {
72
+ /**
73
+ * @param {Object} options
74
+ * @param {Object} [options.pubClient] - Redis client for publishing
75
+ * @param {Object} [options.subClient] - Dedicated Redis client for subscribing
76
+ * @param {Object|Function} [options.redis] - Redis client instance with .duplicate() or factory
77
+ * @param {string} [options.channelPrefix='webspresso:rt:'] - Redis channel prefix
78
+ * @param {string} [options.nodeId] - Unique cluster node identifier
79
+ * @param {Function} [options.serialize] - Custom serializer
80
+ * @param {Function} [options.deserialize] - Custom deserializer
81
+ * @param {boolean} [options.inMemory=false] - Force in-memory distributed bus fallback
82
+ * @param {Object} [options.capabilities] - Custom capabilities override
83
+ */
84
+ constructor(options = {}) {
85
+ this.options = options;
86
+ this.capabilities = {
87
+ send: true,
88
+ perform: true,
89
+ multiplexing: true,
90
+ serverEvents: true,
91
+ distributed: true,
92
+ binary: false,
93
+ ...(options.capabilities || {}),
94
+ };
95
+
96
+ this.channelPrefix = options.channelPrefix || 'webspresso:rt:';
97
+ this.nodeId = options.nodeId || `node_${crypto.randomBytes(6).toString('hex')}`;
98
+ this.serialize = options.serialize || defaultSerialize;
99
+ this.deserialize = options.deserialize || defaultDeserialize;
100
+
101
+ this.pubClient = options.pubClient || null;
102
+ this.subClient = options.subClient || null;
103
+ this._managedClients = false;
104
+
105
+ this._isOpen = false;
106
+ this._listeners = new Map();
107
+ this._channelSubscriptions = new Map(); // fullChannelName -> Set<{ callbacks, params, identifier }>
108
+ this._inMemoryBus = options.inMemoryBus || globalInMemoryBus;
109
+ this._inMemoryUnsubscribers = new Map(); // fullChannelName -> unsubFn
110
+ this._useInMemory = Boolean(options.inMemory || (!this.pubClient && !this.subClient && !options.redis));
111
+ }
112
+
113
+ get isInMemory() {
114
+ return this._useInMemory;
115
+ }
116
+
117
+ on(event, handler) {
118
+ if (!this._listeners.has(event)) {
119
+ this._listeners.set(event, new Set());
120
+ }
121
+ this._listeners.get(event).add(handler);
122
+ return () => this.off(event, handler);
123
+ }
124
+
125
+ off(event, handler) {
126
+ const set = this._listeners.get(event);
127
+ if (set) {
128
+ set.delete(handler);
129
+ }
130
+ }
131
+
132
+ _emit(event, ...args) {
133
+ const set = this._listeners.get(event);
134
+ if (set) {
135
+ for (const handler of Array.from(set)) {
136
+ try {
137
+ handler(...args);
138
+ } catch {
139
+ // ignore handler errors
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ _resolveChannel(identifier) {
146
+ return `${this.channelPrefix}${identifier}`;
147
+ }
148
+
149
+ async connect(context = {}) {
150
+ if (this._isOpen) {
151
+ return;
152
+ }
153
+
154
+ try {
155
+ if (!this._useInMemory) {
156
+ // If a single redis instance with .duplicate() was passed
157
+ if (!this.pubClient && !this.subClient && this.options.redis) {
158
+ if (typeof this.options.redis === 'function') {
159
+ this.pubClient = await this.options.redis('pub');
160
+ this.subClient = await this.options.redis('sub');
161
+ this._managedClients = true;
162
+ } else if (typeof this.options.redis.duplicate === 'function') {
163
+ this.pubClient = this.options.redis;
164
+ this.subClient = this.options.redis.duplicate();
165
+ this._managedClients = true;
166
+ } else {
167
+ this.pubClient = this.options.redis;
168
+ this.subClient = this.options.redis;
169
+ }
170
+ }
171
+
172
+ // Connect if clients have .connect() method (e.g. node-redis v4)
173
+ if (this.pubClient && typeof this.pubClient.connect === 'function' && !this.pubClient.isOpen && !this.pubClient.isReady) {
174
+ await this.pubClient.connect();
175
+ }
176
+ if (this.subClient && typeof this.subClient.connect === 'function' && !this.subClient.isOpen && !this.subClient.isReady) {
177
+ await this.subClient.connect();
178
+ }
179
+
180
+ // Wire incoming messages from subClient
181
+ this._wireSubClient();
182
+ }
183
+
184
+ this._isOpen = true;
185
+ this._emit('open', { nodeId: this.nodeId, inMemory: this._useInMemory });
186
+ } catch (err) {
187
+ this._isOpen = false;
188
+ this._emit('error', err);
189
+ throw err;
190
+ }
191
+ }
192
+
193
+ _wireSubClient() {
194
+ if (!this.subClient) return;
195
+
196
+ const messageHandler = (channel, rawMessage) => {
197
+ this._handleIncomingRawMessage(channel, rawMessage);
198
+ };
199
+
200
+ // Standard EventEmitter / ioredis style (subClient.on('message'))
201
+ if (typeof this.subClient.on === 'function') {
202
+ this.subClient.on('message', messageHandler);
203
+ this.subClient.on('error', (err) => this._emit('error', err));
204
+ this.subClient.on('close', () => {
205
+ this._isOpen = false;
206
+ this._emit('close');
207
+ });
208
+ }
209
+ }
210
+
211
+ _handleIncomingRawMessage(channel, rawMessage) {
212
+ const envelope = this.deserialize(rawMessage);
213
+ if (!envelope || typeof envelope !== 'object') {
214
+ return;
215
+ }
216
+
217
+ const subs = this._channelSubscriptions.get(channel);
218
+ if (!subs || subs.size === 0) {
219
+ return;
220
+ }
221
+
222
+ for (const sub of Array.from(subs)) {
223
+ if (envelope.type === 'action') {
224
+ if (typeof sub.callbacks.action === 'function') {
225
+ sub.callbacks.action(envelope.action, envelope.data, envelope);
226
+ } else if (typeof sub.callbacks.received === 'function') {
227
+ sub.callbacks.received(envelope.data, envelope);
228
+ }
229
+ } else {
230
+ if (typeof sub.callbacks.received === 'function') {
231
+ sub.callbacks.received(envelope.data, envelope);
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ isConnected() {
238
+ return this._isOpen;
239
+ }
240
+
241
+ async disconnect() {
242
+ this._isOpen = false;
243
+
244
+ // Unsubscribe from in-memory bus
245
+ for (const [, unsub] of this._inMemoryUnsubscribers) {
246
+ try {
247
+ unsub();
248
+ } catch {}
249
+ }
250
+ this._inMemoryUnsubscribers.clear();
251
+
252
+ // Unsubscribe from Redis
253
+ if (this.subClient) {
254
+ for (const channel of this._channelSubscriptions.keys()) {
255
+ try {
256
+ if (typeof this.subClient.unsubscribe === 'function') {
257
+ await this.subClient.unsubscribe(channel);
258
+ }
259
+ } catch {}
260
+ }
261
+ }
262
+
263
+ this._channelSubscriptions.clear();
264
+
265
+ // Close clients if managed
266
+ if (this._managedClients) {
267
+ if (this.subClient && typeof this.subClient.quit === 'function') {
268
+ try { await this.subClient.quit(); } catch {}
269
+ }
270
+ if (this.pubClient && typeof this.pubClient.quit === 'function') {
271
+ try { await this.pubClient.quit(); } catch {}
272
+ }
273
+ }
274
+
275
+ this._emit('close');
276
+ }
277
+
278
+ async destroy() {
279
+ await this.disconnect();
280
+ this._listeners.clear();
281
+ }
282
+
283
+ /**
284
+ * Low-level publish raw payload to Redis channel
285
+ * @param {string} identifier
286
+ * @param {Object} envelope
287
+ */
288
+ async _publishEnvelope(identifier, envelope) {
289
+ const channel = this._resolveChannel(identifier);
290
+ const raw = this.serialize(envelope);
291
+
292
+ if (this._useInMemory) {
293
+ this._inMemoryBus.publish(channel, raw);
294
+ return;
295
+ }
296
+
297
+ if (this.pubClient) {
298
+ if (typeof this.pubClient.publish === 'function') {
299
+ await this.pubClient.publish(channel, raw);
300
+ }
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Broadcast message to all cluster nodes on given identifier
306
+ * @param {string} identifier
307
+ * @param {*} data
308
+ */
309
+ async publish(identifier, data) {
310
+ const envelope = {
311
+ type: 'message',
312
+ identifier,
313
+ data,
314
+ senderId: this.nodeId,
315
+ timestamp: Date.now(),
316
+ };
317
+ await this._publishEnvelope(identifier, envelope);
318
+ }
319
+
320
+ /**
321
+ * Broadcast named RPC action to all cluster nodes on given identifier
322
+ * @param {string} identifier
323
+ * @param {string} action
324
+ * @param {*} data
325
+ */
326
+ async perform(identifier, action, data) {
327
+ const envelope = {
328
+ type: 'action',
329
+ identifier,
330
+ action,
331
+ data,
332
+ senderId: this.nodeId,
333
+ timestamp: Date.now(),
334
+ };
335
+ await this._publishEnvelope(identifier, envelope);
336
+ }
337
+
338
+ /**
339
+ * Subscribe to a distributed channel identifier
340
+ * @param {string} identifier
341
+ * @param {Object} params
342
+ * @param {Object} callbacks
343
+ * @returns {Object} subscription handle
344
+ */
345
+ subscribe(identifier, params = {}, callbacks = {}) {
346
+ const channel = this._resolveChannel(identifier);
347
+
348
+ if (!this._channelSubscriptions.has(channel)) {
349
+ this._channelSubscriptions.set(channel, new Set());
350
+
351
+ if (this._useInMemory) {
352
+ const unsub = this._inMemoryBus.subscribe(channel, (ch, raw) => {
353
+ this._handleIncomingRawMessage(ch, raw);
354
+ });
355
+ this._inMemoryUnsubscribers.set(channel, unsub);
356
+ } else if (this.subClient) {
357
+ if (typeof this.subClient.subscribe === 'function') {
358
+ // Supports node-redis v4 callback style or ioredis promise
359
+ const res = this.subClient.subscribe(channel, (message) => {
360
+ // node-redis v4 listener
361
+ this._handleIncomingRawMessage(channel, message);
362
+ });
363
+ if (res && typeof res.catch === 'function') {
364
+ res.catch((err) => this._emit('error', err));
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ const subEntry = {
371
+ identifier,
372
+ params,
373
+ callbacks,
374
+ };
375
+
376
+ this._channelSubscriptions.get(channel).add(subEntry);
377
+
378
+ // Trigger connected callback
379
+ if (typeof callbacks.connected === 'function') {
380
+ Promise.resolve().then(() => {
381
+ if (this._channelSubscriptions.get(channel)?.has(subEntry)) {
382
+ callbacks.connected();
383
+ }
384
+ });
385
+ }
386
+
387
+ return {
388
+ send: (data) => {
389
+ return this.publish(identifier, data);
390
+ },
391
+ perform: (action, data) => {
392
+ return this.perform(identifier, action, data);
393
+ },
394
+ unsubscribe: () => {
395
+ const set = this._channelSubscriptions.get(channel);
396
+ if (set) {
397
+ set.delete(subEntry);
398
+ if (set.size === 0) {
399
+ this._channelSubscriptions.delete(channel);
400
+ if (this._useInMemory) {
401
+ const unsub = this._inMemoryUnsubscribers.get(channel);
402
+ if (unsub) {
403
+ unsub();
404
+ this._inMemoryUnsubscribers.delete(channel);
405
+ }
406
+ } else if (this.subClient && typeof this.subClient.unsubscribe === 'function') {
407
+ this.subClient.unsubscribe(channel);
408
+ }
409
+ }
410
+ }
411
+ },
412
+ };
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Factory for creating a Redis distributed adapter instance
418
+ * @param {Object} options
419
+ * @returns {RedisAdapter}
420
+ */
421
+ function createRedisAdapter(options = {}) {
422
+ return new RedisAdapter(options);
423
+ }
424
+
425
+ module.exports = {
426
+ createRedisAdapter,
427
+ redis: createRedisAdapter,
428
+ RedisAdapter,
429
+ InMemoryDistributedBus,
430
+ globalInMemoryBus,
431
+ };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Socket.IO Adapter for Webspresso Realtime
3
+ * @module core/realtime/adapters/socket-io
4
+ */
5
+
6
+ class SocketIoAdapter {
7
+ /**
8
+ * @param {Object} options
9
+ * @param {string|Function} options.url - Target URL
10
+ * @param {string} [options.path='/socket.io'] - Socket.IO path
11
+ * @param {Function} [options.io] - Socket.IO client factory injection
12
+ * @param {Object} [options.socketOptions] - Custom Socket.IO client options
13
+ * @param {Object} [options.capabilities] - Custom capabilities override
14
+ */
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ this.capabilities = {
18
+ send: true,
19
+ perform: true,
20
+ multiplexing: true,
21
+ serverEvents: true,
22
+ binary: true,
23
+ ...(options.capabilities || {}),
24
+ };
25
+
26
+ this.ioFactory = options.io || (typeof globalThis.io === 'function' ? globalThis.io : null);
27
+ this.socket = null;
28
+ this._listeners = new Map();
29
+ this._subscriptions = new Map();
30
+ this._isOpen = false;
31
+ }
32
+
33
+ isConnected() {
34
+ return Boolean(this.socket && (this.socket.connected || this._isOpen));
35
+ }
36
+
37
+ on(event, handler) {
38
+ if (!this._listeners.has(event)) {
39
+ this._listeners.set(event, new Set());
40
+ }
41
+ this._listeners.get(event).add(handler);
42
+ return () => {
43
+ const set = this._listeners.get(event);
44
+ if (set) set.delete(handler);
45
+ };
46
+ }
47
+
48
+ _emit(event, ...args) {
49
+ const set = this._listeners.get(event);
50
+ if (!set) return;
51
+ for (const handler of set) {
52
+ try {
53
+ handler(...args);
54
+ } catch (err) {
55
+ console.error(`[socket-io-adapter] Error in event listener for "${event}":`, err);
56
+ }
57
+ }
58
+ }
59
+
60
+ async connect(context) {
61
+ if (!this.ioFactory) {
62
+ throw new Error('Socket.IO client implementation not found. Inject options.io or install socket.io-client.');
63
+ }
64
+
65
+ let url = typeof this.options.url === 'function' ? await this.options.url(context) : (this.options.url || '');
66
+
67
+ const clientOpts = {
68
+ path: this.options.path || '/socket.io',
69
+ auth: context.auth ? { token: context.auth.token, ...context.auth } : {},
70
+ autoConnect: true,
71
+ ...(this.options.socketOptions || {}),
72
+ };
73
+
74
+ return new Promise((resolve, reject) => {
75
+ let isSettled = false;
76
+
77
+ try {
78
+ this.socket = this.ioFactory(url, clientOpts);
79
+ } catch (err) {
80
+ return reject(err);
81
+ }
82
+
83
+ this.socket.on('connect', () => {
84
+ this._isOpen = true;
85
+ if (!isSettled) {
86
+ isSettled = true;
87
+ resolve();
88
+ }
89
+ });
90
+
91
+ this.socket.on('disconnect', (reason) => {
92
+ this._isOpen = false;
93
+ this._emit('disconnected', reason);
94
+ });
95
+
96
+ this.socket.on('connect_error', (err) => {
97
+ this._isOpen = false;
98
+ const msg = err && err.message ? err.message.toLowerCase() : '';
99
+ if (msg.includes('unauthorized') || msg.includes('forbidden') || msg.includes('auth')) {
100
+ this._emit('unauthorized', err);
101
+ } else {
102
+ this._emit('error', err);
103
+ }
104
+
105
+ if (!isSettled) {
106
+ isSettled = true;
107
+ reject(err);
108
+ }
109
+ });
110
+ });
111
+ }
112
+
113
+ subscribe(identifier, params, callbacks) {
114
+ if (!this._subscriptions.has(identifier)) {
115
+ this._subscriptions.set(identifier, new Map());
116
+ }
117
+
118
+ const subId = Symbol('socketio-sub');
119
+ this._subscriptions.get(identifier).set(subId, callbacks);
120
+
121
+ if (this.isConnected()) {
122
+ this.socket.emit('subscribe', { identifier, params }, (response) => {
123
+ if (response && response.error) {
124
+ callbacks.rejected?.(new Error(response.error));
125
+ } else {
126
+ callbacks.connected?.();
127
+ }
128
+ });
129
+ }
130
+
131
+ const eventHandler = (data) => {
132
+ callbacks.received?.(data);
133
+ };
134
+
135
+ if (this.socket) {
136
+ this.socket.on(identifier, eventHandler);
137
+ }
138
+
139
+ return {
140
+ send: (data) => this.send(identifier, data),
141
+ perform: (action, data) => this.perform(identifier, action, data),
142
+ unsubscribe: () => this.unsubscribe(identifier, params, subId, eventHandler),
143
+ };
144
+ }
145
+
146
+ unsubscribe(identifier, params, subId, eventHandler) {
147
+ if (this._subscriptions.has(identifier)) {
148
+ const subs = this._subscriptions.get(identifier);
149
+ if (subId) subs.delete(subId);
150
+ if (subs.size === 0 || !subId) {
151
+ this._subscriptions.delete(identifier);
152
+ if (this.socket) {
153
+ this.socket.emit('unsubscribe', { identifier, params });
154
+ if (eventHandler) {
155
+ this.socket.off(identifier, eventHandler);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ send(identifier, data) {
163
+ if (!this.isConnected()) {
164
+ throw new Error('Socket.IO is not connected');
165
+ }
166
+ this.socket.emit('message', { identifier, data });
167
+ }
168
+
169
+ perform(identifier, action, data) {
170
+ if (!this.isConnected()) {
171
+ throw new Error('Socket.IO is not connected');
172
+ }
173
+ this.socket.emit('perform', { identifier, action, data });
174
+ }
175
+
176
+ disconnect() {
177
+ this._isOpen = false;
178
+ if (this.socket) {
179
+ try {
180
+ this.socket.disconnect();
181
+ } catch (e) {
182
+ // Disconnected
183
+ }
184
+ this.socket = null;
185
+ }
186
+ }
187
+
188
+ destroy() {
189
+ this.disconnect();
190
+ this._subscriptions.clear();
191
+ this._listeners.clear();
192
+ }
193
+ }
194
+
195
+ function createSocketIoAdapter(options = {}) {
196
+ return new SocketIoAdapter(options);
197
+ }
198
+
199
+ module.exports = {
200
+ createSocketIoAdapter,
201
+ socketIo: createSocketIoAdapter,
202
+ SocketIoAdapter,
203
+ };