@sanoma-ai/connector-cloudflare 0.1.4 → 0.1.6
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/README.md +24 -20
- package/dist/connector.d.ts +2 -1
- package/dist/connector.js +2 -1
- package/dist/connector.js.map +1 -1
- package/dist/driver.js +31 -34
- package/dist/driver.js.map +1 -1
- package/dist/fake.d.ts +8 -5
- package/dist/fake.js +47 -22
- package/dist/fake.js.map +1 -1
- package/dist/ops.d.ts +34 -11
- package/dist/ops.js +66 -26
- package/dist/ops.js.map +1 -1
- package/dist/registry.d.ts +15 -5
- package/dist/registry.js +44 -12
- package/dist/registry.js.map +1 -1
- package/dist/steps.js +2 -2
- package/dist/steps.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/features/cloudflare.feature +5 -5
- package/package.json +4 -4
- package/worker/instance.d.ts +14 -2
- package/worker/instance.js +68 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanoma-ai/connector-cloudflare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Cloudflare for Sanoma: a Container host that stops when quiet, and a Durable Object alarm that wakes it, from one Worker",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@sanoma-ai/testing": "0.1.0",
|
|
64
|
-
"@sanoma-ai/workflows": "0.
|
|
65
|
-
"msw": "^3.0.
|
|
64
|
+
"@sanoma-ai/workflows": "0.10.0",
|
|
65
|
+
"msw": "^3.0.3",
|
|
66
66
|
"zod": "^4.6.5"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@sanoma-ai/workflows": "^0.
|
|
69
|
+
"@sanoma-ai/workflows": "^0.10.0",
|
|
70
70
|
"zod": "^4"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
package/worker/instance.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const HEALTH_PATH: "/__sanoma/health";
|
|
|
8
8
|
export declare const INACTIVITY_MS: number;
|
|
9
9
|
/** The binding that names the object, in the Worker's env. */
|
|
10
10
|
export declare const NAMESPACE: "SANOMA_INSTANCE";
|
|
11
|
+
/** The name the upload gives the runtime image (`containers[].images`), which the object starts. */
|
|
12
|
+
export declare const IMAGE: "runtime";
|
|
11
13
|
|
|
12
14
|
/** The label naming an image: a digest of it. */
|
|
13
15
|
export declare function imageLabel(image: string): Promise<string>;
|
|
@@ -16,11 +18,21 @@ export declare function imageLabel(image: string): Promise<string>;
|
|
|
16
18
|
export interface ObjectState {
|
|
17
19
|
container: {
|
|
18
20
|
readonly running: boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
/** The images the Worker's upload names (`containers[].images`), by name. */
|
|
22
|
+
readonly images: Record<string, string>;
|
|
23
|
+
start(options: {
|
|
24
|
+
image: string;
|
|
25
|
+
instance: string;
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
enableInternet: boolean;
|
|
28
|
+
labels: Record<string, string>;
|
|
29
|
+
}): void;
|
|
30
|
+
inspect(): Promise<{ image?: string; labels: Record<string, string> } | null>;
|
|
21
31
|
destroy(reason?: string): Promise<void>;
|
|
22
32
|
getTcpPort(port: number): { fetch(input: string | Request, init?: RequestInit): Promise<Response> };
|
|
23
33
|
setInactivityTimeout(ms: number): Promise<void>;
|
|
34
|
+
/** Settles when the container started last ends: resolved on exit 0, rejected on any other. */
|
|
35
|
+
monitor?(): Promise<void>;
|
|
24
36
|
};
|
|
25
37
|
storage: {
|
|
26
38
|
get<T>(key: string): Promise<T | undefined>;
|
package/worker/instance.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// a stubbed `ctx`, so both kits check this code. No imports, so Node loads it as it is.
|
|
4
4
|
//
|
|
5
5
|
// The object runs the company's container (`ctx.container`): it starts it on a request, at the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// Worker's `SANOMA_REVISION` and the image its upload names (`ctx.container.images.runtime`, as
|
|
7
|
+
// the `durable_object` scheduling policy has it: the object says what runs), a running one at
|
|
8
|
+
// another destroyed and started again (a deploy); it waits for `/healthz`, and proxies every
|
|
8
9
|
// request to it. It answers `/__sanoma/health` without starting it (with `?start=1`, once it
|
|
9
10
|
// runs), and holds the company's one wake (`/__sanoma/wake`) as an alarm, which starts the
|
|
10
11
|
// container at `at` and calls its `/wake`.
|
|
@@ -27,11 +28,16 @@ const MAX_POLL_MS = 1_000;
|
|
|
27
28
|
const WAKE_KEY = "wake";
|
|
28
29
|
/** The binding that names the object, in the Worker's env. */
|
|
29
30
|
export const NAMESPACE = "SANOMA_INSTANCE";
|
|
31
|
+
/** The name the upload gives the runtime image (`containers[].images`), which the object starts. */
|
|
32
|
+
export const IMAGE = "runtime";
|
|
33
|
+
/** The container's size, one of the named types `start` takes: ½ vCPU, 4 GiB (`basic`, ¼ vCPU, is refused under this policy). */
|
|
34
|
+
const INSTANCE_TYPE = "standard-1";
|
|
30
35
|
|
|
31
36
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
37
|
const json = (status, body) =>
|
|
33
38
|
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
34
|
-
|
|
39
|
+
/** A refusal; with `exit`, how the container ended: a deploy stops waiting on it. */
|
|
40
|
+
const problem = (status, error, exit) => json(status, { error, ...(exit !== undefined && { exit }) });
|
|
35
41
|
|
|
36
42
|
/** The Worker's text bindings (plain text and secrets): the container's environment. */
|
|
37
43
|
function containerEnv(env) {
|
|
@@ -64,6 +70,12 @@ export class Instance {
|
|
|
64
70
|
#starting;
|
|
65
71
|
/** True once a container this object started (or found) answered `/healthz`. */
|
|
66
72
|
#ready = false;
|
|
73
|
+
/** What the last try of a start saw, for the health route while the container is not ready. */
|
|
74
|
+
#last;
|
|
75
|
+
/** How the container this object started ended, when it did: what `monitor` told. */
|
|
76
|
+
#exit;
|
|
77
|
+
/** The start whose `monitor` may set `#exit`: an earlier container's end is not this one's. */
|
|
78
|
+
#started;
|
|
67
79
|
|
|
68
80
|
constructor(ctx, env) {
|
|
69
81
|
this.ctx = ctx;
|
|
@@ -86,13 +98,15 @@ export class Instance {
|
|
|
86
98
|
try {
|
|
87
99
|
await this.ensureRunning();
|
|
88
100
|
} catch (error) {
|
|
89
|
-
return problem(503, `the company's container did not start: ${error?.message ?? error}
|
|
101
|
+
return problem(503, `the company's container did not start: ${error?.message ?? error}`, this.#exit);
|
|
90
102
|
}
|
|
91
103
|
if (health) return this.health();
|
|
92
104
|
const headers = new Headers(request.headers);
|
|
93
|
-
|
|
105
|
+
// The port's fetch reaches the container whatever the URL's host, which becomes the request's
|
|
106
|
+
// Host header: the one asked for, so the company serves the public site declared on it.
|
|
107
|
+
headers.set("x-forwarded-proto", url.protocol.slice(0, -1));
|
|
94
108
|
const body = request.method === "GET" || request.method === "HEAD" ? undefined : request.body;
|
|
95
|
-
const forwarded = new Request(new URL(url.pathname + url.search,
|
|
109
|
+
const forwarded = new Request(new URL(url.pathname + url.search, `http://${url.host}`), {
|
|
96
110
|
method: request.method,
|
|
97
111
|
headers,
|
|
98
112
|
body,
|
|
@@ -110,7 +124,13 @@ export class Instance {
|
|
|
110
124
|
|
|
111
125
|
/** What the Worker runs, and whether its container does. */
|
|
112
126
|
health() {
|
|
113
|
-
return json(200, {
|
|
127
|
+
return json(200, {
|
|
128
|
+
revision: this.env.SANOMA_REVISION ?? "",
|
|
129
|
+
running: this.ctx.container.running,
|
|
130
|
+
ready: this.#ready,
|
|
131
|
+
...(this.#last !== undefined && !this.#ready && { last: this.#last }),
|
|
132
|
+
...(this.#exit !== undefined && { exit: this.#exit }),
|
|
133
|
+
});
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
/** Starts the container at the Worker's revision and image unless it runs them and answered; one start at a time. */
|
|
@@ -122,13 +142,29 @@ export class Instance {
|
|
|
122
142
|
|
|
123
143
|
async #start() {
|
|
124
144
|
this.#ready = false;
|
|
145
|
+
this.#exit = undefined;
|
|
125
146
|
const container = this.ctx.container;
|
|
126
|
-
const
|
|
147
|
+
const image = container.images?.[IMAGE];
|
|
148
|
+
// Not one to retry: the Worker was uploaded without its image.
|
|
149
|
+
if (!image) throw new Error(`the Worker names no "${IMAGE}" image among its containers' images`);
|
|
150
|
+
const labels = { revision: this.env.SANOMA_REVISION ?? "", image: await imageLabel(image) };
|
|
127
151
|
const until = Date.now() + START_TIMEOUT_MS;
|
|
128
152
|
let last;
|
|
153
|
+
const saw = (what) => (last = this.#last = what);
|
|
129
154
|
let wait = POLL_MS;
|
|
130
155
|
let checked = false;
|
|
131
156
|
while (Date.now() < until) {
|
|
157
|
+
if (this.#exit) {
|
|
158
|
+
// Right after the application is made, the platform may not have assigned it to the
|
|
159
|
+
// namespace yet: that start fails and is tried again. Any other end is the answer, as a
|
|
160
|
+
// container that ended is not started again and again. Outside the try: a try's failure
|
|
161
|
+
// is what one try saw, and would otherwise swallow this.
|
|
162
|
+
if (!this.#exit.includes("no container application assigned")) {
|
|
163
|
+
throw new Error(`${this.#exit} before answering /healthz on port ${this.port} (${last})`);
|
|
164
|
+
}
|
|
165
|
+
saw(this.#exit);
|
|
166
|
+
this.#exit = undefined;
|
|
167
|
+
}
|
|
132
168
|
try {
|
|
133
169
|
if (!checked && container.running) {
|
|
134
170
|
// One running from before: kept only if it says it started at these (a deploy otherwise).
|
|
@@ -138,7 +174,25 @@ export class Instance {
|
|
|
138
174
|
}
|
|
139
175
|
checked = true;
|
|
140
176
|
if (!container.running) {
|
|
141
|
-
container.start({
|
|
177
|
+
container.start({
|
|
178
|
+
image,
|
|
179
|
+
instance: INSTANCE_TYPE,
|
|
180
|
+
env: containerEnv(this.env),
|
|
181
|
+
enableInternet: true,
|
|
182
|
+
labels,
|
|
183
|
+
});
|
|
184
|
+
// `start` returns once its options are checked: a start that fails later, or a container
|
|
185
|
+
// that exits, is what `monitor` tells, and the next try reports it. Only this start's, and
|
|
186
|
+
// only until it answered: an earlier container's monitor may settle after this one started,
|
|
187
|
+
// and one that answered and then stopped (Cloudflare's own stop, a crash) is started again
|
|
188
|
+
// by the next request, not reported as a start that failed.
|
|
189
|
+
this.#exit = undefined;
|
|
190
|
+
const started = (this.#started = {});
|
|
191
|
+
const ended = (how) => void (this.#started === started && !this.#ready && (this.#exit = how));
|
|
192
|
+
container.monitor?.().then(
|
|
193
|
+
() => ended("the container exited"),
|
|
194
|
+
(error) => ended(`the container failed: ${error?.message ?? error}`),
|
|
195
|
+
);
|
|
142
196
|
}
|
|
143
197
|
// A try of its own time: a hung one would hold every request past the deadline.
|
|
144
198
|
const res = await container
|
|
@@ -150,16 +204,17 @@ export class Instance {
|
|
|
150
204
|
this.#ready = true;
|
|
151
205
|
return;
|
|
152
206
|
}
|
|
153
|
-
|
|
207
|
+
saw(`/healthz answered ${res.status}`);
|
|
154
208
|
} catch (error) {
|
|
155
209
|
// Not listening yet, or not yet free to start again after a destroy.
|
|
156
|
-
|
|
210
|
+
saw(error?.message ?? String(error));
|
|
157
211
|
}
|
|
158
|
-
// Doubling to a second: a container that
|
|
212
|
+
// Doubling to a second: a container that is slow to listen is not asked four times a second.
|
|
159
213
|
await sleep(wait);
|
|
160
214
|
wait = Math.min(wait * 2, MAX_POLL_MS);
|
|
161
215
|
}
|
|
162
|
-
|
|
216
|
+
const ended = this.#exit ? `; ${this.#exit}` : "";
|
|
217
|
+
throw new Error(`no answer from /healthz on port ${this.port} in ${START_TIMEOUT_MS / 1000} s (${last}${ended})`);
|
|
163
218
|
}
|
|
164
219
|
|
|
165
220
|
/** `/__sanoma/wake`, with the company's `SANOMA_WAKEUP_TOKEN`: the wake this object holds. */
|