@substrat-run/adapter-cloudflare 0.39.0 → 0.41.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.
- package/dist/control-plane-do.d.ts +52 -15
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +182 -35
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +30 -3
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +214 -31
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/r2.d.ts +49 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +121 -0
- package/dist/r2.js.map +1 -0
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +188 -5
- package/dist/scope-do.js.map +1 -1
- package/dist/scope-sweeper-do.d.ts +139 -0
- package/dist/scope-sweeper-do.d.ts.map +1 -0
- package/dist/scope-sweeper-do.js +193 -0
- package/dist/scope-sweeper-do.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import type { ScopeId, TenantId } from '@substrat-run/contracts';
|
|
3
|
+
import type { ExecutorDrainReport, ScheduleRegistration, ScheduleRunReport, ScheduleSweepReport } from '@substrat-run/kernel';
|
|
4
|
+
/**
|
|
5
|
+
* The singleton's name: every caller reaches the ONE scope sweeper via
|
|
6
|
+
* `env.SWEEPER.get(env.SWEEPER.idFromName(SCOPE_SWEEPER_NAME))` — one roster,
|
|
7
|
+
* one alarm, one pass at a time for the whole deployment, the same guarantee
|
|
8
|
+
* `PLATFORM_SWEEPER_NAME` gives the directory-backed trigger.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SCOPE_SWEEPER_NAME = "scope-sweeper";
|
|
11
|
+
/**
|
|
12
|
+
* What one scope-local pass did. `scopes` is the roster size at the start of the
|
|
13
|
+
* pass — every entry is visited; `schedules.scopes` keeps the kernel report's
|
|
14
|
+
* narrower meaning (scopes a schedule actually fired or failed on).
|
|
15
|
+
*/
|
|
16
|
+
export interface ScopeSweepReport {
|
|
17
|
+
/** Scopes on the roster this pass — each one was drained and schedule-checked. */
|
|
18
|
+
scopes: number;
|
|
19
|
+
/** Executor-drain outcomes summed across the roster (the retry driver). */
|
|
20
|
+
drainTotals: ExecutorDrainReport;
|
|
21
|
+
/** The recurring-schedule outcomes summed across the roster (#383/#461). */
|
|
22
|
+
schedules: ScheduleSweepReport;
|
|
23
|
+
/** Per-unit failures; the pass records and steps over each, never aborts. */
|
|
24
|
+
errors: {
|
|
25
|
+
kind: 'drain' | 'schedule';
|
|
26
|
+
id: string;
|
|
27
|
+
error: string;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** One settled pass: the report, or the error that sank the whole pass. */
|
|
31
|
+
export type ScopeSweepOutcome = ScopeSweepReport | {
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The slice of `CloudflareScopeHost` a scope-local pass drives — structural, so
|
|
36
|
+
* tests can hand in a fake and the DO depends on no host internals. All three
|
|
37
|
+
* are CP-less-safe: `drainDue` gates through `validateScopeAccess` (a null-CP
|
|
38
|
+
* passthrough) and `runDueSchedules` skips the directory read on a CP-less host
|
|
39
|
+
* (#461); neither needs a platform actor.
|
|
40
|
+
*/
|
|
41
|
+
export interface ScopeSweepHost {
|
|
42
|
+
drainDue(tenantId: TenantId, scopeId: ScopeId): Promise<ExecutorDrainReport>;
|
|
43
|
+
registeredSchedules(): ScheduleRegistration[];
|
|
44
|
+
runDueSchedules(moduleId: ScheduleRegistration['moduleId'], tenantId: TenantId, scopeId: ScopeId): Promise<ScheduleRunReport>;
|
|
45
|
+
}
|
|
46
|
+
export interface ScopeSweeperDoConfig<Env> {
|
|
47
|
+
/**
|
|
48
|
+
* Milliseconds between the END of one pass and the START of the next — a gap,
|
|
49
|
+
* not a fixed rate. The next alarm is set only once the current pass settles,
|
|
50
|
+
* so a slow pass delays the next rather than stacking on it.
|
|
51
|
+
*/
|
|
52
|
+
intervalMs: number;
|
|
53
|
+
/**
|
|
54
|
+
* The deployment's host — build it from the env's bindings per invocation,
|
|
55
|
+
* never cached (a `CloudflareScopeHost` holds DO stubs, which are owned by
|
|
56
|
+
* the event that created them). CODE-TIME, closed over like `defineScopeDO`'s
|
|
57
|
+
* module set: a Durable Object cannot receive closures over RPC, and the
|
|
58
|
+
* worker entry that exports this class is the one place that legitimately
|
|
59
|
+
* assembles the module registry.
|
|
60
|
+
*/
|
|
61
|
+
host(env: Env): ScopeSweepHost;
|
|
62
|
+
/**
|
|
63
|
+
* Also drain each scope's due executor deliveries (the retry driver) — the
|
|
64
|
+
* other half of the vertical's own recurring work, with the same "no directory
|
|
65
|
+
* to enumerate" gap as schedules. Default `true`; set `false` to sweep
|
|
66
|
+
* schedules only.
|
|
67
|
+
*/
|
|
68
|
+
drainRetries?: boolean;
|
|
69
|
+
/** Max scopes worked concurrently per pass. Default 8. */
|
|
70
|
+
concurrency?: number;
|
|
71
|
+
/** Observe each pass — logging or a health metric. Never throws into the loop. */
|
|
72
|
+
onPass?(outcome: ScopeSweepOutcome, env: Env): void;
|
|
73
|
+
}
|
|
74
|
+
/** The RPC surface a `defineScopeSweeperDO` class exposes over its stub. */
|
|
75
|
+
export interface ScopeSweeperDo {
|
|
76
|
+
/**
|
|
77
|
+
* Put one scope on the roster and (re)arm the loop. Idempotent — call it on
|
|
78
|
+
* every `/internal/provision` and `/internal/reconcile`; re-noting a known
|
|
79
|
+
* scope is a cheap overwrite.
|
|
80
|
+
*/
|
|
81
|
+
noteScope(tenantId: TenantId, scopeId: ScopeId): Promise<{
|
|
82
|
+
scopes: number;
|
|
83
|
+
}>;
|
|
84
|
+
/** Drop one scope from the roster (the `/internal/delete-scope` mirror). */
|
|
85
|
+
forgetScope(scopeId: ScopeId): Promise<{
|
|
86
|
+
scopes: number;
|
|
87
|
+
}>;
|
|
88
|
+
/** Arm the loop if it is not armed — idempotent, safe on every request. */
|
|
89
|
+
ensureArmed(): Promise<{
|
|
90
|
+
armed: boolean;
|
|
91
|
+
alarmAt: number;
|
|
92
|
+
}>;
|
|
93
|
+
/** Run a pass now (joining any in-flight pass), then re-arm. */
|
|
94
|
+
sweepNow(): Promise<ScopeSweepOutcome>;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* `defineScopeSweeperDO` — the timer a CP-LESS vertical owns (#461): a singleton
|
|
98
|
+
* Durable Object that keeps a roster of this deployment's scopes and, on an
|
|
99
|
+
* alarm, runs each one's due recurring work — `drainDue` (executor retries) and
|
|
100
|
+
* `runDueSchedules` (#383) — through the deployment's own host.
|
|
101
|
+
*
|
|
102
|
+
* Why it exists: `runPlatformSweep`'s schedule and drain phases enumerate scopes
|
|
103
|
+
* via the control-plane directory (`admin.listScopes`), which a CP-less host
|
|
104
|
+
* does not have — so a pushed vertical's declared schedules parsed, granted, and
|
|
105
|
+
* never ran (#461's "unfalsifiable zero"). The scopes a CP-less deployment
|
|
106
|
+
* serves are exactly the ones the platform provisions into it, so the roster IS
|
|
107
|
+
* knowable locally: `/internal/provision` and `/internal/reconcile` call
|
|
108
|
+
* `noteScope`, `/internal/delete-scope` calls `forgetScope`, and no directory is
|
|
109
|
+
* needed. Scopes provisioned before this sweeper shipped join the roster on
|
|
110
|
+
* their next platform reconcile (repair is the roster's backfill).
|
|
111
|
+
*
|
|
112
|
+
* Deliberately NOT noted: snapshot targets (`/internal/snapshot`) and other
|
|
113
|
+
* forks. The platform sweep excludes forks from schedules — recurring side
|
|
114
|
+
* effects must not run off a preview copy — and fork-ness lives only in the
|
|
115
|
+
* directory, so the CP-less roster keeps forks out by never learning them:
|
|
116
|
+
* provision and reconcile are called for primaries only. Do not "self-heal" the
|
|
117
|
+
* roster from routed request traffic — a routed preview would smuggle a fork in.
|
|
118
|
+
*
|
|
119
|
+
* Trigger mechanics — alarm, singleton, non-overlap, never-dies — are exactly
|
|
120
|
+
* `definePlatformSweeperDO`'s (see its docstring for why an alarm and not a
|
|
121
|
+
* cron; `platform-sweeper.test.ts` pins the shared loop shape, including a pass
|
|
122
|
+
* that sinks whole still re-arming). One difference: the alarm re-arms only
|
|
123
|
+
* while the roster is non-empty, so a deployment with no scopes costs nothing —
|
|
124
|
+
* `noteScope` (re)arms when the first scope arrives.
|
|
125
|
+
*
|
|
126
|
+
* Wire-up (the create-substrat template is the reference):
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* export const SweeperDO = defineScopeSweeperDO<Env>({
|
|
130
|
+
* intervalMs: 120_000,
|
|
131
|
+
* host: hostFor, // the same builder the routes use
|
|
132
|
+
* });
|
|
133
|
+
* // package.json substrat.runtimeNeeds.stores: add { binding: 'SWEEPER', class: 'SweeperDO' }
|
|
134
|
+
* // /internal/provision + /internal/reconcile → stub.noteScope(tenantId, scopeId)
|
|
135
|
+
* // /internal/delete-scope → stub.forgetScope(scopeId)
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare function defineScopeSweeperDO<Env>(config: ScopeSweeperDoConfig<Env>): new (ctx: DurableObjectState, env: Env) => DurableObject<Env> & ScopeSweeperDo;
|
|
139
|
+
//# sourceMappingURL=scope-sweeper-do.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-sweeper-do.d.ts","sourceRoot":"","sources":["../src/scope-sweeper-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAKlD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,WAAW,EAAE,mBAAmB,CAAC;IACjC,4EAA4E;IAC5E,SAAS,EAAE,mBAAmB,CAAC;IAC/B,6EAA6E;IAC7E,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,GAAG,UAAU,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrE;AAED,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC7E,mBAAmB,IAAI,oBAAoB,EAAE,CAAC;IAC9C,eAAe,CACb,QAAQ,EAAE,oBAAoB,CAAC,UAAU,CAAC,EAC1C,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB,CAAC,GAAG;IACvC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,cAAc,CAAC;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,MAAM,CAAC,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;CACrD;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,4EAA4E;IAC5E,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,2EAA2E;IAC3E,WAAW,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,gEAAgE;IAChE,QAAQ,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EACtC,MAAM,EAAE,oBAAoB,CAAC,GAAG,CAAC,GAChC,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,KAAK,aAAa,CAAC,GAAG,CAAC,GAAG,cAAc,CA2IhF"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import { scopeId as scopeIdSchema, tenantId as tenantIdSchema } from '@substrat-run/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The singleton's name: every caller reaches the ONE scope sweeper via
|
|
5
|
+
* `env.SWEEPER.get(env.SWEEPER.idFromName(SCOPE_SWEEPER_NAME))` — one roster,
|
|
6
|
+
* one alarm, one pass at a time for the whole deployment, the same guarantee
|
|
7
|
+
* `PLATFORM_SWEEPER_NAME` gives the directory-backed trigger.
|
|
8
|
+
*/
|
|
9
|
+
export const SCOPE_SWEEPER_NAME = 'scope-sweeper';
|
|
10
|
+
/** Roster keys in the sweeper's own storage: `scope:<scopeId>` → tenantId. */
|
|
11
|
+
const ROSTER_PREFIX = 'scope:';
|
|
12
|
+
/**
|
|
13
|
+
* `defineScopeSweeperDO` — the timer a CP-LESS vertical owns (#461): a singleton
|
|
14
|
+
* Durable Object that keeps a roster of this deployment's scopes and, on an
|
|
15
|
+
* alarm, runs each one's due recurring work — `drainDue` (executor retries) and
|
|
16
|
+
* `runDueSchedules` (#383) — through the deployment's own host.
|
|
17
|
+
*
|
|
18
|
+
* Why it exists: `runPlatformSweep`'s schedule and drain phases enumerate scopes
|
|
19
|
+
* via the control-plane directory (`admin.listScopes`), which a CP-less host
|
|
20
|
+
* does not have — so a pushed vertical's declared schedules parsed, granted, and
|
|
21
|
+
* never ran (#461's "unfalsifiable zero"). The scopes a CP-less deployment
|
|
22
|
+
* serves are exactly the ones the platform provisions into it, so the roster IS
|
|
23
|
+
* knowable locally: `/internal/provision` and `/internal/reconcile` call
|
|
24
|
+
* `noteScope`, `/internal/delete-scope` calls `forgetScope`, and no directory is
|
|
25
|
+
* needed. Scopes provisioned before this sweeper shipped join the roster on
|
|
26
|
+
* their next platform reconcile (repair is the roster's backfill).
|
|
27
|
+
*
|
|
28
|
+
* Deliberately NOT noted: snapshot targets (`/internal/snapshot`) and other
|
|
29
|
+
* forks. The platform sweep excludes forks from schedules — recurring side
|
|
30
|
+
* effects must not run off a preview copy — and fork-ness lives only in the
|
|
31
|
+
* directory, so the CP-less roster keeps forks out by never learning them:
|
|
32
|
+
* provision and reconcile are called for primaries only. Do not "self-heal" the
|
|
33
|
+
* roster from routed request traffic — a routed preview would smuggle a fork in.
|
|
34
|
+
*
|
|
35
|
+
* Trigger mechanics — alarm, singleton, non-overlap, never-dies — are exactly
|
|
36
|
+
* `definePlatformSweeperDO`'s (see its docstring for why an alarm and not a
|
|
37
|
+
* cron; `platform-sweeper.test.ts` pins the shared loop shape, including a pass
|
|
38
|
+
* that sinks whole still re-arming). One difference: the alarm re-arms only
|
|
39
|
+
* while the roster is non-empty, so a deployment with no scopes costs nothing —
|
|
40
|
+
* `noteScope` (re)arms when the first scope arrives.
|
|
41
|
+
*
|
|
42
|
+
* Wire-up (the create-substrat template is the reference):
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* export const SweeperDO = defineScopeSweeperDO<Env>({
|
|
46
|
+
* intervalMs: 120_000,
|
|
47
|
+
* host: hostFor, // the same builder the routes use
|
|
48
|
+
* });
|
|
49
|
+
* // package.json substrat.runtimeNeeds.stores: add { binding: 'SWEEPER', class: 'SweeperDO' }
|
|
50
|
+
* // /internal/provision + /internal/reconcile → stub.noteScope(tenantId, scopeId)
|
|
51
|
+
* // /internal/delete-scope → stub.forgetScope(scopeId)
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function defineScopeSweeperDO(config) {
|
|
55
|
+
return class ScopeSweeperDO extends DurableObject {
|
|
56
|
+
/** The in-flight pass, when one is running — the overlap guard. */
|
|
57
|
+
#pass = null;
|
|
58
|
+
async noteScope(tenantId, scopeId) {
|
|
59
|
+
// Parse, don't trust: RPC delivers plain strings; a malformed id must fail
|
|
60
|
+
// loudly here, not surface as a stuck roster entry that never sweeps.
|
|
61
|
+
const t = tenantIdSchema.parse(tenantId);
|
|
62
|
+
const s = scopeIdSchema.parse(scopeId);
|
|
63
|
+
await this.ctx.storage.put(`${ROSTER_PREFIX}${s}`, t);
|
|
64
|
+
await this.ensureArmed();
|
|
65
|
+
return { scopes: await this.#rosterSize() };
|
|
66
|
+
}
|
|
67
|
+
async forgetScope(scopeId) {
|
|
68
|
+
await this.ctx.storage.delete(`${ROSTER_PREFIX}${scopeIdSchema.parse(scopeId)}`);
|
|
69
|
+
// The alarm is left as-is: an empty-roster pass simply does not re-arm.
|
|
70
|
+
return { scopes: await this.#rosterSize() };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Arm the loop if it is not armed. Idempotent: an already-set alarm is left
|
|
74
|
+
* exactly where it is, so this is safe to call on every request.
|
|
75
|
+
*/
|
|
76
|
+
async ensureArmed() {
|
|
77
|
+
const existing = await this.ctx.storage.getAlarm();
|
|
78
|
+
if (existing !== null)
|
|
79
|
+
return { armed: false, alarmAt: existing };
|
|
80
|
+
const alarmAt = Date.now() + config.intervalMs;
|
|
81
|
+
await this.ctx.storage.setAlarm(alarmAt);
|
|
82
|
+
return { armed: true, alarmAt };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run a pass NOW (a manual kick, a just-provisioned scope's first tick, a
|
|
86
|
+
* test). Joins the in-flight pass if one is running — never a second one —
|
|
87
|
+
* and re-arms afterwards while scopes remain.
|
|
88
|
+
*/
|
|
89
|
+
async sweepNow() {
|
|
90
|
+
return this.#run();
|
|
91
|
+
}
|
|
92
|
+
/** The loop: one pass, then re-arm. Never throws (see the class doc). */
|
|
93
|
+
async alarm() {
|
|
94
|
+
await this.#run();
|
|
95
|
+
}
|
|
96
|
+
async #rosterSize() {
|
|
97
|
+
return (await this.ctx.storage.list({ prefix: ROSTER_PREFIX })).size;
|
|
98
|
+
}
|
|
99
|
+
async #run() {
|
|
100
|
+
if (this.#pass)
|
|
101
|
+
return this.#pass;
|
|
102
|
+
this.#pass = (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const report = await this.#sweep();
|
|
105
|
+
config.onPass?.(report, this.env);
|
|
106
|
+
return report;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const outcome = { error: err instanceof Error ? err.message : String(err) };
|
|
110
|
+
config.onPass?.(outcome, this.env);
|
|
111
|
+
return outcome;
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
try {
|
|
115
|
+
return await this.#pass;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.#pass = null;
|
|
119
|
+
// Gap, not rate — and only while there is anything to sweep: the alarm
|
|
120
|
+
// lapses on an empty roster, and `noteScope` restarts the loop.
|
|
121
|
+
if ((await this.#rosterSize()) > 0) {
|
|
122
|
+
await this.ctx.storage.setAlarm(Date.now() + config.intervalMs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** One pass over the roster: drain + due schedules per scope, errors contained. */
|
|
127
|
+
async #sweep() {
|
|
128
|
+
const host = config.host(this.env);
|
|
129
|
+
const roster = await this.ctx.storage.list({ prefix: ROSTER_PREFIX });
|
|
130
|
+
const report = {
|
|
131
|
+
scopes: roster.size,
|
|
132
|
+
drainTotals: { attempted: 0, delivered: 0, retrying: 0, deadLettered: 0 },
|
|
133
|
+
schedules: { scopes: 0, fired: 0, skipped: 0, failed: 0 },
|
|
134
|
+
errors: [],
|
|
135
|
+
};
|
|
136
|
+
const registrations = host.registeredSchedules().filter((r) => r.schedules.length > 0);
|
|
137
|
+
const entries = [...roster].map(([key, tenant]) => ({
|
|
138
|
+
scopeId: key.slice(ROSTER_PREFIX.length),
|
|
139
|
+
tenantId: tenant,
|
|
140
|
+
}));
|
|
141
|
+
const concurrency = config.concurrency ?? 8;
|
|
142
|
+
let next = 0;
|
|
143
|
+
const worker = async () => {
|
|
144
|
+
while (next < entries.length) {
|
|
145
|
+
const { tenantId, scopeId } = entries[next++];
|
|
146
|
+
if (config.drainRetries !== false) {
|
|
147
|
+
try {
|
|
148
|
+
const r = await host.drainDue(tenantId, scopeId);
|
|
149
|
+
report.drainTotals.attempted += r.attempted;
|
|
150
|
+
report.drainTotals.delivered += r.delivered;
|
|
151
|
+
report.drainTotals.retrying += r.retrying;
|
|
152
|
+
report.drainTotals.deadLettered += r.deadLettered;
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
report.errors.push({ kind: 'drain', id: scopeId, error: message(err) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
let touched = false;
|
|
159
|
+
for (const reg of registrations) {
|
|
160
|
+
try {
|
|
161
|
+
const r = await host.runDueSchedules(reg.moduleId, tenantId, scopeId);
|
|
162
|
+
if (r.fired > 0 || r.failed > 0)
|
|
163
|
+
touched = true;
|
|
164
|
+
report.schedules.fired += r.fired;
|
|
165
|
+
report.schedules.skipped += r.skipped;
|
|
166
|
+
report.schedules.failed += r.failed;
|
|
167
|
+
for (const e of r.errors) {
|
|
168
|
+
report.errors.push({
|
|
169
|
+
kind: 'schedule',
|
|
170
|
+
id: `${scopeId}:${e.operation}`,
|
|
171
|
+
error: e.error,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
report.errors.push({
|
|
177
|
+
kind: 'schedule',
|
|
178
|
+
id: `${scopeId}:${reg.moduleId}`,
|
|
179
|
+
error: message(err),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (touched)
|
|
184
|
+
report.schedules.scopes += 1;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, entries.length)) }, worker));
|
|
188
|
+
return report;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const message = (err) => (err instanceof Error ? err.message : String(err));
|
|
193
|
+
//# sourceMappingURL=scope-sweeper-do.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-sweeper-do.js","sourceRoot":"","sources":["../src/scope-sweeper-do.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@substrat-run/adapter-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"description": "Cloudflare Durable-Object scope host (D-14): DO-per-scope, real kernel semantics on Workers",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@substrat-run/kernel": "^0.
|
|
30
|
-
"@substrat-run/contracts": "^0.
|
|
29
|
+
"@substrat-run/kernel": "^0.41.0",
|
|
30
|
+
"@substrat-run/contracts": "^0.41.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@cloudflare/vitest-pool-workers": "^0.8.19",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"wrangler": "^4.110.0",
|
|
36
36
|
"typescript": "^7.0.0",
|
|
37
37
|
"vitest": "^3.0.0",
|
|
38
|
-
"@substrat-run/contract-tests": "^0.
|
|
38
|
+
"@substrat-run/contract-tests": "^0.41.0"
|
|
39
39
|
},
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|