silgi 0.52.2 → 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/adapters/aws-lambda.mjs +1 -1
- package/dist/broker/redis.mjs +1 -2
- package/dist/builder.mjs +35 -7
- package/dist/caller.mjs +67 -50
- package/dist/client/plugins/retry.mjs +9 -4
- package/dist/compile.d.mts +17 -10
- package/dist/compile.mjs +161 -144
- package/dist/core/ctx-symbols.mjs +18 -1
- package/dist/core/handler.d.mts +3 -3
- package/dist/core/handler.mjs +94 -83
- package/dist/core/input.mjs +116 -37
- 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 +36 -8
- package/dist/core/task.mjs +210 -90
- package/dist/core/url.mjs +19 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/integrations/better-auth/index.mjs +11 -2
- package/dist/lazy.mjs +3 -0
- package/dist/map-input.d.mts +8 -6
- package/dist/map-input.mjs +8 -6
- package/dist/plugins/analytics/routes.mjs +25 -13
- package/dist/plugins/cache.d.mts +62 -126
- package/dist/plugins/cache.mjs +146 -134
- package/dist/plugins/coerce.d.mts +3 -2
- package/dist/plugins/coerce.mjs +25 -8
- package/dist/scalar.d.mts +24 -13
- package/dist/scalar.mjs +292 -201
- package/dist/silgi.d.mts +35 -0
- package/dist/silgi.mjs +177 -103
- package/dist/ws.d.mts +26 -27
- package/dist/ws.mjs +128 -89
- package/package.json +2 -4
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,14 +42,15 @@ 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>;
|
|
45
53
|
}>;
|
|
46
|
-
declare function startCronJobs(cronTasks: Array<{
|
|
47
|
-
cron: string;
|
|
48
|
-
task: TaskDef<any, any>;
|
|
49
|
-
}>): Promise<void>;
|
|
50
54
|
interface ScheduledTaskInfo {
|
|
51
55
|
name: string;
|
|
52
56
|
cron: string;
|
|
@@ -56,7 +60,31 @@ interface ScheduledTaskInfo {
|
|
|
56
60
|
runs: number;
|
|
57
61
|
errors: number;
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
interface CronRegistry {
|
|
64
|
+
start: (cronTasks: Array<{
|
|
65
|
+
cron: string;
|
|
66
|
+
task: TaskDef<any, any>;
|
|
67
|
+
}>) => Promise<void>;
|
|
68
|
+
stop: () => void;
|
|
69
|
+
list: () => ScheduledTaskInfo[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
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.
|
|
78
|
+
*/
|
|
79
|
+
declare function createCronRegistry(): CronRegistry;
|
|
80
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
81
|
+
declare const startCronJobs: (cronTasks: Array<{
|
|
82
|
+
cron: string;
|
|
83
|
+
task: TaskDef<any, any>;
|
|
84
|
+
}>) => Promise<void>;
|
|
85
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
86
|
+
declare const stopCronJobs: () => void;
|
|
87
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
88
|
+
declare const getScheduledTasks: () => ScheduledTaskInfo[];
|
|
61
89
|
//#endregion
|
|
62
|
-
export { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
|
90
|
+
export { CronRegistry, ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
package/dist/core/task.mjs
CHANGED
|
@@ -1,19 +1,104 @@
|
|
|
1
1
|
import { validateSchema } from "./schema.mjs";
|
|
2
2
|
//#region src/core/task.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Background tasks
|
|
5
|
+
* ------------------
|
|
6
|
+
*
|
|
7
|
+
* A *task* is a procedure with two extra capabilities:
|
|
8
|
+
*
|
|
9
|
+
* 1. Programmatic `dispatch(input)` — call it directly, outside the
|
|
10
|
+
* HTTP pipeline. Used for async work (send-email, rebuild-index)
|
|
11
|
+
* and as the callback target for cron schedules.
|
|
12
|
+
* 2. Optional `cron` spec — when set, the task is auto-registered
|
|
13
|
+
* with a `croner` job at `serve()` time.
|
|
14
|
+
*
|
|
15
|
+
* Tasks are built via the procedure builder:
|
|
5
16
|
*
|
|
6
|
-
* Tasks are procedures with dispatch + cron capabilities:
|
|
7
17
|
* s.$use(auth).$input(schema).$task({ name: 'send-email', resolve })
|
|
18
|
+
*
|
|
19
|
+
* Dispatch runs through the same root-wrap onion as the HTTP pipeline
|
|
20
|
+
* so tenant scoping, trace propagation, and similar cross-cutting
|
|
21
|
+
* concerns apply uniformly. When no root wraps are configured the
|
|
22
|
+
* dispatch path reduces to a direct `await resolveFn(…)`.
|
|
8
23
|
*/
|
|
9
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Module-global sink for task completion events. This is shared across
|
|
26
|
+
* every silgi instance in the process — analytics is currently wired
|
|
27
|
+
* through the process-default cron registry for the same reason.
|
|
28
|
+
* Per-instance analytics is a future refactor; `setTaskAnalytics(null)`
|
|
29
|
+
* detaches the sink.
|
|
30
|
+
*/
|
|
31
|
+
let onTaskComplete = null;
|
|
10
32
|
function setTaskAnalytics(cb) {
|
|
11
|
-
|
|
33
|
+
onTaskComplete = cb;
|
|
12
34
|
}
|
|
13
|
-
|
|
35
|
+
/** Round a millisecond duration to two decimal places — matches dashboard display. */
|
|
36
|
+
const round2 = (ms) => Math.round(ms * 100) / 100;
|
|
37
|
+
/**
|
|
38
|
+
* Wrap `run` in the root-wrap onion. Root wraps are outermost-first, so
|
|
39
|
+
* we fold from the end of the list: the last wrap wraps `run`, the one
|
|
40
|
+
* before wraps that, and so on. When there are no wraps we just return
|
|
41
|
+
* `run` unchanged — zero onion overhead.
|
|
42
|
+
*
|
|
43
|
+
* Same shape as `composeWraps` in `compile.ts`; kept here privately to
|
|
44
|
+
* avoid a core → compile import cycle.
|
|
45
|
+
*/
|
|
46
|
+
function applyRootWraps(ctx, wraps, run) {
|
|
47
|
+
let chain = run;
|
|
48
|
+
for (let i = wraps.length - 1; i >= 0; i--) {
|
|
49
|
+
const wrap = wraps[i];
|
|
50
|
+
const next = chain;
|
|
51
|
+
chain = () => Promise.resolve(wrap.fn(ctx, next));
|
|
52
|
+
}
|
|
53
|
+
return chain();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Lazy-loaded `RequestTrace` constructor. The analytics module is
|
|
57
|
+
* optional — missing it must not break task dispatch — so we attempt
|
|
58
|
+
* the import once and cache the outcome. `null` means "we've tried;
|
|
59
|
+
* analytics is not available in this build".
|
|
60
|
+
*/
|
|
61
|
+
let requestTraceCtor;
|
|
62
|
+
async function getRequestTrace() {
|
|
63
|
+
if (requestTraceCtor !== void 0) return requestTraceCtor;
|
|
64
|
+
try {
|
|
65
|
+
requestTraceCtor = (await import("../plugins/analytics.mjs")).RequestTrace;
|
|
66
|
+
} catch {
|
|
67
|
+
requestTraceCtor = null;
|
|
68
|
+
}
|
|
69
|
+
return requestTraceCtor;
|
|
70
|
+
}
|
|
71
|
+
/** Record this dispatch as a span on the parent request's trace, if one exists. */
|
|
72
|
+
function recordParentSpan(parentTrace, name, spanStart, err) {
|
|
73
|
+
if (!parentTrace) return;
|
|
74
|
+
const span = {
|
|
75
|
+
name: `task:${name}`,
|
|
76
|
+
kind: "queue",
|
|
77
|
+
durationMs: round2(performance.now() - spanStart),
|
|
78
|
+
startOffsetMs: round2(spanStart - parentTrace.t0),
|
|
79
|
+
detail: `dispatch ${name}`
|
|
80
|
+
};
|
|
81
|
+
if (err !== void 0) span.error = err instanceof Error ? err.message : String(err);
|
|
82
|
+
parentTrace.spans.push(span);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build a `TaskDef` from the procedure builder's `.$task()` configuration.
|
|
86
|
+
*
|
|
87
|
+
* @param rootWrapsGetter A *live* getter so a task constructed before
|
|
88
|
+
* `s.router()` stamps wraps still picks them up at dispatch time. The
|
|
89
|
+
* silgi instance threads a closure over its own rootWraps reference.
|
|
90
|
+
* Pass `null` when no wraps are configured — dispatch then skips the
|
|
91
|
+
* onion entirely.
|
|
92
|
+
*/
|
|
93
|
+
function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFactory, rootWrapsGetter = null) {
|
|
14
94
|
const { name, cron = null, description } = config;
|
|
15
95
|
if (!name) throw new TypeError("Task name is required");
|
|
16
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Resolver called by the *HTTP* pipeline when the task is reachable
|
|
98
|
+
* through the router tree. `ctx` already carries everything the
|
|
99
|
+
* pipeline set up (base context, guards, trace), so we just forward.
|
|
100
|
+
*/
|
|
101
|
+
const pipelineResolve = async (opts) => {
|
|
17
102
|
return resolveFn({
|
|
18
103
|
input: opts.input,
|
|
19
104
|
ctx: opts.ctx,
|
|
@@ -21,61 +106,52 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
21
106
|
scheduledTime: void 0
|
|
22
107
|
});
|
|
23
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Programmatic dispatch — the one users call from another procedure
|
|
111
|
+
* or a cron callback. Validates input, builds its own context, runs
|
|
112
|
+
* the root-wrap onion around the resolver, records analytics.
|
|
113
|
+
*/
|
|
24
114
|
const dispatch = async (rawInput, parentCtx) => {
|
|
25
115
|
const input = inputSchema ? await validateSchema(inputSchema, rawInput) : rawInput;
|
|
26
116
|
const ctx = contextFactory ? await contextFactory() : {};
|
|
27
117
|
const parentTrace = parentCtx?.trace;
|
|
28
118
|
const spanStart = parentTrace ? performance.now() : 0;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
119
|
+
const RequestTrace = await getRequestTrace();
|
|
120
|
+
const selfTrace = RequestTrace ? new RequestTrace() : null;
|
|
121
|
+
if (selfTrace) ctx.trace = selfTrace;
|
|
122
|
+
const runResolver = () => Promise.resolve(resolveFn({
|
|
123
|
+
input,
|
|
124
|
+
ctx,
|
|
125
|
+
name,
|
|
126
|
+
scheduledTime: void 0
|
|
127
|
+
}));
|
|
128
|
+
const wraps = rootWrapsGetter?.() ?? null;
|
|
35
129
|
const t0 = performance.now();
|
|
36
130
|
try {
|
|
37
|
-
const output = await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
name,
|
|
41
|
-
scheduledTime: void 0
|
|
42
|
-
});
|
|
43
|
-
if (parentTrace) parentTrace.spans.push({
|
|
44
|
-
name: `task:${name}`,
|
|
45
|
-
kind: "queue",
|
|
46
|
-
durationMs: Math.round((performance.now() - spanStart) * 100) / 100,
|
|
47
|
-
startOffsetMs: Math.round((spanStart - parentTrace.t0) * 100) / 100,
|
|
48
|
-
detail: `dispatch ${name}`
|
|
49
|
-
});
|
|
50
|
-
if (_onTaskComplete) _onTaskComplete({
|
|
131
|
+
const output = wraps && wraps.length > 0 ? await applyRootWraps(ctx, wraps, runResolver) : await runResolver();
|
|
132
|
+
recordParentSpan(parentTrace, name, spanStart);
|
|
133
|
+
onTaskComplete?.({
|
|
51
134
|
taskName: name,
|
|
52
135
|
trigger: "dispatch",
|
|
53
136
|
timestamp: Date.now(),
|
|
54
|
-
durationMs:
|
|
137
|
+
durationMs: round2(performance.now() - t0),
|
|
55
138
|
status: "success",
|
|
56
139
|
input,
|
|
57
140
|
output,
|
|
58
|
-
spans:
|
|
141
|
+
spans: selfTrace?.spans
|
|
59
142
|
});
|
|
60
143
|
return output;
|
|
61
144
|
} catch (err) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
kind: "queue",
|
|
65
|
-
durationMs: Math.round((performance.now() - spanStart) * 100) / 100,
|
|
66
|
-
startOffsetMs: Math.round((spanStart - parentTrace.t0) * 100) / 100,
|
|
67
|
-
detail: `dispatch ${name}`,
|
|
68
|
-
error: err instanceof Error ? err.message : String(err)
|
|
69
|
-
});
|
|
70
|
-
if (_onTaskComplete) _onTaskComplete({
|
|
145
|
+
recordParentSpan(parentTrace, name, spanStart, err);
|
|
146
|
+
onTaskComplete?.({
|
|
71
147
|
taskName: name,
|
|
72
148
|
trigger: "dispatch",
|
|
73
149
|
timestamp: Date.now(),
|
|
74
|
-
durationMs:
|
|
150
|
+
durationMs: round2(performance.now() - t0),
|
|
75
151
|
status: "error",
|
|
76
152
|
error: err instanceof Error ? err.message : String(err),
|
|
77
153
|
input,
|
|
78
|
-
spans:
|
|
154
|
+
spans: selfTrace?.spans
|
|
79
155
|
});
|
|
80
156
|
throw err;
|
|
81
157
|
}
|
|
@@ -88,7 +164,7 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
88
164
|
output: null,
|
|
89
165
|
errors: null,
|
|
90
166
|
use,
|
|
91
|
-
resolve:
|
|
167
|
+
resolve: pipelineResolve,
|
|
92
168
|
route: description ? {
|
|
93
169
|
summary: description,
|
|
94
170
|
tags: ["Tasks"]
|
|
@@ -98,68 +174,112 @@ function createTaskFromProcedure(config, resolveFn, inputSchema, use, contextFac
|
|
|
98
174
|
dispatch
|
|
99
175
|
};
|
|
100
176
|
}
|
|
101
|
-
|
|
177
|
+
/**
|
|
178
|
+
* In-flight dispatches keyed by task object.
|
|
179
|
+
*
|
|
180
|
+
* `runTask` coalesces concurrent calls so two callers asking for the
|
|
181
|
+
* same task at once share a single execution. Useful for idempotent
|
|
182
|
+
* background jobs that can be kicked off from multiple places.
|
|
183
|
+
*/
|
|
184
|
+
const running = /* @__PURE__ */ new Map();
|
|
102
185
|
async function runTask(task, ...args) {
|
|
103
|
-
const existing =
|
|
186
|
+
const existing = running.get(task);
|
|
104
187
|
if (existing) return existing;
|
|
105
188
|
const promise = task.dispatch(args[0]);
|
|
106
|
-
|
|
189
|
+
running.set(task, promise);
|
|
107
190
|
try {
|
|
108
191
|
return await promise;
|
|
109
192
|
} finally {
|
|
110
|
-
|
|
193
|
+
running.delete(task);
|
|
111
194
|
}
|
|
112
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Walk a router tree and collect every task that has a `cron` field set.
|
|
198
|
+
* Nested namespaces are recursed into; we stop at any node already
|
|
199
|
+
* tagged as a task so a task's internal structure is never inspected.
|
|
200
|
+
*/
|
|
113
201
|
function collectCronTasks(def) {
|
|
114
202
|
const result = [];
|
|
115
|
-
for (const value of Object.values(def))
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
task
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
let _cronEntries = [];
|
|
125
|
-
async function startCronJobs(cronTasks) {
|
|
126
|
-
if (cronTasks.length === 0) return;
|
|
127
|
-
const { Cron } = await import("croner");
|
|
128
|
-
for (const { cron, task } of cronTasks) {
|
|
129
|
-
const entry = {
|
|
130
|
-
name: task.route?.summary || cron,
|
|
131
|
-
cron,
|
|
132
|
-
description: task.route?.summary,
|
|
133
|
-
job: null,
|
|
134
|
-
lastRun: null,
|
|
135
|
-
runs: 0,
|
|
136
|
-
errors: 0
|
|
137
|
-
};
|
|
138
|
-
entry.job = new Cron(cron, async () => {
|
|
139
|
-
entry.lastRun = Date.now();
|
|
140
|
-
entry.runs++;
|
|
141
|
-
task.dispatch(void 0).catch((err) => {
|
|
142
|
-
entry.errors++;
|
|
143
|
-
console.error(`[silgi] Cron task failed:`, err instanceof Error ? err.message : err);
|
|
203
|
+
for (const value of Object.values(def)) {
|
|
204
|
+
if (!value || typeof value !== "object") continue;
|
|
205
|
+
if ("_tag" in value && value._tag === "task") {
|
|
206
|
+
const task = value;
|
|
207
|
+
if (task.cron) result.push({
|
|
208
|
+
cron: task.cron,
|
|
209
|
+
task
|
|
144
210
|
});
|
|
145
|
-
});
|
|
146
|
-
_cronEntries.push(entry);
|
|
211
|
+
} else if (!("_tag" in value)) result.push(...collectCronTasks(value));
|
|
147
212
|
}
|
|
213
|
+
return result;
|
|
148
214
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Create an isolated cron registry.
|
|
217
|
+
*
|
|
218
|
+
* Each silgi instance owns one, so `server.close()` on instance A
|
|
219
|
+
* never stops instance B's jobs and `list()` never returns jobs from
|
|
220
|
+
* another instance. The module-default registry below keeps the
|
|
221
|
+
* legacy top-level exports working.
|
|
222
|
+
*/
|
|
223
|
+
function createCronRegistry() {
|
|
224
|
+
const entries = [];
|
|
225
|
+
return {
|
|
226
|
+
async start(cronTasks) {
|
|
227
|
+
if (cronTasks.length === 0) return;
|
|
228
|
+
const { Cron } = await import("croner");
|
|
229
|
+
for (const { cron, task } of cronTasks) {
|
|
230
|
+
const entry = {
|
|
231
|
+
name: task.route?.summary || cron,
|
|
232
|
+
cron,
|
|
233
|
+
description: task.route?.summary,
|
|
234
|
+
job: null,
|
|
235
|
+
lastRun: null,
|
|
236
|
+
runs: 0,
|
|
237
|
+
errors: 0
|
|
238
|
+
};
|
|
239
|
+
entry.job = new Cron(cron, async () => {
|
|
240
|
+
entry.lastRun = Date.now();
|
|
241
|
+
entry.runs++;
|
|
242
|
+
task.dispatch(void 0).catch((err) => {
|
|
243
|
+
entry.errors++;
|
|
244
|
+
console.error(`[silgi] Cron task failed:`, err instanceof Error ? err.message : err);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
entries.push(entry);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
stop() {
|
|
251
|
+
for (const entry of entries) entry.job.stop();
|
|
252
|
+
entries.length = 0;
|
|
253
|
+
},
|
|
254
|
+
list() {
|
|
255
|
+
return entries.map((entry) => ({
|
|
256
|
+
name: entry.name,
|
|
257
|
+
cron: entry.cron,
|
|
258
|
+
description: entry.description,
|
|
259
|
+
nextRun: entry.job.nextRun()?.getTime() ?? null,
|
|
260
|
+
lastRun: entry.lastRun,
|
|
261
|
+
runs: entry.runs,
|
|
262
|
+
errors: entry.errors
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
163
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Process-default cron registry.
|
|
269
|
+
*
|
|
270
|
+
* @deprecated
|
|
271
|
+
* Shared state. Prefer {@link createCronRegistry} and give each silgi
|
|
272
|
+
* instance its own. The module-default registry is retained so
|
|
273
|
+
* existing imports of `startCronJobs` / `stopCronJobs` /
|
|
274
|
+
* `getScheduledTasks` keep working; a future major will remove these
|
|
275
|
+
* top-level re-exports.
|
|
276
|
+
*/
|
|
277
|
+
const defaultRegistry = createCronRegistry();
|
|
278
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
279
|
+
const startCronJobs = defaultRegistry.start;
|
|
280
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
281
|
+
const stopCronJobs = defaultRegistry.stop;
|
|
282
|
+
/** @deprecated Use {@link createCronRegistry} — each silgi instance owns its own. */
|
|
283
|
+
const getScheduledTasks = defaultRegistry.list;
|
|
164
284
|
//#endregion
|
|
165
|
-
export { collectCronTasks, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
|
285
|
+
export { collectCronTasks, createCronRegistry, createTaskFromProcedure, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs };
|
package/dist/core/url.mjs
CHANGED
|
@@ -10,9 +10,27 @@
|
|
|
10
10
|
* Returns the path portion without query string.
|
|
11
11
|
*
|
|
12
12
|
* Uses manual indexOf — no `new URL()` overhead.
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* Handles both absolute URLs (`http://host/path?q`) and bare paths
|
|
16
|
+
* (`/path?q`). The latter shape is produced by adapters that strip the
|
|
17
|
+
* origin before calling the handler, and by test harnesses constructing
|
|
18
|
+
* synthetic requests. Without the bare-path branch, a missing `//`
|
|
19
|
+
* caused `indexOf('//') + 2 = 1` and `indexOf('/', 1)` returned a bogus
|
|
20
|
+
* offset that silently produced the wrong path.
|
|
13
21
|
*/
|
|
14
22
|
function parseUrlPath(url) {
|
|
15
|
-
|
|
23
|
+
if (url.length > 0 && url.charCodeAt(0) === 47) {
|
|
24
|
+
const qMark = url.indexOf("?");
|
|
25
|
+
return qMark === -1 ? url : url.slice(0, qMark);
|
|
26
|
+
}
|
|
27
|
+
const schemeEnd = url.indexOf("//");
|
|
28
|
+
if (schemeEnd === -1) {
|
|
29
|
+
const qMark = url.indexOf("?");
|
|
30
|
+
return qMark === -1 ? url : url.slice(0, qMark);
|
|
31
|
+
}
|
|
32
|
+
const pathStart = url.indexOf("/", schemeEnd + 2);
|
|
33
|
+
if (pathStart === -1) return "/";
|
|
16
34
|
const qMark = url.indexOf("?", pathStart);
|
|
17
35
|
return qMark === -1 ? url.slice(pathStart) : url.slice(pathStart, qMark);
|
|
18
36
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnySchema, InferSchemaInput, InferSchemaOutput, Schema, ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
|
-
import { ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
2
|
+
import { CronRegistry, ScheduledTaskInfo, TaskDef, TaskEvent, collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { ErrorDef, ErrorDefItem, FailFn, GuardDef, GuardFn, InferClient, InferContextFromUse, InferGuardOutput, Meta, MiddlewareDef, ProcedureDef, ProcedureType, ResolveContext, RouterDef, WrapDef, WrapFn } from "./types.mjs";
|
|
4
4
|
import { ConvertOptions, JSONSchema, SchemaConverter, SchemaRegistry, createSchemaRegistry, schemaToJsonSchema } from "./core/schema-converter.mjs";
|
|
5
5
|
import { ScalarOptions, generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
@@ -18,4 +18,4 @@ import { mapInput } from "./map-input.mjs";
|
|
|
18
18
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
19
19
|
import { ProcedureSummary, collectProcedures, getProcedurePaths, isProcedureDef } from "./core/router-utils.mjs";
|
|
20
20
|
import { LazyRouter, isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
21
|
-
export { type AnySchema, AsyncIteratorClass, type BaseContext, type CallableOptions, type ContextBridge, type ConvertOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type JSONSchema, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureSummary, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type SchemaConverter, type SchemaRegistry, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
|
21
|
+
export { type AnySchema, AsyncIteratorClass, type BaseContext, type CallableOptions, type ContextBridge, type ConvertOptions, type CronRegistry, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type JSONSchema, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureSummary, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type SchemaConverter, type SchemaRegistry, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createCronRegistry, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
|
-
import { collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
2
|
+
import { collectCronTasks, createCronRegistry, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { SilgiError, isDefinedError, isSilgiError, toSilgiError } from "./core/error.mjs";
|
|
4
4
|
import { collectProcedures, getProcedurePaths, isProcedureDef } from "./core/router-utils.mjs";
|
|
5
5
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
@@ -14,4 +14,4 @@ import { mapInput } from "./map-input.mjs";
|
|
|
14
14
|
import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
15
15
|
import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
|
|
16
16
|
import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
17
|
-
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
|
17
|
+
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, collectProcedures, compileProcedure, compileRouter, createContext, createContextBridge, createCronRegistry, createSchemaRegistry, generateOpenAPI, getEventMeta, getProcedurePaths, getScheduledTasks, initStorage, isDefinedError, isLazy, isProcedureDef, isSilgiError, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, schemaToJsonSchema, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|