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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Reconnection & Exponential Backoff Manager
|
|
3
|
+
* @module core/realtime/reconnect-manager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ReconnectManager {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} [options]
|
|
9
|
+
* @param {boolean} [options.enabled=true] - Whether automatic reconnection is enabled
|
|
10
|
+
* @param {number} [options.maxAttempts=10] - Maximum reconnection attempts before giving up
|
|
11
|
+
* @param {number} [options.baseDelay=500] - Initial reconnection delay in ms
|
|
12
|
+
* @param {number} [options.maxDelay=30000] - Maximum reconnection delay in ms
|
|
13
|
+
* @param {number} [options.factor=2] - Exponential growth factor
|
|
14
|
+
* @param {boolean} [options.jitter=true] - Whether to apply randomized jitter
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.enabled = options.enabled !== false;
|
|
18
|
+
this.maxAttempts = options.maxAttempts ?? 10;
|
|
19
|
+
this.baseDelay = options.baseDelay ?? 500;
|
|
20
|
+
this.maxDelay = options.maxDelay ?? 30000;
|
|
21
|
+
this.factor = options.factor ?? 2;
|
|
22
|
+
this.jitter = options.jitter !== false;
|
|
23
|
+
|
|
24
|
+
this.attempts = 0;
|
|
25
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
26
|
+
this._timer = null;
|
|
27
|
+
this._isReconnecting = false;
|
|
28
|
+
this._isAborted = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
calculateDelay(attempt) {
|
|
32
|
+
const rawDelay = this.baseDelay * Math.pow(this.factor, attempt);
|
|
33
|
+
const capped = Math.min(rawDelay, this.maxDelay);
|
|
34
|
+
|
|
35
|
+
if (!this.jitter) {
|
|
36
|
+
return Math.round(capped);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const randomJitter = capped * 0.2 * (Math.random() * 2 - 1);
|
|
40
|
+
return Math.max(0, Math.round(capped + randomJitter));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
canReconnect() {
|
|
44
|
+
if (!this.enabled || this._isAborted) return false;
|
|
45
|
+
if (this.maxAttempts > 0 && this.attempts >= this.maxAttempts) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
isReconnecting() {
|
|
52
|
+
return this._isReconnecting || this._timer !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
schedule(reconnectFn, onAttempt) {
|
|
56
|
+
if (!this.canReconnect()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.isReconnecting()) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const currentAttempt = this.attempts + 1;
|
|
65
|
+
const delay = this.calculateDelay(this.attempts);
|
|
66
|
+
this.attempts = currentAttempt;
|
|
67
|
+
|
|
68
|
+
if (typeof onAttempt === 'function') {
|
|
69
|
+
try {
|
|
70
|
+
onAttempt({ attempt: currentAttempt, delay, maxAttempts: this.maxAttempts });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error('[realtime] Error in onAttempt callback:', e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this._timer = setTimeout(async () => {
|
|
77
|
+
this._timer = null;
|
|
78
|
+
if (this._isAborted) return;
|
|
79
|
+
|
|
80
|
+
this._isReconnecting = true;
|
|
81
|
+
try {
|
|
82
|
+
await reconnectFn();
|
|
83
|
+
this.reset();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this._isReconnecting = false;
|
|
86
|
+
if (this._isAborted) return;
|
|
87
|
+
|
|
88
|
+
if (this.canReconnect()) {
|
|
89
|
+
this.schedule(reconnectFn, onAttempt);
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
this._isReconnecting = false;
|
|
93
|
+
}
|
|
94
|
+
}, delay);
|
|
95
|
+
|
|
96
|
+
if (this._timer && typeof this._timer.unref === 'function') {
|
|
97
|
+
this._timer.unref();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reset() {
|
|
104
|
+
if (this._timer) {
|
|
105
|
+
clearTimeout(this._timer);
|
|
106
|
+
this._timer = null;
|
|
107
|
+
}
|
|
108
|
+
this.attempts = 0;
|
|
109
|
+
this._isReconnecting = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cancel() {
|
|
113
|
+
this._isAborted = true;
|
|
114
|
+
this.reset();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { ReconnectManager };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime Subscription Registry & Subscription Handle
|
|
3
|
+
* @module core/realtime/registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { RealtimeError } = require('./errors');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a deterministic composite key for a subscription from identifier and params
|
|
10
|
+
*
|
|
11
|
+
* @param {string} identifier
|
|
12
|
+
* @param {Object} [params={}]
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function createSubscriptionKey(identifier, params = {}) {
|
|
16
|
+
if (!identifier || typeof identifier !== 'string') {
|
|
17
|
+
throw new RealtimeError('INVALID_ARGUMENT', 'Subscription identifier must be a non-empty string');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let serializedParams = '';
|
|
21
|
+
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
|
|
22
|
+
const sortedKeys = Object.keys(params).sort();
|
|
23
|
+
const sortedObj = {};
|
|
24
|
+
for (const key of sortedKeys) {
|
|
25
|
+
sortedObj[key] = params[key];
|
|
26
|
+
}
|
|
27
|
+
serializedParams = JSON.stringify(sortedObj);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `${identifier}::${serializedParams}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Realtime Subscription Handle
|
|
35
|
+
*/
|
|
36
|
+
class RealtimeSubscription {
|
|
37
|
+
/**
|
|
38
|
+
* @param {Object} options
|
|
39
|
+
* @param {string} options.key - Unique subscription key
|
|
40
|
+
* @param {string} options.identifier - Topic/Channel identifier (e.g. "project:123")
|
|
41
|
+
* @param {Object} [options.params={}] - Parameters passed to subscription
|
|
42
|
+
* @param {Object} [options.callbacks={}] - Callbacks { connected, disconnected, rejected, received }
|
|
43
|
+
* @param {Function} options.onUnsubscribe - Internal unsubscribe callback
|
|
44
|
+
* @param {Function} options.onSend - Internal send handler
|
|
45
|
+
* @param {Function} options.onPerform - Internal perform handler
|
|
46
|
+
*/
|
|
47
|
+
constructor({ key, identifier, params = {}, callbacks = {}, onUnsubscribe, onSend, onPerform }) {
|
|
48
|
+
this.key = key;
|
|
49
|
+
this.identifier = identifier;
|
|
50
|
+
this.params = params;
|
|
51
|
+
this.callbacks = {
|
|
52
|
+
connected: typeof callbacks.connected === 'function' ? callbacks.connected : null,
|
|
53
|
+
disconnected: typeof callbacks.disconnected === 'function' ? callbacks.disconnected : null,
|
|
54
|
+
rejected: typeof callbacks.rejected === 'function' ? callbacks.rejected : null,
|
|
55
|
+
received: typeof callbacks.received === 'function' ? callbacks.received : null,
|
|
56
|
+
};
|
|
57
|
+
this._onUnsubscribe = onUnsubscribe;
|
|
58
|
+
this._onSend = onSend;
|
|
59
|
+
this._onPerform = onPerform;
|
|
60
|
+
this._isUnsubscribed = false;
|
|
61
|
+
this._adapterSub = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Send data to this subscription
|
|
66
|
+
* @param {unknown} data
|
|
67
|
+
*/
|
|
68
|
+
send(data) {
|
|
69
|
+
if (this._isUnsubscribed) {
|
|
70
|
+
throw new RealtimeError('SUBSCRIPTION_REJECTED', 'Cannot send data on an unsubscribed channel');
|
|
71
|
+
}
|
|
72
|
+
if (typeof this._onSend === 'function') {
|
|
73
|
+
this._onSend(this, data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Perform an action on this subscription
|
|
79
|
+
* @param {string} action
|
|
80
|
+
* @param {unknown} [data]
|
|
81
|
+
*/
|
|
82
|
+
perform(action, data) {
|
|
83
|
+
if (this._isUnsubscribed) {
|
|
84
|
+
throw new RealtimeError('SUBSCRIPTION_REJECTED', 'Cannot perform action on an unsubscribed channel');
|
|
85
|
+
}
|
|
86
|
+
if (typeof this._onPerform === 'function') {
|
|
87
|
+
this._onPerform(this, action, data);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Unsubscribe from this channel
|
|
93
|
+
*/
|
|
94
|
+
unsubscribe() {
|
|
95
|
+
if (this._isUnsubscribed) return;
|
|
96
|
+
this._isUnsubscribed = true;
|
|
97
|
+
if (typeof this._onUnsubscribe === 'function') {
|
|
98
|
+
this._onUnsubscribe(this);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
notifyReceived(data) {
|
|
103
|
+
if (this._isUnsubscribed) return;
|
|
104
|
+
if (this.callbacks.received) {
|
|
105
|
+
try {
|
|
106
|
+
this.callbacks.received(data);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`[realtime] Error in received callback for "${this.identifier}":`, err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
notifyConnected() {
|
|
114
|
+
if (this._isUnsubscribed) return;
|
|
115
|
+
if (this.callbacks.connected) {
|
|
116
|
+
try {
|
|
117
|
+
this.callbacks.connected();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`[realtime] Error in connected callback for "${this.identifier}":`, err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
notifyDisconnected() {
|
|
125
|
+
if (this._isUnsubscribed) return;
|
|
126
|
+
if (this.callbacks.disconnected) {
|
|
127
|
+
try {
|
|
128
|
+
this.callbacks.disconnected();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`[realtime] Error in disconnected callback for "${this.identifier}":`, err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
notifyRejected(error) {
|
|
136
|
+
if (this._isUnsubscribed) return;
|
|
137
|
+
if (this.callbacks.rejected) {
|
|
138
|
+
try {
|
|
139
|
+
this.callbacks.rejected(error);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(`[realtime] Error in rejected callback for "${this.identifier}":`, err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Subscription Registry
|
|
149
|
+
*/
|
|
150
|
+
class SubscriptionRegistry {
|
|
151
|
+
constructor() {
|
|
152
|
+
/** @type {Map<string, RealtimeSubscription>} */
|
|
153
|
+
this.subscriptions = new Map();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
has(key) {
|
|
157
|
+
return this.subscriptions.has(key);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get(key) {
|
|
161
|
+
return this.subscriptions.get(key);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
register({ identifier, params = {}, callbacks = {}, onSend, onPerform, onAdapterSubscribe, onAdapterUnsubscribe }) {
|
|
165
|
+
const key = createSubscriptionKey(identifier, params);
|
|
166
|
+
|
|
167
|
+
// If already registered and active, return existing subscription
|
|
168
|
+
if (this.subscriptions.has(key)) {
|
|
169
|
+
const existing = this.subscriptions.get(key);
|
|
170
|
+
if (!existing._isUnsubscribed) {
|
|
171
|
+
if (callbacks.received) existing.callbacks.received = callbacks.received;
|
|
172
|
+
if (callbacks.connected) existing.callbacks.connected = callbacks.connected;
|
|
173
|
+
if (callbacks.disconnected) existing.callbacks.disconnected = callbacks.disconnected;
|
|
174
|
+
if (callbacks.rejected) existing.callbacks.rejected = callbacks.rejected;
|
|
175
|
+
return existing;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const subscription = new RealtimeSubscription({
|
|
180
|
+
key,
|
|
181
|
+
identifier,
|
|
182
|
+
params,
|
|
183
|
+
callbacks,
|
|
184
|
+
onUnsubscribe: (sub) => {
|
|
185
|
+
this.subscriptions.delete(key);
|
|
186
|
+
if (typeof onAdapterUnsubscribe === 'function') {
|
|
187
|
+
onAdapterUnsubscribe(sub);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
onSend,
|
|
191
|
+
onPerform,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.subscriptions.set(key, subscription);
|
|
195
|
+
|
|
196
|
+
if (typeof onAdapterSubscribe === 'function') {
|
|
197
|
+
subscription._adapterSub = onAdapterSubscribe(subscription);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return subscription;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
restoreAll(onAdapterSubscribe) {
|
|
204
|
+
if (typeof onAdapterSubscribe !== 'function') return;
|
|
205
|
+
|
|
206
|
+
for (const [key, sub] of this.subscriptions.entries()) {
|
|
207
|
+
if (!sub._isUnsubscribed) {
|
|
208
|
+
try {
|
|
209
|
+
sub._adapterSub = onAdapterSubscribe(sub);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error(`[realtime] Failed to restore subscription "${sub.identifier}":`, err);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
this.subscriptions.delete(key);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
notifyAllDisconnected() {
|
|
220
|
+
for (const sub of this.subscriptions.values()) {
|
|
221
|
+
if (!sub._isUnsubscribed) {
|
|
222
|
+
sub.notifyDisconnected();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
clear() {
|
|
228
|
+
for (const sub of this.subscriptions.values()) {
|
|
229
|
+
sub.unsubscribe();
|
|
230
|
+
}
|
|
231
|
+
this.subscriptions.clear();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getAll() {
|
|
235
|
+
return Array.from(this.subscriptions.values()).filter((s) => !s._isUnsubscribed);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
createSubscriptionKey,
|
|
241
|
+
RealtimeSubscription,
|
|
242
|
+
SubscriptionRegistry,
|
|
243
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js HTTP Server Adapter
|
|
3
|
+
* Provides server-level connection tracking, graceful closing, and forced termination
|
|
4
|
+
* @module core/shutdown/http-adapter
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class NodeHttpAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('http').Server|import('https').Server} server
|
|
10
|
+
*/
|
|
11
|
+
constructor(server) {
|
|
12
|
+
if (!server) {
|
|
13
|
+
throw new Error('NodeHttpAdapter requires an HTTP server instance');
|
|
14
|
+
}
|
|
15
|
+
this.server = server;
|
|
16
|
+
/** @type {Set<import('net').Socket>} */
|
|
17
|
+
this.sockets = new Set();
|
|
18
|
+
this._closed = false;
|
|
19
|
+
|
|
20
|
+
this._onConnection = this._onConnection.bind(this);
|
|
21
|
+
this._onSecureConnection = this._onSecureConnection.bind(this);
|
|
22
|
+
|
|
23
|
+
this._attachSocketListeners();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_attachSocketListeners() {
|
|
27
|
+
this.server.on('connection', this._onConnection);
|
|
28
|
+
this.server.on('secureConnection', this._onSecureConnection);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_trackSocket(socket) {
|
|
32
|
+
if (!socket || this.sockets.has(socket)) return;
|
|
33
|
+
this.sockets.add(socket);
|
|
34
|
+
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
this.sockets.delete(socket);
|
|
37
|
+
socket.removeListener('close', cleanup);
|
|
38
|
+
socket.removeListener('error', cleanup);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
socket.once('close', cleanup);
|
|
42
|
+
socket.once('error', cleanup);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_onConnection(socket) {
|
|
46
|
+
this._trackSocket(socket);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_onSecureConnection(socket) {
|
|
50
|
+
this._trackSocket(socket);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if the server is currently listening
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
get isListening() {
|
|
58
|
+
return Boolean(this.server && this.server.listening);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop accepting new connections and gracefully close existing connections
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
close() {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
if (!this.server || !this.server.listening) {
|
|
68
|
+
this._closed = true;
|
|
69
|
+
return resolve();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.closeIdleConnections();
|
|
73
|
+
|
|
74
|
+
this.server.close(() => {
|
|
75
|
+
this._closed = true;
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Close idle keep-alive connections if supported by Node.js
|
|
83
|
+
*/
|
|
84
|
+
closeIdleConnections() {
|
|
85
|
+
if (this.server && typeof this.server.closeIdleConnections === 'function') {
|
|
86
|
+
try {
|
|
87
|
+
this.server.closeIdleConnections();
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore errors during idle closing
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Immediately terminate all active connections
|
|
96
|
+
*/
|
|
97
|
+
forceClose() {
|
|
98
|
+
// 1. Try Node.js native closeAllConnections (Node 18.2+)
|
|
99
|
+
if (this.server && typeof this.server.closeAllConnections === 'function') {
|
|
100
|
+
try {
|
|
101
|
+
this.server.closeAllConnections();
|
|
102
|
+
} catch {
|
|
103
|
+
// Fall back to manual socket destruction below
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Destroy all tracked sockets with an explicit error to notify clients
|
|
108
|
+
for (const socket of this.sockets) {
|
|
109
|
+
try {
|
|
110
|
+
if (!socket.destroyed) {
|
|
111
|
+
socket.destroy(new Error('Connection closed by server shutdown'));
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore socket destroy errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.sockets.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Detach all listeners from the server
|
|
122
|
+
*/
|
|
123
|
+
destroy() {
|
|
124
|
+
if (this.server) {
|
|
125
|
+
this.server.removeListener('connection', this._onConnection);
|
|
126
|
+
this.server.removeListener('secureConnection', this._onSecureConnection);
|
|
127
|
+
}
|
|
128
|
+
this.sockets.clear();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
NodeHttpAdapter,
|
|
134
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Shutdown & Lifecycle Management
|
|
3
|
+
* @module core/shutdown
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ShutdownManager } = require('./shutdown-manager');
|
|
7
|
+
const { NodeHttpAdapter } = require('./http-adapter');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
ShutdownManager,
|
|
11
|
+
NodeHttpAdapter,
|
|
12
|
+
};
|