@astrale-os/sdk 0.4.12 → 0.4.14
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/dist/cli/run.d.ts +8 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +51 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/define/remote-function.d.ts +3 -1
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js.map +1 -1
- package/dist/dispatch/dispatcher.d.ts +10 -20
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +55 -54
- package/dist/dispatch/dispatcher.js.map +1 -1
- package/dist/dispatch/execute.d.ts +2 -1
- package/dist/dispatch/execute.d.ts.map +1 -1
- package/dist/dispatch/execute.js +1 -0
- package/dist/dispatch/execute.js.map +1 -1
- package/dist/domain/extend-functions.d.ts.map +1 -1
- package/dist/domain/extend-functions.js +2 -1
- package/dist/domain/extend-functions.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/linter/analyze.d.ts +6 -0
- package/dist/linter/analyze.d.ts.map +1 -0
- package/dist/linter/analyze.js +12 -0
- package/dist/linter/analyze.js.map +1 -0
- package/dist/linter/diagnostic.d.ts +32 -0
- package/dist/linter/diagnostic.d.ts.map +1 -0
- package/dist/linter/diagnostic.js +35 -0
- package/dist/linter/diagnostic.js.map +1 -0
- package/dist/linter/index.d.ts +10 -0
- package/dist/linter/index.d.ts.map +1 -0
- package/dist/linter/index.js +6 -0
- package/dist/linter/index.js.map +1 -0
- package/dist/linter/lint.d.ts +8 -0
- package/dist/linter/lint.d.ts.map +1 -0
- package/dist/linter/lint.js +26 -0
- package/dist/linter/lint.js.map +1 -0
- package/dist/linter/oxlint/config.d.ts +14 -0
- package/dist/linter/oxlint/config.d.ts.map +1 -0
- package/dist/linter/oxlint/config.js +18 -0
- package/dist/linter/oxlint/config.js.map +1 -0
- package/dist/linter/oxlint/plugin.d.ts +13 -0
- package/dist/linter/oxlint/plugin.d.ts.map +1 -0
- package/dist/linter/oxlint/plugin.js +14 -0
- package/dist/linter/oxlint/plugin.js.map +1 -0
- package/dist/linter/oxlint/run.d.ts +7 -0
- package/dist/linter/oxlint/run.d.ts.map +1 -0
- package/dist/linter/oxlint/run.js +122 -0
- package/dist/linter/oxlint/run.js.map +1 -0
- package/dist/linter/preflight.d.ts +2 -0
- package/dist/linter/preflight.d.ts.map +1 -0
- package/dist/linter/preflight.js +162 -0
- package/dist/linter/preflight.js.map +1 -0
- package/dist/linter/project.d.ts +18 -0
- package/dist/linter/project.d.ts.map +1 -0
- package/dist/linter/project.js +150 -0
- package/dist/linter/project.js.map +1 -0
- package/dist/linter/report.d.ts +4 -0
- package/dist/linter/report.d.ts.map +1 -0
- package/dist/linter/report.js +22 -0
- package/dist/linter/report.js.map +1 -0
- package/dist/linter/rules/catalog.d.ts +56 -0
- package/dist/linter/rules/catalog.d.ts.map +1 -0
- package/dist/linter/rules/catalog.js +53 -0
- package/dist/linter/rules/catalog.js.map +1 -0
- package/dist/linter/rules/core-has-no-async.d.ts +3 -0
- package/dist/linter/rules/core-has-no-async.d.ts.map +1 -0
- package/dist/linter/rules/core-has-no-async.js +43 -0
- package/dist/linter/rules/core-has-no-async.js.map +1 -0
- package/dist/linter/rules/core-is-pure.d.ts +3 -0
- package/dist/linter/rules/core-is-pure.d.ts.map +1 -0
- package/dist/linter/rules/core-is-pure.js +74 -0
- package/dist/linter/rules/core-is-pure.js.map +1 -0
- package/dist/linter/rules/no-nested-steps.d.ts +3 -0
- package/dist/linter/rules/no-nested-steps.d.ts.map +1 -0
- package/dist/linter/rules/no-nested-steps.js +32 -0
- package/dist/linter/rules/no-nested-steps.js.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts +3 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.js +64 -0
- package/dist/linter/rules/no-read-inside-mutate.js.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts +3 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.js +23 -0
- package/dist/linter/rules/one-remote-definition-per-file.js.map +1 -0
- package/dist/linter/rules/oxlint-ast.d.ts +30 -0
- package/dist/linter/rules/oxlint-ast.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-ast.js +104 -0
- package/dist/linter/rules/oxlint-ast.js.map +1 -0
- package/dist/linter/rules/oxlint-rule.d.ts +45 -0
- package/dist/linter/rules/oxlint-rule.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-rule.js +2 -0
- package/dist/linter/rules/oxlint-rule.js.map +1 -0
- package/dist/linter/rules/project-rule.d.ts +10 -0
- package/dist/linter/rules/project-rule.d.ts.map +1 -0
- package/dist/linter/rules/project-rule.js +24 -0
- package/dist/linter/rules/project-rule.js.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts +3 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.js +23 -0
- package/dist/linter/rules/thin-schema-composition-root.js.map +1 -0
- package/dist/linter/source.d.ts +24 -0
- package/dist/linter/source.d.ts.map +1 -0
- package/dist/linter/source.js +279 -0
- package/dist/linter/source.js.map +1 -0
- package/dist/linter/suppression.d.ts +3 -0
- package/dist/linter/suppression.d.ts.map +1 -0
- package/dist/linter/suppression.js +11 -0
- package/dist/linter/suppression.js.map +1 -0
- package/dist/method/context.d.ts +3 -1
- package/dist/method/context.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.d.ts +5 -1
- package/dist/server/auxiliary-routes.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.js +19 -10
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/create.js +1 -1
- package/dist/server/create.js.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/service-entry.d.ts +32 -0
- package/dist/server/service-entry.d.ts.map +1 -0
- package/dist/server/service-entry.js +150 -0
- package/dist/server/service-entry.js.map +1 -0
- package/dist/server/start.d.ts.map +1 -1
- package/dist/server/start.js.map +1 -1
- package/dist/server/worker-entry.d.ts +7 -3
- package/dist/server/worker-entry.d.ts.map +1 -1
- package/dist/server/worker-entry.js +7 -1
- package/dist/server/worker-entry.js.map +1 -1
- package/dist/service/functions.d.ts +148 -0
- package/dist/service/functions.d.ts.map +1 -0
- package/dist/service/functions.js +115 -0
- package/dist/service/functions.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +2 -0
- package/dist/service/index.js.map +1 -0
- package/package.json +27 -7
- package/src/cli/run.ts +53 -2
- package/src/define/remote-function.ts +3 -1
- package/src/dispatch/dispatcher.ts +62 -69
- package/src/dispatch/execute.ts +3 -1
- package/src/domain/extend-functions.ts +5 -1
- package/src/index.ts +20 -2
- package/src/linter/analyze.ts +17 -0
- package/src/linter/diagnostic.ts +68 -0
- package/src/linter/docs/DIAGNOSTICS.md +47 -0
- package/src/linter/docs/JUDGMENT.md +81 -0
- package/src/linter/docs/PRINCIPLES.md +97 -0
- package/src/linter/docs/README.md +38 -0
- package/src/linter/docs/RULE-AUTHORING.md +144 -0
- package/src/linter/docs/RULES.md +153 -0
- package/src/linter/index.ts +14 -0
- package/src/linter/lint.ts +32 -0
- package/src/linter/oxlint/astrale-ox.d.ts +5 -0
- package/src/linter/oxlint/config.ts +19 -0
- package/src/linter/oxlint/plugin.js +16 -0
- package/src/linter/oxlint/run.ts +162 -0
- package/src/linter/preflight.ts +181 -0
- package/src/linter/project.ts +190 -0
- package/src/linter/report.ts +25 -0
- package/src/linter/rules/catalog.ts +67 -0
- package/src/linter/rules/core-has-no-async.js +43 -0
- package/src/linter/rules/core-is-pure.ts +103 -0
- package/src/linter/rules/no-nested-steps.js +37 -0
- package/src/linter/rules/no-read-inside-mutate.js +72 -0
- package/src/linter/rules/one-remote-definition-per-file.ts +29 -0
- package/src/linter/rules/oxlint-ast.js +110 -0
- package/src/linter/rules/oxlint-rule.ts +35 -0
- package/src/linter/rules/project-rule.ts +37 -0
- package/src/linter/rules/thin-schema-composition-root.ts +26 -0
- package/src/linter/source.ts +323 -0
- package/src/linter/suppression.ts +11 -0
- package/src/method/context.ts +3 -1
- package/src/server/auxiliary-routes.ts +43 -11
- package/src/server/create.ts +1 -1
- package/src/server/index.ts +10 -2
- package/src/server/service-entry.ts +209 -0
- package/src/server/start.ts +2 -1
- package/src/server/worker-entry.ts +18 -5
- package/src/service/functions.ts +164 -0
- package/src/service/index.ts +18 -0
- package/dist/defer/index.d.ts +0 -24
- package/dist/defer/index.d.ts.map +0 -1
- package/dist/defer/index.js +0 -42
- package/dist/defer/index.js.map +0 -1
- package/src/defer/index.ts +0 -64
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
2
|
+
import type { Node } from '@astrale-os/kernel-core'
|
|
3
|
+
import type { CorsConfig } from '@astrale-os/kernel-server'
|
|
4
|
+
|
|
5
|
+
import { K } from '@astrale-os/kernel-core'
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
|
|
8
|
+
import type { RemoteIdentityConfig } from '../auth/identity.js'
|
|
9
|
+
import type { AnyRemoteFunctionDef } from '../define/remote-function.js'
|
|
10
|
+
import type { Fetcher, WorkerEntry } from './worker-entry.js'
|
|
11
|
+
|
|
12
|
+
import { makeFunctionContext } from '../auth/function-context.js'
|
|
13
|
+
import { buildFunctionSchemas, DEFAULT_FUNCTIONS_FOLDER } from '../domain/extend-functions.js'
|
|
14
|
+
import {
|
|
15
|
+
SERVICE_FUNCTIONS_PATH,
|
|
16
|
+
ServiceFunctionDiscoveryRequestSchema,
|
|
17
|
+
signServiceFunctionManifest,
|
|
18
|
+
toServiceFunctionManifestEntries,
|
|
19
|
+
} from '../service/functions.js'
|
|
20
|
+
import { mountAuxiliaryRoutes } from './auxiliary-routes.js'
|
|
21
|
+
import { createAppWorkerEntry } from './worker-entry.js'
|
|
22
|
+
|
|
23
|
+
export interface ServiceWorkerIdentityEnv {
|
|
24
|
+
IDENTITY_ISS?: string
|
|
25
|
+
IDENTITY_SUB?: string
|
|
26
|
+
IDENTITY_PRIVATE_KEY?: string
|
|
27
|
+
ASTRALE_KERNEL_AUDIENCE?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ServiceWorkerEntryConfig<TEnv extends ServiceWorkerIdentityEnv> {
|
|
31
|
+
functions?: Record<string, AnyRemoteFunctionDef>
|
|
32
|
+
functionsFolder?: string
|
|
33
|
+
cors?: CorsConfig
|
|
34
|
+
resolveUrl?: (env: TEnv, requestOrigin: string) => string
|
|
35
|
+
selfBinding?: (env: TEnv) => Fetcher | null | undefined
|
|
36
|
+
routeSubrequest?: (url: URL, env: TEnv) => Fetcher | null | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A Service worker with zero or more first-class kernel Functions.
|
|
40
|
+
*
|
|
41
|
+
* The function map is the only declaration: it drives both HTTP routes and the
|
|
42
|
+
* signed deploy-time manifest. Function identities are read lazily from the
|
|
43
|
+
* graph after Services has reconciled them, while every Function reuses the
|
|
44
|
+
* Service's one private signing key. */
|
|
45
|
+
export function serviceWorkerEntry<TEnv extends ServiceWorkerIdentityEnv>(
|
|
46
|
+
config: ServiceWorkerEntryConfig<TEnv>,
|
|
47
|
+
): WorkerEntry<TEnv> {
|
|
48
|
+
const functions = config.functions ?? {}
|
|
49
|
+
const functionsFolder = config.functionsFolder ?? DEFAULT_FUNCTIONS_FOLDER
|
|
50
|
+
|
|
51
|
+
return createAppWorkerEntry<TEnv>({
|
|
52
|
+
buildApp: (url, env) => {
|
|
53
|
+
const app = new Hono()
|
|
54
|
+
const serviceIdentity = identityFromEnv(env)
|
|
55
|
+
const { schemas, bindings } = buildFunctionSchemas(functions, url, functionsFolder)
|
|
56
|
+
assertLocalBindings(url, bindings)
|
|
57
|
+
const resolveFunctionIdentity = createFunctionIdentityResolver(env, serviceIdentity)
|
|
58
|
+
|
|
59
|
+
app.get('/', (c) => c.json({ ok: true, kind: 'astrale-service' }))
|
|
60
|
+
app.post(SERVICE_FUNCTIONS_PATH, async (c) => {
|
|
61
|
+
const parsed = ServiceFunctionDiscoveryRequestSchema.safeParse(
|
|
62
|
+
await c.req.json().catch(() => null),
|
|
63
|
+
)
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
return c.json({ error: 'Invalid service function discovery request' }, 400)
|
|
66
|
+
}
|
|
67
|
+
const response = await signServiceFunctionManifest(
|
|
68
|
+
{
|
|
69
|
+
version: 1,
|
|
70
|
+
service: {
|
|
71
|
+
issuer: serviceIdentity.issuer,
|
|
72
|
+
subject: serviceIdentity.subject,
|
|
73
|
+
},
|
|
74
|
+
functions: toServiceFunctionManifestEntries(schemas),
|
|
75
|
+
},
|
|
76
|
+
parsed.data,
|
|
77
|
+
serviceIdentity,
|
|
78
|
+
)
|
|
79
|
+
return c.json(response)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
mountAuxiliaryRoutes({
|
|
83
|
+
app,
|
|
84
|
+
url,
|
|
85
|
+
remoteFunctions: functions,
|
|
86
|
+
remoteFunctionBindings: bindings,
|
|
87
|
+
deps: env,
|
|
88
|
+
resolveIdentity: (kind, slug) => {
|
|
89
|
+
if (kind !== 'remoteFunction') {
|
|
90
|
+
throw new Error(`serviceWorkerEntry cannot resolve ${kind} identity ${slug}`)
|
|
91
|
+
}
|
|
92
|
+
return resolveFunctionIdentity(slug)
|
|
93
|
+
},
|
|
94
|
+
cors: config.cors ?? { origin: '*' },
|
|
95
|
+
})
|
|
96
|
+
return app
|
|
97
|
+
},
|
|
98
|
+
resolveUrl: config.resolveUrl ?? ((_env, requestOrigin) => requestOrigin),
|
|
99
|
+
selfBinding: config.selfBinding,
|
|
100
|
+
routeSubrequest: config.routeSubrequest,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function identityFromEnv<TEnv extends ServiceWorkerIdentityEnv>(env: TEnv): RemoteIdentityConfig {
|
|
105
|
+
return {
|
|
106
|
+
issuer: required(env, 'IDENTITY_ISS'),
|
|
107
|
+
subject: required(env, 'IDENTITY_SUB'),
|
|
108
|
+
privateKey: parsePrivateKey(required(env, 'IDENTITY_PRIVATE_KEY')),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createFunctionIdentityResolver<TEnv extends ServiceWorkerIdentityEnv>(
|
|
113
|
+
env: TEnv,
|
|
114
|
+
serviceIdentity: RemoteIdentityConfig,
|
|
115
|
+
): (slug: string) => Promise<RemoteIdentityConfig> {
|
|
116
|
+
const kernelUrl = required(env, 'ASTRALE_KERNEL_AUDIENCE')
|
|
117
|
+
let cachedAt = 0
|
|
118
|
+
let identities = new Map<string, { issuer: string; subject: string }>()
|
|
119
|
+
const ttlMs = 60_000
|
|
120
|
+
|
|
121
|
+
return async (slug) => {
|
|
122
|
+
if (Date.now() - cachedAt >= ttlMs || !identities.has(slug)) {
|
|
123
|
+
const kernel = await makeFunctionContext(serviceIdentity, env, {
|
|
124
|
+
ref: 'service',
|
|
125
|
+
defaultKernelUrl: kernelUrl,
|
|
126
|
+
}).kernel()
|
|
127
|
+
const service = await kernel.get(`@${serviceIdentity.subject}`)
|
|
128
|
+
if (!service) throw new Error(`Service @${serviceIdentity.subject} is not readable`)
|
|
129
|
+
const folder = await kernel.get(`${service.path.raw}/functions`)
|
|
130
|
+
const nodes = folder
|
|
131
|
+
? await (
|
|
132
|
+
await kernel.children(folder.path, {
|
|
133
|
+
classes: [K.$.c('Function').path.class],
|
|
134
|
+
limit: 500,
|
|
135
|
+
})
|
|
136
|
+
).all()
|
|
137
|
+
: []
|
|
138
|
+
identities = readFunctionIdentities(nodes, serviceIdentity.issuer)
|
|
139
|
+
cachedAt = Date.now()
|
|
140
|
+
}
|
|
141
|
+
const identity = identities.get(slug)
|
|
142
|
+
if (!identity) throw new Error(`Function identity function.${slug} is not registered`)
|
|
143
|
+
return hostedFunctionIdentity(identity, serviceIdentity)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Reuse only the Service's signing key. A hosted Function's inbound audience
|
|
148
|
+
* defaults to its own issuer; the Service HTTP origin is merely transport. */
|
|
149
|
+
export function hostedFunctionIdentity(
|
|
150
|
+
identity: { issuer: string; subject: string },
|
|
151
|
+
serviceIdentity: RemoteIdentityConfig,
|
|
152
|
+
): RemoteIdentityConfig {
|
|
153
|
+
return { ...identity, privateKey: serviceIdentity.privateKey }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readFunctionIdentities(
|
|
157
|
+
nodes: readonly Node[],
|
|
158
|
+
serviceIssuer: string,
|
|
159
|
+
): Map<string, { issuer: string; subject: string }> {
|
|
160
|
+
const result = new Map<string, { issuer: string; subject: string }>()
|
|
161
|
+
for (const node of nodes) {
|
|
162
|
+
const ref = node.props[K.$.i('Function').ref.key]
|
|
163
|
+
const issuer = node.props[K.Identity.iss.key]
|
|
164
|
+
const subject = node.props[K.Identity.sub.key]
|
|
165
|
+
if (
|
|
166
|
+
typeof ref !== 'string' ||
|
|
167
|
+
!ref.startsWith('function.') ||
|
|
168
|
+
typeof issuer !== 'string' ||
|
|
169
|
+
typeof subject !== 'string'
|
|
170
|
+
) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (issuer !== serviceIssuer) {
|
|
174
|
+
throw new Error(`Function ${ref} does not share its hosting Service issuer`)
|
|
175
|
+
}
|
|
176
|
+
result.set(ref.slice('function.'.length), { issuer, subject })
|
|
177
|
+
}
|
|
178
|
+
return result
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertLocalBindings(url: string, bindings: Record<string, FunctionBinding>): void {
|
|
182
|
+
const origin = new URL(url).origin
|
|
183
|
+
for (const [slug, binding] of Object.entries(bindings)) {
|
|
184
|
+
if (!binding.remoteUrl || new URL(binding.remoteUrl).origin !== origin) {
|
|
185
|
+
throw new Error(`serviceWorkerEntry function.${slug} must be hosted by this Service origin`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function required<TEnv extends ServiceWorkerIdentityEnv>(env: TEnv, key: keyof TEnv): string {
|
|
191
|
+
const value = env[key]
|
|
192
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
193
|
+
throw new Error(`Missing service variable ${String(key)}`)
|
|
194
|
+
}
|
|
195
|
+
return value
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parsePrivateKey(value: string): JsonWebKey {
|
|
199
|
+
let parsed: unknown
|
|
200
|
+
try {
|
|
201
|
+
parsed = JSON.parse(value)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error('IDENTITY_PRIVATE_KEY is not valid JSON', { cause: error })
|
|
204
|
+
}
|
|
205
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
206
|
+
throw new Error('IDENTITY_PRIVATE_KEY is not a JWK object')
|
|
207
|
+
}
|
|
208
|
+
return parsed as JsonWebKey
|
|
209
|
+
}
|
package/src/server/start.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Cloudflare, consumers go through `server.app.fetch` directly.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { serve as nodeServe } from '@hono/node-server'
|
|
9
10
|
import type { Hono } from 'hono'
|
|
10
11
|
|
|
11
12
|
import type { RemoteServerHandle } from './handle.js'
|
|
@@ -15,7 +16,7 @@ export async function startNodeServer(app: Hono, port = 3000): Promise<RemoteSer
|
|
|
15
16
|
// bundles (wrangler/esbuild) traverse it even though they never call it. A
|
|
16
17
|
// literal import() would make them resolve the optional peer and fail.
|
|
17
18
|
const nodeServerModule = '@hono/node-server'
|
|
18
|
-
const { serve } = (await import(nodeServerModule)) as typeof
|
|
19
|
+
const { serve } = (await import(nodeServerModule)) as { serve: typeof nodeServe }
|
|
19
20
|
// oxlint-disable-next-line no-explicit-any
|
|
20
21
|
const server = serve({ fetch: app.fetch, port }) as any
|
|
21
22
|
|
|
@@ -30,8 +30,8 @@ import { createRemoteServer } from './create.js'
|
|
|
30
30
|
import { requireEnv } from './require-env.js'
|
|
31
31
|
import { canonicalizeServingUrl } from './serving-url.js'
|
|
32
32
|
|
|
33
|
-
type Fetcher = { fetch(request: Request): Response | Promise<Response> }
|
|
34
|
-
type
|
|
33
|
+
export type Fetcher = { fetch(request: Request): Response | Promise<Response> }
|
|
34
|
+
export type WorkerApp = {
|
|
35
35
|
fetch(
|
|
36
36
|
request: Request,
|
|
37
37
|
env?: unknown,
|
|
@@ -41,7 +41,7 @@ type App = {
|
|
|
41
41
|
|
|
42
42
|
/** A built app cached by its serving URL: `origin` for same-origin matching,
|
|
43
43
|
* `app` for in-process self-dispatch. */
|
|
44
|
-
type CachedApp = { origin: string; app:
|
|
44
|
+
type CachedApp = { origin: string; app: WorkerApp }
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Choose where an OUTBOUND subrequest to `u` is routed — or `null` to let the
|
|
@@ -137,6 +137,10 @@ export interface WorkerEntryConfig<TDeps> {
|
|
|
137
137
|
rewriteRequest?: (env: TDeps, request: Request) => Request
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
export interface AppWorkerEntryConfig<TDeps> extends Omit<WorkerEntryConfig<TDeps>, 'build'> {
|
|
141
|
+
buildApp: (url: string, env: TDeps) => WorkerApp
|
|
142
|
+
}
|
|
143
|
+
|
|
140
144
|
export interface WorkerEntry<TDeps> {
|
|
141
145
|
fetch(request: Request, env: TDeps, executionCtx?: ExecutionContext): Response | Promise<Response>
|
|
142
146
|
}
|
|
@@ -201,6 +205,15 @@ export function assets<TDeps>(opts: {
|
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): WorkerEntry<TDeps> {
|
|
208
|
+
return createAppWorkerEntry({
|
|
209
|
+
...config,
|
|
210
|
+
buildApp: (url, env) => createRemoteServer<TDeps>(config.build(url, env)).app,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createAppWorkerEntry<TDeps>(
|
|
215
|
+
config: AppWorkerEntryConfig<TDeps>,
|
|
216
|
+
): WorkerEntry<TDeps> {
|
|
204
217
|
// Cache the built app per distinct resolved URL — plural and bounded. On the
|
|
205
218
|
// request-origin fallback the URL legitimately alternates for one worker
|
|
206
219
|
// (direct http hits vs https-upgraded tunnel hits, workers.dev + custom
|
|
@@ -212,10 +225,10 @@ export function createWorkerEntry<TDeps>(config: WorkerEntryConfig<TDeps>): Work
|
|
|
212
225
|
let self: Fetcher | null = null
|
|
213
226
|
let routeEnv: TDeps | null = null
|
|
214
227
|
|
|
215
|
-
function getApp(url: string, env: TDeps):
|
|
228
|
+
function getApp(url: string, env: TDeps): WorkerApp {
|
|
216
229
|
const cached = apps.get(url)
|
|
217
230
|
if (cached) return cached.app
|
|
218
|
-
const
|
|
231
|
+
const app = config.buildApp(url, env)
|
|
219
232
|
if (apps.size >= MAX_CACHED_APPS) {
|
|
220
233
|
const oldest = apps.keys().next().value
|
|
221
234
|
if (oldest !== undefined) apps.delete(oldest)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
2
|
+
import type { FunctionSchema } from '@astrale-os/kernel-core/domain'
|
|
3
|
+
|
|
4
|
+
import { deriveAllowedAlgorithms } from '@astrale-os/kernel-core'
|
|
5
|
+
import { functionBindingSchema, functionOutputSchema } from '@astrale-os/kernel-core/domain'
|
|
6
|
+
import { importJWK, jwtVerify } from 'jose'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
import type { RemoteIdentityConfig } from '../auth/identity.js'
|
|
10
|
+
|
|
11
|
+
import { signCredential } from '../auth/sign.js'
|
|
12
|
+
|
|
13
|
+
export const SERVICE_FUNCTIONS_PATH = '/_astrale/functions'
|
|
14
|
+
export const SERVICE_FUNCTIONS_PURPOSE = 'astrale.service-functions.v1'
|
|
15
|
+
|
|
16
|
+
const JsonSchema = z.record(z.string(), z.unknown())
|
|
17
|
+
|
|
18
|
+
export const ServiceFunctionManifestEntrySchema = z.object({
|
|
19
|
+
slug: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
20
|
+
ref: z.string().regex(/^function\.[a-z][a-z0-9-]*$/),
|
|
21
|
+
inputSchema: JsonSchema,
|
|
22
|
+
outputSchema: JsonSchema,
|
|
23
|
+
output: functionOutputSchema.default('value'),
|
|
24
|
+
binding: functionBindingSchema.unwrap(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const ServiceFunctionManifestSchema = z.object({
|
|
28
|
+
version: z.literal(1),
|
|
29
|
+
service: z.object({ issuer: z.string().min(1), subject: z.string().min(1) }),
|
|
30
|
+
functions: z.array(ServiceFunctionManifestEntrySchema).max(500),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const ServiceFunctionDiscoveryRequestSchema = z.object({
|
|
34
|
+
kernelIssuer: z.string().url(),
|
|
35
|
+
nonce: z.string().min(16).max(256),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const ServiceFunctionDiscoveryResponseSchema = ServiceFunctionManifestSchema.extend({
|
|
39
|
+
identity: z.object({ credential: z.string().min(1) }),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export type ServiceFunctionManifestEntry = z.infer<typeof ServiceFunctionManifestEntrySchema>
|
|
43
|
+
export type ServiceFunctionManifest = z.infer<typeof ServiceFunctionManifestSchema>
|
|
44
|
+
export type ServiceFunctionDiscoveryRequest = z.infer<typeof ServiceFunctionDiscoveryRequestSchema>
|
|
45
|
+
export type ServiceFunctionDiscoveryResponse = z.infer<
|
|
46
|
+
typeof ServiceFunctionDiscoveryResponseSchema
|
|
47
|
+
>
|
|
48
|
+
|
|
49
|
+
export function toServiceFunctionManifestEntries(
|
|
50
|
+
schemas: readonly FunctionSchema[],
|
|
51
|
+
): ServiceFunctionManifestEntry[] {
|
|
52
|
+
return schemas
|
|
53
|
+
.map((schema) => {
|
|
54
|
+
const slug = schema.ref.slice('function.'.length)
|
|
55
|
+
return ServiceFunctionManifestEntrySchema.parse({
|
|
56
|
+
slug,
|
|
57
|
+
ref: schema.ref,
|
|
58
|
+
inputSchema: schema.inputSchema,
|
|
59
|
+
outputSchema: schema.outputSchema,
|
|
60
|
+
output: schema.output ?? 'value',
|
|
61
|
+
binding: schema.binding,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function signServiceFunctionManifest(
|
|
68
|
+
manifest: ServiceFunctionManifest,
|
|
69
|
+
request: ServiceFunctionDiscoveryRequest,
|
|
70
|
+
identity: RemoteIdentityConfig,
|
|
71
|
+
): Promise<ServiceFunctionDiscoveryResponse> {
|
|
72
|
+
const parsedManifest = ServiceFunctionManifestSchema.parse(manifest)
|
|
73
|
+
const manifestHash = await hashServiceFunctionManifest(parsedManifest)
|
|
74
|
+
const credential = await signCredential(
|
|
75
|
+
{ purpose: SERVICE_FUNCTIONS_PURPOSE, nonce: request.nonce, manifestHash },
|
|
76
|
+
{
|
|
77
|
+
issuer: identity.issuer,
|
|
78
|
+
subject: identity.subject,
|
|
79
|
+
audience: request.kernelIssuer,
|
|
80
|
+
privateKey: identity.privateKey,
|
|
81
|
+
ttl: '2m',
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
return { ...parsedManifest, identity: { credential } }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function verifyServiceFunctionManifest(
|
|
88
|
+
input: unknown,
|
|
89
|
+
expected: {
|
|
90
|
+
kernelIssuer: string
|
|
91
|
+
nonce: string
|
|
92
|
+
serviceIssuer: string
|
|
93
|
+
serviceSubject: string
|
|
94
|
+
publicKey: JsonWebKey
|
|
95
|
+
},
|
|
96
|
+
): Promise<ServiceFunctionDiscoveryResponse> {
|
|
97
|
+
const response = ServiceFunctionDiscoveryResponseSchema.parse(input)
|
|
98
|
+
if (
|
|
99
|
+
response.service.issuer !== expected.serviceIssuer ||
|
|
100
|
+
response.service.subject !== expected.serviceSubject
|
|
101
|
+
) {
|
|
102
|
+
throw new Error('service function manifest identity does not match the deployed Service')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const algorithms = deriveAllowedAlgorithms(expected.publicKey)
|
|
106
|
+
const algorithm = algorithms[0]
|
|
107
|
+
if (!algorithm) throw new Error('service function manifest public key has no supported algorithm')
|
|
108
|
+
const key = await importJWK(expected.publicKey, algorithm)
|
|
109
|
+
const verified = await jwtVerify(response.identity.credential, key, {
|
|
110
|
+
algorithms,
|
|
111
|
+
issuer: expected.serviceIssuer,
|
|
112
|
+
subject: expected.serviceSubject,
|
|
113
|
+
audience: expected.kernelIssuer,
|
|
114
|
+
})
|
|
115
|
+
const expectedHash = await hashServiceFunctionManifest({
|
|
116
|
+
version: response.version,
|
|
117
|
+
service: response.service,
|
|
118
|
+
functions: response.functions,
|
|
119
|
+
})
|
|
120
|
+
if (verified.payload.purpose !== SERVICE_FUNCTIONS_PURPOSE) {
|
|
121
|
+
throw new Error('service function manifest credential has the wrong purpose')
|
|
122
|
+
}
|
|
123
|
+
if (verified.payload.nonce !== expected.nonce) {
|
|
124
|
+
throw new Error('service function manifest credential has the wrong nonce')
|
|
125
|
+
}
|
|
126
|
+
if (verified.payload.manifestHash !== expectedHash) {
|
|
127
|
+
throw new Error('service function manifest credential does not cover the returned manifest')
|
|
128
|
+
}
|
|
129
|
+
return response
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function hashServiceFunctionManifest(
|
|
133
|
+
manifest: ServiceFunctionManifest,
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const bytes = new TextEncoder().encode(
|
|
136
|
+
canonicalJson(ServiceFunctionManifestSchema.parse(manifest)),
|
|
137
|
+
)
|
|
138
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
|
139
|
+
return `sha256:${hex(new Uint8Array(digest))}`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function functionBindingForManifest(
|
|
143
|
+
binding: FunctionBinding,
|
|
144
|
+
auth: 'required' | 'optional' | 'public' | undefined,
|
|
145
|
+
): FunctionBinding {
|
|
146
|
+
return { ...binding, auth: auth ?? 'required' }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonicalJson(value: unknown): string {
|
|
150
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value)
|
|
151
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
152
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
153
|
+
.filter(([, item]) => item !== undefined)
|
|
154
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
155
|
+
return `{${entries
|
|
156
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
|
|
157
|
+
.join(',')}}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hex(bytes: Uint8Array): string {
|
|
161
|
+
let value = ''
|
|
162
|
+
for (const byte of bytes) value += byte.toString(16).padStart(2, '0')
|
|
163
|
+
return value
|
|
164
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SERVICE_FUNCTIONS_PATH,
|
|
3
|
+
SERVICE_FUNCTIONS_PURPOSE,
|
|
4
|
+
ServiceFunctionDiscoveryRequestSchema,
|
|
5
|
+
ServiceFunctionDiscoveryResponseSchema,
|
|
6
|
+
ServiceFunctionManifestEntrySchema,
|
|
7
|
+
ServiceFunctionManifestSchema,
|
|
8
|
+
hashServiceFunctionManifest,
|
|
9
|
+
signServiceFunctionManifest,
|
|
10
|
+
toServiceFunctionManifestEntries,
|
|
11
|
+
verifyServiceFunctionManifest,
|
|
12
|
+
} from './functions.js'
|
|
13
|
+
export type {
|
|
14
|
+
ServiceFunctionDiscoveryRequest,
|
|
15
|
+
ServiceFunctionDiscoveryResponse,
|
|
16
|
+
ServiceFunctionManifest,
|
|
17
|
+
ServiceFunctionManifestEntry,
|
|
18
|
+
} from './functions.js'
|
package/dist/defer/index.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Context } from 'hono';
|
|
2
|
-
/** Internal structural host capability; not part of the author-facing SDK contract. */
|
|
3
|
-
export type HandlerExecutionLifetime = {
|
|
4
|
-
waitUntil(promise: Promise<unknown>): void;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* Work detached from the current handler result.
|
|
8
|
-
*
|
|
9
|
-
* The callback is deliberately lazy: the SDK registers the resulting task with
|
|
10
|
-
* the active execution backend before it starts. Current request-based hosts
|
|
11
|
-
* extend their request lifetime; future durable backends may provide stronger
|
|
12
|
-
* execution guarantees without changing the author-facing API.
|
|
13
|
-
*/
|
|
14
|
-
export type DeferredTask = () => void | PromiseLike<unknown>;
|
|
15
|
-
export type Defer = (task: DeferredTask) => void;
|
|
16
|
-
export type HandlerLifecycle = {
|
|
17
|
-
defer: Defer;
|
|
18
|
-
settle(): Promise<void>;
|
|
19
|
-
};
|
|
20
|
-
/** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
|
|
21
|
-
export declare function createHandlerLifecycle(lifetime?: HandlerExecutionLifetime): HandlerLifecycle;
|
|
22
|
-
/** Hono omits its execution context in Node and in lightweight unit tests. */
|
|
23
|
-
export declare function executionLifetime(c: Context): HandlerExecutionLifetime | undefined;
|
|
24
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/defer/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;AAE5D,MAAM,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;AAEhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACxB,CAAA;AAED,wFAAwF;AACxF,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,wBAAwB,GAAG,gBAAgB,CA4B5F;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,wBAAwB,GAAG,SAAS,CAOlF"}
|
package/dist/defer/index.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
|
|
2
|
-
export function createHandlerLifecycle(lifetime) {
|
|
3
|
-
const pending = [];
|
|
4
|
-
return {
|
|
5
|
-
defer(task) {
|
|
6
|
-
const promise = Promise.resolve().then(task);
|
|
7
|
-
if (lifetime)
|
|
8
|
-
lifetime.waitUntil(promise);
|
|
9
|
-
else
|
|
10
|
-
pending.push(promise);
|
|
11
|
-
},
|
|
12
|
-
async settle() {
|
|
13
|
-
if (lifetime || pending.length === 0)
|
|
14
|
-
return;
|
|
15
|
-
// A deferred task may register more work before it settles. Drain until
|
|
16
|
-
// the lifecycle is stable so non-request runtimes never abandon children.
|
|
17
|
-
let cursor = 0;
|
|
18
|
-
while (cursor < pending.length) {
|
|
19
|
-
const batch = pending.slice(cursor);
|
|
20
|
-
cursor = pending.length;
|
|
21
|
-
const results = await Promise.allSettled(batch);
|
|
22
|
-
for (const result of results) {
|
|
23
|
-
if (result.status === 'rejected') {
|
|
24
|
-
// oxlint-disable-next-line no-console
|
|
25
|
-
console.error('[sdk] deferred task failed:', result.reason);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/** Hono omits its execution context in Node and in lightweight unit tests. */
|
|
33
|
-
export function executionLifetime(c) {
|
|
34
|
-
try {
|
|
35
|
-
const host = c.executionCtx;
|
|
36
|
-
return { waitUntil: (promise) => host.waitUntil(promise) };
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
//# sourceMappingURL=index.js.map
|
package/dist/defer/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/defer/index.ts"],"names":[],"mappings":"AAwBA,wFAAwF;AACxF,MAAM,UAAU,sBAAsB,CAAC,QAAmC;IACxE,MAAM,OAAO,GAAuB,EAAE,CAAA;IAEtC,OAAO;QACL,KAAK,CAAC,IAAI;YACR,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC5C,IAAI,QAAQ;gBAAE,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;;gBACpC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC5B,CAAC;QACD,KAAK,CAAC,MAAM;YACV,IAAI,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YAE5C,wEAAwE;YACxE,0EAA0E;YAC1E,IAAI,MAAM,GAAG,CAAC,CAAA;YACd,OAAO,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;gBAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;gBACnC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;gBACvB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;gBAC/C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;oBAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;wBACjC,sCAAsC;wBACtC,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;oBAC7D,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,CAAU;IAC1C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,CAAA;QAC3B,OAAO,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAA;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
|
package/src/defer/index.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { Context } from 'hono'
|
|
2
|
-
|
|
3
|
-
/** Internal structural host capability; not part of the author-facing SDK contract. */
|
|
4
|
-
export type HandlerExecutionLifetime = {
|
|
5
|
-
waitUntil(promise: Promise<unknown>): void
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Work detached from the current handler result.
|
|
10
|
-
*
|
|
11
|
-
* The callback is deliberately lazy: the SDK registers the resulting task with
|
|
12
|
-
* the active execution backend before it starts. Current request-based hosts
|
|
13
|
-
* extend their request lifetime; future durable backends may provide stronger
|
|
14
|
-
* execution guarantees without changing the author-facing API.
|
|
15
|
-
*/
|
|
16
|
-
export type DeferredTask = () => void | PromiseLike<unknown>
|
|
17
|
-
|
|
18
|
-
export type Defer = (task: DeferredTask) => void
|
|
19
|
-
|
|
20
|
-
export type HandlerLifecycle = {
|
|
21
|
-
defer: Defer
|
|
22
|
-
settle(): Promise<void>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Bind detached work to the host, with a safe blocking fallback for other runtimes. */
|
|
26
|
-
export function createHandlerLifecycle(lifetime?: HandlerExecutionLifetime): HandlerLifecycle {
|
|
27
|
-
const pending: Promise<unknown>[] = []
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
defer(task) {
|
|
31
|
-
const promise = Promise.resolve().then(task)
|
|
32
|
-
if (lifetime) lifetime.waitUntil(promise)
|
|
33
|
-
else pending.push(promise)
|
|
34
|
-
},
|
|
35
|
-
async settle() {
|
|
36
|
-
if (lifetime || pending.length === 0) return
|
|
37
|
-
|
|
38
|
-
// A deferred task may register more work before it settles. Drain until
|
|
39
|
-
// the lifecycle is stable so non-request runtimes never abandon children.
|
|
40
|
-
let cursor = 0
|
|
41
|
-
while (cursor < pending.length) {
|
|
42
|
-
const batch = pending.slice(cursor)
|
|
43
|
-
cursor = pending.length
|
|
44
|
-
const results = await Promise.allSettled(batch)
|
|
45
|
-
for (const result of results) {
|
|
46
|
-
if (result.status === 'rejected') {
|
|
47
|
-
// oxlint-disable-next-line no-console
|
|
48
|
-
console.error('[sdk] deferred task failed:', result.reason)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Hono omits its execution context in Node and in lightweight unit tests. */
|
|
57
|
-
export function executionLifetime(c: Context): HandlerExecutionLifetime | undefined {
|
|
58
|
-
try {
|
|
59
|
-
const host = c.executionCtx
|
|
60
|
-
return { waitUntil: (promise) => host.waitUntil(promise) }
|
|
61
|
-
} catch {
|
|
62
|
-
return undefined
|
|
63
|
-
}
|
|
64
|
-
}
|