@yss123/cage-kernel 0.0.0-stage → 0.2.0

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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -2
  3. package/dist/brands.d.ts +32 -0
  4. package/dist/brands.js +35 -0
  5. package/dist/brands.js.map +1 -0
  6. package/dist/db/connect.d.ts +28 -0
  7. package/dist/db/connect.js +57 -0
  8. package/dist/db/connect.js.map +1 -0
  9. package/dist/db/ddl.d.ts +69 -0
  10. package/dist/db/ddl.js +522 -0
  11. package/dist/db/ddl.js.map +1 -0
  12. package/dist/db/index.d.ts +5 -0
  13. package/dist/db/index.js +7 -0
  14. package/dist/db/index.js.map +1 -0
  15. package/dist/db/migrate.d.ts +22 -0
  16. package/dist/db/migrate.js +87 -0
  17. package/dist/db/migrate.js.map +1 -0
  18. package/dist/db/rows.d.ts +10 -0
  19. package/dist/db/rows.js +59 -0
  20. package/dist/db/rows.js.map +1 -0
  21. package/dist/db/session.d.ts +4 -0
  22. package/dist/db/session.js +20 -0
  23. package/dist/db/session.js.map +1 -0
  24. package/dist/errors.d.ts +44 -0
  25. package/dist/errors.js +83 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/index.d.ts +23 -0
  28. package/dist/index.js +25 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/kernel.d.ts +99 -0
  31. package/dist/kernel.js +456 -0
  32. package/dist/kernel.js.map +1 -0
  33. package/dist/mask.d.ts +13 -0
  34. package/dist/mask.js +67 -0
  35. package/dist/mask.js.map +1 -0
  36. package/dist/org.d.ts +46 -0
  37. package/dist/org.js +127 -0
  38. package/dist/org.js.map +1 -0
  39. package/dist/pipeline/idempotency.d.ts +10 -0
  40. package/dist/pipeline/idempotency.js +29 -0
  41. package/dist/pipeline/idempotency.js.map +1 -0
  42. package/dist/pipeline/load.d.ts +15 -0
  43. package/dist/pipeline/load.js +87 -0
  44. package/dist/pipeline/load.js.map +1 -0
  45. package/dist/primitives/aggregate.d.ts +75 -0
  46. package/dist/primitives/aggregate.js +69 -0
  47. package/dist/primitives/aggregate.js.map +1 -0
  48. package/dist/primitives/command.d.ts +101 -0
  49. package/dist/primitives/command.js +49 -0
  50. package/dist/primitives/command.js.map +1 -0
  51. package/dist/primitives/constraints.d.ts +36 -0
  52. package/dist/primitives/constraints.js +41 -0
  53. package/dist/primitives/constraints.js.map +1 -0
  54. package/dist/primitives/event.d.ts +31 -0
  55. package/dist/primitives/event.js +44 -0
  56. package/dist/primitives/event.js.map +1 -0
  57. package/dist/primitives/fields.d.ts +127 -0
  58. package/dist/primitives/fields.js +151 -0
  59. package/dist/primitives/fields.js.map +1 -0
  60. package/dist/primitives/flow.d.ts +126 -0
  61. package/dist/primitives/flow.js +90 -0
  62. package/dist/primitives/flow.js.map +1 -0
  63. package/dist/primitives/states.d.ts +19 -0
  64. package/dist/primitives/states.js +37 -0
  65. package/dist/primitives/states.js.map +1 -0
  66. package/dist/read.d.ts +53 -0
  67. package/dist/read.js +217 -0
  68. package/dist/read.js.map +1 -0
  69. package/dist/registry.d.ts +71 -0
  70. package/dist/registry.js +177 -0
  71. package/dist/registry.js.map +1 -0
  72. package/dist/relations.d.ts +14 -0
  73. package/dist/relations.js +58 -0
  74. package/dist/relations.js.map +1 -0
  75. package/dist/scope/index.d.ts +42 -0
  76. package/dist/scope/index.js +90 -0
  77. package/dist/scope/index.js.map +1 -0
  78. package/dist/telemetry.d.ts +11 -0
  79. package/dist/telemetry.js +35 -0
  80. package/dist/telemetry.js.map +1 -0
  81. package/dist/transfer/csv.d.ts +8 -0
  82. package/dist/transfer/csv.js +114 -0
  83. package/dist/transfer/csv.js.map +1 -0
  84. package/dist/transfer/index.d.ts +49 -0
  85. package/dist/transfer/index.js +245 -0
  86. package/dist/transfer/index.js.map +1 -0
  87. package/dist/transfer/schema.d.ts +4 -0
  88. package/dist/transfer/schema.js +31 -0
  89. package/dist/transfer/schema.js.map +1 -0
  90. package/dist/types.d.ts +52 -0
  91. package/dist/types.js +5 -0
  92. package/dist/types.js.map +1 -0
  93. package/dist/util/names.d.ts +9 -0
  94. package/dist/util/names.js +38 -0
  95. package/dist/util/names.js.map +1 -0
  96. package/dist/worker/consumers.d.ts +29 -0
  97. package/dist/worker/consumers.js +79 -0
  98. package/dist/worker/consumers.js.map +1 -0
  99. package/dist/worker/index.d.ts +36 -0
  100. package/dist/worker/index.js +122 -0
  101. package/dist/worker/index.js.map +1 -0
  102. package/dist/worker/outbox.d.ts +38 -0
  103. package/dist/worker/outbox.js +121 -0
  104. package/dist/worker/outbox.js.map +1 -0
  105. package/dist/worker/scheduler.d.ts +7 -0
  106. package/dist/worker/scheduler.js +77 -0
  107. package/dist/worker/scheduler.js.map +1 -0
  108. package/package.json +58 -3
@@ -0,0 +1,122 @@
1
+ // worker 进程:投递 outbox、执行反应、启动审批、跑定时任务、调用第三方。与 api 同一份代码和镜像,只是启动命令不同。
2
+ import { sql } from 'kysely';
3
+ import { dedicatedClient, urlsFromEnv } from '../db/connect.js';
4
+ import { createKernel } from '../kernel.js';
5
+ import { consoleLogger } from '../telemetry.js';
6
+ import { approvalConsumers, extensionConsumers, reactionConsumers, routeEngines, } from './consumers.js';
7
+ import { claimBatch, deliver, MAX_ATTEMPTS, sweepStale } from './outbox.js';
8
+ import { nextRun, tick } from './scheduler.js';
9
+ import { expireTransfers, transferConsumer } from '../transfer/index.js';
10
+ export * from './outbox.js';
11
+ export * from './consumers.js';
12
+ export * from './scheduler.js';
13
+ export function buildConsumers(k, opts = {}) {
14
+ const engines = { ...routeEngines(k), ...(opts.approvalEngines ?? {}) };
15
+ return [transferConsumer(k), ...reactionConsumers(k), ...approvalConsumers(k, engines), ...extensionConsumers(k), ...(opts.consumers ?? [])];
16
+ }
17
+ /** 不启动循环,只创建可手动驱动的 worker(测试用) */
18
+ export function createWorker(k, opts = {}) {
19
+ const consumers = buildConsumers(k, opts);
20
+ const max = opts.maxAttempts ?? MAX_ATTEMPTS;
21
+ return {
22
+ kernel: k,
23
+ consumers,
24
+ async drain(maxRounds = 50) {
25
+ let n = 0;
26
+ for (let i = 0; i < maxRounds; i++) {
27
+ const rows = await claimBatch(k, opts.concurrency ?? 8);
28
+ if (rows.length === 0)
29
+ break;
30
+ await Promise.all(rows.map((r) => deliver(k, r, consumers, max)));
31
+ n += rows.length;
32
+ }
33
+ return n;
34
+ },
35
+ };
36
+ }
37
+ export async function startWorker(opts = {}) {
38
+ const log = opts.logger ?? consoleLogger;
39
+ const urls = opts.urls ?? (opts.kernel ? undefined : urlsFromEnv());
40
+ const k = opts.kernel ?? createKernel({ domain: opts.domain ?? {}, urls, logger: log });
41
+ const w = createWorker(k, opts);
42
+ const concurrency = opts.concurrency ?? 8;
43
+ const poll = opts.pollIntervalMs ?? 500;
44
+ let stopped = false;
45
+ let wake = null;
46
+ const timers = [];
47
+ // LISTEN:有新事件时立刻醒来,不用等轮询
48
+ const appUrl = urls?.app ?? urlsFromEnv().app;
49
+ const listener = await dedicatedClient(appUrl, 'cage-worker-listen');
50
+ await listener.query('LISTEN cage_outbox');
51
+ listener.on('notification', () => wake?.());
52
+ listener.on('error', (e) => log.error({ err: String(e) }, 'outbox 监听连接出错'));
53
+ const loop = (async () => {
54
+ while (!stopped) {
55
+ try {
56
+ const rows = await claimBatch(k, concurrency);
57
+ if (rows.length > 0) {
58
+ await Promise.all(rows.map((r) => deliver(k, r, w.consumers, opts.maxAttempts ?? MAX_ATTEMPTS)));
59
+ continue;
60
+ }
61
+ }
62
+ catch (e) {
63
+ log.error({ err: String(e) }, 'outbox 领取失败');
64
+ }
65
+ await new Promise((resolve) => {
66
+ const t = setTimeout(resolve, poll);
67
+ wake = () => {
68
+ clearTimeout(t);
69
+ resolve();
70
+ };
71
+ });
72
+ wake = null;
73
+ }
74
+ })();
75
+ // 清扫与日常维护
76
+ timers.push(setInterval(() => void sweepStale(k).catch(() => { }), 60_000));
77
+ const housekeeping = async () => {
78
+ try {
79
+ await sql `select sys.ensure_audit_partitions(3)`.execute(k.app);
80
+ await k.app.deleteFrom('sys.idempotency').where('expires_at', '<', new Date()).execute();
81
+ await k.app.deleteFrom('sys.session').where('expires_at', '<', new Date()).execute();
82
+ await expireTransfers(k);
83
+ }
84
+ catch (e) {
85
+ log.warn({ err: String(e) }, '日常维护失败');
86
+ }
87
+ };
88
+ void housekeeping();
89
+ timers.push(setInterval(() => void housekeeping(), 3600_000));
90
+ // 定时任务
91
+ let lockConn = null;
92
+ if (opts.jobs !== false && k.registry.jobs.length > 0) {
93
+ lockConn = await dedicatedClient(appUrl, 'cage-scheduler');
94
+ const due = new Map(k.registry.jobs.map((j) => [j.name, nextRun(j.cron, new Date())]));
95
+ timers.push(setInterval(() => {
96
+ const now = new Date();
97
+ for (const j of k.registry.jobs) {
98
+ const at = due.get(j.name);
99
+ if (now < at)
100
+ continue;
101
+ due.set(j.name, nextRun(j.cron, now));
102
+ void tick(k, j, lockConn, at).catch((e) => log.error({ job: j.name, err: String(e) }, '调度失败'));
103
+ }
104
+ }, 5_000));
105
+ }
106
+ log.info({ consumers: w.consumers.map((c) => c.name), jobs: k.registry.jobs.map((j) => j.name) }, 'worker 已启动');
107
+ return {
108
+ ...w,
109
+ async stop() {
110
+ stopped = true;
111
+ wake?.();
112
+ for (const t of timers)
113
+ clearInterval(t);
114
+ await loop;
115
+ await listener.end().catch(() => { });
116
+ await lockConn?.end().catch(() => { });
117
+ if (!opts.kernel)
118
+ await k.close();
119
+ },
120
+ };
121
+ }
122
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { type Transaction } from 'kysely';
2
+ import type { Kernel } from '../kernel.js';
3
+ export interface OutboxRow {
4
+ id: number;
5
+ tenant_id: string;
6
+ event: string;
7
+ aggregate: string;
8
+ aggregate_id: string;
9
+ payload: Record<string, any>;
10
+ actor: {
11
+ kind?: string;
12
+ id?: string;
13
+ userId?: string | null;
14
+ onBehalfOf?: string | null;
15
+ } | null;
16
+ trace: Record<string, string> | null;
17
+ request_id: string | null;
18
+ status: string;
19
+ attempts: number;
20
+ }
21
+ /** 事件消费者:反应、出站映射、审批启动都实现这个接口。登记去重和业务写入在同一个事务里提交。 */
22
+ export interface EventConsumer {
23
+ /** 唯一名,用于 (事件 id, 消费者) 去重 */
24
+ name: string;
25
+ event: string;
26
+ handle(row: OutboxRow, tx: Transaction<any>, kernel: Kernel): Promise<void>;
27
+ }
28
+ export declare const MAX_ATTEMPTS = 10;
29
+ /** 指数退避:1 秒、2 秒、4 秒……最多 1 小时 */
30
+ export declare function backoffMs(attempts: number): number;
31
+ export declare function claimBatch(k: Kernel, limit: number): Promise<OutboxRow[]>;
32
+ export declare function deliver(k: Kernel, row: OutboxRow, consumers: EventConsumer[], maxAttempts?: number): Promise<'done' | 'retry' | 'dead'>;
33
+ /** 清扫:worker 崩溃后卡在「处理中」超过 5 分钟的事件重置为待投递 */
34
+ export declare function sweepStale(k: Kernel, olderThanMs?: number): Promise<number>;
35
+ /** 人工重放死信:仍然走内核流水线 */
36
+ export declare function replayDeadLetter(k: Kernel, deadLetterId: number, handledBy: string): Promise<void>;
37
+ /** 人工丢弃死信:标记完成,解除对同一聚合后续事件的阻塞 */
38
+ export declare function discardDeadLetter(k: Kernel, deadLetterId: number, handledBy: string): Promise<void>;
@@ -0,0 +1,121 @@
1
+ // outbox 投递器:多个 worker 实例同时领取,靠 FOR UPDATE SKIP LOCKED 避免重复领取;同一聚合的事件严格按顺序处理。
2
+ import { sql } from 'kysely';
3
+ import { isCageError } from '../errors.js';
4
+ import { withCarrier } from '../telemetry.js';
5
+ export const MAX_ATTEMPTS = 10;
6
+ /** 指数退避:1 秒、2 秒、4 秒……最多 1 小时 */
7
+ export function backoffMs(attempts) {
8
+ return Math.min(3600_000, 1000 * 2 ** Math.max(0, attempts - 1));
9
+ }
10
+ export async function claimBatch(k, limit) {
11
+ return k.app.transaction().execute(async (tx) => {
12
+ const { rows } = await sql `
13
+ select o.* from sys.outbox o
14
+ where o.status = 'pending'
15
+ and o.next_at <= now()
16
+ and not exists (
17
+ select 1 from sys.outbox p
18
+ where p.aggregate_id = o.aggregate_id
19
+ and p.id < o.id and p.status <> 'done')
20
+ order by o.id
21
+ limit ${limit}
22
+ for update skip locked`.execute(tx);
23
+ if (rows.length > 0) {
24
+ await tx
25
+ .updateTable('sys.outbox')
26
+ .set({ status: 'processing', locked_at: sql `now()` })
27
+ .where('id', 'in', rows.map((r) => r.id))
28
+ .execute();
29
+ }
30
+ return rows.map((r) => ({ ...r, id: Number(r.id) }));
31
+ });
32
+ }
33
+ /** 业务错误、状态不对、越权这类错误重试也不会成功,直接进死信 */
34
+ function isPermanent(err) {
35
+ const e = err;
36
+ if (e?.permanent === true)
37
+ return true;
38
+ if (e?.retryable === true)
39
+ return false;
40
+ if (isCageError(err))
41
+ return !['CONFLICT', 'INTERNAL', 'RATE_LIMITED'].includes(e.code);
42
+ // 带错误码的框架错误(声明不一致、载荷不符合声明等)重试也不会好
43
+ return /\[CAGE-/.test(String(e?.message ?? ''));
44
+ }
45
+ export async function deliver(k, row, consumers, maxAttempts = MAX_ATTEMPTS) {
46
+ const mine = consumers.filter((c) => c.event === row.event);
47
+ try {
48
+ await withCarrier(row.trace, async () => {
49
+ for (const c of mine) {
50
+ await k.app.transaction().execute(async (tx) => {
51
+ const first = await tx
52
+ .insertInto('sys.processed_event')
53
+ .values({ event_id: row.id, consumer: c.name })
54
+ .onConflict((oc) => oc.columns(['event_id', 'consumer']).doNothing())
55
+ .returning('event_id')
56
+ .executeTakeFirst();
57
+ if (!first)
58
+ return; // 处理过了:直接跳过,保证幂等
59
+ await c.handle(row, tx, k);
60
+ });
61
+ }
62
+ });
63
+ await k.app.updateTable('sys.outbox').set({ status: 'done', done_at: sql `now()`, locked_at: null }).where('id', '=', row.id).execute();
64
+ return 'done';
65
+ }
66
+ catch (err) {
67
+ const attempts = row.attempts + 1;
68
+ const message = isCageError(err) ? JSON.stringify(err.toJSON()) : String(err?.message ?? err);
69
+ if (attempts >= maxAttempts || isPermanent(err)) {
70
+ await k.app.transaction().execute(async (tx) => {
71
+ await tx
72
+ .insertInto('sys.dead_letter')
73
+ .values({ outbox_id: row.id, tenant_id: row.tenant_id, event: row.event, aggregate_id: row.aggregate_id, payload: JSON.stringify(row.payload), error: message, attempts })
74
+ .execute();
75
+ await tx.updateTable('sys.outbox').set({ status: 'dead', attempts, last_error: message, locked_at: null }).where('id', '=', row.id).execute();
76
+ });
77
+ k.log.error({ outboxId: row.id, event: row.event, err: message }, '事件进入死信');
78
+ return 'dead';
79
+ }
80
+ await k.app
81
+ .updateTable('sys.outbox')
82
+ .set({ status: 'pending', attempts, last_error: message, locked_at: null, next_at: new Date(Date.now() + backoffMs(attempts)) })
83
+ .where('id', '=', row.id)
84
+ .execute();
85
+ k.log.warn({ outboxId: row.id, event: row.event, attempts, err: message }, '事件投递失败,稍后重试');
86
+ return 'retry';
87
+ }
88
+ }
89
+ /** 清扫:worker 崩溃后卡在「处理中」超过 5 分钟的事件重置为待投递 */
90
+ export async function sweepStale(k, olderThanMs = 5 * 60_000) {
91
+ const r = await k.app
92
+ .updateTable('sys.outbox')
93
+ .set({ status: 'pending', locked_at: null })
94
+ .where('status', '=', 'processing')
95
+ .where('locked_at', '<', new Date(Date.now() - olderThanMs))
96
+ .executeTakeFirst();
97
+ return Number(r.numUpdatedRows);
98
+ }
99
+ /** 人工重放死信:仍然走内核流水线 */
100
+ export async function replayDeadLetter(k, deadLetterId, handledBy) {
101
+ await k.app.transaction().execute(async (tx) => {
102
+ const d = await tx.selectFrom('sys.dead_letter').selectAll().where('id', '=', deadLetterId).where('handled_at', 'is', null).forUpdate().executeTakeFirst();
103
+ if (!d)
104
+ throw new Error('[CAGE-DLQ-001] 死信不存在或已处理');
105
+ await tx.updateTable('sys.outbox').set({ status: 'pending', attempts: 0, next_at: sql `now()`, last_error: null }).where('id', '=', d.outbox_id).execute();
106
+ await tx.updateTable('sys.dead_letter').set({ handled_by: handledBy, handled_at: sql `now()`, resolution: 'replayed' }).where('id', '=', deadLetterId).execute();
107
+ await sql `select pg_notify('cage_outbox', '')`.execute(tx);
108
+ });
109
+ }
110
+ /** 人工丢弃死信:标记完成,解除对同一聚合后续事件的阻塞 */
111
+ export async function discardDeadLetter(k, deadLetterId, handledBy) {
112
+ await k.app.transaction().execute(async (tx) => {
113
+ const d = await tx.selectFrom('sys.dead_letter').selectAll().where('id', '=', deadLetterId).where('handled_at', 'is', null).forUpdate().executeTakeFirst();
114
+ if (!d)
115
+ throw new Error('[CAGE-DLQ-001] 死信不存在或已处理');
116
+ await tx.updateTable('sys.outbox').set({ status: 'done', done_at: sql `now()` }).where('id', '=', d.outbox_id).execute();
117
+ await tx.updateTable('sys.dead_letter').set({ handled_by: handledBy, handled_at: sql `now()`, resolution: 'discarded' }).where('id', '=', deadLetterId).execute();
118
+ await sql `select pg_notify('cage_outbox', '')`.execute(tx);
119
+ });
120
+ }
121
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../src/worker/outbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type pg from 'pg';
2
+ import type { Kernel } from '../kernel.js';
3
+ import type { Job, JobApi } from '../primitives/flow.js';
4
+ export declare function nextRun(cron: string, after: Date): Date;
5
+ export declare function jobApi(k: Kernel, j: Job, tenantId: string, now: Date): JobApi;
6
+ /** 执行一次任务(拿到锁才执行);返回是否执行了 */
7
+ export declare function tick(k: Kernel, j: Job, lockConn: pg.Client, scheduledAt: Date, force?: boolean): Promise<boolean>;
@@ -0,0 +1,77 @@
1
+ // 定时任务调度:多实例只跑一次(PostgreSQL 咨询锁 + 记录上次执行时间)。任务只能读数据和调用声明过的命令。
2
+ import { CronExpressionParser } from 'cron-parser';
3
+ import { sql } from 'kysely';
4
+ import { cageError } from '../errors.js';
5
+ import { systemCtx } from '../org.js';
6
+ import { scopedFind } from '../pipeline/load.js';
7
+ import { rowToSnapshot } from '../db/rows.js';
8
+ import { hash32 } from '../util/names.js';
9
+ export function nextRun(cron, after) {
10
+ return CronExpressionParser.parse(cron, { currentDate: after }).next().toDate();
11
+ }
12
+ export function jobApi(k, j, tenantId, now) {
13
+ const caps = j.calls.map((c) => k.registry.capabilityOf(c));
14
+ const ctx = systemCtx({ kind: 'job', name: j.name, tenantId, capabilities: caps });
15
+ const allowedReads = new Set(j.reads);
16
+ return {
17
+ tenantId,
18
+ now,
19
+ log: (msg, extra) => k.log.info({ job: j.name, ...extra }, msg),
20
+ query: {
21
+ async list(agg, where = {}, opts = {}) {
22
+ if (!allowedReads.has(agg))
23
+ throw new Error(`[CAGE-JOB-003] 任务 ${j.name} 读取了未声明的聚合 ${agg.name}`);
24
+ const { createdBefore, createdAfter, updatedBefore, ...eq } = where;
25
+ let q = scopedFind(k.app, agg, ctx, eq, { limit: opts.limit ?? 1000 });
26
+ if (createdBefore)
27
+ q = q.where('t.created_at', '<', new Date(createdBefore));
28
+ if (createdAfter)
29
+ q = q.where('t.created_at', '>=', new Date(createdAfter));
30
+ if (updatedBefore)
31
+ q = q.where('t.updated_at', '<', new Date(updatedBefore));
32
+ return (await q.execute()).map((r) => rowToSnapshot(agg, r, false));
33
+ },
34
+ },
35
+ async call(cmd, input) {
36
+ if (!j.calls.includes(cmd))
37
+ throw cageError('FORBIDDEN', { message: `[CAGE-JOB-002] 任务 ${j.name} 调用了未声明的命令` });
38
+ return (await k.execute(cmd, input, ctx));
39
+ },
40
+ };
41
+ }
42
+ /** 执行一次任务(拿到锁才执行);返回是否执行了 */
43
+ export async function tick(k, j, lockConn, scheduledAt, force = false) {
44
+ const lockId = hash32(`cage.job.${j.name}`);
45
+ const r = await lockConn.query('select pg_try_advisory_lock($1) as ok', [lockId]);
46
+ if (!r.rows[0].ok)
47
+ return false; // 别的实例在跑:本实例跳过
48
+ try {
49
+ await k.app.insertInto('sys.job_state').values({ name: j.name }).onConflict((oc) => oc.column('name').doNothing()).execute();
50
+ const st = await k.app.selectFrom('sys.job_state').selectAll().where('name', '=', j.name).executeTakeFirstOrThrow();
51
+ if (!st.enabled && !force)
52
+ return false; // 管理员在 /admin 停用了
53
+ if (!force && st.last_run_at && new Date(st.last_run_at) >= scheduledAt)
54
+ return false; // 这一轮别的实例已经跑过
55
+ const tenants = await k.app.selectFrom('sys.tenant').select('id').execute();
56
+ let error = null;
57
+ for (const t of tenants) {
58
+ try {
59
+ await j.run(jobApi(k, j, t.id, new Date()));
60
+ }
61
+ catch (e) {
62
+ error = String(e?.message ?? e);
63
+ k.log.error({ job: j.name, tenant: t.id, err: error }, '定时任务失败');
64
+ }
65
+ }
66
+ await k.app
67
+ .updateTable('sys.job_state')
68
+ .set({ last_run_at: force ? new Date() : scheduledAt, last_status: error ? 'failed' : 'ok', last_error: error, updated_at: sql `now()` })
69
+ .where('name', '=', j.name)
70
+ .execute();
71
+ return true;
72
+ }
73
+ finally {
74
+ await lockConn.query('select pg_advisory_unlock($1)', [lockId]); // 加锁和解锁必须在同一个连接上
75
+ }
76
+ }
77
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/worker/scheduler.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@yss123/cage-kernel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "笼子内核:声明原语、写入流水线、范围组合子、读侧、worker 运行时",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./worker": {
14
+ "types": "./dist/worker/index.d.ts",
15
+ "default": "./dist/worker/index.js"
16
+ },
17
+ "./brands": {
18
+ "types": "./dist/brands.d.ts",
19
+ "default": "./dist/brands.js"
20
+ },
21
+ "./db": {
22
+ "types": "./dist/db/index.d.ts",
23
+ "default": "./dist/db/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@opentelemetry/api": "^1.9.0",
31
+ "cron-parser": "^5.3.1",
32
+ "kysely": "^0.28.5",
33
+ "pg": "^8.16.3",
34
+ "zod": "^4.1.5"
35
+ },
36
+ "devDependencies": {
37
+ "@types/pg": "^8.15.5",
38
+ "fast-check": "^4.3.0"
39
+ },
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/shunshunyue/cage.git",
44
+ "directory": "packages/kernel"
45
+ },
46
+ "homepage": "https://github.com/shunshunyue/cage#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/shunshunyue/cage/issues"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org/"
53
+ },
54
+ "engines": {
55
+ "node": ">=24"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc -p tsconfig.json",
59
+ "typecheck": "tsc -p tsconfig.json --noEmit"
60
+ }
6
61
  }