@atomservice/functions-sdk 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@atomservice/functions-sdk",
3
+ "version": "0.1.6",
4
+ "description": "函数作者 SDK:定义器(defineFunction 等)、event 类型推导、schema 校验、结构化日志、内置 HTTP 运行时",
5
+ "type": "module",
6
+ "author": "openorson",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/openorson/atomservice.git",
10
+ "directory": "services/functions/sdk"
11
+ },
12
+ "bugs": "https://github.com/openorson/atomservice/issues",
13
+ "homepage": "https://github.com/openorson/atomservice/tree/main/services/functions/sdk#readme",
14
+ "keywords": [
15
+ "atomservice",
16
+ "functions",
17
+ "faas",
18
+ "sdk",
19
+ "bun"
20
+ ],
21
+ "engines": {
22
+ "bun": ">=1.3.14"
23
+ },
24
+ "files": [
25
+ "src"
26
+ ],
27
+ "exports": {
28
+ ".": "./src/index.ts"
29
+ },
30
+ "dependencies": {
31
+ "typebox": "1.2.2"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^6"
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export {
2
+ defineAsyncFunction,
3
+ defineAsyncStreamFunction,
4
+ defineFunction,
5
+ defineStreamFunction,
6
+ } from "./sdk.define.ts"
7
+ export type {
8
+ BusinessErrorPayload,
9
+ ErrorPayload,
10
+ ResultEnvelope,
11
+ SystemErrorPayload,
12
+ } from "./sdk.errors.ts"
13
+ export { BusinessError, isBusinessError, isSystemError, SystemError } from "./sdk.errors.ts"
14
+ export type { LogContext, LogLevel, LogRecord, LogSink } from "./sdk.log.ts"
15
+ export { createLogger, stdoutSink } from "./sdk.log.ts"
16
+ export type { ServeOptions } from "./sdk.runtime.ts"
17
+ export { serve } from "./sdk.runtime.ts"
18
+ export type {
19
+ AnyFunctionDefinition,
20
+ AsyncFunctionDefinition,
21
+ AsyncStreamFunctionDefinition,
22
+ DeepPartial,
23
+ ErrorsSchema,
24
+ ExecutionMode,
25
+ FunctionConfig,
26
+ FunctionContext,
27
+ FunctionDefinition,
28
+ FunctionKind,
29
+ FunctionRegistry,
30
+ LogFields,
31
+ Logger,
32
+ StreamFunctionDefinition,
33
+ } from "./sdk.types.ts"
@@ -0,0 +1,15 @@
1
+ export const INVOKE_PATH_PREFIX = "/__invoke/"
2
+ export const HEALTH_PATH = "/__health"
3
+ export const DEFAULT_BUNDLE_PORT = 8080
4
+ export const REQUEST_ID_HEADER = "x-atomfn-request-id"
5
+
6
+ export const SYSTEM_ERROR = {
7
+ NOT_FOUND: "NOT_FOUND",
8
+ VALIDATION_FAILED: "VALIDATION_FAILED",
9
+ OUTPUT_INVALID: "OUTPUT_INVALID",
10
+ FUNCTION_ERROR: "FUNCTION_ERROR",
11
+ FUNCTION_TIMEOUT: "FUNCTION_TIMEOUT",
12
+ OVERLOADED: "OVERLOADED",
13
+ } as const
14
+
15
+ export type SystemErrorCode = (typeof SYSTEM_ERROR)[keyof typeof SYSTEM_ERROR]
@@ -0,0 +1,170 @@
1
+ import { SQL } from "bun"
2
+ import type { ContextFactory } from "./sdk.executor.ts"
3
+ import { type RawOutcome, runOneShot, runStream } from "./sdk.run.ts"
4
+ import type { AnyFunctionDefinition, FunctionRegistry } from "./sdk.types.ts"
5
+ import { validate } from "./sdk.validate.ts"
6
+
7
+ export interface ConsumerOptions {
8
+ databaseUrl: string
9
+ project: string
10
+ bundle: string
11
+ registry: FunctionRegistry
12
+ makeContext: ContextFactory
13
+ pollIntervalMs?: number
14
+ visibilityTimeoutSec?: number
15
+ }
16
+
17
+ interface ClaimedJob {
18
+ id: string
19
+ fn: string
20
+ event: unknown
21
+ webhook: string | null
22
+ attempts: number
23
+ maxAttempts: number
24
+ }
25
+
26
+ function isAsyncKind(def: AnyFunctionDefinition): boolean {
27
+ return def.kind === "asyncFunction" || def.kind === "asyncStreamFunction"
28
+ }
29
+
30
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value)
32
+ }
33
+
34
+ function deepMerge(target: unknown, patch: unknown): unknown {
35
+ if (!isPlainObject(target) || !isPlainObject(patch)) return patch
36
+ const result: Record<string, unknown> = { ...target }
37
+ for (const [key, value] of Object.entries(patch)) {
38
+ result[key] = isPlainObject(result[key]) && isPlainObject(value) ? deepMerge(result[key], value) : value
39
+ }
40
+ return result
41
+ }
42
+
43
+ function sleep(ms: number): Promise<void> {
44
+ return new Promise((resolve) => setTimeout(resolve, ms))
45
+ }
46
+
47
+ export function startConsumer(options: ConsumerOptions): () => void {
48
+ const asyncNames = Object.entries(options.registry)
49
+ .filter(([, def]) => isAsyncKind(def))
50
+ .map(([name]) => name)
51
+ if (asyncNames.length === 0) return () => {}
52
+
53
+ const sql = new SQL(options.databaseUrl)
54
+ const interval = options.pollIntervalMs ?? 1000
55
+ const visibilitySec = options.visibilityTimeoutSec ?? 300
56
+ const asyncNamesLiteral = `{${asyncNames.join(",")}}`
57
+ let stopped = false
58
+
59
+ async function claim(): Promise<ClaimedJob | null> {
60
+ const rows = (await sql`
61
+ UPDATE "Job" SET status = 'running', "lockedAt" = now(), attempts = attempts + 1, "updatedAt" = now()
62
+ WHERE id = (
63
+ SELECT id FROM "Job"
64
+ WHERE project = ${options.project} AND bundle = ${options.bundle}
65
+ AND fn = ANY(${asyncNamesLiteral}::text[])
66
+ AND "availableAt" <= now()
67
+ AND (
68
+ status = 'queued'
69
+ OR (status = 'running' AND "lockedAt" < now() - make_interval(secs => ${visibilitySec}))
70
+ )
71
+ ORDER BY "availableAt"
72
+ FOR UPDATE SKIP LOCKED LIMIT 1
73
+ )
74
+ RETURNING id, fn, event, webhook, attempts, "maxAttempts"
75
+ `) as ClaimedJob[]
76
+ return rows[0] ?? null
77
+ }
78
+
79
+ async function execute(job: ClaimedJob): Promise<RawOutcome> {
80
+ const def = options.registry[job.fn]
81
+ if (!def) return { kind: "crash", message: `未注册函数 ${job.fn}` }
82
+ const ctx = options.makeContext(job.fn, job.id)
83
+
84
+ const rawEvent = typeof job.event === "string" ? JSON.parse(job.event) : job.event
85
+ const parsed = validate(def.input, rawEvent)
86
+ if (!parsed.ok) return { kind: "crash", message: `入参校验失败:${parsed.issues.join("; ")}` }
87
+ const event = parsed.value
88
+
89
+ if (def.kind === "asyncStreamFunction") {
90
+ const generator = runStream(def, event, ctx)
91
+ let merged: unknown = {}
92
+ while (true) {
93
+ const next = await generator.next()
94
+ if (next.done) {
95
+ if (next.value.kind === "ok") return { kind: "ok", data: merged }
96
+ return next.value
97
+ }
98
+ merged = deepMerge(merged, next.value)
99
+ }
100
+ }
101
+ return runOneShot(def, event, ctx)
102
+ }
103
+
104
+ async function finalize(job: ClaimedJob, outcome: RawOutcome): Promise<void> {
105
+ if (outcome.kind === "ok") {
106
+ await sql`UPDATE "Job" SET status = 'done', result = ${JSON.stringify(outcome.data ?? null)}::jsonb, error = NULL, "updatedAt" = now() WHERE id = ${job.id}`
107
+ await notify(job, "done")
108
+ return
109
+ }
110
+
111
+ if (outcome.kind === "business") {
112
+ const error = { type: "business", code: outcome.code, data: outcome.data }
113
+ await sql`UPDATE "Job" SET status = 'failed', error = ${JSON.stringify(error)}::jsonb, "updatedAt" = now() WHERE id = ${job.id}`
114
+ await notify(job, "failed")
115
+ return
116
+ }
117
+
118
+ const error = { type: "system", code: "FUNCTION_ERROR", message: outcome.message, retryable: true }
119
+ if (job.attempts < job.maxAttempts) {
120
+ const backoffSec = Math.min(2 ** job.attempts, 300)
121
+ await sql`UPDATE "Job" SET status = 'queued', error = ${JSON.stringify(error)}::jsonb, "lockedAt" = NULL, "availableAt" = now() + make_interval(secs => ${backoffSec}), "updatedAt" = now() WHERE id = ${job.id}`
122
+ return
123
+ }
124
+ await sql`UPDATE "Job" SET status = 'failed', error = ${JSON.stringify(error)}::jsonb, "updatedAt" = now() WHERE id = ${job.id}`
125
+ await notify(job, "failed")
126
+ }
127
+
128
+ async function notify(job: ClaimedJob, status: "done" | "failed"): Promise<void> {
129
+ if (!job.webhook) return
130
+ await fetch(job.webhook, {
131
+ method: "POST",
132
+ headers: { "content-type": "application/json" },
133
+ body: JSON.stringify({ jobId: job.id, status }),
134
+ }).catch(() => {})
135
+ }
136
+
137
+ const heartbeatIntervalMs = Math.floor((visibilitySec / 3) * 1000)
138
+
139
+ function startHeartbeat(jobId: string): () => void {
140
+ const timer = setInterval(() => {
141
+ sql`UPDATE "Job" SET "lockedAt" = now() WHERE id = ${jobId} AND status = 'running'`.catch(() => {})
142
+ }, heartbeatIntervalMs)
143
+ return () => clearInterval(timer)
144
+ }
145
+
146
+ async function loop(): Promise<void> {
147
+ while (!stopped) {
148
+ let job: ClaimedJob | null = null
149
+ try {
150
+ job = await claim()
151
+ } catch {
152
+ await sleep(interval)
153
+ continue
154
+ }
155
+ if (!job) {
156
+ await sleep(interval)
157
+ continue
158
+ }
159
+ const stopHeartbeat = startHeartbeat(job.id)
160
+ const outcome = await execute(job)
161
+ stopHeartbeat()
162
+ await finalize(job, outcome).catch(() => {})
163
+ }
164
+ }
165
+
166
+ void loop()
167
+ return () => {
168
+ stopped = true
169
+ }
170
+ }
@@ -0,0 +1,25 @@
1
+ import { BusinessError } from "./sdk.errors.ts"
2
+ import { createLogger, type LogSink } from "./sdk.log.ts"
3
+ import type { ErrorsSchema, FunctionContext } from "./sdk.types.ts"
4
+
5
+ export interface ContextInput {
6
+ project: string
7
+ bundle: string
8
+ function: string
9
+ requestId: string
10
+ sink: LogSink
11
+ }
12
+
13
+ export function createContext<TErrors extends ErrorsSchema>(input: ContextInput): FunctionContext<TErrors> {
14
+ const log = createLogger(
15
+ { project: input.project, bundle: input.bundle, function: input.function, requestId: input.requestId },
16
+ input.sink,
17
+ )
18
+ return {
19
+ requestId: input.requestId,
20
+ log,
21
+ error(code, data) {
22
+ return new BusinessError(code, data)
23
+ },
24
+ }
25
+ }
@@ -0,0 +1,91 @@
1
+ import type { Static, TSchema } from "typebox"
2
+ import type {
3
+ AsyncFunctionDefinition,
4
+ AsyncStreamFunctionDefinition,
5
+ DeepPartial,
6
+ ErrorsSchema,
7
+ FunctionConfig,
8
+ FunctionContext,
9
+ FunctionDefinition,
10
+ StreamFunctionDefinition,
11
+ } from "./sdk.types.ts"
12
+
13
+ interface OneShotInput<TInput extends TSchema, TOutput extends TSchema, TErrors extends ErrorsSchema> {
14
+ input: TInput
15
+ output: TOutput
16
+ errors?: TErrors
17
+ config?: FunctionConfig
18
+ handler: (event: Static<TInput>, ctx: FunctionContext<TErrors>) => Bun.MaybePromise<Static<TOutput>>
19
+ }
20
+
21
+ interface StreamInput<TInput extends TSchema, TOutput extends TSchema, TErrors extends ErrorsSchema> {
22
+ input: TInput
23
+ output: TOutput
24
+ errors?: TErrors
25
+ config?: FunctionConfig
26
+ handler: (
27
+ event: Static<TInput>,
28
+ ctx: FunctionContext<TErrors>,
29
+ ) => AsyncGenerator<DeepPartial<Static<TOutput>>, void, void>
30
+ }
31
+
32
+ const EMPTY_ERRORS = {} as ErrorsSchema
33
+ const EMPTY_CONFIG: FunctionConfig = {}
34
+
35
+ export function defineFunction<TInput extends TSchema, TOutput extends TSchema, const TErrors extends ErrorsSchema>(
36
+ def: OneShotInput<TInput, TOutput, TErrors>,
37
+ ): FunctionDefinition<TInput, TOutput, TErrors> {
38
+ return {
39
+ kind: "function",
40
+ input: def.input,
41
+ output: def.output,
42
+ errors: (def.errors ?? EMPTY_ERRORS) as TErrors,
43
+ config: def.config ?? EMPTY_CONFIG,
44
+ handler: def.handler,
45
+ }
46
+ }
47
+
48
+ export function defineStreamFunction<
49
+ TInput extends TSchema,
50
+ TOutput extends TSchema,
51
+ const TErrors extends ErrorsSchema,
52
+ >(def: StreamInput<TInput, TOutput, TErrors>): StreamFunctionDefinition<TInput, TOutput, TErrors> {
53
+ return {
54
+ kind: "streamFunction",
55
+ input: def.input,
56
+ output: def.output,
57
+ errors: (def.errors ?? EMPTY_ERRORS) as TErrors,
58
+ config: def.config ?? EMPTY_CONFIG,
59
+ handler: def.handler,
60
+ }
61
+ }
62
+
63
+ export function defineAsyncFunction<
64
+ TInput extends TSchema,
65
+ TOutput extends TSchema,
66
+ const TErrors extends ErrorsSchema,
67
+ >(def: OneShotInput<TInput, TOutput, TErrors>): AsyncFunctionDefinition<TInput, TOutput, TErrors> {
68
+ return {
69
+ kind: "asyncFunction",
70
+ input: def.input,
71
+ output: def.output,
72
+ errors: (def.errors ?? EMPTY_ERRORS) as TErrors,
73
+ config: def.config ?? EMPTY_CONFIG,
74
+ handler: def.handler,
75
+ }
76
+ }
77
+
78
+ export function defineAsyncStreamFunction<
79
+ TInput extends TSchema,
80
+ TOutput extends TSchema,
81
+ const TErrors extends ErrorsSchema,
82
+ >(def: StreamInput<TInput, TOutput, TErrors>): AsyncStreamFunctionDefinition<TInput, TOutput, TErrors> {
83
+ return {
84
+ kind: "asyncStreamFunction",
85
+ input: def.input,
86
+ output: def.output,
87
+ errors: (def.errors ?? EMPTY_ERRORS) as TErrors,
88
+ config: def.config ?? EMPTY_CONFIG,
89
+ handler: def.handler,
90
+ }
91
+ }
@@ -0,0 +1,56 @@
1
+ export interface BusinessErrorPayload {
2
+ type: "business"
3
+ code: string
4
+ data: unknown
5
+ }
6
+
7
+ export interface SystemErrorPayload {
8
+ type: "system"
9
+ code: string
10
+ message: string
11
+ retryable: boolean
12
+ }
13
+
14
+ export type ErrorPayload = BusinessErrorPayload | SystemErrorPayload
15
+
16
+ export type ResultEnvelope<TOutput = unknown> = { ok: true; data: TOutput } | { ok: false; error: ErrorPayload }
17
+
18
+ export class BusinessError extends Error {
19
+ readonly code: string
20
+ readonly data: unknown
21
+
22
+ constructor(code: string, data: unknown) {
23
+ super(`business error: ${code}`)
24
+ this.name = "BusinessError"
25
+ this.code = code
26
+ this.data = data
27
+ }
28
+
29
+ toPayload(): BusinessErrorPayload {
30
+ return { type: "business", code: this.code, data: this.data }
31
+ }
32
+ }
33
+
34
+ export class SystemError extends Error {
35
+ readonly code: string
36
+ readonly retryable: boolean
37
+
38
+ constructor(code: string, message: string, retryable = false) {
39
+ super(message)
40
+ this.name = "SystemError"
41
+ this.code = code
42
+ this.retryable = retryable
43
+ }
44
+
45
+ toPayload(): SystemErrorPayload {
46
+ return { type: "system", code: this.code, message: this.message, retryable: this.retryable }
47
+ }
48
+ }
49
+
50
+ export function isBusinessError(value: unknown): value is BusinessError {
51
+ return value instanceof BusinessError
52
+ }
53
+
54
+ export function isSystemError(value: unknown): value is SystemError {
55
+ return value instanceof SystemError
56
+ }
@@ -0,0 +1,270 @@
1
+ import { type RawOutcome, runOneShot, runStream } from "./sdk.run.ts"
2
+ import type { FunctionContext, FunctionRegistry } from "./sdk.types.ts"
3
+
4
+ export type ExecOutcome = RawOutcome | { kind: "timeout" }
5
+
6
+ export interface ExecuteInput {
7
+ name: string
8
+ event: unknown
9
+ requestId: string
10
+ timeout: number
11
+ }
12
+
13
+ export interface Executor {
14
+ oneShot(input: ExecuteInput): Promise<ExecOutcome>
15
+ stream(input: ExecuteInput): AsyncGenerator<unknown, ExecOutcome, void>
16
+ dispose(): Promise<void>
17
+ }
18
+
19
+ export type ContextFactory = (name: string, requestId: string) => FunctionContext<Record<string, never>>
20
+
21
+ function timeoutGuard(ms: number): { promise: Promise<{ kind: "timeout" }>; cancel: () => void } {
22
+ let timer: ReturnType<typeof setTimeout> | undefined
23
+ const promise = new Promise<{ kind: "timeout" }>((resolve) => {
24
+ timer = setTimeout(() => resolve({ kind: "timeout" }), ms)
25
+ })
26
+ return { promise, cancel: () => timer && clearTimeout(timer) }
27
+ }
28
+
29
+ export class InlineExecutor implements Executor {
30
+ constructor(
31
+ private readonly registry: FunctionRegistry,
32
+ private readonly makeContext: ContextFactory,
33
+ ) {}
34
+
35
+ async oneShot({ name, event, requestId, timeout }: ExecuteInput): Promise<ExecOutcome> {
36
+ const def = this.registry[name]
37
+ if (!def) return { kind: "crash", message: `未注册函数 ${name}` }
38
+ const ctx = this.makeContext(name, requestId)
39
+ const guard = timeoutGuard(timeout)
40
+ try {
41
+ return await Promise.race([runOneShot(def, event, ctx), guard.promise])
42
+ } finally {
43
+ guard.cancel()
44
+ }
45
+ }
46
+
47
+ async *stream({ name, event, requestId, timeout }: ExecuteInput): AsyncGenerator<unknown, ExecOutcome, void> {
48
+ const def = this.registry[name]
49
+ if (!def) return { kind: "crash", message: `未注册函数 ${name}` }
50
+ const ctx = this.makeContext(name, requestId)
51
+ const deadline = Date.now() + timeout
52
+ const generator = runStream(def, event, ctx)
53
+ while (true) {
54
+ const next = await generator.next()
55
+ if (next.done) return next.value
56
+ if (Date.now() > deadline) return { kind: "timeout" }
57
+ yield next.value
58
+ }
59
+ }
60
+
61
+ async dispose(): Promise<void> {}
62
+ }
63
+
64
+ interface PendingOneShot {
65
+ kind: "oneShot"
66
+ resolve: (outcome: RawOutcome) => void
67
+ }
68
+
69
+ interface PendingStream {
70
+ kind: "stream"
71
+ push: (chunk: unknown) => void
72
+ end: (outcome: RawOutcome) => void
73
+ }
74
+
75
+ type WorkerMessage =
76
+ | { t: "ready" }
77
+ | { t: "result"; id: number; outcome: RawOutcome }
78
+ | { t: "chunk"; id: number; data: unknown }
79
+ | { t: "end"; id: number; outcome: RawOutcome }
80
+
81
+ class WorkerHandle {
82
+ private readonly proc: Bun.Subprocess
83
+ private readonly pending = new Map<number, PendingOneShot | PendingStream>()
84
+ private seq = 0
85
+ private markReady!: () => void
86
+ readonly ready: Promise<void>
87
+
88
+ constructor(args: string[], env: Record<string, string>) {
89
+ this.ready = new Promise((resolve) => {
90
+ this.markReady = resolve
91
+ })
92
+ this.proc = Bun.spawn(args, {
93
+ env,
94
+ stdio: ["ignore", "inherit", "inherit"],
95
+ serialization: "json",
96
+ ipc: (message) => this.onMessage(message as WorkerMessage),
97
+ })
98
+ }
99
+
100
+ private onMessage(message: WorkerMessage): void {
101
+ if (message.t === "ready") {
102
+ this.markReady()
103
+ return
104
+ }
105
+ const entry = this.pending.get(message.id)
106
+ if (!entry) return
107
+ if (message.t === "result" && entry.kind === "oneShot") {
108
+ this.pending.delete(message.id)
109
+ entry.resolve(message.outcome)
110
+ } else if (message.t === "chunk" && entry.kind === "stream") {
111
+ entry.push(message.data)
112
+ } else if (message.t === "end" && entry.kind === "stream") {
113
+ this.pending.delete(message.id)
114
+ entry.end(message.outcome)
115
+ }
116
+ }
117
+
118
+ oneShot(name: string, event: unknown, requestId: string): Promise<RawOutcome> {
119
+ const id = ++this.seq
120
+ return new Promise<RawOutcome>((resolve) => {
121
+ this.pending.set(id, { kind: "oneShot", resolve })
122
+ this.proc.send({ t: "call", id, stream: false, name, event, requestId })
123
+ })
124
+ }
125
+
126
+ stream(name: string, event: unknown, requestId: string): AsyncGenerator<unknown, RawOutcome, void> {
127
+ const id = ++this.seq
128
+ const queue: unknown[] = []
129
+ let terminal: RawOutcome | undefined
130
+ let notify: (() => void) | undefined
131
+
132
+ const wake = () => {
133
+ const fn = notify
134
+ notify = undefined
135
+ fn?.()
136
+ }
137
+ this.pending.set(id, {
138
+ kind: "stream",
139
+ push: (chunk) => {
140
+ queue.push(chunk)
141
+ wake()
142
+ },
143
+ end: (outcome) => {
144
+ terminal = outcome
145
+ wake()
146
+ },
147
+ })
148
+ this.proc.send({ t: "call", id, stream: true, name, event, requestId })
149
+
150
+ async function* drain(): AsyncGenerator<unknown, RawOutcome, void> {
151
+ while (true) {
152
+ if (queue.length > 0) {
153
+ yield queue.shift()
154
+ continue
155
+ }
156
+ if (terminal) return terminal
157
+ await new Promise<void>((resolve) => {
158
+ notify = resolve
159
+ })
160
+ }
161
+ }
162
+ return drain()
163
+ }
164
+
165
+ kill(): void {
166
+ this.proc.kill()
167
+ }
168
+ }
169
+
170
+ export interface ProcessExecutorOptions {
171
+ args: string[]
172
+ env: Record<string, string>
173
+ pooled: boolean
174
+ size: number
175
+ }
176
+
177
+ export class ProcessExecutor implements Executor {
178
+ private readonly idle: WorkerHandle[] = []
179
+ private readonly waiters: Array<(worker: WorkerHandle) => void> = []
180
+ private created = 0
181
+
182
+ constructor(private readonly options: ProcessExecutorOptions) {}
183
+
184
+ private spawn(): WorkerHandle {
185
+ return new WorkerHandle(this.options.args, { ...this.options.env, ATOMFN_WORKER: "1" })
186
+ }
187
+
188
+ private async acquire(): Promise<WorkerHandle> {
189
+ if (!this.options.pooled) {
190
+ const worker = this.spawn()
191
+ await worker.ready
192
+ return worker
193
+ }
194
+ const existing = this.idle.pop()
195
+ if (existing) return existing
196
+ if (this.created < this.options.size) {
197
+ this.created += 1
198
+ const worker = this.spawn()
199
+ await worker.ready
200
+ return worker
201
+ }
202
+ return new Promise<WorkerHandle>((resolve) => this.waiters.push(resolve))
203
+ }
204
+
205
+ private release(worker: WorkerHandle): void {
206
+ if (!this.options.pooled) {
207
+ worker.kill()
208
+ return
209
+ }
210
+ const waiter = this.waiters.shift()
211
+ if (waiter) waiter(worker)
212
+ else this.idle.push(worker)
213
+ }
214
+
215
+ private discard(worker: WorkerHandle): void {
216
+ worker.kill()
217
+ if (!this.options.pooled) return
218
+ this.created -= 1
219
+ const waiter = this.waiters.shift()
220
+ if (waiter) {
221
+ this.created += 1
222
+ const replacement = this.spawn()
223
+ replacement.ready.then(() => waiter(replacement))
224
+ }
225
+ }
226
+
227
+ async oneShot({ name, event, requestId, timeout }: ExecuteInput): Promise<ExecOutcome> {
228
+ const worker = await this.acquire()
229
+ const guard = timeoutGuard(timeout)
230
+ try {
231
+ const outcome = await Promise.race([worker.oneShot(name, event, requestId), guard.promise])
232
+ if (outcome.kind === "timeout") {
233
+ this.discard(worker)
234
+ return outcome
235
+ }
236
+ this.release(worker)
237
+ return outcome
238
+ } finally {
239
+ guard.cancel()
240
+ }
241
+ }
242
+
243
+ async *stream({ name, event, requestId, timeout }: ExecuteInput): AsyncGenerator<unknown, ExecOutcome, void> {
244
+ const worker = await this.acquire()
245
+ const deadline = Date.now() + timeout
246
+ const generator = worker.stream(name, event, requestId)
247
+ try {
248
+ while (true) {
249
+ const next = await generator.next()
250
+ if (next.done) {
251
+ this.release(worker)
252
+ return next.value
253
+ }
254
+ if (Date.now() > deadline) {
255
+ this.discard(worker)
256
+ return { kind: "timeout" }
257
+ }
258
+ yield next.value
259
+ }
260
+ } catch (error) {
261
+ this.discard(worker)
262
+ return { kind: "crash", message: error instanceof Error ? error.message : String(error) }
263
+ }
264
+ }
265
+
266
+ async dispose(): Promise<void> {
267
+ for (const worker of this.idle) worker.kill()
268
+ this.idle.length = 0
269
+ }
270
+ }
@@ -0,0 +1,140 @@
1
+ import { SYSTEM_ERROR } from "./sdk.consts.ts"
2
+ import type { ResultEnvelope, SystemErrorPayload } from "./sdk.errors.ts"
3
+ import type { ExecOutcome, Executor } from "./sdk.executor.ts"
4
+ import type {
5
+ AnyFunctionDefinition,
6
+ AsyncFunctionDefinition,
7
+ AsyncStreamFunctionDefinition,
8
+ FunctionDefinition,
9
+ StreamFunctionDefinition,
10
+ } from "./sdk.types.ts"
11
+ import { validate } from "./sdk.validate.ts"
12
+
13
+ export interface InvokeDeps {
14
+ executor: Executor
15
+ name: string
16
+ requestId: string
17
+ defaultTimeout: number
18
+ }
19
+
20
+ export interface OneShotResult {
21
+ status: number
22
+ envelope: ResultEnvelope
23
+ }
24
+
25
+ function systemError(code: string, message: string, retryable = false): SystemErrorPayload {
26
+ return { type: "system", code, message, retryable }
27
+ }
28
+
29
+ function mapTerminal(def: AnyFunctionDefinition, outcome: ExecOutcome): OneShotResult {
30
+ if (outcome.kind === "timeout") {
31
+ return { status: 504, envelope: { ok: false, error: systemError(SYSTEM_ERROR.FUNCTION_TIMEOUT, "执行超时") } }
32
+ }
33
+ if (outcome.kind === "business") {
34
+ return {
35
+ status: 200,
36
+ envelope: { ok: false, error: { type: "business", code: outcome.code, data: outcome.data } },
37
+ }
38
+ }
39
+ if (outcome.kind === "crash") {
40
+ return { status: 500, envelope: { ok: false, error: systemError(SYSTEM_ERROR.FUNCTION_ERROR, outcome.message) } }
41
+ }
42
+ const checked = validate(def.output, outcome.data)
43
+ if (!checked.ok) {
44
+ return {
45
+ status: 500,
46
+ envelope: { ok: false, error: systemError(SYSTEM_ERROR.OUTPUT_INVALID, checked.issues.join("; ")) },
47
+ }
48
+ }
49
+ return { status: 200, envelope: { ok: true, data: checked.value } }
50
+ }
51
+
52
+ export async function invokeOneShot(
53
+ def: FunctionDefinition | AsyncFunctionDefinition,
54
+ body: unknown,
55
+ deps: InvokeDeps,
56
+ ): Promise<OneShotResult> {
57
+ const parsed = validate(def.input, body)
58
+ if (!parsed.ok) {
59
+ return {
60
+ status: 422,
61
+ envelope: { ok: false, error: systemError(SYSTEM_ERROR.VALIDATION_FAILED, parsed.issues.join("; ")) },
62
+ }
63
+ }
64
+
65
+ const timeout = def.config.timeout ?? deps.defaultTimeout
66
+ const outcome = await deps.executor.oneShot({
67
+ name: deps.name,
68
+ event: parsed.value,
69
+ requestId: deps.requestId,
70
+ timeout,
71
+ })
72
+ return mapTerminal(def, outcome)
73
+ }
74
+
75
+ const SSE_HEADERS = { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" }
76
+
77
+ function sseEvent(event: string, data: unknown): string {
78
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
79
+ }
80
+
81
+ export function invokeStream(
82
+ def: StreamFunctionDefinition | AsyncStreamFunctionDefinition,
83
+ body: unknown,
84
+ deps: InvokeDeps,
85
+ ): Response {
86
+ const parsed = validate(def.input, body)
87
+ if (!parsed.ok) {
88
+ const envelope: ResultEnvelope = {
89
+ ok: false,
90
+ error: systemError(SYSTEM_ERROR.VALIDATION_FAILED, parsed.issues.join("; ")),
91
+ }
92
+ return Response.json(envelope, { status: 422 })
93
+ }
94
+
95
+ const timeout = def.config.timeout ?? deps.defaultTimeout
96
+ const encoder = new TextEncoder()
97
+
98
+ const stream = new ReadableStream<Uint8Array>({
99
+ async start(controller) {
100
+ const generator = deps.executor.stream({
101
+ name: deps.name,
102
+ event: parsed.value,
103
+ requestId: deps.requestId,
104
+ timeout,
105
+ })
106
+ while (true) {
107
+ const next = await generator.next()
108
+ if (next.done) {
109
+ const outcome = next.value
110
+ if (outcome.kind === "ok") {
111
+ controller.enqueue(encoder.encode(sseEvent("done", {})))
112
+ } else if (outcome.kind === "business") {
113
+ controller.enqueue(
114
+ encoder.encode(sseEvent("error", { type: "business", code: outcome.code, data: outcome.data })),
115
+ )
116
+ } else if (outcome.kind === "timeout") {
117
+ controller.enqueue(
118
+ encoder.encode(sseEvent("error", systemError(SYSTEM_ERROR.FUNCTION_TIMEOUT, "执行超时"))),
119
+ )
120
+ } else {
121
+ controller.enqueue(
122
+ encoder.encode(sseEvent("error", systemError(SYSTEM_ERROR.FUNCTION_ERROR, outcome.message))),
123
+ )
124
+ }
125
+ break
126
+ }
127
+ controller.enqueue(encoder.encode(sseEvent("chunk", next.value)))
128
+ }
129
+ controller.close()
130
+ },
131
+ })
132
+
133
+ return new Response(stream, { headers: SSE_HEADERS })
134
+ }
135
+
136
+ export function isStreamKind(
137
+ def: AnyFunctionDefinition,
138
+ ): def is StreamFunctionDefinition | AsyncStreamFunctionDefinition {
139
+ return def.kind === "streamFunction" || def.kind === "asyncStreamFunction"
140
+ }
package/src/sdk.log.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { LogFields, Logger } from "./sdk.types.ts"
2
+
3
+ export type LogLevel = "debug" | "info" | "warn" | "error"
4
+
5
+ export interface LogRecord {
6
+ ts: string
7
+ level: LogLevel
8
+ project: string
9
+ bundle: string
10
+ function: string
11
+ requestId: string
12
+ msg: string
13
+ fields?: LogFields
14
+ }
15
+
16
+ export interface LogSink {
17
+ write(record: LogRecord): void
18
+ }
19
+
20
+ export interface LogContext {
21
+ project: string
22
+ bundle: string
23
+ function: string
24
+ requestId: string
25
+ }
26
+
27
+ export const stdoutSink: LogSink = {
28
+ write(record) {
29
+ const { fields, ...rest } = record
30
+ process.stdout.write(`${JSON.stringify({ ...rest, ...fields })}\n`)
31
+ },
32
+ }
33
+
34
+ export function createLogger(ctx: LogContext, sink: LogSink): Logger {
35
+ const emit = (level: LogLevel, msg: string, fields?: LogFields) => {
36
+ sink.write({ ts: new Date().toISOString(), level, ...ctx, msg, fields })
37
+ }
38
+ return {
39
+ debug: (msg, fields) => emit("debug", msg, fields),
40
+ info: (msg, fields) => emit("info", msg, fields),
41
+ warn: (msg, fields) => emit("warn", msg, fields),
42
+ error: (msg, fields) => emit("error", msg, fields),
43
+ }
44
+ }
package/src/sdk.run.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { BusinessError } from "./sdk.errors.ts"
2
+ import type { AnyFunctionDefinition, FunctionContext } from "./sdk.types.ts"
3
+
4
+ export type RawOutcome =
5
+ | { kind: "ok"; data: unknown }
6
+ | { kind: "business"; code: string; data: unknown }
7
+ | { kind: "crash"; message: string }
8
+
9
+ function crashMessage(error: unknown): string {
10
+ return error instanceof Error ? error.message : String(error)
11
+ }
12
+
13
+ function fromError(error: unknown): RawOutcome {
14
+ if (error instanceof BusinessError) return { kind: "business", code: error.code, data: error.data }
15
+ return { kind: "crash", message: crashMessage(error) }
16
+ }
17
+
18
+ export async function runOneShot(
19
+ def: AnyFunctionDefinition,
20
+ event: unknown,
21
+ ctx: FunctionContext<Record<string, never>>,
22
+ ): Promise<RawOutcome> {
23
+ try {
24
+ const data = await (def.handler as (e: unknown, c: unknown) => unknown)(event, ctx)
25
+ return { kind: "ok", data }
26
+ } catch (error) {
27
+ return fromError(error)
28
+ }
29
+ }
30
+
31
+ export async function* runStream(
32
+ def: AnyFunctionDefinition,
33
+ event: unknown,
34
+ ctx: FunctionContext<Record<string, never>>,
35
+ ): AsyncGenerator<unknown, RawOutcome, void> {
36
+ try {
37
+ const iterator = (def.handler as (e: unknown, c: unknown) => AsyncIterable<unknown>)(event, ctx)
38
+ for await (const partial of iterator) yield partial
39
+ return { kind: "ok", data: undefined }
40
+ } catch (error) {
41
+ return fromError(error)
42
+ }
43
+ }
@@ -0,0 +1,177 @@
1
+ import { DEFAULT_BUNDLE_PORT, HEALTH_PATH, INVOKE_PATH_PREFIX, REQUEST_ID_HEADER, SYSTEM_ERROR } from "./sdk.consts.ts"
2
+ import { startConsumer } from "./sdk.consumer.ts"
3
+ import { createContext } from "./sdk.context.ts"
4
+ import { type ContextFactory, type Executor, InlineExecutor, ProcessExecutor } from "./sdk.executor.ts"
5
+ import { invokeOneShot, invokeStream, isStreamKind } from "./sdk.invoke.ts"
6
+ import { type LogSink, stdoutSink } from "./sdk.log.ts"
7
+ import type { AnyFunctionDefinition, ExecutionMode, FunctionRegistry } from "./sdk.types.ts"
8
+ import { runWorker } from "./sdk.worker.ts"
9
+
10
+ export interface ServeOptions {
11
+ port: number
12
+ project: string
13
+ bundle: string
14
+ sink: LogSink
15
+ defaultTimeout: number
16
+ poolSize: number
17
+ memoryLimitMb?: number
18
+ }
19
+
20
+ const DEFAULT_TIMEOUT = 30_000
21
+ const MEMORY_BACKPRESSURE_RATIO = 0.9
22
+
23
+ function resolveOptions(overrides?: Partial<ServeOptions>): ServeOptions {
24
+ const env = process.env
25
+ const memory = env.ATOMFN_MEMORY_MB ? Number(env.ATOMFN_MEMORY_MB) : undefined
26
+ return {
27
+ port: overrides?.port ?? (env.ATOMFN_PORT ? Number(env.ATOMFN_PORT) : DEFAULT_BUNDLE_PORT),
28
+ project: overrides?.project ?? env.ATOMFN_PROJECT ?? "default",
29
+ bundle: overrides?.bundle ?? env.ATOMFN_BUNDLE ?? "default",
30
+ sink: overrides?.sink ?? stdoutSink,
31
+ defaultTimeout: overrides?.defaultTimeout ?? DEFAULT_TIMEOUT,
32
+ poolSize: overrides?.poolSize ?? Math.max(1, navigator.hardwareConcurrency || 1),
33
+ memoryLimitMb: overrides?.memoryLimitMb ?? (memory && Number.isFinite(memory) ? memory : undefined),
34
+ }
35
+ }
36
+
37
+ function requestId(request: Request): string {
38
+ return request.headers.get(REQUEST_ID_HEADER) ?? crypto.randomUUID()
39
+ }
40
+
41
+ function makeContextFactory(options: ServeOptions): ContextFactory {
42
+ return (name, id) =>
43
+ createContext<Record<string, never>>({
44
+ project: options.project,
45
+ bundle: options.bundle,
46
+ function: name,
47
+ requestId: id,
48
+ sink: options.sink,
49
+ })
50
+ }
51
+
52
+ interface ExecutorRouter {
53
+ pick: (def: AnyFunctionDefinition) => Executor
54
+ dispose: () => Promise<void>
55
+ }
56
+
57
+ function buildExecutorRouter(registry: FunctionRegistry, options: ServeOptions): ExecutorRouter {
58
+ const inline = new InlineExecutor(registry, makeContextFactory(options))
59
+ const workerEnv: Record<string, string> = {
60
+ ...(process.env as Record<string, string>),
61
+ ATOMFN_PROJECT: options.project,
62
+ ATOMFN_BUNDLE: options.bundle,
63
+ }
64
+ let pooled: ProcessExecutor | undefined
65
+ let isolated: ProcessExecutor | undefined
66
+
67
+ function pick(def: AnyFunctionDefinition): Executor {
68
+ const mode: ExecutionMode = def.config.execution ?? "inline"
69
+ if (mode === "inline") return inline
70
+ if (mode === "pooled") {
71
+ pooled ??= new ProcessExecutor({ args: [...process.argv], env: workerEnv, pooled: true, size: options.poolSize })
72
+ return pooled
73
+ }
74
+ isolated ??= new ProcessExecutor({ args: [...process.argv], env: workerEnv, pooled: false, size: 0 })
75
+ return isolated
76
+ }
77
+
78
+ async function dispose(): Promise<void> {
79
+ await inline.dispose()
80
+ await pooled?.dispose()
81
+ await isolated?.dispose()
82
+ }
83
+
84
+ return { pick, dispose }
85
+ }
86
+
87
+ function overloaded(options: ServeOptions): boolean {
88
+ if (!options.memoryLimitMb) return false
89
+ const rssMb = process.memoryUsage().rss / (1024 * 1024)
90
+ return rssMb > options.memoryLimitMb * MEMORY_BACKPRESSURE_RATIO
91
+ }
92
+
93
+ export function serve(
94
+ registry: FunctionRegistry,
95
+ overrides?: Partial<ServeOptions>,
96
+ ): Bun.Server<undefined> | undefined {
97
+ const options = resolveOptions(overrides)
98
+
99
+ if (process.env.ATOMFN_WORKER === "1") {
100
+ runWorker(registry, makeContextFactory(options))
101
+ return undefined
102
+ }
103
+
104
+ const router = buildExecutorRouter(registry, options)
105
+
106
+ const databaseUrl = process.env.ATOMFN_DATABASE_URL?.trim()
107
+ if (databaseUrl) {
108
+ startConsumer({
109
+ databaseUrl,
110
+ project: options.project,
111
+ bundle: options.bundle,
112
+ registry,
113
+ makeContext: makeContextFactory(options),
114
+ })
115
+ }
116
+
117
+ return Bun.serve({
118
+ port: options.port,
119
+ async fetch(request) {
120
+ const url = new URL(request.url)
121
+
122
+ if (request.method === "GET" && url.pathname === HEALTH_PATH) {
123
+ const functions = Object.entries(registry).map(([name, def]) => ({ name, kind: def.kind }))
124
+ return Response.json({ ok: true, functions })
125
+ }
126
+
127
+ if (request.method === "POST" && url.pathname.startsWith(INVOKE_PATH_PREFIX)) {
128
+ const name = url.pathname.slice(INVOKE_PATH_PREFIX.length)
129
+ const def = registry[name]
130
+ if (!def) {
131
+ return Response.json(
132
+ {
133
+ ok: false,
134
+ error: { type: "system", code: SYSTEM_ERROR.NOT_FOUND, message: `未找到函数 ${name}`, retryable: false },
135
+ },
136
+ { status: 404 },
137
+ )
138
+ }
139
+
140
+ const error = {
141
+ type: "system" as const,
142
+ code: "OVERLOADED",
143
+ message: "内存接近上限,请稍后重试",
144
+ retryable: true,
145
+ }
146
+ if (overloaded(options)) {
147
+ return Response.json({ ok: false, error }, { status: 503, headers: { "retry-after": "1" } })
148
+ }
149
+
150
+ const id = requestId(request)
151
+ const body = await readBody(request)
152
+ const deps = { executor: router.pick(def), name, requestId: id, defaultTimeout: options.defaultTimeout }
153
+
154
+ if (isStreamKind(def)) {
155
+ const response = invokeStream(def, body, deps)
156
+ response.headers.set(REQUEST_ID_HEADER, id)
157
+ return response
158
+ }
159
+
160
+ const { status, envelope } = await invokeOneShot(def, body, deps)
161
+ return Response.json(envelope, { status, headers: { [REQUEST_ID_HEADER]: id } })
162
+ }
163
+
164
+ return new Response("Not Found", { status: 404 })
165
+ },
166
+ })
167
+ }
168
+
169
+ async function readBody(request: Request): Promise<unknown> {
170
+ const text = await request.text()
171
+ if (!text) return undefined
172
+ try {
173
+ return JSON.parse(text)
174
+ } catch {
175
+ return text
176
+ }
177
+ }
@@ -0,0 +1,94 @@
1
+ import type { Static, TSchema } from "typebox"
2
+ import type { BusinessError } from "./sdk.errors.ts"
3
+
4
+ export type ExecutionMode = "inline" | "pooled" | "isolated"
5
+
6
+ export type ErrorsSchema = Record<string, TSchema>
7
+
8
+ export type DeepPartial<T> = T extends (infer U)[]
9
+ ? DeepPartial<U>[]
10
+ : T extends object
11
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
12
+ : T
13
+
14
+ export interface LogFields {
15
+ [key: string]: unknown
16
+ }
17
+
18
+ export interface Logger {
19
+ debug(message: string, fields?: LogFields): void
20
+ info(message: string, fields?: LogFields): void
21
+ warn(message: string, fields?: LogFields): void
22
+ error(message: string, fields?: LogFields): void
23
+ }
24
+
25
+ export interface FunctionContext<TErrors extends ErrorsSchema> {
26
+ readonly requestId: string
27
+ readonly log: Logger
28
+ error<TCode extends keyof TErrors & string>(code: TCode, data: Static<TErrors[TCode]>): BusinessError
29
+ }
30
+
31
+ export interface FunctionConfig {
32
+ execution?: ExecutionMode
33
+ timeout?: number
34
+ maxConcurrency?: number
35
+ }
36
+
37
+ export type FunctionKind = "function" | "streamFunction" | "asyncFunction" | "asyncStreamFunction"
38
+
39
+ interface BaseDefinition<TInput extends TSchema, TOutput extends TSchema, TErrors extends ErrorsSchema> {
40
+ readonly input: TInput
41
+ readonly output: TOutput
42
+ readonly errors: TErrors
43
+ readonly config: FunctionConfig
44
+ }
45
+
46
+ export interface FunctionDefinition<
47
+ TInput extends TSchema = TSchema,
48
+ TOutput extends TSchema = TSchema,
49
+ TErrors extends ErrorsSchema = ErrorsSchema,
50
+ > extends BaseDefinition<TInput, TOutput, TErrors> {
51
+ readonly kind: "function"
52
+ readonly handler: (event: Static<TInput>, ctx: FunctionContext<TErrors>) => Bun.MaybePromise<Static<TOutput>>
53
+ }
54
+
55
+ export interface StreamFunctionDefinition<
56
+ TInput extends TSchema = TSchema,
57
+ TOutput extends TSchema = TSchema,
58
+ TErrors extends ErrorsSchema = ErrorsSchema,
59
+ > extends BaseDefinition<TInput, TOutput, TErrors> {
60
+ readonly kind: "streamFunction"
61
+ readonly handler: (
62
+ event: Static<TInput>,
63
+ ctx: FunctionContext<TErrors>,
64
+ ) => AsyncGenerator<DeepPartial<Static<TOutput>>, void, void>
65
+ }
66
+
67
+ export interface AsyncFunctionDefinition<
68
+ TInput extends TSchema = TSchema,
69
+ TOutput extends TSchema = TSchema,
70
+ TErrors extends ErrorsSchema = ErrorsSchema,
71
+ > extends BaseDefinition<TInput, TOutput, TErrors> {
72
+ readonly kind: "asyncFunction"
73
+ readonly handler: (event: Static<TInput>, ctx: FunctionContext<TErrors>) => Bun.MaybePromise<Static<TOutput>>
74
+ }
75
+
76
+ export interface AsyncStreamFunctionDefinition<
77
+ TInput extends TSchema = TSchema,
78
+ TOutput extends TSchema = TSchema,
79
+ TErrors extends ErrorsSchema = ErrorsSchema,
80
+ > extends BaseDefinition<TInput, TOutput, TErrors> {
81
+ readonly kind: "asyncStreamFunction"
82
+ readonly handler: (
83
+ event: Static<TInput>,
84
+ ctx: FunctionContext<TErrors>,
85
+ ) => AsyncGenerator<DeepPartial<Static<TOutput>>, void, void>
86
+ }
87
+
88
+ export type AnyFunctionDefinition =
89
+ | FunctionDefinition
90
+ | StreamFunctionDefinition
91
+ | AsyncFunctionDefinition
92
+ | AsyncStreamFunctionDefinition
93
+
94
+ export type FunctionRegistry = Record<string, AnyFunctionDefinition>
@@ -0,0 +1,11 @@
1
+ import type { TSchema } from "typebox"
2
+ import { Value } from "typebox/value"
3
+
4
+ export type ValidationResult = { ok: true; value: unknown } | { ok: false; issues: string[] }
5
+
6
+ export function validate(schema: TSchema, value: unknown): ValidationResult {
7
+ const filled = Value.Default(schema, Value.Clone(value))
8
+ if (Value.Check(schema, filled)) return { ok: true, value: filled }
9
+ const issues = [...Value.Errors(schema, filled)].map((e) => `${e.instancePath || "/"}: ${e.message}`)
10
+ return { ok: false, issues }
11
+ }
@@ -0,0 +1,43 @@
1
+ import type { ContextFactory } from "./sdk.executor.ts"
2
+ import { runOneShot, runStream } from "./sdk.run.ts"
3
+ import type { FunctionRegistry } from "./sdk.types.ts"
4
+
5
+ interface CallMessage {
6
+ t: "call"
7
+ id: number
8
+ stream: boolean
9
+ name: string
10
+ event: unknown
11
+ requestId: string
12
+ }
13
+
14
+ export function runWorker(registry: FunctionRegistry, makeContext: ContextFactory): void {
15
+ process.on("message", async (raw: unknown) => {
16
+ const message = raw as CallMessage
17
+ if (message.t !== "call") return
18
+
19
+ const def = registry[message.name]
20
+ if (!def) {
21
+ process.send?.({ t: "result", id: message.id, outcome: { kind: "crash", message: `未注册函数 ${message.name}` } })
22
+ return
23
+ }
24
+
25
+ const ctx = makeContext(message.name, message.requestId)
26
+ if (message.stream) {
27
+ const generator = runStream(def, message.event, ctx)
28
+ while (true) {
29
+ const next = await generator.next()
30
+ if (next.done) {
31
+ process.send?.({ t: "end", id: message.id, outcome: next.value })
32
+ break
33
+ }
34
+ process.send?.({ t: "chunk", id: message.id, data: next.value })
35
+ }
36
+ } else {
37
+ const outcome = await runOneShot(def, message.event, ctx)
38
+ process.send?.({ t: "result", id: message.id, outcome })
39
+ }
40
+ })
41
+
42
+ process.send?.({ t: "ready" })
43
+ }