Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
// ==========================================
|
|
// Chat Switchboard — SDK: Pipe/Filter Pipeline
|
|
// ==========================================
|
|
// Three-stage pipeline: pre-send, stream, render.
|
|
// Verbatim port of switchboard-sdk.js pipe code.
|
|
//
|
|
// Factory: createPipe()
|
|
// ==========================================
|
|
|
|
/**
|
|
* Create the pipe/filter pipeline.
|
|
* @returns {object} pipe
|
|
*/
|
|
export function createPipe() {
|
|
|
|
const _chains = {
|
|
pre: [], // { priority, fn, scope, source, stats }
|
|
stream: [],
|
|
render: [],
|
|
};
|
|
|
|
/**
|
|
* Register a filter into a pipe stage.
|
|
*/
|
|
function _registerFilter(stage, priority, fn, opts) {
|
|
if (typeof fn !== 'function') {
|
|
console.error(`[sw.pipe] ${stage}: filter must be a function`);
|
|
return;
|
|
}
|
|
const chain = _chains[stage];
|
|
if (!chain) {
|
|
console.error(`[sw.pipe] ${stage}: unknown stage`);
|
|
return;
|
|
}
|
|
const _opts = opts || {};
|
|
const entry = {
|
|
priority,
|
|
fn,
|
|
scope: _opts.scope || null,
|
|
source: _opts.source || _inferSource(),
|
|
stats: { calls: 0, totalMs: 0, errors: 0 },
|
|
};
|
|
|
|
const dup = chain.find(e => e.source === entry.source && e.priority === entry.priority);
|
|
if (dup) {
|
|
console.warn(`[sw.pipe] ${stage}: duplicate registration (source=${entry.source}, priority=${priority})`);
|
|
}
|
|
|
|
chain.push(entry);
|
|
chain.sort((a, b) => a.priority - b.priority);
|
|
}
|
|
|
|
/**
|
|
* Infer source label from call stack.
|
|
*/
|
|
function _inferSource() {
|
|
try {
|
|
const stack = new Error().stack || '';
|
|
const extMatch = stack.match(/extensions\/([^/]+)\//);
|
|
if (extMatch) return extMatch[1];
|
|
} catch (_) { /* best effort */ }
|
|
return 'anonymous';
|
|
}
|
|
|
|
/**
|
|
* Run a filter chain against a context.
|
|
* Sync-only — filters must not return Promises.
|
|
*/
|
|
function _runChain(stage, ctx) {
|
|
const chain = _chains[stage];
|
|
if (!chain || chain.length === 0) return ctx;
|
|
|
|
const channelType = ctx.channel?.type || null;
|
|
|
|
for (const entry of chain) {
|
|
if (entry.scope?.channelType) {
|
|
if (!channelType || !entry.scope.channelType.includes(channelType)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const t0 = performance.now();
|
|
try {
|
|
const result = entry.fn(ctx);
|
|
entry.stats.calls++;
|
|
entry.stats.totalMs += performance.now() - t0;
|
|
|
|
if (result instanceof Promise) {
|
|
console.error(`[sw.pipe] ${stage}: filter '${entry.source}' returned Promise (async not supported)`);
|
|
entry.stats.errors++;
|
|
continue;
|
|
}
|
|
|
|
if (result === null || result === undefined) {
|
|
return null; // halt chain
|
|
}
|
|
ctx = result;
|
|
} catch (e) {
|
|
entry.stats.calls++;
|
|
entry.stats.errors++;
|
|
entry.stats.totalMs += performance.now() - t0;
|
|
console.error(`[sw.pipe] ${stage} filter '${entry.source}' (p=${entry.priority}) threw:`, e);
|
|
// Continue chain — error isolation
|
|
}
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
// ── Public API ──────────────────────────────
|
|
|
|
return {
|
|
pre(priority, fn, opts) { _registerFilter('pre', priority, fn, opts); },
|
|
stream(priority, fn, opts) { _registerFilter('stream', priority, fn, opts); },
|
|
render(priority, fn, opts) { _registerFilter('render', priority, fn, opts); },
|
|
|
|
list() {
|
|
const result = {};
|
|
for (const [stage, chain] of Object.entries(_chains)) {
|
|
result[stage] = chain.map(e => ({
|
|
priority: e.priority,
|
|
source: e.source,
|
|
scope: e.scope,
|
|
calls: e.stats.calls,
|
|
avgMs: e.stats.calls > 0
|
|
? Math.round((e.stats.totalMs / e.stats.calls) * 100) / 100
|
|
: 0,
|
|
errors: e.stats.errors,
|
|
}));
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_runPre(ctx) { return _runChain('pre', ctx); },
|
|
_runStream(ctx) { return _runChain('stream', ctx); },
|
|
_runRender(ctx) { return _runChain('render', ctx); },
|
|
};
|
|
}
|