diff --git a/packages/linejs/client/features/call/planet/schema.ts b/packages/linejs/client/features/call/planet/schema.ts index 9f50d87..31c7ff6 100644 --- a/packages/linejs/client/features/call/planet/schema.ts +++ b/packages/linejs/client/features/call/planet/schema.ts @@ -1194,6 +1194,25 @@ export function packPlanetScMsgKaReq(inner: Uint8Array): Uint8Array { return finalize(b); } +// ─── cc_upd_rsp (mid-call quality report / keepalive) ─────────────────── + +/** + * Pack a flat-format UPD_RSP message. The relay sends a flood of cc + * messages with `{ 1:16, 2:, 3:300, 4: }` after `conn_req`. + * linejs must echo back an equivalent to avoid T103. + * + * The flat format bypasses the normal cc_msg oneof wrapper — it is NOT + * `wrapCcMsg(CC_MSG.UPD_RSP, ...)`, but a top-level protobuf placed + * directly in field 3 (cc) of the outer planet_msg. + */ +export function packCcUpdRsp(seq: bigint, intervalMs = 300): Uint8Array { + const b: Buf = { bytes: [] }; + emitUint32(b, 1, CC_MSG.UPD_RSP); + emitUint64(b, 2, seq); + emitUint32(b, 3, intervalMs); + return finalize(b); +} + // ─── handshake helper — extract rmt_nonce from incoming msg ───────────── /** Extract the loc_nonce (field 6) from a decrypted incoming planet_msg. @@ -1348,6 +1367,8 @@ export interface DecodedPlanetCcMsg { bodyTag?: number; bodyName?: string; bodyBytes?: Uint8Array; + /** Raw decoded fields for non-standard (flat) cc messages. */ + rawFields?: DecodedField[]; } export interface DecodedPlanetMcMsg { @@ -1387,6 +1408,15 @@ export function decodePlanetCcMsg(bytes: Uint8Array): DecodedPlanetCcMsg { decoded.bodyBytes = oneof.value; } } + if (!decoded.bodyTag && !hdrBytes && !body && fields.length > 0) { + const typeVal = asNumberField(fields, 1); + if (typeVal !== undefined) { + decoded.bodyTag = typeVal; + decoded.bodyName = CC_MSG_NAMES[typeVal] ?? + `cc_flat_${typeVal}`; + decoded.rawFields = fields; + } + } return decoded; } diff --git a/packages/linejs/client/features/call/planet/transport.ts b/packages/linejs/client/features/call/planet/transport.ts index e1386a6..1fb8c73 100644 --- a/packages/linejs/client/features/call/planet/transport.ts +++ b/packages/linejs/client/features/call/planet/transport.ts @@ -65,6 +65,7 @@ import { packCcParticipateReq, packCcRelReq, packCcSetupReq, + packCcUpdRsp, packKeepaliveReq, packMcDataReq, packMcDataRsp, @@ -1274,6 +1275,9 @@ export class PlanetTransport implements CallTransport { }, ).catch(() => {}); } + if (msg.cc?.rawFields && msg.cc.bodyTag === CC_MSG.UPD_RSP) { + void this.#handleFlatCcUpd(msg.cc.rawFields).catch(() => {}); + } } catch { // Keep the raw reply flowing even if a newer message type is unknown. } @@ -2308,6 +2312,53 @@ export class PlanetTransport implements CallTransport { ); } + async #handleFlatCcUpd(rawFields: DecodedField[]): Promise { + const seq = rawFields.find((f) => f.tag === 2)?.value; + const seqBig = typeof seq === "bigint" ? seq : 0n; + const interval = fieldNumber(rawFields, 3) ?? 300; + if (!this.#updRspActive) { + this.#updRspActive = true; + this.#updRspIntervalMs = interval; + this.#startCcUpdTimer(); + } + this.#lastUpdSeq = seqBig; + } + + #updRspActive = false; + #updRspIntervalMs = 300; + #lastUpdSeq = 0n; + #ccUpdTimer: ReturnType | undefined; + #ccUpdSeq = 0n; + + #startCcUpdTimer() { + this.#clearCcUpdTimer(); + const tick = () => { + if (this.#closed) return; + void this.#sendCcUpdRsp().catch(() => {}).finally(() => { + if (!this.#closed && this.#updRspActive) { + this.#ccUpdTimer = setTimeout(tick, this.#updRspIntervalMs); + } + }); + }; + this.#ccUpdTimer = setTimeout(tick, this.#updRspIntervalMs); + } + + #clearCcUpdTimer() { + if (this.#ccUpdTimer !== undefined) { + clearTimeout(this.#ccUpdTimer); + this.#ccUpdTimer = undefined; + } + } + + async #sendCcUpdRsp(): Promise { + this.#ccUpdSeq++; + const updBytes = packCcUpdRsp(this.#ccUpdSeq, this.#updRspIntervalMs); + await this.#sendEnvelope( + { kind: "cc", data: updBytes }, + { msgId: ccMsgId(CC_MSG.UPD_RSP) }, + ); + } + #clearKeepalive() { if (this.#keepaliveTimer !== undefined) { clearTimeout(this.#keepaliveTimer); @@ -2347,6 +2398,7 @@ export class PlanetTransport implements CallTransport { async close(): Promise { this.#closed = true; this.#clearKeepalive(); + this.#clearCcUpdTimer(); try { if ( this.#setupSent && this.#route && (this.#sock || this.#opts.wireSend)