Let the session own its bind state, written by bound() alone #42
@@ -309,6 +309,7 @@ the file.
|
||||
- A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a delivery,
|
||||
a `data_sm` by whichever it stands in for.
|
||||
- A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.
|
||||
- The bind state is the session's, `bound()` alone writes it, and it holds through a reconnect's gap.
|
||||
- A message id base is merged at most once.
|
||||
- A send that never reached the socket waits for the next link; one that did is counted, not resent.
|
||||
- A send queued for a send-window slot is bounded by the caller's `signal`, and by nothing else.
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
`alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back under. The
|
||||
two alternate names are gone from `tlvs` too, which is now typed by `TlvName`: narrow a `string` with `isTlvName()` before indexing it.
|
||||
- `cmds.broadcast_sm_resp.tlvMap` is removed; nothing read it.
|
||||
- `session.boundAs` and `session.peerInterfaceVersion` are read-only, and `session.loggedIn` is
|
||||
removed: read `session.boundAs !== undefined`. A session you wire yourself records the bind it
|
||||
accepted or had accepted with `session.bound(bindType, declaredVersion)`, which returns `err` for a
|
||||
bind type or version it cannot record. An assignment to either field does not compile in
|
||||
TypeScript, throws a `TypeError` in strict-mode code (every ES module, and any file under
|
||||
`'use strict'`), and is ignored otherwise.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
|
||||
+5
-1
@@ -13,7 +13,8 @@ shape is the same, connect, send, listen for delivery reports, with callbacks re
|
||||
a `session` event. It no longer calls back once per connection.
|
||||
- **The id a message is answered with goes to `sendResp({ smsId })`.** `sms.smsId` is read-only: the
|
||||
id the segments were answered with, the id `sendResp()` was given, or the generated UUID v7.
|
||||
Assigning to it throws a `TypeError`, since modules are strict mode.
|
||||
Assigning to it throws a `TypeError` in strict-mode code (every ES module, and any file under
|
||||
`'use strict'`), and is ignored otherwise.
|
||||
- **`smsIds` from `sendSms()` is `(string | undefined)[]`**, one entry per segment, positional with
|
||||
`pduObjs`, `undefined` where the SMSC took the segment without naming an id.
|
||||
- **`checkuserpass` is `authenticate`**, takes `{ password, session, systemId, systemType }` and
|
||||
@@ -36,6 +37,9 @@ shape is the same, connect, send, listen for delivery reports, with callbacks re
|
||||
Write `alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back
|
||||
under, for `alert_on_msg_delivery` and `failed_broadcast_area_identifier`, which are gone from
|
||||
`tlvs` too.
|
||||
- **`session.loggedIn` and the `loggedIn` event are gone.** `client()` resolves once bound,
|
||||
`session.boundAs !== undefined` says a bind happened, and `disconnected`/`reconnected` say whether
|
||||
the link is up now. A session you construct yourself records a bind with `session.bound()`.
|
||||
- **The `error` event is `sessionError`**, and `serverError` on the server handle.
|
||||
- **`log`** takes any object with `debug`, `error`, `info`, `verbose` and `warn` methods instead of
|
||||
a `larvitutils` one, and is silent by default: [README](README.md#logging).
|
||||
|
||||
@@ -421,8 +421,18 @@ const { err, pduObj } = await session.send({
|
||||
- `acceptsOptionalParams()`: whether the peer declared SMPP 3.4 or later, the version from which
|
||||
optional parameters may be sent to it. The library's own senders check it before attaching a TLV;
|
||||
a `send()` you build is passed through as written, so check it yourself.
|
||||
- `peerInterfaceVersion`: the version the peer declared, `0x00` if none.
|
||||
- `peerInterfaceVersion`: the version the peer declared, `0x00` if none, `undefined` before any bind.
|
||||
- `bindAllows(cmdName)` and `boundAs`: what the bind direction carries: [Bind direction](#bind-direction).
|
||||
- `boundAs` and `peerInterfaceVersion` are read-only, and hold through a reconnect's gap until the
|
||||
link binds again.
|
||||
- `bound(bindType, declaredVersion)`: how a session you construct yourself records a bind, whichever
|
||||
end accepted it, on every link it binds. `bindType` is `receiver`, `transceiver` or `transmitter`;
|
||||
`declaredVersion` is 0-255, or `undefined` where the peer declared none. Anything else returns `err`
|
||||
and records nothing.
|
||||
- An ESME wired by hand sends its own `bind_<bindType>` through `session.send()`, after it is
|
||||
constructed and again in `reconnect.onConnected`, and records each accepted one with
|
||||
`session.bound(bindType, pduObj.tlvs.sc_interface_version?.tagValue)`, where `bindType` is the one
|
||||
it sent and `pduObj` the `bind_resp` that `send()` resolved with.
|
||||
|
||||
## Receiving in depth
|
||||
|
||||
@@ -539,7 +549,9 @@ if (err) throw err;
|
||||
- `enquire_link` and `unbind` reach the hook too, and an unanswered `enquire_link` has the peer drop
|
||||
the link. Guard on the command name, as above, and a failing hook costs only its own request.
|
||||
- A `Session` you construct yourself takes the same hook as a session option, and that is where a
|
||||
peer's bind gets accepted, since a hand-wired session has no bind handling of its own.
|
||||
peer's bind gets accepted, since a hand-wired session has no bind handling of its own: call
|
||||
`session.bound(pduObj.cmdName.slice('bind_'.length), pduObj.params.interface_version)` before
|
||||
answering it, and refuse the bind with `ESME_RBINDFAIL` where that returns `err`.
|
||||
|
||||
**`sendDlr()`** takes `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
||||
`ACCEPTED`, `UNKNOWN`, `REJECTED` or `SKIPPED`. The first two go out as intermediate delivery
|
||||
|
||||
@@ -715,6 +715,14 @@ rule and an index of the titles below.
|
||||
assembled that way. What goes there is traffic already answered, which is why each group reaches
|
||||
`sessionError` like every other one given up on.
|
||||
|
||||
- **The bind state is the session's, `bound()` alone writes it, and it holds through a reconnect's
|
||||
gap.** Maintainer's call, 2026-09-28. `client()` and `server()` record their bind through
|
||||
`bound()`, the call a hand-wired session makes, so the state has one writer — goal 8. Clearing the state at `teardown()` was rejected: `bindAllows()` and
|
||||
`acceptsOptionalParams()` then answer yes to everything while the link is down, so a
|
||||
receiver-bound client queues a `submit_sm` the peer refuses and a receipt built then carries TLVs a
|
||||
pre-3.4 peer must not get — goal 4. Valid while the reconnect loop binds again with the same bind
|
||||
type to the same peer.
|
||||
|
||||
- **A message id base is merged at most once.** A receipt carries nothing but `<base>-<n>`, so a
|
||||
straggler for a message whose group is gone cannot be told from a receipt for a later message the
|
||||
peer handed the same ids — an SMSC whose id counter restarts with its process is the realistic
|
||||
|
||||
@@ -62,6 +62,6 @@ describe('smppload (blocked)', () => {
|
||||
await waitFor(() => (closed ? true : undefined), 5000);
|
||||
assert.equal(closed, true);
|
||||
assert.ok(bound);
|
||||
assert.equal(bound.loggedIn, false);
|
||||
assert.equal(bound.boundAs, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-7
@@ -8,7 +8,7 @@ export type { BindType };
|
||||
|
||||
import { ReconnectLoop } from './reconnect-loop.ts';
|
||||
import { Session } from './session.ts';
|
||||
import { checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { checkSessionOptions } from './session-options.ts';
|
||||
import { connect as netConnect } from 'node:net';
|
||||
import { connect as tlsConnect } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
@@ -174,13 +174,10 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
|
||||
return { err: new Error(`Remote host refused login: ${sent.pduObj.cmdStatus ?? 'unknown'}`) };
|
||||
}
|
||||
|
||||
const declared = sent.pduObj.tlvs.sc_interface_version?.tagValue;
|
||||
const recorded = session.bound(bindType, sent.pduObj.tlvs.sc_interface_version?.tagValue);
|
||||
|
||||
if (recorded.err) return recorded;
|
||||
|
||||
session.boundAs = bindType;
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
: undeclaredInterfaceVersion;
|
||||
session.log.info('client - bound', { bindType, systemId });
|
||||
|
||||
return {};
|
||||
|
||||
+20
-23
@@ -1,12 +1,12 @@
|
||||
import type { CloseOptions, OnRequest } from './session-options.ts';
|
||||
import type { BindType, CloseOptions, OnRequest } from './session-options.ts';
|
||||
import type { PduObject, TlvInputs } from './pdu.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { Server as NetServer, Socket } from 'node:net';
|
||||
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||
import type { SmppLog } from './log.ts';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
||||
import { bindTypeFromCommand, checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
||||
import { Session, defaultSystemId } from './session.ts';
|
||||
import { bindTypeFromCommand, checkSessionOptions } from './session-options.ts';
|
||||
import { createServer as createNetServer } from 'node:net';
|
||||
import { createServer as createTlsServer } from 'node:tls';
|
||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||
@@ -170,24 +170,12 @@ function bindRespTlvs(session: Session, options: ServerOptions): TlvInputs | und
|
||||
};
|
||||
}
|
||||
|
||||
async function acceptBind(
|
||||
async function onBind(
|
||||
session: Session,
|
||||
pduObj: PduObject,
|
||||
bindType: BindType,
|
||||
options: ServerOptions,
|
||||
identity: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const declared = pduObj.params.interface_version;
|
||||
|
||||
session.boundAs = bindTypeFromCommand(pduObj.cmdName);
|
||||
session.loggedIn = true;
|
||||
session.peerInterfaceVersion = typeof declared === 'number'
|
||||
? declared
|
||||
: undeclaredInterfaceVersion;
|
||||
|
||||
await session.sendReturn(pduObj, 'ESME_ROK', identity, bindRespTlvs(session, options));
|
||||
}
|
||||
|
||||
async function onBind(session: Session, pduObj: PduObject, options: ServerOptions): Promise<void> {
|
||||
const identity = { system_id: options.systemId ?? defaults.systemId };
|
||||
const systemId = paramText(pduObj.params.system_id);
|
||||
|
||||
@@ -198,7 +186,16 @@ async function onBind(session: Session, pduObj: PduObject, options: ServerOption
|
||||
return;
|
||||
}
|
||||
|
||||
await acceptBind(session, pduObj, options, identity);
|
||||
const recorded = session.bound(bindType, pduObj.params.interface_version);
|
||||
|
||||
if (recorded.err) {
|
||||
session.log.info('server - bind refused', { message: recorded.err.message, systemId });
|
||||
await session.sendReturn(pduObj, 'ESME_RBINDFAIL', identity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await session.sendReturn(pduObj, 'ESME_ROK', identity, bindRespTlvs(session, options));
|
||||
session.log.verbose('server - bound', { systemId });
|
||||
}
|
||||
|
||||
@@ -211,16 +208,16 @@ async function handleRequest(
|
||||
pduObj: PduObject,
|
||||
options: ServerOptions,
|
||||
): Promise<boolean> {
|
||||
const isBind = bindCommands.includes(pduObj.cmdName);
|
||||
const bindType = bindTypeFromCommand(pduObj.cmdName);
|
||||
|
||||
if (session.loggedIn) {
|
||||
if (isBind || !options.onRequest) return false;
|
||||
if (session.boundAs !== undefined) {
|
||||
if (bindType || !options.onRequest) return false;
|
||||
|
||||
return options.onRequest(session, pduObj);
|
||||
}
|
||||
|
||||
if (isBind) {
|
||||
await onBind(session, pduObj, options);
|
||||
if (bindType) {
|
||||
await onBind(session, pduObj, bindType, options);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+26
-1
@@ -121,6 +121,31 @@ export const defaultSystemId = '';
|
||||
/** SMPP 3.4: a peer that declares no version at all is one from before optional parameters. */
|
||||
export const undeclaredInterfaceVersion = 0x00;
|
||||
|
||||
export type SessionBind = { as: BindType; peerVersion: number };
|
||||
|
||||
function quoted(value: unknown): string {
|
||||
return typeof value === 'string' ? JSON.stringify(value) : namedValue(value);
|
||||
}
|
||||
|
||||
function isBindType(value: unknown): value is BindType {
|
||||
return typeof value === 'string' && bindTypeFromCommand(`bind_${value}`) !== undefined;
|
||||
}
|
||||
|
||||
/** A bind as `Session.bound()` records it: undefined declares no version, which is pre-3.4. */
|
||||
export function checkedBind(bindType: unknown, declaredVersion: unknown): Result<{ bind: SessionBind }> {
|
||||
if (!isBindType(bindType)) {
|
||||
return { err: new Error(`bindType must be receiver, transceiver or transmitter, the bind command's name without "bind_", got ${quoted(bindType)}`) };
|
||||
}
|
||||
|
||||
if (declaredVersion === undefined) return { bind: { as: bindType, peerVersion: undeclaredInterfaceVersion } };
|
||||
|
||||
if (typeof declaredVersion !== 'number' || !Number.isInteger(declaredVersion) || declaredVersion < 0 || declaredVersion > 0xFF) {
|
||||
return { err: new Error(`declaredVersion must be an integer 0-255, the interface_version param or the sc_interface_version TLV's tagValue, or undefined where the peer declared none, got ${quoted(declaredVersion)}`) };
|
||||
}
|
||||
|
||||
return { bind: { as: bindType, peerVersion: declaredVersion } };
|
||||
}
|
||||
|
||||
export const defaults = {
|
||||
/** Receipts of a multipart message can be a working day apart, so the cap does the bounding. */
|
||||
dlrMergeTimeout: 86_400_000,
|
||||
@@ -176,7 +201,7 @@ const maxTimerDelay = 2_147_483_647;
|
||||
function checkConnectTimeout(connectTimeout: unknown): VoidResult {
|
||||
if (connectTimeout === undefined || connectTimeout === false) return {};
|
||||
|
||||
const got = typeof connectTimeout === 'string' ? `"${connectTimeout}"` : namedValue(connectTimeout);
|
||||
const got = quoted(connectTimeout);
|
||||
|
||||
if (typeof connectTimeout !== 'number' || !Number.isInteger(connectTimeout) || connectTimeout < 1) {
|
||||
return { err: new Error(`connectTimeout must be a whole number of milliseconds, 1 or more, got ${got}; false waits the OS out instead`) };
|
||||
|
||||
+26
-13
@@ -4,7 +4,7 @@ import type { MessageDlr } from './dlr-merger.ts';
|
||||
import type { ParamValue } from './defs/types.ts';
|
||||
import type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts';
|
||||
import type { PduRefusedError } from './pdu-refusal.ts';
|
||||
import type { BindType, CloseOptions, LinkEnd, OnRequest, ReconnectOptions, SendOptions, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { BindType, CloseOptions, LinkEnd, OnRequest, ReconnectOptions, SendOptions, SessionBind, SessionEvents, SessionOptions } from './session-options.ts';
|
||||
import type { Result, VoidResult } from './result.ts';
|
||||
import type { SendSmsOptions, SendSmsResult } from './send-sms.ts';
|
||||
import type { SmppLog } from './log.ts';
|
||||
@@ -19,7 +19,7 @@ import { ReconnectLoop } from './reconnect-loop.ts';
|
||||
import { leftOf } from './idle-waiters.ts';
|
||||
import { errorFrom } from './error-from.ts';
|
||||
import { optionalParamsMinVersion } from './defs/constants.ts';
|
||||
import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts';
|
||||
import { bindCarries, bindCommands, checkedBind, defaultSystemId, defaults } from './session-options.ts';
|
||||
import { isResp, objToPdu, pduReturn } from './pdu.ts';
|
||||
import { refusalAnswer } from './pdu-refusal.ts';
|
||||
import { guardedLog } from './log.ts';
|
||||
@@ -54,15 +54,12 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
|
||||
readonly log: SmppLog;
|
||||
|
||||
/** The role the ESME bound with, whichever end of the link this is. Undefined before any bind. */
|
||||
boundAs: BindType | undefined = undefined;
|
||||
/** Which end of the link this is. `server()` sets it; a hand-wired SMSC must set it too. */
|
||||
linkEnd: LinkEnd = 'esme';
|
||||
loggedIn = false;
|
||||
/** What the peer declared when binding: 0x00 if it declared none, undefined before any bind. */
|
||||
peerInterfaceVersion: number | undefined = undefined;
|
||||
userData: unknown = undefined;
|
||||
|
||||
private bind: SessionBind | undefined = undefined;
|
||||
|
||||
private readonly concatReference = new ConcatReference();
|
||||
private readonly dlrMerger: DlrMerger;
|
||||
private readonly incoming: IncomingRequests;
|
||||
@@ -154,6 +151,25 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
return this.transport.sock;
|
||||
}
|
||||
|
||||
/** The role the ESME bound with, whichever end of the link this is. Undefined before any bind. */
|
||||
get boundAs(): BindType | undefined {
|
||||
return this.bind?.as;
|
||||
}
|
||||
|
||||
/** What the peer declared when binding: 0x00 if it declared none, undefined before any bind. */
|
||||
get peerInterfaceVersion(): number | undefined {
|
||||
return this.bind?.peerVersion;
|
||||
}
|
||||
|
||||
/** Records a bind this link accepted or had accepted, until the next one. */
|
||||
bound(bindType: string, declaredVersion: unknown): VoidResult {
|
||||
const checked = checkedBind(bindType, declaredVersion);
|
||||
|
||||
if (!checked.err) this.bind = checked.bind;
|
||||
|
||||
return checked.err ? { err: checked.err } : {};
|
||||
}
|
||||
|
||||
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
|
||||
bindAllows(cmdName: string): boolean {
|
||||
return bindCarries(this.boundAs, cmdName, this.linkEnd);
|
||||
@@ -161,8 +177,7 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
|
||||
/** SMPP 3.4 forbids sending optional parameters to a peer that declared an older version. */
|
||||
acceptsOptionalParams(): boolean {
|
||||
return this.peerInterfaceVersion === undefined
|
||||
|| this.peerInterfaceVersion >= optionalParamsMinVersion;
|
||||
return this.peerInterfaceVersion === undefined || this.peerInterfaceVersion >= optionalParamsMinVersion;
|
||||
}
|
||||
|
||||
/** Sends a request and resolves with the peer's response. */
|
||||
@@ -171,15 +186,13 @@ export class Session extends EventEmitter<SessionEvents> {
|
||||
}
|
||||
|
||||
/** Answers a request the peer sent us. Responses are never waited on. */
|
||||
async sendReturn(
|
||||
sendReturn(
|
||||
pdu: PduObject,
|
||||
status: ErrorName = 'ESME_ROK',
|
||||
params: Record<string, ParamValue> = {},
|
||||
tlvs?: TlvInputs,
|
||||
): Promise<VoidResult> {
|
||||
return Promise.resolve(
|
||||
this.answer(pduReturn(pdu, status, params, tlvs), pdu.cmdName, pdu.seqNr),
|
||||
);
|
||||
return Promise.resolve(this.answer(pduReturn(pdu, status, params, tlvs), pdu.cmdName, pdu.seqNr));
|
||||
}
|
||||
|
||||
private answer(built: Result<{ buffer: Buffer }>, cmdName: string, seqNr: number): VoidResult {
|
||||
|
||||
@@ -628,6 +628,10 @@ describe('reconnect', () => {
|
||||
|
||||
const reconnected = once<true>(resolve => { session.on('reconnected', () => { resolve(true); }); });
|
||||
const halfPdu = once<true>(resolve => { session.on('data', () => { resolve(true); }); });
|
||||
const boundBefore = [session.boundAs, session.peerInterfaceVersion];
|
||||
const whileDown = once<unknown[]>(resolve => {
|
||||
session.on('disconnected', () => { resolve([session.boundAs, session.peerInterfaceVersion]); });
|
||||
});
|
||||
|
||||
// A PDU header promising 32 octets and sending 8: the next link must not continue it.
|
||||
peerOf(smpp).sock.write(Buffer.from([0, 0, 0, 32, 0, 0, 0, 4]));
|
||||
@@ -640,7 +644,9 @@ describe('reconnect', () => {
|
||||
|
||||
await reconnected;
|
||||
|
||||
assert.ok(session.loggedIn);
|
||||
// The loop rebinds as before, so the gap keeps answering bindAllows() for the bind to come.
|
||||
assert.deepEqual(await whileDown, boundBefore);
|
||||
assert.equal(session.boundAs, 'transceiver');
|
||||
|
||||
// The session object survives the drop, so listeners stay attached and it is usable again.
|
||||
const sent = await session.sendSms({
|
||||
@@ -867,7 +873,7 @@ describe('reconnect from the first bind', () => {
|
||||
|
||||
assert.equal(err, undefined);
|
||||
assert.ok(session);
|
||||
assert.equal(session.loggedIn, true);
|
||||
assert.equal(session.boundAs, 'transceiver');
|
||||
assert.ok(spy.delays.length >= 3, 'the SMSC was down for several attempts');
|
||||
assert.deepEqual(spy.delays.slice(0, 3), [10, 20, 40], 'each wait doubles, up to maxDelay');
|
||||
assert.ok(
|
||||
|
||||
+30
-1
@@ -229,7 +229,7 @@ describe('bind', () => {
|
||||
|
||||
assert.equal(err, undefined);
|
||||
assert.ok(session);
|
||||
assert.ok(session.loggedIn);
|
||||
assert.equal(session.boundAs, 'transceiver');
|
||||
|
||||
assert.deepEqual(await session.unbind(), {});
|
||||
});
|
||||
@@ -448,6 +448,35 @@ describe('bind', () => {
|
||||
assert.ok(session.acceptsOptionalParams());
|
||||
});
|
||||
|
||||
test('records a hand-wired bind through bound(), and nothing else writes it', t => {
|
||||
const session = new Session({ sock: new net.Socket() });
|
||||
|
||||
closeAfter(t, session);
|
||||
assert.equal(session.boundAs, undefined);
|
||||
assert.equal(session.peerInterfaceVersion, undefined);
|
||||
|
||||
assert.deepEqual(session.bound('receiver', 0x34), {});
|
||||
assert.equal(session.boundAs, 'receiver');
|
||||
assert.equal(session.peerInterfaceVersion, 0x34);
|
||||
assert.equal(session.bindAllows('submit_sm'), false);
|
||||
|
||||
assert.deepEqual(session.bound('transmitter', undefined), {});
|
||||
assert.equal(session.peerInterfaceVersion, 0x00, 'no declared version is pre-3.4');
|
||||
|
||||
// The likeliest mistakes: the TLV object for its value, a version past int8, the command's name.
|
||||
assert.match(session.bound('receiver', { tagValue: 0x34 }).err?.message ?? '', /tagValue, or undefined .* got object$/);
|
||||
assert.ok(session.bound('receiver', 0x100).err);
|
||||
assert.ok(session.bound('receiver', 3.4).err);
|
||||
assert.match(session.bound('bind_receiver', 0x34).err?.message ?? '', /without "bind_", got "bind_receiver"$/);
|
||||
|
||||
assert.equal(session.boundAs, 'transmitter', 'a refused bind leaves the recorded one alone');
|
||||
assert.equal(session.peerInterfaceVersion, 0x00);
|
||||
assert.equal(Reflect.set(session, 'boundAs', 'transceiver'), false);
|
||||
assert.equal(Reflect.set(session, 'peerInterfaceVersion', 0x50), false);
|
||||
assert.equal(session.boundAs, 'transmitter');
|
||||
assert.equal('loggedIn' in session, false);
|
||||
});
|
||||
|
||||
// The spec: an absent sc_interface_version means the SMSC supports no optional parameters.
|
||||
test('takes an SMSC that declares no version as older than 3.4', async t => {
|
||||
const peer = await smscPeer(t);
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ describe('tls', () => {
|
||||
|
||||
assert.equal(err, undefined);
|
||||
assert.ok(session);
|
||||
assert.ok(session.loggedIn);
|
||||
assert.equal(session.boundAs, 'transceiver');
|
||||
closeAfter(t, session);
|
||||
|
||||
const sock = session.sock;
|
||||
|
||||
@@ -204,11 +204,6 @@ and 5. Every seat ranked the session's lifecycle hardest and least wanted to mod
|
||||
|
||||
- [ ] **Lift Locality to 7, and confirm it with a scoring run.** A run reading 7.0 or above also
|
||||
retires the #30 decision. The sub-items are what the 2026-09-28 run named, most seats first.
|
||||
- [ ] **Let `Session` own its bind state.** `client.ts` and `server.ts` write `boundAs`, `loggedIn`
|
||||
and `peerInterfaceVersion` onto the session from outside, and `loggedIn` duplicates
|
||||
`boundAs !== undefined`. A method such as `session.bound(bindType, declaredVersion)` with the
|
||||
three fields read-only changes the public surface a hand-wired SMSC uses, so it needs the
|
||||
maintainer's call first. All four seats.
|
||||
- [ ] **Name what `request()`, `carry()` and `now()` each skip.** Three ways onto the wire differ
|
||||
only in which of the drain, the gate and the window they bypass, and none of the names says
|
||||
which. Three seats.
|
||||
|
||||
Reference in New Issue
Block a user