Pin only the unbind ordering, type peerUnbound, and drop what the port made stale
Test / lint (pull_request) Successful in 23s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 34s
Test / test (22) (pull_request) Successful in 35s
Test / test (24) (pull_request) Successful in 35s
Test / test (26) (pull_request) Successful in 34s
Mirror / push (push) Successful in 6s

This commit was merged in pull request #33.
This commit is contained in:
2026-09-28 00:43:26 +02:00
parent b2183ea30e
commit efb59b6cff
4 changed files with 32 additions and 37 deletions
+1 -2
View File
@@ -88,8 +88,7 @@ src/
Imports point one way: `defs` knows nothing above it but `result.ts`, `pdu` uses `defs`, `session` Imports point one way: `defs` knows nothing above it but `result.ts`, `pdu` uses `defs`, `session`
uses `pdu`, and `client`/`server` use `session`. The one way back up is the `Session` handed to uses `pdu`, and `client`/`server` use `session`. The one way back up is the `Session` handed to
`createSms()`, imported as a type only; `IncomingRequests` reaches its session through the `createSms()`, imported as a type only.
`IncomingDeps` port.
**Parameter order is wire order.** The key order inside `cmds.*.params` is the order the fields are **Parameter order is wire order.** The key order inside `cmds.*.params` is the order the fields are
written to and read from the buffer. Never sort those alphabetically — the alphabetical-ordering written to and read from the buffer. Never sort those alphabetically — the alphabetical-ordering
+1 -1
View File
@@ -57,7 +57,7 @@ export type IncomingDeps = {
offerSms: (sms: Sms) => boolean; offerSms: (sms: Sms) => boolean;
/** The application's first refusal, answering true where it took the request itself. */ /** The application's first refusal, answering true where it took the request itself. */
onRequest?: ((pduObj: PduObject) => Promise<boolean> | boolean) | undefined; onRequest?: ((pduObj: PduObject) => Promise<boolean> | boolean) | undefined;
peerUnbound: () => Promise<unknown>; peerUnbound: () => Promise<VoidResult>;
reportDlr: (dlr: Dlr, pduObj: PduObject) => void; reportDlr: (dlr: Dlr, pduObj: PduObject) => void;
reportError: (err: Error) => void; reportError: (err: Error) => void;
reportMessageDlr: (merged: MessageDlr) => void; reportMessageDlr: (merged: MessageDlr) => void;
+29 -32
View File
@@ -100,7 +100,6 @@ function abortAfter(
}); });
} }
/** A port with no link behind it; the session is only what an Sms carries and answers through. */
function stubPort(session: Session, port: Partial<IncomingDeps> = {}): IncomingDeps { function stubPort(session: Session, port: Partial<IncomingDeps> = {}): IncomingDeps {
return { return {
answer: (pduObj, status, params) => session.sendReturn(pduObj, status, params), answer: (pduObj, status, params) => session.sendReturn(pduObj, status, params),
@@ -109,7 +108,7 @@ function stubPort(session: Session, port: Partial<IncomingDeps> = {}): IncomingD
createSms: (input, handlers) => createSms({ ...input, session }, handlers), createSms: (input, handlers) => createSms({ ...input, session }, handlers),
linkEnd: () => 'smsc', linkEnd: () => 'smsc',
offerSms: sms => session.emit('sms', sms), offerSms: sms => session.emit('sms', sms),
peerUnbound: () => Promise.resolve(), peerUnbound: () => Promise.resolve({}),
reportDlr: () => undefined, reportDlr: () => undefined,
reportError: () => undefined, reportError: () => undefined,
reportMessageDlr: () => undefined, reportMessageDlr: () => undefined,
@@ -773,36 +772,6 @@ describe('reconnect', () => {
assert.equal(messages, 1, 'the harness delivers a message whose link stayed'); assert.equal(messages, 1, 'the harness delivers a message whose link stayed');
}); });
test('answers an unbind before asking the session to end, and answers a command outside the bind', async t => {
const session = new Session({ sock: new net.Socket() });
closeAfter(t, session);
const calls: string[] = [];
const incoming = new IncomingRequests({
deps: stubPort(session, {
answer: (pduObj, status) => {
calls.push(`${pduObj.cmdName} ${status ?? 'ESME_ROK'}`);
return Promise.resolve({});
},
bindAllows: cmdName => cmdName !== 'submit_sm',
peerUnbound: () => {
calls.push('peerUnbound');
return Promise.resolve();
},
}),
dlrMerger: new DlrMerger({ log: silentLog, max: 10, timeout: 10_000 }),
log: silentLog,
});
await incoming.handle(submitPdu(1));
await incoming.handle({ ...submitPdu(2), cmdId: 0x00000006, cmdName: 'unbind', params: {} });
assert.deepEqual(calls, ['submit_sm ESME_RINVBNDSTS', 'unbind ESME_ROK', 'peerUnbound']);
});
test('does not reconnect after an explicit close', async t => { test('does not reconnect after an explicit close', async t => {
const smpp = await startServer(t); const smpp = await startServer(t);
const { session } = await connect(t, smpp, { reconnect: { maxDelay: 50, minDelay: 10 } }); const { session } = await connect(t, smpp, { reconnect: { maxDelay: 50, minDelay: 10 } });
@@ -3114,6 +3083,34 @@ describe('graceful shutdown', () => {
assert.deepEqual(reported, []); assert.deepEqual(reported, []);
}); });
test('answers a peer\'s unbind before asking the session to end', async t => {
const session = new Session({ sock: new net.Socket() });
closeAfter(t, session);
const calls: string[] = [];
const incoming = new IncomingRequests({
deps: stubPort(session, {
answer: pduObj => {
calls.push(pduObj.cmdName);
return Promise.resolve({});
},
peerUnbound: () => {
calls.push('peerUnbound');
return Promise.resolve({});
},
}),
dlrMerger: new DlrMerger({ log: silentLog, max: 10, timeout: 10_000 }),
log: silentLog,
});
await incoming.handle({ ...submitPdu(1), cmdId: 0x00000006, cmdName: 'unbind', params: {} });
assert.deepEqual(calls, ['unbind', 'peerUnbound']);
});
}); });
describe('message id notation', () => { describe('message id notation', () => {
+1 -2
View File
@@ -301,8 +301,7 @@ next work ([decision](docs/decisions.md#internals-and-tests)).
says "`src/` stays flat until a module has to move for another reason. Valid while that map is says "`src/` stays flat until a module has to move for another reason. Valid while that map is
what a reader navigates by" — and both architects reported that the map is now AGENTS.md rather what a reader navigates by" — and both architects reported that the map is now AGENTS.md rather
than the tree, which is that premise failing. `todo.md` already carries the opposite than the tree, which is that premise failing. `todo.md` already carries the opposite
instruction under Worth doing. Two records, opposite answers; one has to go. Do it in the same instruction under Worth doing. Two records, opposite answers; one has to go.
change as the `IncomingRequests` port or the imports are rewritten twice.
- [ ] **Split `test/session-extras.test.ts` by the question each block answers.** 3,010 lines, 19 - [ ] **Split `test/session-extras.test.ts` by the question each block answers.** 3,010 lines, 19
unrelated `describe` blocks whose names are already the file names they should be. With unrelated `describe` blocks whose names are already the file names they should be. With