Have drop() name the event a lost link warrants, and name requestOnCurrentLink() for what it does
Mirror / push (push) Successful in 6s
Test / test (18) (pull_request) Successful in 31s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 31s
Test / lint (pull_request) Successful in 26s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 31s

This commit is contained in:
2026-09-28 21:31:48 +02:00
parent 2152b995d4
commit fe320d23a8
5 changed files with 27 additions and 17 deletions
+4 -4
View File
@@ -107,14 +107,14 @@ export class LinkLife {
this.release({});
}
/** The attached link is gone. False means there was none to lose. */
drop(): boolean {
if (!this.isAttached()) return false;
/** The attached link is gone: the event that says so, or undefined when there was none to lose. */
drop(): 'close' | 'disconnected' | undefined {
if (!this.isAttached()) return undefined;
this.phase = 'down';
this.drops++;
return true;
return this.retrying() ? 'disconnected' : 'close';
}
stop(): void {
+2 -2
View File
@@ -97,7 +97,7 @@ export class OutgoingRequests {
if (bindCommands.includes(input.cmdName)) {
const shut = this.link.refusal();
return shut ? { err: shut } : this.requestPastDrainGateAndWindow(input, options);
return shut ? { err: shut } : this.requestOnCurrentLink(input, options);
}
const waitForLink = this.link.hold(options.signal);
@@ -118,7 +118,7 @@ export class OutgoingRequests {
}
/** Straight onto the current link, for what has to go out either way. */
async requestPastDrainGateAndWindow(
async requestOnCurrentLink(
input: PduObjectInput,
options: SendOptions = {},
): Promise<Result<{ pduObj: PduObject }>> {
+5 -5
View File
@@ -237,7 +237,7 @@ export class Session extends EventEmitter<SessionEvents> {
const drained = await this.drain(undefined);
const wasOpen = this.link.isAttached();
const sent = wasOpen
? await this.outgoing.requestPastDrainGateAndWindow({ cmdName: 'unbind' })
? await this.outgoing.requestOnCurrentLink({ cmdName: 'unbind' })
: { err: new Error('Session is closed') };
const closedOnUnbind = wasOpen && !this.link.isAttached();
@@ -376,17 +376,17 @@ export class Session extends EventEmitter<SessionEvents> {
}
private teardown(): void {
// Read once: clear() reports lost segments, and a listener could stop the session between reads.
const retrying = this.link.retrying();
const lost = this.link.drop();
if (!this.link.drop()) return;
if (!lost) return;
this.outgoing.linkLost();
this.timers.clear();
this.incoming.clear();
this.sock.destroy();
if (retrying) this.emit('disconnected');
// Not re-read: clear() reports lost segments, and a listener can stop the session in between.
if (lost === 'disconnected') this.emit('disconnected');
else this.emitClose();
}
+6 -5
View File
@@ -1460,19 +1460,20 @@ describe('LinkLife', () => {
assert.match(link.refusal()?.message ?? '', /closed/, 'stopped while down');
link.end();
assert.equal(link.awaitsNextLink(), false, 'ended');
assert.equal(new LinkLife({ log: silentLog, reconnects: false, timeout: 100 }).drop(), true);
});
test('drops an attached link once, and counts each drop', () => {
test('drops an attached link once, counts each drop, and names the event it warrants', () => {
const link = new LinkLife({ log: silentLog, reconnects: true, timeout: 100 });
const generation = link.generation();
assert.equal(link.drop(), true);
assert.equal(link.drop(), false, 'already down');
assert.equal(link.drop(), 'disconnected');
assert.equal(link.drop(), undefined, 'already down');
assert.equal(link.generation(), generation + 1);
link.attach();
assert.equal(link.drop(), true, 'a new link drops again');
link.stop();
assert.equal(link.drop(), 'close', 'a new link drops again, with none to follow it');
assert.equal(link.generation(), generation + 2);
assert.equal(new LinkLife({ log: silentLog, reconnects: false, timeout: 100 }).drop(), 'close');
});
test('releases a held request with the reason once the link ends', async () => {
+10 -1
View File
@@ -201,13 +201,22 @@ next work ([decision](docs/decisions.md#internals-and-tests)).
A second four-seat run on 2026-09-28, after #35–#40, read 6, 6, 7 and 6 again, Locality 5, 5, 6
and 5. Every seat ranked the session's lifecycle hardest and least wanted to modify it. A third,
after #46, read 6, 6, 7 and 7, Locality 5, 5, 6 and 6; the link's liveness now has one owner.
after #46, read 6, 6, 7 and 7, Locality 5, 5, 6 and 6. A fourth, after the link's liveness got one
owner in #48, read 6, 6, 6 and 6, Locality 5 from every seat: all four still ranked `Session.teardown()`
hardest, and the held-message flow across `incoming-requests.ts`, `held-messages.ts`, `sms.ts` and
`Session`'s rejection handler second.
- [ ] **Lift Locality to 7, and confirm it with a scoring run.** A run reading 7.0 or above also
retires the #30 and #46 decision.
### Correctness
- [ ] **Register a multipart send's receipt merge before its segments go out.** `Session.sendSms()`
calls `dlrMerger.expect()` only once `submitSms()` resolves, after the last segment's response,
so a receipt for an early segment that arrives first is logged at `debug` as naming no merge,
and the group then waits out `dlrMergeTimeout` with no `messageDlr`. Likeliest with a fast SMSC
or more segments than `maxOutstanding`. Goal 2. From the 2026-09-28 scoring run on #48.
- [ ] **Settle what a repeated tag not marked `multiple` reads as, and pin it in a test.** A vendor
tag or a known single-value tag a peer sends twice keeps the last occurrence and drops the
rest silently, which goal 3 argues against; listing it would change every such tag's shape.