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 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 `data_sm` by whichever it stands in for.
|
||||||
- A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.
|
- 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 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 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.
|
- A send queued for a send-window slot is bounded by the caller's `signal`, and by nothing else.
|
||||||
|
|||||||
@@ -74,6 +74,10 @@
|
|||||||
`alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back under. The
|
`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.
|
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.
|
- `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)`; an assignment to either
|
||||||
|
field now throws a `TypeError`.
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
|
|||||||
@@ -421,8 +421,12 @@ const { err, pduObj } = await session.send({
|
|||||||
- `acceptsOptionalParams()`: whether the peer declared SMPP 3.4 or later, the version from which
|
- `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;
|
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.
|
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).
|
- `bindAllows(cmdName)` and `boundAs`: what the bind direction carries: [Bind direction](#bind-direction).
|
||||||
|
- Both hold through a reconnect's gap: the loop binds again as before.
|
||||||
|
- `bound(bindType, declaredVersion)`: how a session you construct yourself records a bind, whichever
|
||||||
|
end accepted it. A `declaredVersion` that is not a number is none declared. Both fields are
|
||||||
|
read-only.
|
||||||
|
|
||||||
## Receiving in depth
|
## Receiving in depth
|
||||||
|
|
||||||
@@ -539,7 +543,8 @@ if (err) throw err;
|
|||||||
- `enquire_link` and `unbind` reach the hook too, and an unanswered `enquire_link` has the peer drop
|
- `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.
|
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
|
- 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(bindType, pduObj.params.interface_version)` before answering it.
|
||||||
|
|
||||||
**`sendDlr()`** takes `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
**`sendDlr()`** takes `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
||||||
`ACCEPTED`, `UNKNOWN`, `REJECTED` or `SKIPPED`. The first two go out as intermediate delivery
|
`ACCEPTED`, `UNKNOWN`, `REJECTED` or `SKIPPED`. The first two go out as intermediate delivery
|
||||||
|
|||||||
@@ -715,6 +715,15 @@ rule and an index of the titles below.
|
|||||||
assembled that way. What goes there is traffic already answered, which is why each group reaches
|
assembled that way. What goes there is traffic already answered, which is why each group reaches
|
||||||
`sessionError` like every other one given up on.
|
`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()` call `bound()` as a hand-wired
|
||||||
|
session does, so one method is the one spelling, and `loggedIn` went as a second spelling of
|
||||||
|
`boundAs !== undefined`. 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
|
- **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
|
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
|
peer handed the same ids — an SMSC whose id counter restarts with its process is the realistic
|
||||||
|
|||||||
+2
-8
@@ -8,7 +8,7 @@ export type { BindType };
|
|||||||
|
|
||||||
import { ReconnectLoop } from './reconnect-loop.ts';
|
import { ReconnectLoop } from './reconnect-loop.ts';
|
||||||
import { Session } from './session.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 netConnect } from 'node:net';
|
||||||
import { connect as tlsConnect } from 'node:tls';
|
import { connect as tlsConnect } from 'node:tls';
|
||||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||||
@@ -174,13 +174,7 @@ async function bind(session: Session, options: ClientOptions): Promise<VoidResul
|
|||||||
return { err: new Error(`Remote host refused login: ${sent.pduObj.cmdStatus ?? 'unknown'}`) };
|
return { err: new Error(`Remote host refused login: ${sent.pduObj.cmdStatus ?? 'unknown'}`) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const declared = sent.pduObj.tlvs.sc_interface_version?.tagValue;
|
session.bound(bindType, sent.pduObj.tlvs.sc_interface_version?.tagValue);
|
||||||
|
|
||||||
session.boundAs = bindType;
|
|
||||||
session.loggedIn = true;
|
|
||||||
session.peerInterfaceVersion = typeof declared === 'number'
|
|
||||||
? declared
|
|
||||||
: undeclaredInterfaceVersion;
|
|
||||||
session.log.info('client - bound', { bindType, systemId });
|
session.log.info('client - bound', { bindType, systemId });
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
+17
-17
@@ -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 { PduObject, TlvInputs } from './pdu.ts';
|
||||||
import type { Result, VoidResult } from './result.ts';
|
import type { Result, VoidResult } from './result.ts';
|
||||||
import type { Server as NetServer, Socket } from 'node:net';
|
import type { Server as NetServer, Socket } from 'node:net';
|
||||||
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||||
import type { SmppLog } from './log.ts';
|
import type { SmppLog } from './log.ts';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
import { Session, defaultSystemId } from './session.ts';
|
||||||
import { bindTypeFromCommand, checkSessionOptions, undeclaredInterfaceVersion } from './session-options.ts';
|
import { bindTypeFromCommand, checkSessionOptions } from './session-options.ts';
|
||||||
import { createServer as createNetServer } from 'node:net';
|
import { createServer as createNetServer } from 'node:net';
|
||||||
import { createServer as createTlsServer } from 'node:tls';
|
import { createServer as createTlsServer } from 'node:tls';
|
||||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||||
@@ -173,21 +173,21 @@ function bindRespTlvs(session: Session, options: ServerOptions): TlvInputs | und
|
|||||||
async function acceptBind(
|
async function acceptBind(
|
||||||
session: Session,
|
session: Session,
|
||||||
pduObj: PduObject,
|
pduObj: PduObject,
|
||||||
|
bindType: BindType,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
identity: Record<string, string>,
|
identity: Record<string, string>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const declared = pduObj.params.interface_version;
|
session.bound(bindType, 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));
|
await session.sendReturn(pduObj, 'ESME_ROK', identity, bindRespTlvs(session, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBind(session: Session, pduObj: PduObject, options: ServerOptions): Promise<void> {
|
async function onBind(
|
||||||
|
session: Session,
|
||||||
|
pduObj: PduObject,
|
||||||
|
bindType: BindType,
|
||||||
|
options: ServerOptions,
|
||||||
|
): Promise<void> {
|
||||||
const identity = { system_id: options.systemId ?? defaults.systemId };
|
const identity = { system_id: options.systemId ?? defaults.systemId };
|
||||||
const systemId = paramText(pduObj.params.system_id);
|
const systemId = paramText(pduObj.params.system_id);
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ async function onBind(session: Session, pduObj: PduObject, options: ServerOption
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await acceptBind(session, pduObj, options, identity);
|
await acceptBind(session, pduObj, bindType, options, identity);
|
||||||
session.log.verbose('server - bound', { systemId });
|
session.log.verbose('server - bound', { systemId });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,16 +211,16 @@ async function handleRequest(
|
|||||||
pduObj: PduObject,
|
pduObj: PduObject,
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const isBind = bindCommands.includes(pduObj.cmdName);
|
const bindType = bindTypeFromCommand(pduObj.cmdName);
|
||||||
|
|
||||||
if (session.loggedIn) {
|
if (session.boundAs !== undefined) {
|
||||||
if (isBind || !options.onRequest) return false;
|
if (bindType || !options.onRequest) return false;
|
||||||
|
|
||||||
return options.onRequest(session, pduObj);
|
return options.onRequest(session, pduObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBind) {
|
if (bindType) {
|
||||||
await onBind(session, pduObj, options);
|
await onBind(session, pduObj, bindType, options);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -19,7 +19,7 @@ import { ReconnectLoop } from './reconnect-loop.ts';
|
|||||||
import { leftOf } from './idle-waiters.ts';
|
import { leftOf } from './idle-waiters.ts';
|
||||||
import { errorFrom } from './error-from.ts';
|
import { errorFrom } from './error-from.ts';
|
||||||
import { optionalParamsMinVersion } from './defs/constants.ts';
|
import { optionalParamsMinVersion } from './defs/constants.ts';
|
||||||
import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts';
|
import { bindCarries, bindCommands, defaultSystemId, defaults, undeclaredInterfaceVersion } from './session-options.ts';
|
||||||
import { isResp, objToPdu, pduReturn } from './pdu.ts';
|
import { isResp, objToPdu, pduReturn } from './pdu.ts';
|
||||||
import { refusalAnswer } from './pdu-refusal.ts';
|
import { refusalAnswer } from './pdu-refusal.ts';
|
||||||
import { guardedLog } from './log.ts';
|
import { guardedLog } from './log.ts';
|
||||||
@@ -54,15 +54,12 @@ export class Session extends EventEmitter<SessionEvents> {
|
|||||||
|
|
||||||
readonly log: SmppLog;
|
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. */
|
/** Which end of the link this is. `server()` sets it; a hand-wired SMSC must set it too. */
|
||||||
linkEnd: LinkEnd = 'esme';
|
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;
|
userData: unknown = undefined;
|
||||||
|
|
||||||
|
private bind: { as: BindType; peerVersion: number } | undefined = undefined;
|
||||||
|
|
||||||
private readonly concatReference = new ConcatReference();
|
private readonly concatReference = new ConcatReference();
|
||||||
private readonly dlrMerger: DlrMerger;
|
private readonly dlrMerger: DlrMerger;
|
||||||
private readonly incoming: IncomingRequests;
|
private readonly incoming: IncomingRequests;
|
||||||
@@ -154,6 +151,23 @@ export class Session extends EventEmitter<SessionEvents> {
|
|||||||
return this.transport.sock;
|
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. A version that is not a number is none. */
|
||||||
|
bound(bindType: BindType, declaredVersion: unknown): void {
|
||||||
|
const peerVersion = typeof declaredVersion === 'number' ? declaredVersion : undeclaredInterfaceVersion;
|
||||||
|
|
||||||
|
this.bind = { as: bindType, peerVersion };
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
|
/** Whether this session's bind direction carries a command. Consulted by the library's senders. */
|
||||||
bindAllows(cmdName: string): boolean {
|
bindAllows(cmdName: string): boolean {
|
||||||
return bindCarries(this.boundAs, cmdName, this.linkEnd);
|
return bindCarries(this.boundAs, cmdName, this.linkEnd);
|
||||||
@@ -171,15 +185,13 @@ export class Session extends EventEmitter<SessionEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Answers a request the peer sent us. Responses are never waited on. */
|
/** Answers a request the peer sent us. Responses are never waited on. */
|
||||||
async sendReturn(
|
sendReturn(
|
||||||
pdu: PduObject,
|
pdu: PduObject,
|
||||||
status: ErrorName = 'ESME_ROK',
|
status: ErrorName = 'ESME_ROK',
|
||||||
params: Record<string, ParamValue> = {},
|
params: Record<string, ParamValue> = {},
|
||||||
tlvs?: TlvInputs,
|
tlvs?: TlvInputs,
|
||||||
): Promise<VoidResult> {
|
): Promise<VoidResult> {
|
||||||
return Promise.resolve(
|
return Promise.resolve(this.answer(pduReturn(pdu, status, params, tlvs), pdu.cmdName, pdu.seqNr));
|
||||||
this.answer(pduReturn(pdu, status, params, tlvs), pdu.cmdName, pdu.seqNr),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private answer(built: Result<{ buffer: Buffer }>, cmdName: string, seqNr: number): VoidResult {
|
private answer(built: Result<{ buffer: Buffer }>, cmdName: string, seqNr: number): VoidResult {
|
||||||
|
|||||||
@@ -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
|
- [ ] **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.
|
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
|
- [ ] **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
|
only in which of the drain, the gate and the window they bypass, and none of the names says
|
||||||
which. Three seats.
|
which. Three seats.
|
||||||
|
|||||||
Reference in New Issue
Block a user