SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,225 @@
|
||||
// OIDB service layer — the next architectural revision on top of the
|
||||
// area-grouped Api facade pattern (InteractionApi / ProfileApi / …).
|
||||
//
|
||||
// Each OIDB cmd is modelled as a TypeScript namespace whose exported
|
||||
// members ARE the `OidbCallSpec` shape (structural typing makes this
|
||||
// implicit — no `implements` clause needed). The namespace also exports
|
||||
// a thin `invoke()` that hands itself to `invokeOidb` for dispatch.
|
||||
//
|
||||
// Why namespace-as-spec instead of class:
|
||||
// - stateless: no `new` overhead at the call site
|
||||
// - no `this` binding, no implicit lifecycle
|
||||
// - tree-shakes per-export (unused services drop out of the bundle)
|
||||
// - spec ↔ code 1:1 (one file = one (cmd, subcmd) tuple)
|
||||
//
|
||||
// Why Pick-style minimal `Deps` per service:
|
||||
// - Interface Segregation: a wire-only cmd doesn't see identity/events
|
||||
// - tests mock only what's used (typically just `sendRawPacket`)
|
||||
// - changes to BridgeContext don't bleed into services
|
||||
// - the dep list IS the documentation of side-effects
|
||||
//
|
||||
// Why `ctx` threads into serialize/deserialize:
|
||||
// - the namespace is then genuinely self-contained — "what does this
|
||||
// cmd do on the wire + what state does it touch" lives in one file
|
||||
// - earlier shape forced an `invoke = { ...Self, serialize: p =>
|
||||
// localSerialize(p, await ctx.resolveUserUid(...)) }` rewrite for
|
||||
// every cmd that needed identity/uid resolution; ctx-aware
|
||||
// serialize collapses that to a single async serialize.
|
||||
// - serialize is now `TReq | Promise<TReq>` so async lookups
|
||||
// (resolveUserUid, identity reads) happen in place.
|
||||
|
||||
import { protobuf_decode } from '@snowluma/proton';
|
||||
import type { OidbBase, OidbBaseMeta } from '@snowluma/proto-defs/oidb';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
|
||||
import { makeOidbEnvelope } from './bridge-oidb';
|
||||
|
||||
/**
|
||||
* Capability narrow enough for any wire-only OIDB call. Anything that
|
||||
* also needs identity / event bus / upload cache declares those via
|
||||
* `Pick<BridgeContext, 'identity' | …>` and intersects with this.
|
||||
*/
|
||||
export interface OidbSender {
|
||||
sendRawPacket(
|
||||
serviceCmd: string,
|
||||
body: Uint8Array,
|
||||
timeoutMs?: number,
|
||||
): Promise<SendPacketResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The shape every cmd namespace exports. Structural typing means a
|
||||
* namespace whose top-level exports match these names + types IS an
|
||||
* `OidbCallSpec` — no `implements` clause needed.
|
||||
*
|
||||
* `TCtx` is the namespace's capability surface — always at least
|
||||
* `OidbSender` (so `invokeOidb` can dispatch through it) plus any
|
||||
* `Pick<BridgeContext, ...>` slices the cmd actually needs.
|
||||
*
|
||||
* One of `subCommand` (static) or `resolveSubCommand` (dynamic from
|
||||
* params and context) must be present. `resolveSubCommand` wins if both
|
||||
* are given. `resolveCommand` likewise wins over static `command`.
|
||||
*/
|
||||
export interface OidbCallSpec<TCtx extends OidbSender, TReq, TResp, TParams, TResult> {
|
||||
command: number;
|
||||
resolveCommand?(params: TParams, ctx: TCtx): number;
|
||||
subCommand?: number;
|
||||
resolveSubCommand?(params: TParams, ctx: TCtx): number;
|
||||
/** Packet and body envelope codes treated as success in addition to 0. */
|
||||
acceptedEnvelopeCodes?: readonly number[];
|
||||
/** Set OIDB envelope `reserved = 1` (UIN-form variant, see makeOidbEnvelope's isUid). */
|
||||
uinForm?: boolean;
|
||||
/** Override the default `OidbSvcTrpcTcp.0xNNNN_N` wire name. A few
|
||||
* cmds route through different SSO names (e.g. 0xE17_0 lives at
|
||||
* `MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0`); supply a function
|
||||
* that takes the resolved `(cmd, subCmd)` and returns the actual
|
||||
* wire name. Omit for the default scheme. */
|
||||
wireName?(command: number, subCommand: number): string;
|
||||
/** Business params → wire-shaped request body. May be async (e.g.
|
||||
* when ctx.resolveUserUid hits the cache or the network). */
|
||||
serialize(ctx: TCtx, params: TParams): TReq | Promise<TReq>;
|
||||
/** Wire-shaped response body → business result. */
|
||||
deserialize(ctx: TCtx, body: TResp): TResult;
|
||||
/** Per-type protobuf encoder — concrete generic call so the Vite plugin can monomorphize. */
|
||||
encode(env: OidbBase<TReq>): Uint8Array;
|
||||
/** Per-type protobuf decoder. */
|
||||
decode(bytes: Uint8Array): OidbBase<TResp>;
|
||||
}
|
||||
|
||||
function resolveOidbCall<TCtx extends OidbSender, TReq, TResp, TParams, TResult>(
|
||||
spec: OidbCallSpec<TCtx, TReq, TResp, TParams, TResult>,
|
||||
params: TParams,
|
||||
ctx: TCtx,
|
||||
): { command: number; subCommand: number } {
|
||||
const command = spec.resolveCommand ? spec.resolveCommand(params, ctx) : spec.command;
|
||||
const subCommand = spec.resolveSubCommand
|
||||
? spec.resolveSubCommand(params, ctx)
|
||||
: spec.subCommand!;
|
||||
return { command, subCommand };
|
||||
}
|
||||
|
||||
function isAcceptedEnvelopeCode<TCtx extends OidbSender, TReq, TResp, TParams, TResult>(
|
||||
spec: OidbCallSpec<TCtx, TReq, TResp, TParams, TResult>,
|
||||
code: number | null | undefined,
|
||||
): boolean {
|
||||
if (code == null || code === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return spec.acceptedEnvelopeCodes?.includes(code) === true;
|
||||
}
|
||||
|
||||
export class OidbError extends Error {
|
||||
constructor(
|
||||
readonly code: number,
|
||||
readonly serverMsg: string,
|
||||
readonly command: number,
|
||||
readonly subCommand: number,
|
||||
) {
|
||||
super(`OIDB error ${code} on 0x${command.toString(16)}_${subCommand}: ${serverMsg}`);
|
||||
this.name = 'OidbError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method for every OIDB call. Builds the envelope, encodes it,
|
||||
* sends through the ctx's `sendRawPacket`, validates the envelope's
|
||||
* `errorCode`, decodes the body, and hands the wire-shaped response to
|
||||
* the spec's `deserialize` for transformation into the business result.
|
||||
*
|
||||
* `ctx` is threaded into both `serialize` and `deserialize` so the
|
||||
* namespace can do uid resolution / identity reads in-place rather
|
||||
* than forcing the caller to pre-bake values into the params.
|
||||
*/
|
||||
export async function invokeOidb<TCtx extends OidbSender, TReq, TResp, TParams, TResult>(
|
||||
ctx: TCtx,
|
||||
spec: OidbCallSpec<TCtx, TReq, TResp, TParams, TResult>,
|
||||
params: TParams,
|
||||
timeoutMs?: number,
|
||||
): Promise<TResult> {
|
||||
const { command, subCommand } = resolveOidbCall(spec, params, ctx);
|
||||
const reqBody = await spec.serialize(ctx, params);
|
||||
const env = makeOidbEnvelope(command, subCommand, reqBody, spec.uinForm ?? false);
|
||||
const reqBytes = spec.encode(env);
|
||||
const wireName = spec.wireName
|
||||
? spec.wireName(command, subCommand)
|
||||
: `OidbSvcTrpcTcp.0x${command.toString(16)}_${subCommand}`;
|
||||
|
||||
const result =
|
||||
timeoutMs === undefined
|
||||
? await ctx.sendRawPacket(wireName, reqBytes)
|
||||
: await ctx.sendRawPacket(wireName, reqBytes, timeoutMs);
|
||||
|
||||
if (!result.gotResponse) {
|
||||
throw new Error(result.errorMessage || 'no response');
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (result.errorCode && result.errorCode !== 0) {
|
||||
if (!isAcceptedEnvelopeCode(spec, result.errorCode)) {
|
||||
throw new OidbError(
|
||||
result.errorCode,
|
||||
result.errorMessage || '',
|
||||
command,
|
||||
subCommand,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// success=false without an OIDB error code = the packet send itself failed;
|
||||
// surface it instead of silently resolving with an undecoded response.
|
||||
throw new Error(result.errorMessage || 'packet send failed');
|
||||
}
|
||||
}
|
||||
|
||||
const respBytes = result.responseData ?? new Uint8Array(0);
|
||||
|
||||
if (respBytes.length > 0) {
|
||||
const meta = protobuf_decode<OidbBaseMeta>(respBytes);
|
||||
const code = meta?.errorCode;
|
||||
|
||||
if (code && code !== 0 && !isAcceptedEnvelopeCode(spec, code)) {
|
||||
throw new OidbError(code, meta?.errorMsg ?? '', command, subCommand);
|
||||
}
|
||||
}
|
||||
|
||||
// Server may legitimately respond with an empty envelope (ack-only
|
||||
// cmds like 0x9082_1 — the response carries only `errorCode = 0`
|
||||
// and no body). Substitute an empty placeholder so `deserialize`
|
||||
// always receives a defined object; specs that read fields off the
|
||||
// body must already guard with `?? []` etc.
|
||||
const respBody = spec.decode(respBytes).body ?? ({} as TResp);
|
||||
return spec.deserialize(ctx, respBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the request wire bytes for a spec without sending — useful for
|
||||
* wire dump debugging, capability probing, and unit tests of the
|
||||
* encode path. Threads `ctx` because `serialize` may want it (and
|
||||
* because matching `invokeOidb`'s shape avoids two call patterns).
|
||||
*/
|
||||
export async function buildOidbRequest<TCtx extends OidbSender, TReq, TResp, TParams, TResult>(
|
||||
ctx: TCtx,
|
||||
spec: OidbCallSpec<TCtx, TReq, TResp, TParams, TResult>,
|
||||
params: TParams,
|
||||
): Promise<{ wireName: string; bytes: Uint8Array }> {
|
||||
const { command, subCommand } = resolveOidbCall(spec, params, ctx);
|
||||
const reqBody = await spec.serialize(ctx, params);
|
||||
const env = makeOidbEnvelope(command, subCommand, reqBody, spec.uinForm ?? false);
|
||||
const bytes = spec.encode(env);
|
||||
const wireName = spec.wireName
|
||||
? spec.wireName(command, subCommand)
|
||||
: `OidbSvcTrpcTcp.0x${command.toString(16)}_${subCommand}`;
|
||||
return { wireName, bytes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode raw wire bytes into the business result via a spec's
|
||||
* deserialize path — symmetric debug helper for buildOidbRequest.
|
||||
*/
|
||||
export function parseOidbResponse<TCtx extends OidbSender, TReq, TResp, TParams, TResult>(
|
||||
ctx: TCtx,
|
||||
spec: OidbCallSpec<TCtx, TReq, TResp, TParams, TResult>,
|
||||
bytes: Uint8Array,
|
||||
): TResult {
|
||||
return spec.deserialize(ctx, spec.decode(bytes).body ?? ({} as TResp));
|
||||
}
|
||||
Reference in new issue
Block a user