| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- /** Fire-and-forget webhook rule registry and Workspace-backed Session runtime. */
- import { Context, Service } from '@deepseek-ai/cordis'
- import { deepFreeze, errorChain } from '@deepseek-ai/dsh-llm'
- import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
- import type { WebhookRuleId } from './brand.ts'
- import { createWebhookSession } from './session.ts'
- import type { VerifiedWebhookDelivery, WebhookRule, WebhookSessionRequest } from './types.ts'
- export * from './brand.ts'
- export type * from './types.ts'
- declare module '@deepseek-ai/cordis' {
- interface Context {
- webhookRuntime: WebhookRuntime
- }
- }
- /** Internal type erasure after public generic registration validates the provider kind. */
- interface AnyWebhookRule {
- readonly id: WebhookRuleId
- readonly kind: string
- run(
- delivery: Readonly<VerifiedWebhookDelivery>,
- signal: AbortSignal,
- ): WebhookSessionRequest | null | Promise<WebhookSessionRequest | null>
- }
- /** One effect-owned rule registration and the invocations that currently use it. */
- interface RuleRegistration {
- readonly rule: AnyWebhookRule
- readonly controller: AbortController
- readonly active: Set<Promise<void>>
- closing: boolean
- disposal?: Promise<void>
- }
- /** Validate and detach one delivery before sharing it across arbitrary rules. */
- function snapshotDelivery(delivery: VerifiedWebhookDelivery): VerifiedWebhookDelivery {
- if (typeof delivery.kind !== 'string' || delivery.kind.trim() === '') {
- throw new TypeError('webhook delivery kind must be a non-empty string')
- }
- if (typeof delivery.source !== 'string' || delivery.source.trim() === '') {
- throw new TypeError('webhook delivery source must be a non-empty string')
- }
- if (typeof delivery.deliveryId !== 'string' || delivery.deliveryId.trim() === '') {
- throw new TypeError('webhook delivery id must be a non-empty string')
- }
- if (!Number.isSafeInteger(delivery.receivedAt) || delivery.receivedAt < 0) {
- throw new TypeError('webhook delivery receivedAt must be a non-negative safe integer')
- }
- const snapshot = snapshotJsonValue(delivery)
- if (snapshot === undefined) throw new TypeError('webhook delivery must be lossless JSON')
- return deepFreeze(snapshot)
- }
- /** Fire-and-forget rule runtime. Session creation is the only built-in action. */
- export class WebhookRuntime extends Service {
- static inject = [
- 'agents',
- 'agentDefaultModel',
- 'agentPresets',
- 'permissionPresets',
- 'sessionTitle',
- 'workspaceRegistry',
- ]
- private readonly rules = new Map<WebhookRuleId, RuleRegistration>()
- private readonly selfCtx: Context
- private closing = false
- constructor(ctx: Context) {
- super(ctx, 'webhookRuntime')
- this.selfCtx = ctx
- ctx.effect(() => async () => {
- this.closing = true
- /* v8 ignore next -- caller-owned registration effects normally dispose first; this covers provider-first unload. */
- await Promise.all(
- [...this.rules.values()].map(rule => this.disposeRegistration(rule)),
- )
- }, 'webhookRuntime.lifecycle()')
- }
- /**
- * Register one trusted programmatic rule.
- * @param rule - unique id, provider kind, and arbitrary callback.
- * @returns awaitable effect disposer that aborts and drains this rule's active callbacks.
- */
- register<K extends string>(rule: WebhookRule<K>): () => Promise<void> {
- if (this.closing) throw new Error('webhook runtime is closing')
- if (typeof rule.id !== 'string' || rule.id.trim() === '') {
- throw new TypeError('webhook rule id must be a non-empty string')
- }
- if (typeof rule.kind !== 'string' || rule.kind.trim() === '') {
- throw new TypeError(`webhook rule "${String(rule.id)}" kind must be a non-empty string`)
- }
- if (typeof rule.run !== 'function') {
- throw new TypeError(`webhook rule "${String(rule.id)}" requires run()`)
- }
- // The public generic preserves adapter-specific authoring types. The runtime
- // stores one erased callback after validating the shared provider tag.
- const erased = rule as unknown as AnyWebhookRule
- let registration!: RuleRegistration
- const disposeEffect = this.ctx.effect(() => {
- /* v8 ignore next -- no await separates the public liveness check from this initializer. */
- if (this.closing) throw new Error('webhook runtime is closing')
- if (this.rules.has(rule.id)) throw new Error(`webhook rule "${rule.id}" is already registered`)
- registration = {
- rule: erased,
- controller: new AbortController(),
- active: new Set(),
- closing: false,
- }
- this.rules.set(rule.id, registration)
- return () => this.disposeRegistration(registration)
- }, `webhookRuntime.register(${rule.id})`)
- return async () => { await disposeEffect() }
- }
- /**
- * Start every currently matching rule and return before any callback settles.
- * @param delivery - authenticated provider data; snapshotted before dispatch.
- * @throws synchronously when the runtime is closing or the delivery is malformed.
- */
- dispatch<K extends string>(delivery: VerifiedWebhookDelivery<K>): void {
- if (this.closing) throw new Error('webhook runtime is closing')
- const snapshot = snapshotDelivery(delivery)
- for (const registration of [...this.rules.values()]) {
- if (registration.closing || registration.rule.kind !== snapshot.kind) continue
- this.startInvocation(registration, snapshot)
- }
- }
- /** Start one contained invocation and attach it to registration teardown. */
- private startInvocation(registration: RuleRegistration, delivery: VerifiedWebhookDelivery): void {
- const tracked = Promise.resolve().then(async () => {
- registration.controller.signal.throwIfAborted()
- const request = await registration.rule.run(delivery, registration.controller.signal)
- registration.controller.signal.throwIfAborted()
- if (request !== null) {
- await createWebhookSession(
- this.selfCtx,
- delivery,
- registration.rule.id,
- request,
- registration.controller.signal,
- )
- }
- }).catch((error: unknown) => {
- const invocation = `webhook: provider=${JSON.stringify(delivery.kind)} source=${JSON.stringify(delivery.source)} `
- + `delivery=${JSON.stringify(delivery.deliveryId)} rule=${JSON.stringify(registration.rule.id)}`
- if (registration.controller.signal.aborted) {
- this.selfCtx.logger.debug(`${invocation} stopped after disposal: ${errorChain(error)}`)
- } else {
- this.selfCtx.logger.warn(`${invocation} failed: ${errorChain(error)}`)
- }
- }).finally(() => {
- registration.active.delete(tracked)
- })
- registration.active.add(tracked)
- }
- /** Memoized registration teardown: hide, abort, then drain. */
- private disposeRegistration(registration: RuleRegistration): Promise<void> {
- registration.disposal ??= (async () => {
- registration.closing = true
- this.rules.delete(registration.rule.id)
- registration.controller.abort(new Error(`webhook rule "${registration.rule.id}" was disposed`))
- while (registration.active.size > 0) {
- await Promise.allSettled([...registration.active])
- }
- })()
- return registration.disposal
- }
- }
- export default WebhookRuntime
|