@sanoma-ai/connector-cloudflare 0.3.2 → 0.5.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/README.md +32 -21
- package/dist/connector.d.ts +3 -1
- package/dist/connector.js +26 -4
- package/dist/connector.js.map +1 -1
- package/dist/driver.js +32 -3
- package/dist/driver.js.map +1 -1
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/fake.d.ts +14 -2
- package/dist/fake.js +60 -4
- package/dist/fake.js.map +1 -1
- package/dist/ops.d.ts +34 -5
- package/dist/ops.js +149 -69
- package/dist/ops.js.map +1 -1
- package/dist/records.js +2 -1
- package/dist/records.js.map +1 -1
- package/dist/resources.d.ts +3 -1
- package/dist/sites.d.ts +58 -0
- package/dist/sites.js +153 -0
- package/dist/sites.js.map +1 -0
- package/dist/worker.d.ts +8 -1
- package/dist/worker.js +8 -0
- package/dist/worker.js.map +1 -1
- package/package.json +4 -3
- package/worker/index.js +7 -2
- package/worker/instance.d.ts +11 -1
- package/worker/instance.js +75 -14
- package/worker/sites.js +17 -0
package/worker/instance.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// another destroyed and started again (a deploy); it waits for `/healthz`, and proxies every
|
|
9
9
|
// request to it. It answers `/__sanoma/health` without starting it (with `?start=1`, once it
|
|
10
10
|
// runs), and holds the company's one wake (`/__sanoma/wake`) as an alarm, which starts the
|
|
11
|
-
// container at `at` and calls its `/wake`.
|
|
11
|
+
// container at `at` and calls its `/wake`. `startNow`, which the sites router calls over RPC
|
|
12
|
+
// when a page on the Worker queued a run, starts the container from an alarm too, without the
|
|
13
|
+
// caller waiting for it, and calls its `/wake`: a worker that stopped itself runs the queued run.
|
|
12
14
|
|
|
13
15
|
/** The object's own route: the wake it holds. */
|
|
14
16
|
export const WAKE_PATH = "/__sanoma/wake";
|
|
@@ -26,6 +28,13 @@ const POLL_MS = 250;
|
|
|
26
28
|
const MAX_POLL_MS = 1_000;
|
|
27
29
|
/** Where the wake is kept, in the object's storage. */
|
|
28
30
|
const WAKE_KEY = "wake";
|
|
31
|
+
/** Where a start `startNow` asked for is kept until the alarm that makes it rings. */
|
|
32
|
+
const START_KEY = "start";
|
|
33
|
+
/**
|
|
34
|
+
* The retries of an alarm whose start keeps failing before that start is given up, so the wake
|
|
35
|
+
* held for later is set again: Cloudflare retries an alarm that throws up to six times, then drops it.
|
|
36
|
+
*/
|
|
37
|
+
const START_RETRIES = 5;
|
|
29
38
|
/** The binding that names the object, in the Worker's env. */
|
|
30
39
|
export const NAMESPACE = "SANOMA_INSTANCE";
|
|
31
40
|
/** The name the upload gives the runtime image (`containers[].images`), which the object starts. */
|
|
@@ -44,7 +53,7 @@ function containerEnv(env) {
|
|
|
44
53
|
return Object.fromEntries(Object.entries(env).filter(([, value]) => typeof value === "string"));
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
/** The label naming an image: a digest of it, as a label holds 64 bytes and a pinned image is longer. */
|
|
56
|
+
/** The label naming an image (or the sites' domains): a digest of it, as a label holds 64 bytes and a pinned image is longer. */
|
|
48
57
|
export async function imageLabel(image) {
|
|
49
58
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(image));
|
|
50
59
|
return [...new Uint8Array(digest).slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -152,6 +161,8 @@ export class Instance {
|
|
|
152
161
|
revision: this.env.SANOMA_REVISION ?? "",
|
|
153
162
|
image: await imageLabel(image),
|
|
154
163
|
...(this.env.SANOMA_CONFIG_REVISION && { configuration: this.env.SANOMA_CONFIG_REVISION }),
|
|
164
|
+
// The sites' domains, which its environment and the sites bundle it built name.
|
|
165
|
+
...(this.env.SANOMA_DOMAINS && { domains: await imageLabel(this.env.SANOMA_DOMAINS) }),
|
|
155
166
|
};
|
|
156
167
|
const until = Date.now() + START_TIMEOUT_MS;
|
|
157
168
|
let last;
|
|
@@ -178,8 +189,9 @@ export class Instance {
|
|
|
178
189
|
info &&
|
|
179
190
|
info.labels?.revision === labels.revision &&
|
|
180
191
|
(info.labels?.image ?? "") === labels.image &&
|
|
181
|
-
(info.labels?.configuration ?? "") === (labels.configuration ?? "")
|
|
182
|
-
|
|
192
|
+
(info.labels?.configuration ?? "") === (labels.configuration ?? "") &&
|
|
193
|
+
(info.labels?.domains ?? "") === (labels.domains ?? "");
|
|
194
|
+
if (!same) await container.destroy("a new revision, image, configuration or domains");
|
|
183
195
|
}
|
|
184
196
|
checked = true;
|
|
185
197
|
if (!container.running) {
|
|
@@ -235,9 +247,11 @@ export class Instance {
|
|
|
235
247
|
}
|
|
236
248
|
const storage = this.ctx.storage;
|
|
237
249
|
if (request.method === "GET") return json(200, (await storage.get(WAKE_KEY)) ?? { at: null });
|
|
250
|
+
// A start `startNow` asked for keeps its alarm, which sets the wake's once it rang.
|
|
251
|
+
const starting = Boolean(await storage.get(START_KEY));
|
|
238
252
|
if (request.method === "DELETE") {
|
|
239
253
|
await storage.delete(WAKE_KEY);
|
|
240
|
-
await storage.deleteAlarm();
|
|
254
|
+
if (!starting) await storage.deleteAlarm();
|
|
241
255
|
return new Response(null, { status: 204 });
|
|
242
256
|
}
|
|
243
257
|
if (request.method !== "PUT") return problem(405, "GET, PUT or DELETE");
|
|
@@ -245,14 +259,55 @@ export class Instance {
|
|
|
245
259
|
if (!wake) return problem(400, "the body must be { at: an ISO 8601 time }");
|
|
246
260
|
// Whatever it held is replaced: a wake that rings for an older one starts a worker with nothing to do.
|
|
247
261
|
await storage.put(WAKE_KEY, wake);
|
|
248
|
-
await storage
|
|
262
|
+
if (starting) await alarmBy(storage, Date.parse(wake.at));
|
|
263
|
+
else await storage.setAlarm(Date.parse(wake.at));
|
|
249
264
|
return new Response(null, { status: 204 });
|
|
250
265
|
}
|
|
251
266
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Starts the container soon and sends it `/wake`, without waiting for it: a run was queued for
|
|
269
|
+
* it (the sites router's call, over RPC, once a page on the Worker started one). An alarm now,
|
|
270
|
+
* which outlives the request that asked, makes the start; a wake held for later is set again once it is made.
|
|
271
|
+
*/
|
|
272
|
+
async startNow() {
|
|
273
|
+
const storage = this.ctx.storage;
|
|
274
|
+
await storage.put(START_KEY, true);
|
|
275
|
+
await alarmBy(storage, Date.now());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A start `startNow` asked for, then the wake if it is due: start the container and send it
|
|
280
|
+
* `/wake` (once for both), then note when the wake rang. A wake not due yet is set again. A throw makes Cloudflare retry the
|
|
281
|
+
* alarm; a start that still fails after `START_RETRIES` is given up, the wake kept.
|
|
282
|
+
*/
|
|
283
|
+
async alarm(info) {
|
|
284
|
+
const storage = this.ctx.storage;
|
|
285
|
+
let woke = false;
|
|
286
|
+
if (await storage.get(START_KEY)) {
|
|
287
|
+
try {
|
|
288
|
+
await this.#wakeContainer();
|
|
289
|
+
woke = true;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if ((info?.retryCount ?? 0) < START_RETRIES) throw error;
|
|
292
|
+
console.error(`starting the container for a queued run failed: ${error?.message ?? error}`);
|
|
293
|
+
}
|
|
294
|
+
await storage.delete(START_KEY);
|
|
295
|
+
}
|
|
296
|
+
const wake = await storage.get(WAKE_KEY);
|
|
297
|
+
if (!wake || wake.rang) return;
|
|
298
|
+
const at = Date.parse(wake.at);
|
|
299
|
+
if (at > Date.now()) return storage.setAlarm(at);
|
|
300
|
+
if (!woke) await this.#wakeContainer();
|
|
301
|
+
// When it rang, which a GET shows: unless another wake replaced it meanwhile.
|
|
302
|
+
const held = await this.ctx.storage.get(WAKE_KEY);
|
|
303
|
+
if (held?.at === wake.at) await this.ctx.storage.put(WAKE_KEY, { ...held, rang: new Date().toISOString() });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Starts the container unless it runs, then sends it `/wake`: a container that is up may hold a
|
|
308
|
+
* worker that stopped itself, which only a wake (or a write through its app) starts again.
|
|
309
|
+
*/
|
|
310
|
+
async #wakeContainer() {
|
|
256
311
|
await this.ensureRunning();
|
|
257
312
|
const res = await this.ctx.container.getTcpPort(this.port).fetch("http://container/wake");
|
|
258
313
|
await res.body?.cancel();
|
|
@@ -260,13 +315,19 @@ export class Instance {
|
|
|
260
315
|
console.error(`GET /wake answered ${res.status}`);
|
|
261
316
|
throw new Error(`the container answered /wake with ${res.status}`);
|
|
262
317
|
}
|
|
263
|
-
// When it rang, which a GET shows: unless another wake replaced it meanwhile.
|
|
264
|
-
const held = await this.ctx.storage.get(WAKE_KEY);
|
|
265
|
-
if (held?.at === wake.at) await this.ctx.storage.put(WAKE_KEY, { ...held, rang: new Date().toISOString() });
|
|
266
318
|
}
|
|
267
319
|
}
|
|
268
320
|
|
|
321
|
+
/** Sets the object's alarm to ring by `at`: an alarm set to ring sooner is left as it is. */
|
|
322
|
+
async function alarmBy(storage, at) {
|
|
323
|
+
const set = await storage.getAlarm();
|
|
324
|
+
if (set === null || set > at) await storage.setAlarm(at);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The Worker's one object, which every request and the sites router's `startNow` go to. */
|
|
328
|
+
export const instanceOf = (env) => env[NAMESPACE].getByName("instance");
|
|
329
|
+
|
|
269
330
|
/** The Worker's default export: every request goes to the one object. */
|
|
270
331
|
export const worker = {
|
|
271
|
-
fetch: (request, env) => env
|
|
332
|
+
fetch: (request, env) => instanceOf(env).fetch(request),
|
|
272
333
|
};
|
package/worker/sites.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The company's Worker when it serves public sites: the main module `cloudflare.container`
|
|
2
|
+
// uploads in place of `index.js` once the container built the sites bundle (`sites/index.js`,
|
|
3
|
+
// `buildWorker` in `@sanoma-ai/app/build`). A request the bundle serves (a page of a public site
|
|
4
|
+
// without a login, on its domain) is answered here, on the Worker, and never waits for the
|
|
5
|
+
// container; everything else goes to the one object, as `index.js` sends it. A run a page starts
|
|
6
|
+
// has the object start the container (`startNow`), which runs it.
|
|
7
|
+
import { instanceOf, worker } from "./instance.js";
|
|
8
|
+
import sites from "./sites/index.js";
|
|
9
|
+
|
|
10
|
+
export { SanomaInstance } from "./index.js";
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
fetch(request, env, ctx) {
|
|
14
|
+
if (!sites.serves(request, env)) return worker.fetch(request, env);
|
|
15
|
+
return sites.fetch(request, env, ctx, { startNow: () => instanceOf(env).startNow() });
|
|
16
|
+
},
|
|
17
|
+
};
|