silgi 0.53.0 → 0.53.1
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/builder.mjs +32 -6
- package/dist/caller.mjs +65 -55
- package/dist/compile.d.mts +15 -8
- package/dist/compile.mjs +157 -142
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +69 -73
- package/dist/core/input.mjs +95 -33
- package/dist/core/schema-converter.d.mts +68 -63
- package/dist/core/schema-converter.mjs +85 -56
- package/dist/core/serve.d.mts +18 -17
- package/dist/core/serve.mjs +154 -64
- package/dist/core/sse.d.mts +5 -6
- package/dist/core/sse.mjs +86 -46
- package/dist/core/task.d.mts +15 -4
- package/dist/core/task.mjs +160 -76
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -128
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.mjs +160 -117
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +126 -87
- package/package.json +1 -1
package/dist/core/serve.mjs
CHANGED
|
@@ -3,19 +3,156 @@ import { createFetchHandler, wrapHandler } from "./handler.mjs";
|
|
|
3
3
|
import { _createWSHooks } from "../ws.mjs";
|
|
4
4
|
import { serve } from "srvx";
|
|
5
5
|
//#region src/core/serve.ts
|
|
6
|
+
/**
|
|
7
|
+
* `serve()` orchestrator
|
|
8
|
+
* ------------------------
|
|
9
|
+
*
|
|
10
|
+
* Builds a Node/Bun/Deno HTTP server from a silgi router. The heavy
|
|
11
|
+
* lifting (the compiled Fetch handler, analytics/Scalar wrappers) is
|
|
12
|
+
* already done elsewhere; this module stitches them together with the
|
|
13
|
+
* runtime-specific WebSocket upgrade plumbing and hands off to `srvx`.
|
|
14
|
+
*
|
|
15
|
+
* The only per-runtime work that lives here is mounting WebSocket hooks
|
|
16
|
+
* for subscriptions:
|
|
17
|
+
*
|
|
18
|
+
* - **Bun** — inject crossws into `serve({ bun: { websocket } })`
|
|
19
|
+
* and intercept upgrade requests at the Fetch layer.
|
|
20
|
+
* - **Deno** — intercept upgrade requests at the Fetch layer and
|
|
21
|
+
* call the crossws Deno adapter directly.
|
|
22
|
+
* - **Node** — after srvx exposes the `http.Server`, attach crossws
|
|
23
|
+
* via `server.on('upgrade', …)`.
|
|
24
|
+
*
|
|
25
|
+
* Everything else is shared: URL resolution, graceful shutdown, hook
|
|
26
|
+
* firing, and the startup banner.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Detect the current JavaScript runtime from well-known globals.
|
|
30
|
+
*
|
|
31
|
+
* Written as a plain function rather than reading a module-global so
|
|
32
|
+
* the check re-evaluates when the module is imported into a different
|
|
33
|
+
* runtime (e.g. a test that spawns a Bun child process).
|
|
34
|
+
*/
|
|
6
35
|
function detectRuntime() {
|
|
7
36
|
if (typeof globalThis.Bun !== "undefined") return "bun";
|
|
8
37
|
if (typeof globalThis.Deno !== "undefined") return "deno";
|
|
9
38
|
return "node";
|
|
10
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Walk the router tree looking for a subscription. We stop at the first
|
|
42
|
+
* hit because we only need to decide whether to wire up WS at all — we
|
|
43
|
+
* do not need an inventory.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the helper in `silgi.ts`; kept here so `core/serve.ts` has no
|
|
46
|
+
* runtime dependency on the top-level instance module.
|
|
47
|
+
*/
|
|
11
48
|
function routerHasSubscription(def) {
|
|
12
49
|
if (!def || typeof def !== "object") return false;
|
|
13
50
|
if (def.type === "subscription") return true;
|
|
14
|
-
for (const
|
|
51
|
+
for (const child of Object.values(def)) if (routerHasSubscription(child)) return true;
|
|
15
52
|
return false;
|
|
16
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Translate our `gracefulShutdown` option into the shape srvx expects.
|
|
56
|
+
*
|
|
57
|
+
* srvx uses `gracefulTimeout`, we expose `timeout` — the rename keeps
|
|
58
|
+
* the public API readable without leaking srvx vocabulary.
|
|
59
|
+
*/
|
|
60
|
+
function resolveShutdown(option) {
|
|
61
|
+
if (option === void 0 || typeof option === "boolean") return option ?? true;
|
|
62
|
+
return {
|
|
63
|
+
gracefulTimeout: option.timeout,
|
|
64
|
+
forceTimeout: option.forceTimeout
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the WS wiring for the current runtime.
|
|
69
|
+
*
|
|
70
|
+
* Everything stays lazy: when no subscriptions exist or WS is
|
|
71
|
+
* explicitly disabled, we return `{ fetch: httpHandler }` and never
|
|
72
|
+
* import the crossws adapters.
|
|
73
|
+
*/
|
|
74
|
+
async function wireWebSocket(routerDef, httpHandler, enabled, wsOpts, runtime, bunServerRef) {
|
|
75
|
+
if (!enabled) return { fetch: httpHandler };
|
|
76
|
+
const hooksObj = _createWSHooks(routerDef, wsOpts);
|
|
77
|
+
if (runtime === "bun") {
|
|
78
|
+
const bunAdapter = (await import("crossws/adapters/bun")).default;
|
|
79
|
+
const adapter = bunAdapter({ hooks: hooksObj });
|
|
80
|
+
return {
|
|
81
|
+
bunWebsocket: adapter.websocket,
|
|
82
|
+
fetch: (async (req) => {
|
|
83
|
+
if (req.headers.get("upgrade") === "websocket" && bunServerRef.current) {
|
|
84
|
+
const res = await adapter.handleUpgrade(req, bunServerRef.current);
|
|
85
|
+
if (res) return res;
|
|
86
|
+
}
|
|
87
|
+
return httpHandler(req);
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (runtime === "deno") {
|
|
92
|
+
const denoAdapter = (await import("crossws/adapters/deno")).default;
|
|
93
|
+
const adapter = denoAdapter({ hooks: hooksObj });
|
|
94
|
+
return { fetch: (async (req) => {
|
|
95
|
+
if (req.headers.get("upgrade") === "websocket") return adapter.handleUpgrade(req, {});
|
|
96
|
+
return httpHandler(req);
|
|
97
|
+
}) };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
fetch: httpHandler,
|
|
101
|
+
attachNode: async (httpServer) => {
|
|
102
|
+
const { attachWebSocket } = await import("../ws.mjs");
|
|
103
|
+
await attachWebSocket(httpServer, routerDef, wsOpts);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute the final server URL from srvx's output.
|
|
109
|
+
*
|
|
110
|
+
* srvx usually populates `server.url` itself, but when the caller
|
|
111
|
+
* requests port `0` (pick-any-free) or when HTTP/2 is on, we have to
|
|
112
|
+
* piece it together from the runtime-specific socket info. Trailing
|
|
113
|
+
* slashes are stripped so `${url}/api/foo` always produces exactly one
|
|
114
|
+
* separator.
|
|
115
|
+
*/
|
|
116
|
+
function resolveUrl(server, requestedPort, hostname, http2) {
|
|
117
|
+
let port = requestedPort;
|
|
118
|
+
if (server.node?.server) {
|
|
119
|
+
const addr = server.node.server.address();
|
|
120
|
+
if (addr && typeof addr === "object") port = addr.port;
|
|
121
|
+
} else if (server.bun?.server) port = server.bun.server.port ?? requestedPort;
|
|
122
|
+
const protocol = http2 ? "https" : "http";
|
|
123
|
+
const raw = server.url || `${protocol}://${hostname}:${port}`;
|
|
124
|
+
return {
|
|
125
|
+
url: raw.endsWith("/") ? raw.slice(0, -1) : raw,
|
|
126
|
+
port
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Print the startup banner.
|
|
131
|
+
*
|
|
132
|
+
* Side-effect-y and intentionally not bypassable — a server starting
|
|
133
|
+
* silently is a surprising default; `silent: true` on srvx suppresses
|
|
134
|
+
* *its* banner, but silgi still wants to show where it bound.
|
|
135
|
+
*/
|
|
136
|
+
function printBanner(url, hostname, port, runtime, options, wsEnabled) {
|
|
137
|
+
console.log(`\nSilgi server running at ${url}`);
|
|
138
|
+
if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
|
|
139
|
+
if (wsEnabled) console.log(` WebSocket RPC at ws://${hostname}:${port}/_ws (${runtime})`);
|
|
140
|
+
if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
|
|
141
|
+
if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
|
|
142
|
+
console.log();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build and start the HTTP (and optionally WebSocket) server.
|
|
146
|
+
*
|
|
147
|
+
* The function is intentionally long because the steps are strictly
|
|
148
|
+
* ordered: WS wiring has to happen before srvx starts (Bun needs its
|
|
149
|
+
* websocket handler at construction time); the `http.Server` only
|
|
150
|
+
* exists once srvx returns (Node attaches WS there); and the banner
|
|
151
|
+
* wants real bound port info. Splitting further would hide the
|
|
152
|
+
* ordering more than it would simplify anything.
|
|
153
|
+
*/
|
|
17
154
|
async function createServeHandler(routerDef, contextFactory, hooks, options, schemaRegistry, bridge) {
|
|
18
|
-
const
|
|
155
|
+
const requestedPort = options?.port ?? 3e3;
|
|
19
156
|
const hostname = options?.hostname ?? "127.0.0.1";
|
|
20
157
|
const prefix = options?.basePath ? normalizePrefix(options.basePath) : void 0;
|
|
21
158
|
const httpHandler = wrapHandler(createFetchHandler(routerDef, contextFactory, hooks, prefix, bridge), routerDef, options ? {
|
|
@@ -26,89 +163,42 @@ async function createServeHandler(routerDef, contextFactory, hooks, options, sch
|
|
|
26
163
|
schemaRegistry,
|
|
27
164
|
hooks
|
|
28
165
|
}, prefix);
|
|
29
|
-
const shutdownOpt = options?.gracefulShutdown ?? true;
|
|
30
|
-
let gracefulShutdown;
|
|
31
|
-
if (typeof shutdownOpt === "object") gracefulShutdown = {
|
|
32
|
-
gracefulTimeout: shutdownOpt.timeout,
|
|
33
|
-
forceTimeout: shutdownOpt.forceTimeout
|
|
34
|
-
};
|
|
35
|
-
else gracefulShutdown = shutdownOpt;
|
|
36
|
-
const wsExplicitlyDisabled = options?.ws === false;
|
|
37
|
-
const wsOpts = typeof options?.ws === "object" ? options.ws : void 0;
|
|
38
|
-
const wsEnabled = !wsExplicitlyDisabled && routerHasSubscription(routerDef);
|
|
39
166
|
const runtime = detectRuntime();
|
|
40
|
-
|
|
41
|
-
|
|
167
|
+
const wsEnabled = !(options?.ws === false) && routerHasSubscription(routerDef);
|
|
168
|
+
const wsOpts = typeof options?.ws === "object" ? options.ws : void 0;
|
|
42
169
|
const bunServerRef = { current: void 0 };
|
|
43
|
-
|
|
44
|
-
if (wsEnabled) {
|
|
45
|
-
const hooksObj = _createWSHooks(routerDef, wsOpts);
|
|
46
|
-
if (runtime === "bun") {
|
|
47
|
-
const bunAdapter = (await import("crossws/adapters/bun")).default;
|
|
48
|
-
const adapter = bunAdapter({ hooks: hooksObj });
|
|
49
|
-
bunWebsocket = adapter.websocket;
|
|
50
|
-
fetchHandler = (async (req) => {
|
|
51
|
-
if (req.headers.get("upgrade") === "websocket" && bunServerRef.current) {
|
|
52
|
-
const res = await adapter.handleUpgrade(req, bunServerRef.current);
|
|
53
|
-
if (res) return res;
|
|
54
|
-
}
|
|
55
|
-
return httpHandler(req);
|
|
56
|
-
});
|
|
57
|
-
} else if (runtime === "deno") {
|
|
58
|
-
const denoAdapter = (await import("crossws/adapters/deno")).default;
|
|
59
|
-
const adapter = denoAdapter({ hooks: hooksObj });
|
|
60
|
-
fetchHandler = (async (req) => {
|
|
61
|
-
if (req.headers.get("upgrade") === "websocket") return adapter.handleUpgrade(req, {});
|
|
62
|
-
return httpHandler(req);
|
|
63
|
-
});
|
|
64
|
-
} else nodeAttach = async (httpServer) => {
|
|
65
|
-
const { attachWebSocket } = await import("../ws.mjs");
|
|
66
|
-
await attachWebSocket(httpServer, routerDef, wsOpts);
|
|
67
|
-
};
|
|
68
|
-
}
|
|
170
|
+
const wiring = await wireWebSocket(routerDef, httpHandler, wsEnabled, wsOpts, runtime, bunServerRef);
|
|
69
171
|
const server = await serve({
|
|
70
|
-
port,
|
|
172
|
+
port: requestedPort,
|
|
71
173
|
hostname,
|
|
72
|
-
fetch:
|
|
73
|
-
gracefulShutdown,
|
|
174
|
+
fetch: wiring.fetch,
|
|
175
|
+
gracefulShutdown: resolveShutdown(options?.gracefulShutdown),
|
|
74
176
|
silent: true,
|
|
75
|
-
...options?.http2
|
|
177
|
+
...options?.http2 ? { tls: {
|
|
76
178
|
cert: options.http2.cert,
|
|
77
179
|
key: options.http2.key
|
|
78
|
-
} },
|
|
79
|
-
...bunWebsocket ? { bun: { websocket: bunWebsocket } } : {}
|
|
180
|
+
} } : {},
|
|
181
|
+
...wiring.bunWebsocket ? { bun: { websocket: wiring.bunWebsocket } } : {}
|
|
80
182
|
});
|
|
81
183
|
await server.ready();
|
|
82
184
|
if (runtime === "bun" && server.bun?.server) bunServerRef.current = server.bun.server;
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const addr = server.node.server.address();
|
|
87
|
-
if (addr && typeof addr === "object") resolvedPort = addr.port;
|
|
88
|
-
} else if (server.bun?.server) resolvedPort = server.bun.server.port ?? port;
|
|
89
|
-
const protocol = options?.http2 ? "https" : "http";
|
|
90
|
-
const rawUrl = server.url || `${protocol}://${hostname}:${resolvedPort}`;
|
|
91
|
-
const url = rawUrl.endsWith("/") ? rawUrl.slice(0, -1) : rawUrl;
|
|
92
|
-
console.log(`\nSilgi server running at ${url}`);
|
|
93
|
-
if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
|
|
94
|
-
if (wsEnabled) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}/_ws (${runtime})`);
|
|
95
|
-
if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
|
|
96
|
-
if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
|
|
97
|
-
console.log();
|
|
185
|
+
if (wiring.attachNode && server.node?.server) await wiring.attachNode(server.node.server);
|
|
186
|
+
const { url, port } = resolveUrl(server, requestedPort, hostname, Boolean(options?.http2));
|
|
187
|
+
printBanner(url, hostname, port, runtime, options, wsEnabled);
|
|
98
188
|
await hooks.callHook("serve:start", {
|
|
99
189
|
url,
|
|
100
|
-
port
|
|
190
|
+
port,
|
|
101
191
|
hostname
|
|
102
192
|
});
|
|
103
193
|
return {
|
|
104
194
|
url,
|
|
105
|
-
port
|
|
195
|
+
port,
|
|
106
196
|
hostname,
|
|
107
197
|
async close(forceCloseConnections = false) {
|
|
108
198
|
await server.close(forceCloseConnections);
|
|
109
199
|
await hooks.callHook("serve:stop", {
|
|
110
200
|
url,
|
|
111
|
-
port
|
|
201
|
+
port,
|
|
112
202
|
hostname
|
|
113
203
|
});
|
|
114
204
|
}
|
package/dist/core/sse.d.mts
CHANGED
|
@@ -4,15 +4,14 @@ interface EventMeta {
|
|
|
4
4
|
retry?: number;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Attach SSE
|
|
7
|
+
* Attach SSE `id` / `retry` metadata to a yielded value.
|
|
8
8
|
*
|
|
9
|
-
* Only
|
|
10
|
-
*
|
|
9
|
+
* Only object-shaped values can carry metadata; primitives cannot be
|
|
10
|
+
* keyed in the `WeakMap` and are returned unchanged. Wrap primitives
|
|
11
|
+
* in a one-field object when you need metadata on them.
|
|
11
12
|
*/
|
|
12
13
|
declare function withEventMeta<T>(value: T, meta: EventMeta): T;
|
|
13
|
-
/**
|
|
14
|
-
* Read SSE metadata from a value (if attached).
|
|
15
|
-
*/
|
|
14
|
+
/** Read SSE metadata previously attached via `withEventMeta`. */
|
|
16
15
|
declare function getEventMeta(value: unknown): EventMeta | undefined;
|
|
17
16
|
//#endregion
|
|
18
17
|
export { EventMeta, getEventMeta, withEventMeta };
|
package/dist/core/sse.mjs
CHANGED
|
@@ -1,36 +1,54 @@
|
|
|
1
1
|
import { SilgiError } from "./error.mjs";
|
|
2
2
|
//#region src/core/sse.ts
|
|
3
3
|
/**
|
|
4
|
-
* Server-Sent Events
|
|
4
|
+
* Server-Sent Events
|
|
5
|
+
* -------------------
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - done: the return value (stream complete)
|
|
7
|
+
* silgi subscriptions are yielded to the client as an SSE event stream.
|
|
8
|
+
* This module holds the encoder, the streaming decoder (for client-side
|
|
9
|
+
* consumption), and the iterator ↔ stream bridges in both directions.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Wire vocabulary
|
|
12
|
+
* ---------------
|
|
13
|
+
*
|
|
14
|
+
* `event: message` → one yielded value
|
|
15
|
+
* `event: error` → resolver threw (sanitized for undefined errors)
|
|
16
|
+
* `event: done` → generator returned; `data` is the return value
|
|
17
|
+
* `: <comment>` → keepalive or boot marker; ignored by clients
|
|
18
|
+
*
|
|
19
|
+
* Event metadata (SSE `id` / `retry`) can be attached to any object
|
|
20
|
+
* value via `withEventMeta()` and round-trips through the decoder.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Metadata store for SSE `id` / `retry` fields.
|
|
24
|
+
*
|
|
25
|
+
* This `WeakMap` is module-scoped (i.e. shared across every subscription
|
|
26
|
+
* in the process). That is safe: entries are keyed by the *value object*
|
|
27
|
+
* the user passes in, and GC reclaims entries as soon as those objects
|
|
28
|
+
* become unreachable. A per-iterator store would add plumbing for
|
|
29
|
+
* nothing — two subscriptions yielding distinct objects never collide.
|
|
13
30
|
*/
|
|
14
|
-
const
|
|
31
|
+
const metaStore = /* @__PURE__ */ new WeakMap();
|
|
15
32
|
/**
|
|
16
|
-
* Attach SSE
|
|
33
|
+
* Attach SSE `id` / `retry` metadata to a yielded value.
|
|
17
34
|
*
|
|
18
|
-
* Only
|
|
19
|
-
*
|
|
35
|
+
* Only object-shaped values can carry metadata; primitives cannot be
|
|
36
|
+
* keyed in the `WeakMap` and are returned unchanged. Wrap primitives
|
|
37
|
+
* in a one-field object when you need metadata on them.
|
|
20
38
|
*/
|
|
21
39
|
function withEventMeta(value, meta) {
|
|
22
|
-
if (typeof value === "object" && value !== null)
|
|
40
|
+
if (typeof value === "object" && value !== null) metaStore.set(value, meta);
|
|
23
41
|
return value;
|
|
24
42
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Read SSE metadata from a value (if attached).
|
|
27
|
-
*/
|
|
43
|
+
/** Read SSE metadata previously attached via `withEventMeta`. */
|
|
28
44
|
function getEventMeta(value) {
|
|
29
45
|
if (typeof value !== "object" || value === null) return void 0;
|
|
30
|
-
return
|
|
46
|
+
return metaStore.get(value);
|
|
31
47
|
}
|
|
32
48
|
/**
|
|
33
|
-
*
|
|
49
|
+
* Serialize an `EventMessage` into SSE wire format (one event terminated
|
|
50
|
+
* by a blank line). Multi-line `data` and `comment` are split across
|
|
51
|
+
* multiple fields per the SSE spec so embedded newlines survive.
|
|
34
52
|
*/
|
|
35
53
|
function encodeEventMessage(msg) {
|
|
36
54
|
const lines = [];
|
|
@@ -42,15 +60,60 @@ function encodeEventMessage(msg) {
|
|
|
42
60
|
return lines.join("\n") + "\n\n";
|
|
43
61
|
}
|
|
44
62
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
63
|
+
* Build an SSE `ReadableStream` that consumes an async iterator.
|
|
64
|
+
*
|
|
65
|
+
* Each yielded value becomes a `message` event; the iterator's return
|
|
66
|
+
* value becomes the `done` event; a thrown error becomes an `error`
|
|
67
|
+
* event (and only the message is exposed when the error is not a
|
|
68
|
+
* `SilgiError` flagged `defined` — undefined errors must not leak
|
|
69
|
+
* internals).
|
|
70
|
+
*
|
|
71
|
+
* A comment-only `keepalive` event is emitted every `keepAliveMs` so
|
|
72
|
+
* intermediaries (proxies, load balancers) do not close the connection
|
|
73
|
+
* while the resolver is quiet.
|
|
48
74
|
*/
|
|
49
75
|
function iteratorToEventStream(iterator, options = {}) {
|
|
50
76
|
const serialize = options.serialize ?? JSON.stringify;
|
|
51
77
|
const keepAliveMs = options.keepAliveMs ?? 3e4;
|
|
52
78
|
let keepAliveTimer;
|
|
53
79
|
let cancelled = false;
|
|
80
|
+
/** Build the wire form of one yielded value, carrying any attached meta. */
|
|
81
|
+
const encodeValue = (value) => {
|
|
82
|
+
const meta = getEventMeta(value);
|
|
83
|
+
return encodeEventMessage({
|
|
84
|
+
event: "message",
|
|
85
|
+
data: serialize(value),
|
|
86
|
+
id: meta?.id,
|
|
87
|
+
retry: meta?.retry
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
/** Build the wire form of the terminal `done` event, if a return value was yielded. */
|
|
91
|
+
const encodeDone = (value) => {
|
|
92
|
+
return encodeEventMessage({
|
|
93
|
+
event: "done",
|
|
94
|
+
data: value !== void 0 ? serialize(value) : void 0
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Build the wire form of an `error` event.
|
|
99
|
+
*
|
|
100
|
+
* Only `SilgiError` with `defined === true` surfaces its `code` and
|
|
101
|
+
* `message` to the wire — the author opted into publishing those by
|
|
102
|
+
* declaring the error. Everything else collapses to a generic 500
|
|
103
|
+
* shape so we do not leak stack traces or internal codes.
|
|
104
|
+
*/
|
|
105
|
+
const encodeError = (err) => {
|
|
106
|
+
return encodeEventMessage({
|
|
107
|
+
event: "error",
|
|
108
|
+
data: err instanceof SilgiError && err.defined ? JSON.stringify({
|
|
109
|
+
message: err.message,
|
|
110
|
+
code: err.code
|
|
111
|
+
}) : JSON.stringify({
|
|
112
|
+
message: "Internal server error",
|
|
113
|
+
code: "INTERNAL_SERVER_ERROR"
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
};
|
|
54
117
|
return new ReadableStream({
|
|
55
118
|
start(controller) {
|
|
56
119
|
if (options.initialComment !== void 0) controller.enqueue(encodeEventMessage({ comment: options.initialComment }));
|
|
@@ -64,38 +127,15 @@ function iteratorToEventStream(iterator, options = {}) {
|
|
|
64
127
|
if (cancelled) return;
|
|
65
128
|
if (result.done) {
|
|
66
129
|
clearInterval(keepAliveTimer);
|
|
67
|
-
|
|
68
|
-
controller.enqueue(encodeEventMessage({
|
|
69
|
-
event: "done",
|
|
70
|
-
data
|
|
71
|
-
}));
|
|
130
|
+
controller.enqueue(encodeDone(result.value));
|
|
72
131
|
controller.close();
|
|
73
132
|
return;
|
|
74
133
|
}
|
|
75
|
-
|
|
76
|
-
const msg = {
|
|
77
|
-
event: "message",
|
|
78
|
-
data: serialize(result.value),
|
|
79
|
-
id: meta?.id,
|
|
80
|
-
retry: meta?.retry
|
|
81
|
-
};
|
|
82
|
-
controller.enqueue(encodeEventMessage(msg));
|
|
134
|
+
controller.enqueue(encodeValue(result.value));
|
|
83
135
|
} catch (error) {
|
|
84
136
|
clearInterval(keepAliveTimer);
|
|
85
137
|
if (cancelled) return;
|
|
86
|
-
|
|
87
|
-
if (error instanceof SilgiError && error.defined) errorData = JSON.stringify({
|
|
88
|
-
message: error.message,
|
|
89
|
-
code: error.code
|
|
90
|
-
});
|
|
91
|
-
else errorData = JSON.stringify({
|
|
92
|
-
message: "Internal server error",
|
|
93
|
-
code: "INTERNAL_SERVER_ERROR"
|
|
94
|
-
});
|
|
95
|
-
controller.enqueue(encodeEventMessage({
|
|
96
|
-
event: "error",
|
|
97
|
-
data: errorData
|
|
98
|
-
}));
|
|
138
|
+
controller.enqueue(encodeError(error));
|
|
99
139
|
controller.close();
|
|
100
140
|
}
|
|
101
141
|
},
|
package/dist/core/task.d.mts
CHANGED
|
@@ -23,7 +23,10 @@ interface TaskDef<TInput = unknown, TOutput = unknown> {
|
|
|
23
23
|
} | null;
|
|
24
24
|
readonly meta: null;
|
|
25
25
|
readonly _contextFactory: (() => unknown | Promise<unknown>) | null;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Dispatch the task. Pass the *parent* request's `ctx` as the second
|
|
28
|
+
* argument to fold the dispatch into that request's trace span.
|
|
29
|
+
*/
|
|
27
30
|
dispatch: undefined extends TInput ? (input?: TInput, ctx?: Record<string, unknown>) => Promise<TOutput> : (input: TInput, ctx?: Record<string, unknown>) => Promise<TOutput>;
|
|
28
31
|
}
|
|
29
32
|
type TaskCompleteCallback = (entry: {
|
|
@@ -39,6 +42,11 @@ type TaskCompleteCallback = (entry: {
|
|
|
39
42
|
}) => void;
|
|
40
43
|
declare function setTaskAnalytics(cb: TaskCompleteCallback | null): void;
|
|
41
44
|
declare function runTask<TInput, TOutput>(task: TaskDef<TInput, TOutput>, ...args: undefined extends TInput ? [input?: TInput] : [input: TInput]): Promise<TOutput>;
|
|
45
|
+
/**
|
|
46
|
+
* Walk a router tree and collect every task that has a `cron` field set.
|
|
47
|
+
* Nested namespaces are recursed into; we stop at any node already
|
|
48
|
+
* tagged as a task so a task's internal structure is never inspected.
|
|
49
|
+
*/
|
|
42
50
|
declare function collectCronTasks(def: Record<string, unknown>): Array<{
|
|
43
51
|
cron: string;
|
|
44
52
|
task: TaskDef<any, any>;
|
|
@@ -61,9 +69,12 @@ interface CronRegistry {
|
|
|
61
69
|
list: () => ScheduledTaskInfo[];
|
|
62
70
|
}
|
|
63
71
|
/**
|
|
64
|
-
* Create an isolated cron registry.
|
|
65
|
-
*
|
|
66
|
-
* `
|
|
72
|
+
* Create an isolated cron registry.
|
|
73
|
+
*
|
|
74
|
+
* Each silgi instance owns one, so `server.close()` on instance A
|
|
75
|
+
* never stops instance B's jobs and `list()` never returns jobs from
|
|
76
|
+
* another instance. The module-default registry below keeps the
|
|
77
|
+
* legacy top-level exports working.
|
|
67
78
|
*/
|
|
68
79
|
declare function createCronRegistry(): CronRegistry;
|
|
69
80
|
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|