@substrat-run/adapter-cloudflare 0.120.0 → 0.122.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 +153 -4
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +317 -41
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +103 -4
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +507 -89
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kick-coalescer-do.d.ts +57 -0
- package/dist/kick-coalescer-do.d.ts.map +1 -0
- package/dist/kick-coalescer-do.js +106 -0
- package/dist/kick-coalescer-do.js.map +1 -0
- package/dist/scope-do.d.ts +7 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +91 -27
- package/dist/scope-do.js.map +1 -1
- package/package.json +7 -5
package/dist/index.d.ts
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* driving `drainDue`/`runDueSchedules` per scope (#461)
|
|
15
15
|
*/
|
|
16
16
|
export { CloudflareScopeHost } from './host.js';
|
|
17
|
-
export type { CloudflareScopeHostOptions, ConnectorDelegation, EventDrainDelegation, PeerSwitchDelegation, SystemSwitchDelegation, } from './host.js';
|
|
17
|
+
export type { CloudflareScopeHostOptions, ConnectorDelegation, EventDrainDelegation, PeerSwitchDelegation, ImportCursorDelegation, SystemSwitchDelegation, } from './host.js';
|
|
18
18
|
export { defineScopeDO } from './scope-do.js';
|
|
19
19
|
export { ControlPlaneDO } from './control-plane-do.js';
|
|
20
20
|
export { OperationQueue } from './serialization.js';
|
|
21
21
|
export { doScopedSql } from './sql.js';
|
|
22
22
|
export { createDoTupleChecker } from './checker.js';
|
|
23
23
|
export { definePlatformSweeperDO, PLATFORM_SWEEPER_NAME } from './platform-sweeper-do.js';
|
|
24
|
+
export { defineKickCoalescerDO, kickCoalescerName, CROSS_VERTICAL_KICK_WINDOW_MS, type KickCoalescerConfig, type KickCoalescerDo, type KickOutcome, type KickProducer, } from './kick-coalescer-do.js';
|
|
24
25
|
export type { PlatformSweeperDoConfig, PlatformSweepOutcome } from './platform-sweeper-do.js';
|
|
25
26
|
export { defineScopeSweeperDO, SCOPE_SWEEPER_NAME } from './scope-sweeper-do.js';
|
|
26
27
|
export type { ScopeSweeperDo, ScopeSweeperDoConfig, ScopeSweepHost, ScopeSweepOutcome, ScopeSweepReport, } from './scope-sweeper-do.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,YAAY,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,YAAY,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAE1F,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,6BAA6B,EAC7B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACjF,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC7E,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export { OperationQueue } from './serialization.js';
|
|
|
20
20
|
export { doScopedSql } from './sql.js';
|
|
21
21
|
export { createDoTupleChecker } from './checker.js';
|
|
22
22
|
export { definePlatformSweeperDO, PLATFORM_SWEEPER_NAME } from './platform-sweeper-do.js';
|
|
23
|
+
// #1705 PR 2: the global bound on the router's cross-vertical kick, one object per producer.
|
|
24
|
+
export { defineKickCoalescerDO, kickCoalescerName, CROSS_VERTICAL_KICK_WINDOW_MS, } from './kick-coalescer-do.js';
|
|
23
25
|
export { defineScopeSweeperDO, SCOPE_SWEEPER_NAME } from './scope-sweeper-do.js';
|
|
24
26
|
export { createD1TenantStores, d1TenantRelationalStore, tenantStoreDatabaseName, } from './d1.js';
|
|
25
27
|
export { blobStoreBucketName, createR2BlobStores, r2TenantBlobStore, } from './r2.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAShD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC1F,6FAA6F;AAC7F,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,6BAA6B,GAK9B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAQjF,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
import type { ScopeId, TenantId } from '@substrat-run/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* How long one producer's cross-vertical kicks coalesce (#1705 PR 2): after a pass STARTS, the
|
|
5
|
+
* next may start no sooner than this. A burst inside the window costs the pass that started it
|
|
6
|
+
* plus one trailing pass at the window's end, fleet-wide. It is not per isolate, because one
|
|
7
|
+
* Durable Object per producer holds the bit.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CROSS_VERTICAL_KICK_WINDOW_MS = 5000;
|
|
10
|
+
/** What one kick did: started a pass, joined the one running, or left it for the window's end. */
|
|
11
|
+
export type KickOutcome = 'ran' | 'coalesced' | 'deferred';
|
|
12
|
+
/** The producer a kick names: the scope the router resolved, and its tenant. Nothing else. */
|
|
13
|
+
export interface KickProducer {
|
|
14
|
+
tenantId: TenantId;
|
|
15
|
+
scopeId: ScopeId;
|
|
16
|
+
}
|
|
17
|
+
/** The RPC surface a `defineKickCoalescerDO` class exposes over its stub. */
|
|
18
|
+
export interface KickCoalescerDo {
|
|
19
|
+
kick(tenantId: TenantId, scopeId: ScopeId): Promise<KickOutcome>;
|
|
20
|
+
}
|
|
21
|
+
export interface KickCoalescerConfig<Env> {
|
|
22
|
+
/** Default {@link CROSS_VERTICAL_KICK_WINDOW_MS}. */
|
|
23
|
+
windowMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* One pass for this producer. CODE-TIME, closed over like `definePlatformSweeperDO`'s
|
|
26
|
+
* `sweep`, since a Durable Object cannot receive a closure over RPC. It is where the
|
|
27
|
+
* authority lives: the deployment runs `runCrossVerticalFrom` here with the same reach and
|
|
28
|
+
* gates as its sweep, so this object decides only WHEN, never what or for whom.
|
|
29
|
+
*/
|
|
30
|
+
run(env: Env, producer: KickProducer): Promise<void>;
|
|
31
|
+
/** A pass that threw. Never rethrown: a lost kick costs latency, and the sweep is the backstop. */
|
|
32
|
+
onError?(err: unknown, env: Env): void;
|
|
33
|
+
}
|
|
34
|
+
/** The one instance per producer: `ns.get(ns.idFromName(kickCoalescerName(tenantId, scopeId)))`. */
|
|
35
|
+
export declare function kickCoalescerName(tenantId: TenantId, scopeId: ScopeId): string;
|
|
36
|
+
/**
|
|
37
|
+
* `defineKickCoalescerDO` — the global bound on the router kick (#1705 PR 2).
|
|
38
|
+
*
|
|
39
|
+
* The kick is raised by a response header that tenant code sets, so a vertical can raise it on
|
|
40
|
+
* every response it serves. Each one is a producer-scoped cross-vertical pass: a directory read,
|
|
41
|
+
* registry reads, and up to `maxConsumers × (1 + 2 × sources)` `/internal` calls. The phase caps
|
|
42
|
+
* one pass, not passes per unit of time. This object is that second cap, and it holds nothing
|
|
43
|
+
* else: no authority and no data, only a start time and a dirty bit.
|
|
44
|
+
*
|
|
45
|
+
* - A kick with no pass running and none started within the window starts one, and waits for it.
|
|
46
|
+
* - A kick while a pass runs marks the producer dirty and returns at once. When the pass ends,
|
|
47
|
+
* a dirty producer gets one trailing pass at the window's end, by alarm.
|
|
48
|
+
* - A kick inside the window marks it dirty and returns at once. The same alarm runs the
|
|
49
|
+
* trailing pass.
|
|
50
|
+
*
|
|
51
|
+
* So passes for one producer start at least `windowMs` apart. A burst of N kicks costs one pass
|
|
52
|
+
* plus one trailing pass, and nothing a tenant sends can raise that. The trailing pass is what
|
|
53
|
+
* keeps coalescing lossless: an event committed after the first pass read its outbox is still
|
|
54
|
+
* moved within one window, not left for the next sweep.
|
|
55
|
+
*/
|
|
56
|
+
export declare function defineKickCoalescerDO<Env>(config: KickCoalescerConfig<Env>): new (ctx: DurableObjectState, env: Env) => DurableObject<Env> & KickCoalescerDo;
|
|
57
|
+
//# sourceMappingURL=kick-coalescer-do.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kick-coalescer-do.d.ts","sourceRoot":"","sources":["../src/kick-coalescer-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,OAAQ,CAAC;AAEnD,kGAAkG;AAClG,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,CAAC;AAE3D,8FAA8F;AAC9F,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,mBAAmB,CAAC,GAAG;IACtC,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,mGAAmG;IACnG,OAAO,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;CACxC;AAED,oGAAoG;AACpG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAE9E;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EACvC,MAAM,EAAE,mBAAmB,CAAC,GAAG,CAAC,GAC/B,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG,KAAK,aAAa,CAAC,GAAG,CAAC,GAAG,eAAe,CAwEjF"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
/**
|
|
3
|
+
* How long one producer's cross-vertical kicks coalesce (#1705 PR 2): after a pass STARTS, the
|
|
4
|
+
* next may start no sooner than this. A burst inside the window costs the pass that started it
|
|
5
|
+
* plus one trailing pass at the window's end, fleet-wide. It is not per isolate, because one
|
|
6
|
+
* Durable Object per producer holds the bit.
|
|
7
|
+
*/
|
|
8
|
+
export const CROSS_VERTICAL_KICK_WINDOW_MS = 5_000;
|
|
9
|
+
/** The one instance per producer: `ns.get(ns.idFromName(kickCoalescerName(tenantId, scopeId)))`. */
|
|
10
|
+
export function kickCoalescerName(tenantId, scopeId) {
|
|
11
|
+
return `${tenantId}:${scopeId}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* `defineKickCoalescerDO` — the global bound on the router kick (#1705 PR 2).
|
|
15
|
+
*
|
|
16
|
+
* The kick is raised by a response header that tenant code sets, so a vertical can raise it on
|
|
17
|
+
* every response it serves. Each one is a producer-scoped cross-vertical pass: a directory read,
|
|
18
|
+
* registry reads, and up to `maxConsumers × (1 + 2 × sources)` `/internal` calls. The phase caps
|
|
19
|
+
* one pass, not passes per unit of time. This object is that second cap, and it holds nothing
|
|
20
|
+
* else: no authority and no data, only a start time and a dirty bit.
|
|
21
|
+
*
|
|
22
|
+
* - A kick with no pass running and none started within the window starts one, and waits for it.
|
|
23
|
+
* - A kick while a pass runs marks the producer dirty and returns at once. When the pass ends,
|
|
24
|
+
* a dirty producer gets one trailing pass at the window's end, by alarm.
|
|
25
|
+
* - A kick inside the window marks it dirty and returns at once. The same alarm runs the
|
|
26
|
+
* trailing pass.
|
|
27
|
+
*
|
|
28
|
+
* So passes for one producer start at least `windowMs` apart. A burst of N kicks costs one pass
|
|
29
|
+
* plus one trailing pass, and nothing a tenant sends can raise that. The trailing pass is what
|
|
30
|
+
* keeps coalescing lossless: an event committed after the first pass read its outbox is still
|
|
31
|
+
* moved within one window, not left for the next sweep.
|
|
32
|
+
*/
|
|
33
|
+
export function defineKickCoalescerDO(config) {
|
|
34
|
+
const windowMs = config.windowMs ?? CROSS_VERTICAL_KICK_WINDOW_MS;
|
|
35
|
+
return class KickCoalescerDO extends DurableObject {
|
|
36
|
+
/** The in-flight pass, when one is running. */
|
|
37
|
+
#running = null;
|
|
38
|
+
async kick(tenantId, scopeId) {
|
|
39
|
+
const prior = await this.ctx.storage.get('state');
|
|
40
|
+
const state = prior ?? { producer: { tenantId, scopeId }, lastStartedAt: 0, dirty: false };
|
|
41
|
+
if (this.#running) {
|
|
42
|
+
await this.ctx.storage.put('state', { ...state, dirty: true });
|
|
43
|
+
return 'coalesced';
|
|
44
|
+
}
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
if (now - state.lastStartedAt < windowMs) {
|
|
47
|
+
await this.ctx.storage.put('state', { ...state, dirty: true });
|
|
48
|
+
await this.#armFor(state.lastStartedAt);
|
|
49
|
+
return 'deferred';
|
|
50
|
+
}
|
|
51
|
+
await this.#pass(state);
|
|
52
|
+
return 'ran';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The window's end: the trailing pass if a kick arrived that no pass has covered, and
|
|
56
|
+
* otherwise the end of this object. Its storage is deleted, so a producer that kicked once
|
|
57
|
+
* leaves nothing behind. That waits for the window's end, so a kick after the delete may run
|
|
58
|
+
* at once without breaking the bound.
|
|
59
|
+
*/
|
|
60
|
+
async alarm() {
|
|
61
|
+
const state = await this.ctx.storage.get('state');
|
|
62
|
+
if (!state || this.#running)
|
|
63
|
+
return;
|
|
64
|
+
// The window holds even against an early alarm: re-arm for its end rather than act.
|
|
65
|
+
if (Date.now() - state.lastStartedAt < windowMs) {
|
|
66
|
+
await this.#armFor(state.lastStartedAt);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (state.dirty) {
|
|
70
|
+
await this.#pass(state);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await this.ctx.storage.deleteAlarm();
|
|
74
|
+
await this.ctx.storage.deleteAll();
|
|
75
|
+
}
|
|
76
|
+
async #pass(state) {
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
await this.ctx.storage.put('state', { ...state, lastStartedAt: startedAt, dirty: false });
|
|
79
|
+
this.#running = (async () => {
|
|
80
|
+
try {
|
|
81
|
+
await config.run(this.env, state.producer);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
config.onError?.(err, this.env);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
try {
|
|
88
|
+
await this.#running;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.#running = null;
|
|
92
|
+
// Always, at this window's end: the alarm runs the trailing pass if a kick arrived since
|
|
93
|
+
// this one started, and clears this object's storage if none did.
|
|
94
|
+
await this.#armFor(startedAt);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Arm the trailing pass for the end of the window that started at `startedAt`. */
|
|
98
|
+
async #armFor(startedAt) {
|
|
99
|
+
const at = startedAt + windowMs;
|
|
100
|
+
const existing = await this.ctx.storage.getAlarm();
|
|
101
|
+
if (existing === null || existing > at)
|
|
102
|
+
await this.ctx.storage.setAlarm(at);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=kick-coalescer-do.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kick-coalescer-do.js","sourceRoot":"","sources":["../src/kick-coalescer-do.ts"],"names":[],"mappings":"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"}
|
package/dist/scope-do.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
2
|
import { type WireFailure } from '@substrat-run/contracts';
|
|
3
|
-
import { type ModuleRegistration, type OperationHandler } from '@substrat-run/kernel';
|
|
3
|
+
import { type SwitchSql, type ModuleRegistration, type OperationHandler } from '@substrat-run/kernel';
|
|
4
4
|
/**
|
|
5
5
|
* `defineScopeDO` — one Durable Object per scope, the CF analogue of a single
|
|
6
6
|
* `SqliteScopeHost` scope runtime (D-14). It closes over a CODE-TIME module set
|
|
@@ -42,6 +42,12 @@ export type CapabilityAttachmentReply<T> = {
|
|
|
42
42
|
} | {
|
|
43
43
|
failure: WireFailure;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* The kernel's two-method SQL handle (`SwitchSql`) over a Durable Object's storage — what the
|
|
47
|
+
* shared switch rules (#1666) and the directory's switch record (#1674) run through. One
|
|
48
|
+
* wrapper for both DOs, as the pure adapter has one `switchSqlOf`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function switchSqlOver(sql: SqlStorage): SwitchSql;
|
|
45
51
|
export declare function splitSqlStatements(sql: string): string[];
|
|
46
52
|
export declare function defineScopeDO(modules: ModuleRegistration[], bareOps: Record<string, OperationHandler<never, unknown>>): new (ctx: DurableObjectState, env: ScopeDoEnv) => DurableObject;
|
|
47
53
|
//# sourceMappingURL=scope-do.d.ts.map
|
package/dist/scope-do.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAWL,KAAK,WAAW,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAWL,KAAK,WAAW,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAiCL,KAAK,SAAS,EAkBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAgEtB,MAAM,sBAAsB,CAAC;AA+E9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwaD;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAgDxG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CA0gJjE"}
|
package/dist/scope-do.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DurableObject } from 'cloudflare:workers';
|
|
2
2
|
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, toWireFailure, grantRefFromProof, principalId, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, requestFingerprint, substratError, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
3
|
-
import { ulid, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl,
|
|
3
|
+
import { ulid, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, IDEMPOTENCY_DDL, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, jobRunListLimit, } from '@substrat-run/kernel';
|
|
4
4
|
import { isUpgradeRequest, readSubscription, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, } from './live-reads.js';
|
|
5
5
|
import { OperationQueue } from './serialization.js';
|
|
6
6
|
import { doScopedSql, doSpineSql } from './sql.js';
|
|
@@ -414,6 +414,19 @@ function cellToJson(v) {
|
|
|
414
414
|
*/
|
|
415
415
|
const IS_CREATE_TRIGGER = /^\s*CREATE\s+(TEMP\s+|TEMPORARY\s+)?TRIGGER\b/i;
|
|
416
416
|
const ENDS_WITH_END = /\bEND\s*$/i;
|
|
417
|
+
/**
|
|
418
|
+
* The kernel's two-method SQL handle (`SwitchSql`) over a Durable Object's storage — what the
|
|
419
|
+
* shared switch rules (#1666) and the directory's switch record (#1674) run through. One
|
|
420
|
+
* wrapper for both DOs, as the pure adapter has one `switchSqlOf`.
|
|
421
|
+
*/
|
|
422
|
+
export function switchSqlOver(sql) {
|
|
423
|
+
return {
|
|
424
|
+
all: (q, ...params) => sql.exec(q, ...params).toArray(),
|
|
425
|
+
run: (q, ...params) => {
|
|
426
|
+
sql.exec(q, ...params);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
417
430
|
export function splitSqlStatements(sql) {
|
|
418
431
|
const out = [];
|
|
419
432
|
let cur = '';
|
|
@@ -601,11 +614,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
601
614
|
this.crossVertical.register(manifest, registration.imports);
|
|
602
615
|
this.modules.set(manifest.id, {
|
|
603
616
|
id: manifest.id,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
...searchIndexMigrations(manifest.id, manifest.searchables),
|
|
607
|
-
...listIndexMigrations(manifest.id, manifest.lists),
|
|
608
|
-
],
|
|
617
|
+
// The order the kernel writes once (#1677): authored, then search, then list indexes.
|
|
618
|
+
migrations: moduleMigrations(registration),
|
|
609
619
|
consumers: Object.entries(registration.consumers ?? {}).map(([eventType, handler]) => ({
|
|
610
620
|
eventType,
|
|
611
621
|
handler,
|
|
@@ -889,6 +899,35 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
889
899
|
}));
|
|
890
900
|
return { consumes, cursors };
|
|
891
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* #1705 PR 2: does this DO hold a scope PROVISIONED here for `tenantId`?
|
|
904
|
+
*
|
|
905
|
+
* A CP-less deployment has no directory to ask whether it serves a scope. What it does
|
|
906
|
+
* have is what provisioning wrote: `provisionScopeLocal` projects the vertical's role
|
|
907
|
+
* definitions under the tenant (`_substrat_roles`), and a restore re-projects them. A scope
|
|
908
|
+
* this deployment never provisioned has none of that. Its DO is empty, and a cross-vertical
|
|
909
|
+
* verb answering from it is a wrong answer rather than a failure: a watermark of "never
|
|
910
|
+
* read", or a delivery journaled into a scope that is not the install. A scope provisioned
|
|
911
|
+
* for ANOTHER tenant has roles under that tenant's id only, so the same read is K-3's pair
|
|
912
|
+
* check too.
|
|
913
|
+
*
|
|
914
|
+
* Read without migrating, so asking about a foreign scope leaves its DO as empty as it found it.
|
|
915
|
+
*/
|
|
916
|
+
async servesTenant(tenantId) {
|
|
917
|
+
const hasRoles = this.sql.exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_substrat_roles'`).toArray().length > 0;
|
|
918
|
+
if (!hasRoles)
|
|
919
|
+
return false;
|
|
920
|
+
return this.sql.exec('SELECT 1 FROM _substrat_roles WHERE tenant_id = ? LIMIT 1', tenantId).toArray().length > 0;
|
|
921
|
+
}
|
|
922
|
+
/** #1705 PR 2: the outbox's insertion mark (`OUTBOX_MARK_SQL`). */
|
|
923
|
+
outboxMark() {
|
|
924
|
+
return Number(this.sql.exec(OUTBOX_MARK_SQL).toArray()[0]?.mark ?? 0);
|
|
925
|
+
}
|
|
926
|
+
/** #1705 PR 2: exported-type rows added above `mark`. */
|
|
927
|
+
exportedSince(types, mark) {
|
|
928
|
+
const q = exportedSinceQuery(types, mark);
|
|
929
|
+
return Number(this.sql.exec(q.sql, ...q.params).toArray()[0]?.n ?? 0);
|
|
930
|
+
}
|
|
892
931
|
/**
|
|
893
932
|
* #1705: apply a batch another vertical exported. The pure adapter's `deliverToPeer`
|
|
894
933
|
* body, on this scope's queue: the compare-and-set on the watermark, a dead letter per
|
|
@@ -1002,10 +1041,13 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1002
1041
|
// on it would claim twice the egress it performed, and would drift again
|
|
1003
1042
|
// with any future index. The queue has this scope to itself, so a count
|
|
1004
1043
|
// taken here is the count the loop below goes on to change.
|
|
1005
|
-
|
|
1044
|
+
//
|
|
1045
|
+
// The ids travel as ONE JSON array (#1776): a DO binds at most 100 parameters, and a
|
|
1046
|
+
// drain batch is 200 by default, so one `?` per id failed every default drain. `IN` over
|
|
1047
|
+
// a subquery is a set test, as the list was, so a repeated id is still counted once.
|
|
1006
1048
|
const drained = this.sql
|
|
1007
1049
|
.exec(`SELECT COUNT(*) AS c FROM _substrat_outbox
|
|
1008
|
-
WHERE drained_at IS NULL AND id IN (
|
|
1050
|
+
WHERE drained_at IS NULL AND id IN (SELECT value FROM json_each(?))`, JSON.stringify(eventIds))
|
|
1009
1051
|
.toArray()[0].c;
|
|
1010
1052
|
for (const id of eventIds) {
|
|
1011
1053
|
// Only an UNDRAINED row is stamped, so a replayed batch cannot move an
|
|
@@ -1033,17 +1075,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1033
1075
|
*/
|
|
1034
1076
|
async redrainEvents(drainedBefore) {
|
|
1035
1077
|
return await this.queue.enqueue(() => {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1078
|
+
// The batch is chosen by ONE subquery, written once, that both statements read. It
|
|
1079
|
+
// used to be chosen as a list of ids bound back one `?` each, which a DO refuses past
|
|
1080
|
+
// 100 parameters (#1776) while the batch is 5000. The two statements run in the same
|
|
1081
|
+
// synchronous turn, so nothing writes between them, and `ORDER BY id` over the primary
|
|
1082
|
+
// key makes the LIMIT pick the same rows twice: the count is the rows the update clears.
|
|
1083
|
+
const batch = `SELECT id FROM _substrat_outbox
|
|
1084
|
+
WHERE drained_at IS NOT NULL AND drained_at < ?
|
|
1085
|
+
ORDER BY id LIMIT ?`;
|
|
1086
|
+
const reopened = this.sql.exec(`SELECT COUNT(*) AS c FROM (${batch})`, drainedBefore, REDRAIN_BATCH).toArray()[0].c;
|
|
1087
|
+
if (reopened === 0)
|
|
1042
1088
|
return 0;
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.sql.exec(`UPDATE _substrat_outbox SET drained_at = NULL WHERE id IN (${ids.map(() => '?').join(',')})`, ...ids.map((r) => r.id));
|
|
1046
|
-
return ids.length;
|
|
1089
|
+
this.sql.exec(`UPDATE _substrat_outbox SET drained_at = NULL WHERE id IN (${batch})`, drainedBefore, REDRAIN_BATCH);
|
|
1090
|
+
return reopened;
|
|
1047
1091
|
});
|
|
1048
1092
|
}
|
|
1049
1093
|
/**
|
|
@@ -1459,6 +1503,11 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1459
1503
|
* which is the floor this whole surface sits on.
|
|
1460
1504
|
*/
|
|
1461
1505
|
const liveSince = this.liveHighWaterMark();
|
|
1506
|
+
// #1705 PR 2: the outbox's insertion mark, so the envelope can say whether this
|
|
1507
|
+
// invoke (or a consumer in its tail) committed an exported type. Only in a deployment
|
|
1508
|
+
// that exports something, and never for a read-only session, which commits nothing.
|
|
1509
|
+
const exportTypes = impersonation?.mode === 'read-only' ? [] : this.crossVertical.exportTypes();
|
|
1510
|
+
const exportMark = exportTypes.length > 0 ? this.outboxMark() : null;
|
|
1462
1511
|
let result;
|
|
1463
1512
|
let committedVersion = null;
|
|
1464
1513
|
// #116: set when this invocation was answered from a recording rather
|
|
@@ -1594,9 +1643,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1594
1643
|
// call's own work — the same tail its consumers' emits are stamped in.
|
|
1595
1644
|
if (!replayed)
|
|
1596
1645
|
await this.settleCommitted(tenantId, scopeId, liveSince, this.invocationId);
|
|
1646
|
+
// After the tail, so an exported type a consumer emitted in it counts too.
|
|
1647
|
+
const exported = exportMark !== null && !replayed ? this.exportedSince(exportTypes, exportMark) : 0;
|
|
1597
1648
|
return {
|
|
1598
1649
|
result,
|
|
1599
1650
|
platformRequests: signals.platformRequests,
|
|
1651
|
+
// #1705 PR 2: the coordinator fires `onExportedEvents` from this. Omitted at 0, so
|
|
1652
|
+
// the envelope of a deployment that exports nothing is byte-for-byte what it was.
|
|
1653
|
+
...(exported > 0 ? { exported } : {}),
|
|
1600
1654
|
...(impersonation ? { impersonation: { honoured: true } } : {}),
|
|
1601
1655
|
// #1672: the acknowledgement the coordinator's skew check reads — see `invoke`.
|
|
1602
1656
|
...(capabilitySession !== undefined ? { capability: { honoured: true } } : {}),
|
|
@@ -2166,12 +2220,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2166
2220
|
}
|
|
2167
2221
|
/** The kernel's schedule-switch SQL (#1666), over this DO's storage. */
|
|
2168
2222
|
switchSql() {
|
|
2169
|
-
return
|
|
2170
|
-
all: (sql, ...params) => this.sql.exec(sql, ...params).toArray(),
|
|
2171
|
-
run: (sql, ...params) => {
|
|
2172
|
-
this.sql.exec(sql, ...params);
|
|
2173
|
-
},
|
|
2174
|
-
};
|
|
2223
|
+
return switchSqlOver(this.sql);
|
|
2175
2224
|
}
|
|
2176
2225
|
/**
|
|
2177
2226
|
* Where a module's schedules stand on this scope (#383, #1666) — the kernel's
|
|
@@ -2215,6 +2264,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2215
2264
|
async switchSystemSchedules(moduleId, scopeId, to, at) {
|
|
2216
2265
|
return this.queue.enqueue(() => this.ctx.storage.transactionSync(() => switchSystemSchedules(this.switchSql(), { moduleId, scopeId, to, at })));
|
|
2217
2266
|
}
|
|
2267
|
+
/**
|
|
2268
|
+
* The replay lever on this scope (#1705 PR 3): the kernel's `moveImportCursor`, queued with
|
|
2269
|
+
* every `importApply` on this scope and run as one `transactionSync`. A delivery on the same
|
|
2270
|
+
* edge is therefore wholly before the move, or refused by its compare-and-set after it.
|
|
2271
|
+
* Migrated first: the rows a replay moves aside go to `_substrat_import_replays`.
|
|
2272
|
+
*/
|
|
2273
|
+
async importCursorMove(input) {
|
|
2274
|
+
await this.ensureMigrations();
|
|
2275
|
+
// The consumer's own running imports decide whether the edge exists: read here, in the
|
|
2276
|
+
// deployment whose handlers would run, never from the platform's request.
|
|
2277
|
+
const imports = this.crossVertical.consumes();
|
|
2278
|
+
return this.queue.enqueue(() => this.ctx.storage.transactionSync(() => moveImportCursor(this.switchSql(), { ...input, imports })));
|
|
2279
|
+
}
|
|
2218
2280
|
/**
|
|
2219
2281
|
* Move one PEER's kill switch on this scope (#1706) — the kernel's `switchPeer`, which is
|
|
2220
2282
|
* the schedule switch's statement with a `vertical:` subject. Queued, one transaction.
|
|
@@ -2331,9 +2393,11 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2331
2393
|
out[t] = { observedAt: null, stateAt: null, stateOutcome: null };
|
|
2332
2394
|
if (types.length === 0)
|
|
2333
2395
|
return out;
|
|
2334
|
-
|
|
2396
|
+
// Each list travels as ONE bound JSON array (#1776): a DO binds at most 100 parameters,
|
|
2397
|
+
// and nothing caps how many event types the registered modules declare a window for.
|
|
2335
2398
|
for (const row of this.sql
|
|
2336
|
-
.exec(`SELECT type, MAX(occurred_at) AS at FROM _substrat_outbox
|
|
2399
|
+
.exec(`SELECT type, MAX(occurred_at) AS at FROM _substrat_outbox
|
|
2400
|
+
WHERE type IN (SELECT value FROM json_each(?)) GROUP BY type`, JSON.stringify(types))
|
|
2337
2401
|
.toArray()) {
|
|
2338
2402
|
if (row.at !== null)
|
|
2339
2403
|
out[row.type].observedAt = row.at;
|
|
@@ -2341,7 +2405,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2341
2405
|
const keys = types.map((t) => `freshness:${t}`);
|
|
2342
2406
|
for (const row of this.sql
|
|
2343
2407
|
.exec(`SELECT schedule_op, last_run_at, last_status FROM _substrat_schedule_state
|
|
2344
|
-
WHERE kind = 'freshness' AND schedule_op IN (
|
|
2408
|
+
WHERE kind = 'freshness' AND schedule_op IN (SELECT value FROM json_each(?))`, JSON.stringify(keys))
|
|
2345
2409
|
.toArray()) {
|
|
2346
2410
|
const t = row.schedule_op.slice('freshness:'.length);
|
|
2347
2411
|
if (out[t]) {
|