@sockethub/platform-xmpp 5.0.0-alpha.10 → 5.0.0-alpha.11
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/dist/index.js +68 -55
- package/dist/index.js.map +4 -4
- package/package.json +5 -4
- package/src/incoming-handlers.js +12 -10
- package/src/incoming-handlers.test.js +24 -9
- package/src/index.js +85 -37
- package/src/index.test.js +109 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sockethub/platform-xmpp",
|
|
3
3
|
"description": "A sockethub platform module implementing XMPP functionality",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.11",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Nick Jennings <nick@silverbucket.net>",
|
|
@@ -42,24 +42,25 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "bun build src/index.js --outdir dist --target node --format esm --sourcemap=external",
|
|
44
44
|
"clean": "rm -rf dist",
|
|
45
|
+
"clean:deps": "rm -rf node_modules",
|
|
45
46
|
"doc": "jsdoc2md --no-gfm --heading-depth 1 src/index.js > API.md"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
49
|
"@xmpp/client": "^0.13.6"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@sockethub/schemas": "^3.0.0-alpha.
|
|
52
|
+
"@sockethub/schemas": "^3.0.0-alpha.11",
|
|
52
53
|
"@xmpp/xml": "^0.13.2",
|
|
53
54
|
"jsdoc-to-markdown": "^8.0.3",
|
|
54
55
|
"sinon": "^17.0.2"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
|
-
"@sockethub/server": "^5.0.0-alpha.
|
|
58
|
+
"@sockethub/server": "^5.0.0-alpha.11"
|
|
58
59
|
},
|
|
59
60
|
"peerDependenciesMeta": {
|
|
60
61
|
"@sockethub/server": {
|
|
61
62
|
"optional": true
|
|
62
63
|
}
|
|
63
64
|
},
|
|
64
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "c243fa9e76c688ce5ffcf524400b1dd27dcce615"
|
|
65
66
|
}
|
package/src/incoming-handlers.js
CHANGED
|
@@ -63,7 +63,9 @@ export class IncomingHandlers {
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
this.session.debug(
|
|
66
|
+
this.session.log.debug(
|
|
67
|
+
"received close event with no handler specified",
|
|
68
|
+
);
|
|
67
69
|
if (this.session.actor && this.session.sendToClient) {
|
|
68
70
|
this.session.sendToClient({
|
|
69
71
|
context: "xmpp",
|
|
@@ -71,7 +73,7 @@ export class IncomingHandlers {
|
|
|
71
73
|
actor: this.session.actor,
|
|
72
74
|
target: this.session.actor,
|
|
73
75
|
});
|
|
74
|
-
this.session.debug(
|
|
76
|
+
this.session.log.debug(
|
|
75
77
|
`**** xmpp this.session.for ${this.session.actor.id} closed`,
|
|
76
78
|
);
|
|
77
79
|
}
|
|
@@ -95,7 +97,7 @@ export class IncomingHandlers {
|
|
|
95
97
|
},
|
|
96
98
|
});
|
|
97
99
|
} catch (e) {
|
|
98
|
-
this.session.debug("*** XMPP ERROR (rl catch): ", e);
|
|
100
|
+
this.session.log.debug("*** XMPP ERROR (rl catch): ", e);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -120,14 +122,14 @@ export class IncomingHandlers {
|
|
|
120
122
|
} else {
|
|
121
123
|
obj.actor.name = stanza.attrs.from.split("/")[1];
|
|
122
124
|
}
|
|
123
|
-
this.session.debug(
|
|
125
|
+
this.session.log.debug(
|
|
124
126
|
`received contact presence update from ${stanza.attrs.from}`,
|
|
125
127
|
);
|
|
126
128
|
this.session.sendToClient(obj);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
subscribe(to, from, name) {
|
|
130
|
-
this.session.debug(`received subscribe request from ${from}`);
|
|
132
|
+
this.session.log.debug(`received subscribe request from ${from}`);
|
|
131
133
|
const actor = { id: from, type: "person" };
|
|
132
134
|
if (name) {
|
|
133
135
|
actor.name = name;
|
|
@@ -141,7 +143,7 @@ export class IncomingHandlers {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
// unsubscribe(from) {
|
|
144
|
-
// this.session.debug('received unsubscribe request from ' + from);
|
|
146
|
+
// this.session.log.debug('received unsubscribe request from ' + from);
|
|
145
147
|
// this.session.sendToClient({
|
|
146
148
|
// type: "remove-friend",
|
|
147
149
|
// actor: { id: from },
|
|
@@ -262,7 +264,7 @@ export class IncomingHandlers {
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
online() {
|
|
265
|
-
this.session.debug("online");
|
|
267
|
+
this.session.log.debug("online");
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
/**
|
|
@@ -281,7 +283,7 @@ export class IncomingHandlers {
|
|
|
281
283
|
stanza.attrs.id === "muc_id" &&
|
|
282
284
|
stanza.attrs.type === "result"
|
|
283
285
|
) {
|
|
284
|
-
this.session.debug("got room attendance list");
|
|
286
|
+
this.session.log.debug("got room attendance list");
|
|
285
287
|
return this.notifyRoomAttendance(stanza);
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -293,7 +295,7 @@ export class IncomingHandlers {
|
|
|
293
295
|
if (!entries.hasOwn(e)) {
|
|
294
296
|
continue;
|
|
295
297
|
}
|
|
296
|
-
this.session.debug("STANZA ATTRS: ", entries[e].attrs);
|
|
298
|
+
this.session.log.debug("STANZA ATTRS: ", entries[e].attrs);
|
|
297
299
|
if (entries[e].attrs.subscription === "both") {
|
|
298
300
|
this.session.sendToClient({
|
|
299
301
|
context: "xmpp",
|
|
@@ -342,7 +344,7 @@ export class IncomingHandlers {
|
|
|
342
344
|
}
|
|
343
345
|
}
|
|
344
346
|
} else {
|
|
345
|
-
this.session.debug(`got XMPP unknown stanza... ${stanza}`);
|
|
347
|
+
this.session.log.debug(`got XMPP unknown stanza... ${stanza}`);
|
|
346
348
|
}
|
|
347
349
|
}
|
|
348
350
|
}
|
|
@@ -15,7 +15,7 @@ describe("Incoming handlers", () => {
|
|
|
15
15
|
sendToClient = sinon.fake();
|
|
16
16
|
ih = new IncomingHandlers({
|
|
17
17
|
sendToClient: sendToClient,
|
|
18
|
-
debug: sinon.fake(),
|
|
18
|
+
log: { debug: sinon.fake() },
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -43,11 +43,16 @@ describe("Incoming handlers", () => {
|
|
|
43
43
|
|
|
44
44
|
it("close() should handle session without actor gracefully", () => {
|
|
45
45
|
const sendToClient = sinon.fake();
|
|
46
|
-
const
|
|
46
|
+
const log = {
|
|
47
|
+
error: sinon.fake(),
|
|
48
|
+
warn: sinon.fake(),
|
|
49
|
+
info: sinon.fake(),
|
|
50
|
+
debug: sinon.fake(),
|
|
51
|
+
};
|
|
47
52
|
|
|
48
53
|
const ih = new IncomingHandlers({
|
|
49
54
|
sendToClient: sendToClient,
|
|
50
|
-
|
|
55
|
+
log: log,
|
|
51
56
|
actor: undefined,
|
|
52
57
|
connection: undefined
|
|
53
58
|
});
|
|
@@ -55,17 +60,22 @@ describe("Incoming handlers", () => {
|
|
|
55
60
|
// This should not throw an error
|
|
56
61
|
expect(() => ih.close()).not.toThrow();
|
|
57
62
|
|
|
58
|
-
// Should still call debug
|
|
59
|
-
sinon.assert.calledWith(debug, "received close event with no handler specified");
|
|
63
|
+
// Should still call log.debug
|
|
64
|
+
sinon.assert.calledWith(log.debug, "received close event with no handler specified");
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
it("close() should handle session without connection gracefully", () => {
|
|
63
68
|
const sendToClient = sinon.fake();
|
|
64
|
-
const
|
|
69
|
+
const log = {
|
|
70
|
+
error: sinon.fake(),
|
|
71
|
+
warn: sinon.fake(),
|
|
72
|
+
info: sinon.fake(),
|
|
73
|
+
debug: sinon.fake(),
|
|
74
|
+
};
|
|
65
75
|
|
|
66
76
|
const ih = new IncomingHandlers({
|
|
67
77
|
sendToClient: sendToClient,
|
|
68
|
-
|
|
78
|
+
log: log,
|
|
69
79
|
actor: { id: "test@example.com" },
|
|
70
80
|
connection: undefined
|
|
71
81
|
});
|
|
@@ -76,11 +86,16 @@ describe("Incoming handlers", () => {
|
|
|
76
86
|
|
|
77
87
|
it("close() should handle session with invalid connection gracefully", () => {
|
|
78
88
|
const sendToClient = sinon.fake();
|
|
79
|
-
const
|
|
89
|
+
const log = {
|
|
90
|
+
error: sinon.fake(),
|
|
91
|
+
warn: sinon.fake(),
|
|
92
|
+
info: sinon.fake(),
|
|
93
|
+
debug: sinon.fake(),
|
|
94
|
+
};
|
|
80
95
|
|
|
81
96
|
const ih = new IncomingHandlers({
|
|
82
97
|
sendToClient: sendToClient,
|
|
83
|
-
|
|
98
|
+
log: log,
|
|
84
99
|
actor: { id: "test@example.com" },
|
|
85
100
|
connection: { disconnect: null } // invalid disconnect method
|
|
86
101
|
});
|
package/src/index.js
CHANGED
|
@@ -40,10 +40,10 @@ export default class XMPP {
|
|
|
40
40
|
this.config = {
|
|
41
41
|
connectTimeoutMs: 10000,
|
|
42
42
|
persist: true,
|
|
43
|
-
initialized: false,
|
|
44
43
|
requireCredentials: ["connect"],
|
|
45
44
|
};
|
|
46
|
-
this.
|
|
45
|
+
this.__initialized = false; // Private state for initialization tracking
|
|
46
|
+
this.log = session.log;
|
|
47
47
|
this.sendToClient = session.sendToClient;
|
|
48
48
|
this.createClient();
|
|
49
49
|
this.createXml();
|
|
@@ -61,15 +61,15 @@ export default class XMPP {
|
|
|
61
61
|
* @param {boolean} stopReconnection - If true, stop automatic reconnection
|
|
62
62
|
*/
|
|
63
63
|
__markDisconnected(stopReconnection = false) {
|
|
64
|
-
this.debug(`marking client as disconnected for ${this.id}`);
|
|
64
|
+
this.log.debug(`marking client as disconnected for ${this.id}`);
|
|
65
65
|
|
|
66
66
|
if (stopReconnection && this.__client) {
|
|
67
|
-
this.debug(`stopping automatic reconnection for ${this.id}`);
|
|
67
|
+
this.log.debug(`stopping automatic reconnection for ${this.id}`);
|
|
68
68
|
this.__client.stop();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
this.__client = undefined;
|
|
72
|
-
this.
|
|
72
|
+
this.__initialized = false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
@@ -132,7 +132,7 @@ export default class XMPP {
|
|
|
132
132
|
this.__client.status === "online"
|
|
133
133
|
);
|
|
134
134
|
} catch (err) {
|
|
135
|
-
this.debug("Error checking client connection status:", err);
|
|
135
|
+
this.log.debug("Error checking client connection status:", err);
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -183,6 +183,17 @@ export default class XMPP {
|
|
|
183
183
|
return PlatformSchema;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Returns whether the platform is ready to handle jobs.
|
|
188
|
+
* For XMPP, this means we have successfully connected to the server.
|
|
189
|
+
* During temporary network interruptions with automatic reconnection,
|
|
190
|
+
* remains true to allow queued jobs to retry rather than fail.
|
|
191
|
+
* @returns {boolean} true if ready to handle jobs
|
|
192
|
+
*/
|
|
193
|
+
isInitialized() {
|
|
194
|
+
return this.__initialized;
|
|
195
|
+
}
|
|
196
|
+
|
|
186
197
|
/**
|
|
187
198
|
* Connect to the XMPP server.
|
|
188
199
|
*
|
|
@@ -205,15 +216,17 @@ export default class XMPP {
|
|
|
205
216
|
*/
|
|
206
217
|
connect(job, credentials, done) {
|
|
207
218
|
if (this.__isClientConnected()) {
|
|
208
|
-
this.debug(
|
|
209
|
-
|
|
219
|
+
this.log.debug(
|
|
220
|
+
`client connection already exists for ${job.actor.id}`,
|
|
221
|
+
);
|
|
222
|
+
this.__initialized = true;
|
|
210
223
|
return done();
|
|
211
224
|
}
|
|
212
|
-
this.debug(`connect() called for ${job.actor.id}`);
|
|
225
|
+
this.log.debug(`connect() called for ${job.actor.id}`);
|
|
213
226
|
|
|
214
227
|
// Log credential processing
|
|
215
228
|
const xmppCreds = utils.buildXmppCredentials(credentials);
|
|
216
|
-
this.debug(
|
|
229
|
+
this.log.debug(
|
|
217
230
|
`building XMPP credentials for ${job.actor.id}:`,
|
|
218
231
|
JSON.stringify({
|
|
219
232
|
service: xmppCreds.service,
|
|
@@ -224,26 +237,59 @@ export default class XMPP {
|
|
|
224
237
|
);
|
|
225
238
|
|
|
226
239
|
// Log before client creation
|
|
227
|
-
this.debug(`creating XMPP client for ${job.actor.id}`);
|
|
240
|
+
this.log.debug(`creating XMPP client for ${job.actor.id}`);
|
|
228
241
|
|
|
229
242
|
try {
|
|
230
243
|
this.__client = this.__clientConstructor({
|
|
231
244
|
...xmppCreds,
|
|
232
245
|
...{ timeout: this.config.connectTimeoutMs, tls: false },
|
|
233
246
|
});
|
|
234
|
-
this.debug(
|
|
247
|
+
this.log.debug(
|
|
248
|
+
`XMPP client created successfully for ${job.actor.id}`,
|
|
249
|
+
);
|
|
235
250
|
} catch (err) {
|
|
236
|
-
this.debug(
|
|
251
|
+
this.log.debug(
|
|
252
|
+
`XMPP client creation failed for ${job.actor.id}:`,
|
|
253
|
+
err,
|
|
254
|
+
);
|
|
237
255
|
return done(`client creation failed: ${err.message}`);
|
|
238
256
|
}
|
|
239
257
|
|
|
240
258
|
this.__client.on("offline", () => {
|
|
241
|
-
this.debug(`offline event received for ${job.actor.id}`);
|
|
242
|
-
|
|
259
|
+
this.log.debug(`offline event received for ${job.actor.id}`);
|
|
260
|
+
// If we were never initialized, mark as disconnected (connection failed)
|
|
261
|
+
// If we were previously initialized, keep state (will auto-reconnect)
|
|
262
|
+
// This preserves initialized state during brief network interruptions
|
|
263
|
+
// while properly handling initial connection failures
|
|
264
|
+
if (!this.__initialized) {
|
|
265
|
+
this.log.debug(
|
|
266
|
+
`offline during initial connection for ${job.actor.id}`,
|
|
267
|
+
);
|
|
268
|
+
this.__markDisconnected();
|
|
269
|
+
} else {
|
|
270
|
+
this.log.debug(
|
|
271
|
+
`offline after successful connection for ${job.actor.id}, will auto-reconnect`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
243
274
|
});
|
|
244
275
|
|
|
245
276
|
this.__client.on("error", (err) => {
|
|
246
|
-
|
|
277
|
+
// Internal code errors (TypeError, ReferenceError, etc.) indicate bugs
|
|
278
|
+
// in our code. These should crash the platform process immediately
|
|
279
|
+
// as we can't trust the state after such errors.
|
|
280
|
+
if (
|
|
281
|
+
err instanceof TypeError ||
|
|
282
|
+
err instanceof ReferenceError ||
|
|
283
|
+
err instanceof SyntaxError
|
|
284
|
+
) {
|
|
285
|
+
this.log.error(
|
|
286
|
+
`FATAL: Internal code error in XMPP platform: ${err.toString()}`,
|
|
287
|
+
);
|
|
288
|
+
this.log.error(err.stack);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.log.debug(
|
|
247
293
|
`network error event for ${job.actor.id}:${err.toString()}`,
|
|
248
294
|
);
|
|
249
295
|
|
|
@@ -256,9 +302,9 @@ export default class XMPP {
|
|
|
256
302
|
};
|
|
257
303
|
|
|
258
304
|
if (errorType === "RECOVERABLE") {
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
305
|
+
// For recoverable errors, keep initialized=true and let the client
|
|
306
|
+
// auto-reconnect. Don't call stop() or clear the client reference.
|
|
307
|
+
// This allows queued jobs to wait for reconnection instead of failing.
|
|
262
308
|
as.error = `Connection lost: ${err.toString()}. Attempting automatic reconnection...`;
|
|
263
309
|
as.object = {
|
|
264
310
|
type: "connect",
|
|
@@ -266,7 +312,7 @@ export default class XMPP {
|
|
|
266
312
|
condition: err.condition || "network",
|
|
267
313
|
};
|
|
268
314
|
} else {
|
|
269
|
-
//
|
|
315
|
+
// On unrecoverable errors, mark as uninitialized and stop reconnection
|
|
270
316
|
this.__markDisconnected(true);
|
|
271
317
|
|
|
272
318
|
as.error = `Connection failed: ${err.toString()}. Manual reconnection required.`;
|
|
@@ -280,10 +326,10 @@ export default class XMPP {
|
|
|
280
326
|
});
|
|
281
327
|
|
|
282
328
|
this.__client.on("online", () => {
|
|
283
|
-
this.debug(`online event received for ${job.actor.id}`);
|
|
329
|
+
this.log.debug(`online event received for ${job.actor.id}`);
|
|
284
330
|
});
|
|
285
331
|
|
|
286
|
-
this.debug(`starting XMPP client connection for ${job.actor.id}`);
|
|
332
|
+
this.log.debug(`starting XMPP client connection for ${job.actor.id}`);
|
|
287
333
|
const startTime = Date.now();
|
|
288
334
|
|
|
289
335
|
this.__client
|
|
@@ -291,16 +337,16 @@ export default class XMPP {
|
|
|
291
337
|
.then(() => {
|
|
292
338
|
// connected
|
|
293
339
|
const duration = Date.now() - startTime;
|
|
294
|
-
this.debug(
|
|
340
|
+
this.log.debug(
|
|
295
341
|
`connection successful for ${job.actor.id} after ${duration}ms`,
|
|
296
342
|
);
|
|
297
|
-
this.
|
|
343
|
+
this.__initialized = true;
|
|
298
344
|
this.__registerHandlers();
|
|
299
345
|
return done();
|
|
300
346
|
})
|
|
301
347
|
.catch((err) => {
|
|
302
348
|
const duration = Date.now() - startTime;
|
|
303
|
-
this.debug(
|
|
349
|
+
this.log.debug(
|
|
304
350
|
`connection failed for ${job.actor.id} after ${duration}ms:`,
|
|
305
351
|
{
|
|
306
352
|
error: err,
|
|
@@ -339,7 +385,7 @@ export default class XMPP {
|
|
|
339
385
|
* }
|
|
340
386
|
*/
|
|
341
387
|
async join(job, done) {
|
|
342
|
-
this.debug(
|
|
388
|
+
this.log.debug(
|
|
343
389
|
`sending join from ${job.actor.id} to ` +
|
|
344
390
|
`${job.target.id}/${job.actor.name}`,
|
|
345
391
|
);
|
|
@@ -382,7 +428,7 @@ export default class XMPP {
|
|
|
382
428
|
* }
|
|
383
429
|
*/
|
|
384
430
|
leave(job, done) {
|
|
385
|
-
this.debug(
|
|
431
|
+
this.log.debug(
|
|
386
432
|
`sending leave from ${job.actor.id} to ` +
|
|
387
433
|
`${job.target.id}/${job.actor.name}`,
|
|
388
434
|
);
|
|
@@ -452,7 +498,7 @@ export default class XMPP {
|
|
|
452
498
|
*
|
|
453
499
|
*/
|
|
454
500
|
send(job, done) {
|
|
455
|
-
this.debug(`send() called for ${job.actor.id}`);
|
|
501
|
+
this.log.debug(`send() called for ${job.actor.id}`);
|
|
456
502
|
// send message
|
|
457
503
|
const message = this.__xml(
|
|
458
504
|
"message",
|
|
@@ -496,7 +542,7 @@ export default class XMPP {
|
|
|
496
542
|
* }
|
|
497
543
|
*/
|
|
498
544
|
update(job, done) {
|
|
499
|
-
this.debug(`update() called for ${job.actor.id}`);
|
|
545
|
+
this.log.debug(`update() called for ${job.actor.id}`);
|
|
500
546
|
const props = {};
|
|
501
547
|
const show = {};
|
|
502
548
|
const status = {};
|
|
@@ -510,7 +556,7 @@ export default class XMPP {
|
|
|
510
556
|
status.status = job.object.content;
|
|
511
557
|
}
|
|
512
558
|
// setting presence
|
|
513
|
-
this.debug(`setting presence: ${job.object.presence}`);
|
|
559
|
+
this.log.debug(`setting presence: ${job.object.presence}`);
|
|
514
560
|
this.__client
|
|
515
561
|
.send(this.__xml("presence", props, show, status))
|
|
516
562
|
.then(done);
|
|
@@ -540,7 +586,7 @@ export default class XMPP {
|
|
|
540
586
|
* }
|
|
541
587
|
*/
|
|
542
588
|
"request-friend"(job, done) {
|
|
543
|
-
this.debug(`request-friend() called for ${job.actor.id}`);
|
|
589
|
+
this.log.debug(`request-friend() called for ${job.actor.id}`);
|
|
544
590
|
this.__client
|
|
545
591
|
.send(
|
|
546
592
|
this.__xml("presence", {
|
|
@@ -572,7 +618,7 @@ export default class XMPP {
|
|
|
572
618
|
* }
|
|
573
619
|
*/
|
|
574
620
|
"remove-friend"(job, done) {
|
|
575
|
-
this.debug(`remove-friend() called for ${job.actor.id}`);
|
|
621
|
+
this.log.debug(`remove-friend() called for ${job.actor.id}`);
|
|
576
622
|
this.__client
|
|
577
623
|
.send(
|
|
578
624
|
this.__xml("presence", {
|
|
@@ -604,7 +650,7 @@ export default class XMPP {
|
|
|
604
650
|
* }
|
|
605
651
|
*/
|
|
606
652
|
"make-friend"(job, done) {
|
|
607
|
-
this.debug(`make-friend() called for ${job.actor.id}`);
|
|
653
|
+
this.log.debug(`make-friend() called for ${job.actor.id}`);
|
|
608
654
|
this.__client
|
|
609
655
|
.send(
|
|
610
656
|
this.__xml("presence", {
|
|
@@ -664,7 +710,9 @@ export default class XMPP {
|
|
|
664
710
|
* }
|
|
665
711
|
*/
|
|
666
712
|
query(job, done) {
|
|
667
|
-
this.debug(
|
|
713
|
+
this.log.debug(
|
|
714
|
+
`sending query from ${job.actor.id} for ${job.target.id}`,
|
|
715
|
+
);
|
|
668
716
|
this.__client
|
|
669
717
|
.send(
|
|
670
718
|
this.__xml(
|
|
@@ -700,7 +748,7 @@ export default class XMPP {
|
|
|
700
748
|
* }
|
|
701
749
|
*/
|
|
702
750
|
disconnect(job, done) {
|
|
703
|
-
this.debug("disconnecting");
|
|
751
|
+
this.log.debug("disconnecting");
|
|
704
752
|
this.cleanup(done);
|
|
705
753
|
}
|
|
706
754
|
|
|
@@ -710,8 +758,8 @@ export default class XMPP {
|
|
|
710
758
|
* @param {function} done - callback when complete
|
|
711
759
|
*/
|
|
712
760
|
cleanup(done) {
|
|
713
|
-
this.debug("cleanup");
|
|
714
|
-
this.
|
|
761
|
+
this.log.debug("cleanup");
|
|
762
|
+
this.__initialized = false;
|
|
715
763
|
this.__client.stop();
|
|
716
764
|
done();
|
|
717
765
|
}
|
package/src/index.test.js
CHANGED
|
@@ -185,7 +185,12 @@ describe("XMPP", () => {
|
|
|
185
185
|
|
|
186
186
|
xp = new TestXMPP({
|
|
187
187
|
id: actor,
|
|
188
|
-
|
|
188
|
+
log: {
|
|
189
|
+
error: sinon.fake(),
|
|
190
|
+
warn: sinon.fake(),
|
|
191
|
+
info: sinon.fake(),
|
|
192
|
+
debug: sinon.fake(),
|
|
193
|
+
},
|
|
189
194
|
sendToClient: sinon.fake(),
|
|
190
195
|
});
|
|
191
196
|
});
|
|
@@ -240,6 +245,107 @@ describe("XMPP", () => {
|
|
|
240
245
|
});
|
|
241
246
|
});
|
|
242
247
|
|
|
248
|
+
describe("Error handling", () => {
|
|
249
|
+
let processExitStub;
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
// Stub process.exit to prevent test process from exiting
|
|
253
|
+
processExitStub = sinon.stub(process, "exit");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(() => {
|
|
257
|
+
processExitStub.restore();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("terminates process on TypeError (internal code error)", (done) => {
|
|
261
|
+
xp.connect(job.connect, credentials, () => {
|
|
262
|
+
// Get the error event handler that was registered
|
|
263
|
+
const errorHandler = clientObjectFake.on.getCalls().find(
|
|
264
|
+
(call) => call.args[0] === "error",
|
|
265
|
+
).args[1];
|
|
266
|
+
|
|
267
|
+
// Simulate a TypeError (internal code error)
|
|
268
|
+
const typeError = new TypeError("this.session.debug is not a function");
|
|
269
|
+
errorHandler(typeError);
|
|
270
|
+
|
|
271
|
+
// Verify process.exit(1) was called
|
|
272
|
+
sinon.assert.calledOnce(processExitStub);
|
|
273
|
+
sinon.assert.calledWith(processExitStub, 1);
|
|
274
|
+
|
|
275
|
+
// Verify error was logged
|
|
276
|
+
sinon.assert.called(xp.log.error);
|
|
277
|
+
expect(
|
|
278
|
+
xp.log.error
|
|
279
|
+
.getCalls()
|
|
280
|
+
.some((call) =>
|
|
281
|
+
call.args[0].includes("FATAL: Internal code error"),
|
|
282
|
+
),
|
|
283
|
+
).toBeTrue();
|
|
284
|
+
|
|
285
|
+
done();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("terminates process on ReferenceError (internal code error)", (done) => {
|
|
290
|
+
xp.connect(job.connect, credentials, () => {
|
|
291
|
+
const errorHandler = clientObjectFake.on.getCalls().find(
|
|
292
|
+
(call) => call.args[0] === "error",
|
|
293
|
+
).args[1];
|
|
294
|
+
|
|
295
|
+
const refError = new ReferenceError("foo is not defined");
|
|
296
|
+
errorHandler(refError);
|
|
297
|
+
|
|
298
|
+
sinon.assert.calledOnce(processExitStub);
|
|
299
|
+
sinon.assert.calledWith(processExitStub, 1);
|
|
300
|
+
sinon.assert.called(xp.log.error);
|
|
301
|
+
|
|
302
|
+
done();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("does NOT terminate process on network errors (recoverable)", (done) => {
|
|
307
|
+
xp.connect(job.connect, credentials, () => {
|
|
308
|
+
const errorHandler = clientObjectFake.on.getCalls().find(
|
|
309
|
+
(call) => call.args[0] === "error",
|
|
310
|
+
).args[1];
|
|
311
|
+
|
|
312
|
+
// Simulate a network error
|
|
313
|
+
const networkError = new Error("ECONNRESET");
|
|
314
|
+
networkError.code = "ECONNRESET";
|
|
315
|
+
errorHandler(networkError);
|
|
316
|
+
|
|
317
|
+
// Should NOT call process.exit for network errors
|
|
318
|
+
sinon.assert.notCalled(processExitStub);
|
|
319
|
+
|
|
320
|
+
// Should send error to client instead
|
|
321
|
+
sinon.assert.called(xp.sendToClient);
|
|
322
|
+
|
|
323
|
+
done();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("does NOT terminate process on XMPP protocol errors (non-recoverable)", (done) => {
|
|
328
|
+
xp.connect(job.connect, credentials, () => {
|
|
329
|
+
const errorHandler = clientObjectFake.on.getCalls().find(
|
|
330
|
+
(call) => call.args[0] === "error",
|
|
331
|
+
).args[1];
|
|
332
|
+
|
|
333
|
+
// Simulate an XMPP protocol error (e.g., auth failure)
|
|
334
|
+
const authError = new Error("not-authorized");
|
|
335
|
+
authError.condition = "not-authorized";
|
|
336
|
+
errorHandler(authError);
|
|
337
|
+
|
|
338
|
+
// Should NOT call process.exit for XMPP errors
|
|
339
|
+
sinon.assert.notCalled(processExitStub);
|
|
340
|
+
|
|
341
|
+
// Should send error to client and mark disconnected
|
|
342
|
+
sinon.assert.called(xp.sendToClient);
|
|
343
|
+
|
|
344
|
+
done();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
243
349
|
describe("Platform functionality", () => {
|
|
244
350
|
beforeEach((done) => {
|
|
245
351
|
xp.connect(job.join, credentials, () => done());
|
|
@@ -484,9 +590,9 @@ describe("XMPP", () => {
|
|
|
484
590
|
|
|
485
591
|
describe("#cleanup", () => {
|
|
486
592
|
it("calls client.stop", (done) => {
|
|
487
|
-
expect(xp.
|
|
593
|
+
expect(xp.isInitialized()).toEqual(true);
|
|
488
594
|
xp.cleanup(() => {
|
|
489
|
-
expect(xp.
|
|
595
|
+
expect(xp.isInitialized()).toEqual(false);
|
|
490
596
|
sinon.assert.calledOnce(xp.__client.stop);
|
|
491
597
|
done()
|
|
492
598
|
});
|