@pramen/server 0.0.8 → 0.0.10
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/durable-object.d.ts +20 -0
- package/dist/durable-object.js +88 -4
- package/dist/index.d.ts +1 -1
- package/dist/pramen.d.ts +8 -2
- package/dist/pramen.js +5 -2
- package/dist/runtime/dispatch.d.ts +9 -1
- package/dist/runtime/dispatch.js +21 -2
- package/dist/runtime/outbox.d.ts +57 -0
- package/dist/runtime/outbox.js +128 -0
- package/dist/sdk/handlers.d.ts +28 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +89 -1
- package/package.json +1 -1
- package/src/durable-object.ts +96 -4
- package/src/index.ts +1 -1
- package/src/pramen.ts +10 -3
- package/src/runtime/dispatch.ts +25 -3
- package/src/runtime/outbox.ts +204 -0
- package/src/sdk/handlers.ts +27 -0
- package/src/worker.ts +91 -1
package/src/worker.ts
CHANGED
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
// returned fetch with the matching DO class; a consumer just re-exports both.
|
|
6
6
|
|
|
7
7
|
import { authorizeTenant, HmacStrategy, isAdmin, JwksStrategy, resolveIdentity, type VerifyStrategy } from "./auth";
|
|
8
|
-
import { dispatch } from "./runtime/dispatch";
|
|
8
|
+
import { dispatch, tasksFacade, bindTasks } from "./runtime/dispatch";
|
|
9
|
+
import { ensureOutbox, drainOutbox, listTasks } from "./runtime/outbox";
|
|
9
10
|
import { migrate } from "./runtime/migrate";
|
|
10
11
|
import { compileAcl } from "./runtime/acl";
|
|
12
|
+
import { Db } from "./runtime/db";
|
|
11
13
|
import { D1Driver, type Driver } from "./runtime/driver";
|
|
12
14
|
import { toResponse } from "./runtime/errors";
|
|
13
15
|
import { Kv } from "./runtime/kv";
|
|
14
16
|
import { listDOs, partitionDoName } from "./runtime/registry";
|
|
15
17
|
import { createFiles, handleFileRequest, R2Adapter } from "./runtime/storage";
|
|
16
18
|
import type { Identity } from "./sdk/acl";
|
|
19
|
+
import type { HandlerContext } from "./sdk/handlers";
|
|
17
20
|
import { DEFAULT_PARTITION } from "./sdk/schema";
|
|
18
21
|
import type { PramenApp } from "./pramen";
|
|
19
22
|
|
|
@@ -129,6 +132,7 @@ export function makeWorker(app: PramenApp) {
|
|
|
129
132
|
const ensureD1Migrated = (driver: Driver, allowDestructive: boolean): Promise<void> => {
|
|
130
133
|
if (!d1Ready) {
|
|
131
134
|
d1Ready = migrate(driver, app.schema, { allowDestructive })
|
|
135
|
+
.then(() => ensureOutbox(driver)) // the deferred-tasks table also lives in D1
|
|
132
136
|
.then(() => undefined)
|
|
133
137
|
.catch((e) => {
|
|
134
138
|
d1Ready = undefined;
|
|
@@ -138,6 +142,32 @@ export function makeWorker(app: PramenApp) {
|
|
|
138
142
|
return d1Ready;
|
|
139
143
|
};
|
|
140
144
|
|
|
145
|
+
// A privileged, system-scoped context for running task handlers on the D1 (Worker)
|
|
146
|
+
// path — mirrors the DO's taskCtx. No live socket, so no DO; drained by a Cron / the
|
|
147
|
+
// /admin/tasks/drain route, never a DO alarm.
|
|
148
|
+
const d1TaskCtx = (driver: Driver, env: Env): HandlerContext => {
|
|
149
|
+
const identity: Identity = { roles: ["admin"] };
|
|
150
|
+
const files = createFiles({ tenant: "main", secret: filesSecret(env), adapter: new R2Adapter(env.FILES) });
|
|
151
|
+
const db = new Db(driver, { acl: d1Acl, identity, system: true, schema: app.schema }, app.schema);
|
|
152
|
+
return { db, kv: new Kv(env.KV), files, env: env as unknown as Record<string, unknown>, identity, tasks: tasksFacade(driver) };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Drain the D1 outbox in the Worker (no DO/alarm on this path) — called by the
|
|
156
|
+
* /admin/tasks/drain route with `x-pramen-store: d1`, and by `scheduled()` (Cron). */
|
|
157
|
+
const drainD1 = async (env: Env): Promise<unknown> => {
|
|
158
|
+
if (!env.DB) throw new Error("D1 store is not configured");
|
|
159
|
+
const driver = new D1Driver(env.DB);
|
|
160
|
+
await ensureD1Migrated(driver, env.PRAMEN_ALLOW_DESTRUCTIVE === "true");
|
|
161
|
+
return drainOutbox(driver, bindTasks(app.tasks, d1TaskCtx(driver, env)), Date.now());
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const listD1Tasks = async (env: Env, status?: string, limit?: number): Promise<unknown> => {
|
|
165
|
+
if (!env.DB) throw new Error("D1 store is not configured");
|
|
166
|
+
const driver = new D1Driver(env.DB);
|
|
167
|
+
await ensureD1Migrated(driver, env.PRAMEN_ALLOW_DESTRUCTIVE === "true");
|
|
168
|
+
return listTasks(driver, { status, limit });
|
|
169
|
+
};
|
|
170
|
+
|
|
141
171
|
return {
|
|
142
172
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
143
173
|
const url = new URL(request.url);
|
|
@@ -239,6 +269,60 @@ export function makeWorker(app: PramenApp) {
|
|
|
239
269
|
return withCors(res, cors);
|
|
240
270
|
}
|
|
241
271
|
|
|
272
|
+
// --- admin: drain the deferred-task outbox now (the DO also self-drains via an
|
|
273
|
+
// alarm; this is the manual / Cron entry, and the ONLY drain for the D1 path).
|
|
274
|
+
// `x-pramen-store: d1` drains the D1 outbox in the Worker; else the tenant's DO. ---
|
|
275
|
+
if (url.pathname === "/admin/tasks/drain" && request.method === "POST") {
|
|
276
|
+
if (!isAdmin(identity)) return forbidden("tasks");
|
|
277
|
+
if (request.headers.get("x-pramen-store") === "d1") {
|
|
278
|
+
try {
|
|
279
|
+
return withCors(json({ ok: true, result: await drainD1(env) }), cors);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
const { status, body } = toResponse(err);
|
|
282
|
+
return withCors(json(body, status), cors);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const body = (await request.json().catch(() => ({}))) as { tenant?: unknown; partition?: unknown };
|
|
286
|
+
const tenant = typeof body.tenant === "string" && body.tenant ? body.tenant : "main";
|
|
287
|
+
const partition = typeof body.partition === "string" && body.partition ? body.partition : DEFAULT_PARTITION;
|
|
288
|
+
const stub = partitionStubFor(env, tenant, partition);
|
|
289
|
+
const res = await stub.fetch(
|
|
290
|
+
new Request("https://do/__admin/tasks/drain", {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: { "content-type": "application/json", "x-pramen-tenant": tenant, "x-pramen-partition": partition },
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
return withCors(res, cors);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- admin: list outbox tasks (inspect dead-letters etc.). ?status=&limit=,
|
|
299
|
+
// ?tenant=&partition= (DO store) or x-pramen-store: d1 (Worker outbox). ---
|
|
300
|
+
if (url.pathname === "/admin/tasks/list") {
|
|
301
|
+
if (!isAdmin(identity)) return withCors(forbidden("tasks"), cors);
|
|
302
|
+
const status = url.searchParams.get("status") ?? undefined;
|
|
303
|
+
const limit = Number(url.searchParams.get("limit")) || undefined;
|
|
304
|
+
if (request.headers.get("x-pramen-store") === "d1") {
|
|
305
|
+
try {
|
|
306
|
+
return withCors(json({ ok: true, result: await listD1Tasks(env, status, limit) }), cors);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
const { status: s, body } = toResponse(err);
|
|
309
|
+
return withCors(json(body, s), cors);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const tenant = url.searchParams.get("tenant") ?? "main";
|
|
313
|
+
const partition = url.searchParams.get("partition") || DEFAULT_PARTITION;
|
|
314
|
+
const q = new URLSearchParams();
|
|
315
|
+
if (status) q.set("status", status);
|
|
316
|
+
if (limit) q.set("limit", String(limit));
|
|
317
|
+
const stub = partitionStubFor(env, tenant, partition);
|
|
318
|
+
const res = await stub.fetch(
|
|
319
|
+
new Request(`https://do/__admin/tasks/list?${q}`, {
|
|
320
|
+
headers: { "x-pramen-tenant": tenant, "x-pramen-partition": partition },
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
return withCors(res, cors);
|
|
324
|
+
}
|
|
325
|
+
|
|
242
326
|
const isRpc = url.pathname.startsWith("/rpc/");
|
|
243
327
|
const isLive = url.pathname === "/live";
|
|
244
328
|
|
|
@@ -303,5 +387,11 @@ export function makeWorker(app: PramenApp) {
|
|
|
303
387
|
const res = await stub.fetch(new Request(req, { headers }));
|
|
304
388
|
return isWs ? res : withCors(res, cors);
|
|
305
389
|
},
|
|
390
|
+
|
|
391
|
+
// Cron Trigger entry: drains the D1 outbox (the DO path self-drains via an alarm,
|
|
392
|
+
// so it needs no cron). Wire a `[triggers] crons` in wrangler/oblaka to call this.
|
|
393
|
+
async scheduled(_event: unknown, env: Env): Promise<void> {
|
|
394
|
+
if (env.DB) await drainD1(env);
|
|
395
|
+
},
|
|
306
396
|
};
|
|
307
397
|
}
|