webspresso 0.0.90 → 0.0.91
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 +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Core Client
|
|
3
|
+
* @module core/realtime/core
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { EventEmitter } = require('./emitter');
|
|
7
|
+
const { AuthManager } = require('./auth-manager');
|
|
8
|
+
const { SubscriptionRegistry } = require('./registry');
|
|
9
|
+
const { ReconnectManager } = require('./reconnect-manager');
|
|
10
|
+
const { RealtimeError, normalizeRealtimeError } = require('./errors');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CAPABILITIES = Object.freeze({
|
|
13
|
+
send: true,
|
|
14
|
+
perform: true,
|
|
15
|
+
multiplexing: true,
|
|
16
|
+
serverEvents: true,
|
|
17
|
+
binary: false,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check whether the current execution context is in a browser environment
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
function isBrowserEnvironment() {
|
|
25
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class RealtimeClient {
|
|
29
|
+
/**
|
|
30
|
+
* @param {Object} options
|
|
31
|
+
* @param {Object} options.adapter - Realtime transport adapter (e.g. websocket, sse, socketIo)
|
|
32
|
+
* @param {Object|boolean} [options.auth=false] - Auth strategy configuration
|
|
33
|
+
* @param {boolean} [options.autoConnect=true] - Auto-connect when browser is ready
|
|
34
|
+
* @param {Object} [options.reconnect] - Reconnection options
|
|
35
|
+
* @param {boolean} [options.isBrowser] - Force browser environment flag for testing
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
if (!options.adapter || typeof options.adapter !== 'object') {
|
|
39
|
+
throw new RealtimeError('INVALID_ARGUMENT', 'RealtimeClient requires a valid adapter');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.options = options;
|
|
43
|
+
this.adapter = options.adapter;
|
|
44
|
+
this.auth = new AuthManager(options.auth);
|
|
45
|
+
this.registry = new SubscriptionRegistry();
|
|
46
|
+
this.reconnectManager = new ReconnectManager(options.reconnect);
|
|
47
|
+
this.emitter = new EventEmitter();
|
|
48
|
+
|
|
49
|
+
this._autoConnect = options.autoConnect !== false;
|
|
50
|
+
this._isBrowserOverride = typeof options.isBrowser === 'boolean' ? options.isBrowser : null;
|
|
51
|
+
this._isConnecting = false;
|
|
52
|
+
this._isDestroyed = false;
|
|
53
|
+
this._hasConnectedBefore = false;
|
|
54
|
+
|
|
55
|
+
// Wire up adapter low-level events
|
|
56
|
+
this._wireAdapterEvents();
|
|
57
|
+
|
|
58
|
+
// Auto-connect if in browser environment
|
|
59
|
+
if (this._autoConnect && this.isBrowser()) {
|
|
60
|
+
Promise.resolve().then(() => {
|
|
61
|
+
if (!this._isDestroyed && !this.isConnected()) {
|
|
62
|
+
this.connect().catch(() => {});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isBrowser() {
|
|
69
|
+
if (this._isBrowserOverride !== null) {
|
|
70
|
+
return this._isBrowserOverride;
|
|
71
|
+
}
|
|
72
|
+
return isBrowserEnvironment();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get capabilities() {
|
|
76
|
+
if (this.adapter && this.adapter.capabilities && typeof this.adapter.capabilities === 'object') {
|
|
77
|
+
return { ...DEFAULT_CAPABILITIES, ...this.adapter.capabilities };
|
|
78
|
+
}
|
|
79
|
+
return { ...DEFAULT_CAPABILITIES };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
isConnected() {
|
|
83
|
+
if (this._isDestroyed) return false;
|
|
84
|
+
if (this.adapter && typeof this.adapter.isConnected === 'function') {
|
|
85
|
+
return this.adapter.isConnected();
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
on(event, handler) {
|
|
91
|
+
return this.emitter.on(event, handler);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
once(event, handler) {
|
|
95
|
+
return this.emitter.once(event, handler);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
off(event, handler) {
|
|
99
|
+
this.emitter.off(event, handler);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async connect(overrideAuth = null) {
|
|
103
|
+
if (this._isDestroyed) {
|
|
104
|
+
throw new RealtimeError('DESTROYED', 'Cannot connect on a destroyed Realtime instance');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// SSR Guard: Never open network connections during server-side rendering
|
|
108
|
+
if (!this.isBrowser()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.isConnected() || this._isConnecting) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._isConnecting = true;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
let authMetadata = overrideAuth;
|
|
120
|
+
if (!authMetadata) {
|
|
121
|
+
authMetadata = await this.auth.resolveAuthMetadata();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const connectionContext = {
|
|
125
|
+
auth: authMetadata,
|
|
126
|
+
client: this,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await this.adapter.connect(connectionContext);
|
|
130
|
+
|
|
131
|
+
this._isConnecting = false;
|
|
132
|
+
const isReconnection = this._hasConnectedBefore;
|
|
133
|
+
this._hasConnectedBefore = true;
|
|
134
|
+
|
|
135
|
+
this.reconnectManager.reset();
|
|
136
|
+
|
|
137
|
+
this.registry.restoreAll((sub) => {
|
|
138
|
+
return this._adapterSubscribe(sub);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (isReconnection) {
|
|
142
|
+
this.emitter.emit('reconnected');
|
|
143
|
+
}
|
|
144
|
+
this.emitter.emit('connected');
|
|
145
|
+
} catch (err) {
|
|
146
|
+
this._isConnecting = false;
|
|
147
|
+
const error = normalizeRealtimeError(err, 'CONNECTION_FAILED', 'Realtime connection failed');
|
|
148
|
+
|
|
149
|
+
if (error.code === 'UNAUTHORIZED') {
|
|
150
|
+
this.auth.handleUnauthorized(error);
|
|
151
|
+
this.emitter.emit('unauthorized', error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.emitter.emit('error', error);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
disconnect() {
|
|
160
|
+
if (this._isDestroyed) return;
|
|
161
|
+
|
|
162
|
+
this.reconnectManager.reset();
|
|
163
|
+
|
|
164
|
+
if (this.adapter && typeof this.adapter.disconnect === 'function') {
|
|
165
|
+
try {
|
|
166
|
+
this.adapter.disconnect();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('[realtime] Error during adapter disconnect:', err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.registry.notifyAllDisconnected();
|
|
173
|
+
this.emitter.emit('disconnected');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async reconnect() {
|
|
177
|
+
if (this._isDestroyed) {
|
|
178
|
+
throw new RealtimeError('DESTROYED', 'Cannot reconnect on a destroyed Realtime instance');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.disconnect();
|
|
182
|
+
return this.connect();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async reauthenticate() {
|
|
186
|
+
if (this._isDestroyed) {
|
|
187
|
+
throw new RealtimeError('DESTROYED', 'Cannot reauthenticate on a destroyed Realtime instance');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this.isBrowser()) return;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
this.disconnect();
|
|
194
|
+
const newAuth = await this.auth.refreshToken();
|
|
195
|
+
await this.connect(newAuth);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const error = normalizeRealtimeError(err, 'TOKEN_REFRESH_FAILED', 'Reauthentication failed');
|
|
198
|
+
this.emitter.emit('error', error);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
subscribe(identifier, params = {}, callbacks = {}) {
|
|
204
|
+
if (this._isDestroyed) {
|
|
205
|
+
throw new RealtimeError('DESTROYED', 'Cannot subscribe on a destroyed Realtime instance');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return this.registry.register({
|
|
209
|
+
identifier,
|
|
210
|
+
params,
|
|
211
|
+
callbacks,
|
|
212
|
+
onSend: (sub, data) => this._handleSubscriptionSend(sub, data),
|
|
213
|
+
onPerform: (sub, action, data) => this._handleSubscriptionPerform(sub, action, data),
|
|
214
|
+
onAdapterSubscribe: (sub) => {
|
|
215
|
+
if (this.isConnected()) {
|
|
216
|
+
return this._adapterSubscribe(sub);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
},
|
|
220
|
+
onAdapterUnsubscribe: (sub) => {
|
|
221
|
+
if (this.adapter && typeof this.adapter.unsubscribe === 'function') {
|
|
222
|
+
try {
|
|
223
|
+
this.adapter.unsubscribe(sub.identifier, sub.params, sub);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error(`[realtime] Error unsubscribing "${sub.identifier}":`, err);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_adapterSubscribe(sub) {
|
|
233
|
+
if (!this.adapter || typeof this.adapter.subscribe !== 'function') {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const adapterCallbacks = {
|
|
238
|
+
connected: () => sub.notifyConnected(),
|
|
239
|
+
disconnected: () => sub.notifyDisconnected(),
|
|
240
|
+
rejected: (err) => {
|
|
241
|
+
const error = normalizeRealtimeError(err, 'SUBSCRIPTION_REJECTED', `Subscription "${sub.identifier}" rejected`);
|
|
242
|
+
sub.notifyRejected(error);
|
|
243
|
+
this.emitter.emit('error', error);
|
|
244
|
+
},
|
|
245
|
+
received: (data) => sub.notifyReceived(data),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
return this.adapter.subscribe(sub.identifier, sub.params, adapterCallbacks);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const error = normalizeRealtimeError(err, 'SUBSCRIPTION_REJECTED', `Failed to subscribe to "${sub.identifier}"`);
|
|
252
|
+
sub.notifyRejected(error);
|
|
253
|
+
this.emitter.emit('error', error);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_handleSubscriptionSend(sub, data) {
|
|
259
|
+
if (!this.capabilities.send) {
|
|
260
|
+
throw new RealtimeError('UNSUPPORTED_CAPABILITY', 'Active realtime adapter does not support send()');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.adapter && typeof this.adapter.send === 'function') {
|
|
264
|
+
this.adapter.send(sub.identifier, data, sub);
|
|
265
|
+
} else if (sub._adapterSub && typeof sub._adapterSub.send === 'function') {
|
|
266
|
+
sub._adapterSub.send(data);
|
|
267
|
+
} else {
|
|
268
|
+
throw new RealtimeError('UNSUPPORTED_CAPABILITY', 'Adapter does not implement send');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_handleSubscriptionPerform(sub, action, data) {
|
|
273
|
+
if (this.capabilities.perform) {
|
|
274
|
+
if (this.adapter && typeof this.adapter.perform === 'function') {
|
|
275
|
+
this.adapter.perform(sub.identifier, action, data, sub);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (sub._adapterSub && typeof sub._adapterSub.perform === 'function') {
|
|
280
|
+
sub._adapterSub.perform(action, data);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// If perform is unsupported but send is available, map to generic action command
|
|
286
|
+
if (this.capabilities.send) {
|
|
287
|
+
this._handleSubscriptionSend(sub, { action, data });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
throw new RealtimeError('UNSUPPORTED_CAPABILITY', 'Active realtime adapter does not support perform() or send()');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_wireAdapterEvents() {
|
|
295
|
+
if (!this.adapter || typeof this.adapter.on !== 'function') return;
|
|
296
|
+
|
|
297
|
+
this.adapter.on('disconnected', (reason) => {
|
|
298
|
+
this.registry.notifyAllDisconnected();
|
|
299
|
+
this.emitter.emit('disconnected', reason);
|
|
300
|
+
|
|
301
|
+
if (!this._isDestroyed && this.reconnectManager.canReconnect()) {
|
|
302
|
+
this.reconnectManager.schedule(
|
|
303
|
+
async () => {
|
|
304
|
+
await this.connect();
|
|
305
|
+
},
|
|
306
|
+
({ attempt, delay, maxAttempts }) => {
|
|
307
|
+
this.emitter.emit('reconnecting', { attempt, delay, maxAttempts });
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
this.adapter.on('unauthorized', (err) => {
|
|
314
|
+
const error = normalizeRealtimeError(err, 'UNAUTHORIZED', 'Realtime connection unauthorized');
|
|
315
|
+
this.auth.handleUnauthorized(error);
|
|
316
|
+
this.emitter.emit('unauthorized', error);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
this.adapter.on('error', (err) => {
|
|
320
|
+
const error = normalizeRealtimeError(err, 'CONNECTION_FAILED', 'Adapter error');
|
|
321
|
+
this.emitter.emit('error', error);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
browserReady() {
|
|
326
|
+
if (this._isDestroyed) return;
|
|
327
|
+
if (this._autoConnect && !this.isConnected() && !this._isConnecting) {
|
|
328
|
+
this.connect().catch(() => {});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
destroy() {
|
|
333
|
+
if (this._isDestroyed) return;
|
|
334
|
+
this._isDestroyed = true;
|
|
335
|
+
|
|
336
|
+
this.reconnectManager.cancel();
|
|
337
|
+
this.registry.clear();
|
|
338
|
+
|
|
339
|
+
if (this.adapter) {
|
|
340
|
+
try {
|
|
341
|
+
if (typeof this.adapter.disconnect === 'function') {
|
|
342
|
+
this.adapter.disconnect();
|
|
343
|
+
}
|
|
344
|
+
if (typeof this.adapter.destroy === 'function') {
|
|
345
|
+
this.adapter.destroy();
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error('[realtime] Error destroying adapter:', err);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.emitter.clear();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
RealtimeClient,
|
|
358
|
+
DEFAULT_CAPABILITIES,
|
|
359
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight, zero-dependency Event Emitter
|
|
3
|
+
* @module core/realtime/emitter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class EventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
/** @type {Map<string, Set<Function>>} */
|
|
9
|
+
this._listeners = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to an event
|
|
14
|
+
*
|
|
15
|
+
* @param {string} event
|
|
16
|
+
* @param {Function} handler
|
|
17
|
+
* @returns {() => void} Unsubscribe function
|
|
18
|
+
*/
|
|
19
|
+
on(event, handler) {
|
|
20
|
+
if (typeof handler !== 'function') {
|
|
21
|
+
throw new TypeError(`Event handler for "${event}" must be a function`);
|
|
22
|
+
}
|
|
23
|
+
if (!this._listeners.has(event)) {
|
|
24
|
+
this._listeners.set(event, new Set());
|
|
25
|
+
}
|
|
26
|
+
this._listeners.get(event).add(handler);
|
|
27
|
+
|
|
28
|
+
return () => this.off(event, handler);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to an event once
|
|
33
|
+
*
|
|
34
|
+
* @param {string} event
|
|
35
|
+
* @param {Function} handler
|
|
36
|
+
* @returns {() => void} Unsubscribe function
|
|
37
|
+
*/
|
|
38
|
+
once(event, handler) {
|
|
39
|
+
if (typeof handler !== 'function') {
|
|
40
|
+
throw new TypeError(`Event handler for "${event}" must be a function`);
|
|
41
|
+
}
|
|
42
|
+
const wrapper = (...args) => {
|
|
43
|
+
this.off(event, wrapper);
|
|
44
|
+
handler(...args);
|
|
45
|
+
};
|
|
46
|
+
return this.on(event, wrapper);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Unsubscribe from an event
|
|
51
|
+
*
|
|
52
|
+
* @param {string} event
|
|
53
|
+
* @param {Function} handler
|
|
54
|
+
*/
|
|
55
|
+
off(event, handler) {
|
|
56
|
+
const set = this._listeners.get(event);
|
|
57
|
+
if (set) {
|
|
58
|
+
set.delete(handler);
|
|
59
|
+
if (set.size === 0) {
|
|
60
|
+
this._listeners.delete(event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Emit an event to all subscribers
|
|
67
|
+
*
|
|
68
|
+
* @param {string} event
|
|
69
|
+
* @param {...any} args
|
|
70
|
+
*/
|
|
71
|
+
emit(event, ...args) {
|
|
72
|
+
const set = this._listeners.get(event);
|
|
73
|
+
if (!set || set.size === 0) return;
|
|
74
|
+
|
|
75
|
+
// Iterate over shallow copy to allow listeners to modify set during execution
|
|
76
|
+
const handlers = Array.from(set);
|
|
77
|
+
for (const handler of handlers) {
|
|
78
|
+
try {
|
|
79
|
+
handler(...args);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (event !== 'error' && this._listeners.has('error')) {
|
|
82
|
+
this.emit('error', err);
|
|
83
|
+
} else {
|
|
84
|
+
console.error(`[realtime] Error in event listener for "${event}":`, err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear all registered listeners
|
|
92
|
+
*/
|
|
93
|
+
clear() {
|
|
94
|
+
this._listeners.clear();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { EventEmitter };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime error classes & normalization utilities
|
|
3
|
+
* @module core/realtime/errors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strips tokens, secrets, and sensitive query parameters from strings/URLs
|
|
8
|
+
*
|
|
9
|
+
* @param {string} input
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeErrorMessage(input) {
|
|
13
|
+
if (typeof input !== 'string') return '';
|
|
14
|
+
return input
|
|
15
|
+
.replace(/([?&](?:token|access_token|secret|key|auth|password|apiKey)=)[^&#\s]+/gi, '$1***')
|
|
16
|
+
.replace(/(Bearer\s+)[A-Za-z0-9-_=.]+/gi, '$1***')
|
|
17
|
+
.replace(/(Basic\s+)[A-Za-z0-9-_=.]+/gi, '$1***');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Standard Realtime Error
|
|
22
|
+
*/
|
|
23
|
+
class RealtimeError extends Error {
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} code - Standardized error code
|
|
26
|
+
* @param {string} message - Human-readable error description (sanitized)
|
|
27
|
+
* @param {Error|unknown} [cause] - Original root cause error
|
|
28
|
+
*/
|
|
29
|
+
constructor(code, message, cause = null) {
|
|
30
|
+
const sanitizedMsg = sanitizeErrorMessage(message);
|
|
31
|
+
super(sanitizedMsg);
|
|
32
|
+
this.name = 'RealtimeError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
if (cause) {
|
|
35
|
+
this.cause = cause instanceof Error ? cause : new Error(sanitizeErrorMessage(String(cause)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toJSON() {
|
|
40
|
+
return {
|
|
41
|
+
code: this.code,
|
|
42
|
+
message: this.message,
|
|
43
|
+
cause: this.cause ? this.cause.message || String(this.cause) : null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize any error into a standard RealtimeError
|
|
50
|
+
*
|
|
51
|
+
* @param {unknown} err
|
|
52
|
+
* @param {string} [defaultCode='REALTIME_ERROR']
|
|
53
|
+
* @param {string} [defaultMessage='An unexpected realtime error occurred']
|
|
54
|
+
* @returns {RealtimeError}
|
|
55
|
+
*/
|
|
56
|
+
function normalizeRealtimeError(err, defaultCode = 'REALTIME_ERROR', defaultMessage = 'An unexpected realtime error occurred') {
|
|
57
|
+
if (err instanceof RealtimeError) {
|
|
58
|
+
return err;
|
|
59
|
+
}
|
|
60
|
+
if (err instanceof Error) {
|
|
61
|
+
const code = err.code && typeof err.code === 'string' ? err.code : defaultCode;
|
|
62
|
+
return new RealtimeError(code, err.message || defaultMessage, err);
|
|
63
|
+
}
|
|
64
|
+
return new RealtimeError(defaultCode, typeof err === 'string' ? err : defaultMessage, err);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
RealtimeError,
|
|
69
|
+
normalizeRealtimeError,
|
|
70
|
+
sanitizeErrorMessage,
|
|
71
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Realtime Core
|
|
3
|
+
* @module core/realtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { RealtimeClient, DEFAULT_CAPABILITIES } = require('./core');
|
|
7
|
+
const { RealtimeSubscription, SubscriptionRegistry, createSubscriptionKey } = require('./registry');
|
|
8
|
+
const { AuthManager } = require('./auth-manager');
|
|
9
|
+
const { ReconnectManager } = require('./reconnect-manager');
|
|
10
|
+
const { RealtimeError, normalizeRealtimeError, sanitizeErrorMessage } = require('./errors');
|
|
11
|
+
const { EventEmitter } = require('./emitter');
|
|
12
|
+
|
|
13
|
+
// Adapters
|
|
14
|
+
const { createWebSocketAdapter, websocket, WebSocketAdapter, resolveWebSocketUrl } = require('./adapters/websocket');
|
|
15
|
+
const { createSseAdapter, sse, SseAdapter, resolveSseUrl } = require('./adapters/sse');
|
|
16
|
+
const { createSocketIoAdapter, socketIo, SocketIoAdapter } = require('./adapters/socket-io');
|
|
17
|
+
const { createRedisAdapter, redis, RedisAdapter, InMemoryDistributedBus } = require('./adapters/redis');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Factory to create a RealtimeClient instance
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Object} options.adapter - Transport adapter (e.g. websocket, sse, socketIo, redis)
|
|
24
|
+
* @param {Object|boolean} [options.auth=false] - Auth strategy configuration
|
|
25
|
+
* @param {boolean} [options.autoConnect=true] - Auto-connect when browser is ready
|
|
26
|
+
* @param {Object} [options.reconnect] - Reconnection options
|
|
27
|
+
* @returns {RealtimeClient}
|
|
28
|
+
*/
|
|
29
|
+
function createRealtime(options = {}) {
|
|
30
|
+
return new RealtimeClient(options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Universal realtime helper
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} options
|
|
37
|
+
* @returns {RealtimeClient}
|
|
38
|
+
*/
|
|
39
|
+
function realtime(options = {}) {
|
|
40
|
+
const client = createRealtime(options);
|
|
41
|
+
return client;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
createRealtime,
|
|
46
|
+
realtime,
|
|
47
|
+
RealtimeClient,
|
|
48
|
+
RealtimeSubscription,
|
|
49
|
+
SubscriptionRegistry,
|
|
50
|
+
createSubscriptionKey,
|
|
51
|
+
AuthManager,
|
|
52
|
+
ReconnectManager,
|
|
53
|
+
RealtimeError,
|
|
54
|
+
normalizeRealtimeError,
|
|
55
|
+
sanitizeErrorMessage,
|
|
56
|
+
EventEmitter,
|
|
57
|
+
DEFAULT_CAPABILITIES,
|
|
58
|
+
|
|
59
|
+
// Adapters
|
|
60
|
+
websocket,
|
|
61
|
+
createWebSocketAdapter,
|
|
62
|
+
WebSocketAdapter,
|
|
63
|
+
resolveWebSocketUrl,
|
|
64
|
+
|
|
65
|
+
sse,
|
|
66
|
+
createSseAdapter,
|
|
67
|
+
SseAdapter,
|
|
68
|
+
resolveSseUrl,
|
|
69
|
+
|
|
70
|
+
socketIo,
|
|
71
|
+
createSocketIoAdapter,
|
|
72
|
+
SocketIoAdapter,
|
|
73
|
+
|
|
74
|
+
redis,
|
|
75
|
+
createRedisAdapter,
|
|
76
|
+
RedisAdapter,
|
|
77
|
+
InMemoryDistributedBus,
|
|
78
|
+
};
|
|
79
|
+
|