mirror of
https://github.com/immich-app/static-pages.git
synced 2026-09-30 13:23:05 +08:00
fix(survey): make the worker module work on a fresh environment (#703)
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
export { SurveyDO } from './survey-do';
|
||||
|
||||
// Legacy alias for the original KV-backed class (see wrangler-do.jsonc v1
|
||||
// migration: `new_classes: ["SurveySession"]`). Cloudflare rejects a deploy
|
||||
// that removes a class still declared in the migration chain, so the alias
|
||||
// stays exported until a future `deleted_classes` migration retires v1.
|
||||
export { SurveyDO as SurveySession } from './survey-do';
|
||||
|
||||
export default {
|
||||
async fetch(): Promise<Response> {
|
||||
return new Response('This worker only hosts Durable Objects', { status: 404 });
|
||||
|
||||
@@ -451,6 +451,13 @@ export class SurveyDO extends DurableObject {
|
||||
answers: Array<{ questionId: string; value: string; otherText?: string; answerMs?: number }>;
|
||||
};
|
||||
await this.respondentService.submitBatch(survey.slug!, respondentId, answers, survey);
|
||||
|
||||
// The unload beacon submits over HTTP even in WebSocket mode, so without this
|
||||
// the cached choice answers miss whatever was flushed on the way out and the
|
||||
// live tallies under-count once the respondent completes.
|
||||
for (const a of answers) {
|
||||
this.cache.setAnswer(respondentId, a.questionId, a.value, a.otherText ?? null);
|
||||
}
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
|
||||
@@ -262,10 +262,6 @@ async function handleOp(
|
||||
if (error) throw new ServiceError(error, 400);
|
||||
}
|
||||
|
||||
for (const a of answers) {
|
||||
cache.setAnswer(respondentId, a.questionId, a.value, a.otherText ?? null);
|
||||
}
|
||||
|
||||
// answer_ms is clamped via the shared helper so it stays in lockstep with the
|
||||
// HTTP path in respondent.service.
|
||||
const now = new Date().toISOString();
|
||||
@@ -278,6 +274,13 @@ async function handleOp(
|
||||
`INSERT OR REPLACE INTO answers (respondent_id, question_id, answer, other_text, answered_at, answer_ms) VALUES ${placeholders}`,
|
||||
...values,
|
||||
);
|
||||
|
||||
// Cache only after the write lands: a throw here leaves answers in
|
||||
// choiceAnswers that were never persisted, and completion would fold them
|
||||
// into the live tallies.
|
||||
for (const a of answers) {
|
||||
cache.setAnswer(respondentId, a.questionId, a.value, a.otherText ?? null);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
// SurveyDO is the only Durable Object class and is SQLite-backed from the
|
||||
// start. (The earlier v1 "SurveySession" class never existed — it only
|
||||
// produced a "no such Durable Object class is exported" warning on boot.)
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
|
||||
@@ -11,19 +11,12 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
// The deployed sessions worker reached tag "v2" through this chain, so it must
|
||||
// be preserved as-is: v1 created the original "SurveySession" class and v2
|
||||
// introduced the SQLite-backed "SurveyDO". The Terraform deploy (worker.tf)
|
||||
// asserts old_tag == new_tag == "v2" against this state. `durable-objects/
|
||||
// index.ts` still exports `SurveyDO as SurveySession` so the v1 class name
|
||||
// remains resolvable.
|
||||
// This config only drives `wrangler build`; Terraform deploys the worker and
|
||||
// applies the migration itself, so this must stay in sync with the block in
|
||||
// deployment/modules/cloudflare/workers/survey/worker.tf.
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_classes": ["SurveySession"],
|
||||
},
|
||||
{
|
||||
"tag": "v2",
|
||||
"new_sqlite_classes": ["SurveyDO"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,6 +13,10 @@ resource "null_resource" "d1_migrations" {
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
# local-exec defaults to /bin/sh, which is dash on the CI runners and has no
|
||||
# pipefail.
|
||||
interpreter = ["/bin/bash", "-c"]
|
||||
|
||||
# Re-running an already-applied migration is benign ("already exists" /
|
||||
# "duplicate column"); every other error must exit non-zero so the failure is
|
||||
# visible in the workflow instead of being masked.
|
||||
|
||||
@@ -109,6 +109,51 @@ resource "cloudflare_worker" "sessions" {
|
||||
}
|
||||
}
|
||||
|
||||
# Cloudflare applies a version's migration when the version is deployed, and
|
||||
# rejects it unless old_tag matches the tag the Worker is already on. A fresh
|
||||
# Worker therefore needs the class-creating migration with no old_tag, and every
|
||||
# later deploy needs a no-op migration carrying that same tag — no single block
|
||||
# is valid in both states. This version exists only to run the first migration
|
||||
# and is frozen once created, so each new environment bootstraps itself and the
|
||||
# rolling version below stays on the no-op tag forever after.
|
||||
resource "cloudflare_worker_version" "sessions_bootstrap" {
|
||||
account_id = var.cloudflare_account_id
|
||||
worker_id = cloudflare_worker.sessions.id
|
||||
compatibility_date = "2025-06-03"
|
||||
|
||||
main_module = "sessions.js"
|
||||
|
||||
modules = [{
|
||||
name = "sessions.js"
|
||||
content_file = "${var.dist_dir}/sessions.js"
|
||||
content_type = "application/javascript+module"
|
||||
}]
|
||||
|
||||
migrations = {
|
||||
new_tag = "v1"
|
||||
new_sqlite_classes = ["SurveyDO"]
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_workers_deployment" "sessions_bootstrap" {
|
||||
account_id = var.cloudflare_account_id
|
||||
script_name = cloudflare_worker.sessions.name
|
||||
strategy = "percentage"
|
||||
|
||||
versions = [{
|
||||
version_id = cloudflare_worker_version.sessions_bootstrap.id
|
||||
percentage = 100
|
||||
}]
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_worker_version" "sessions" {
|
||||
account_id = var.cloudflare_account_id
|
||||
worker_id = cloudflare_worker.sessions.id
|
||||
@@ -123,9 +168,11 @@ resource "cloudflare_worker_version" "sessions" {
|
||||
}]
|
||||
|
||||
migrations = {
|
||||
old_tag = "v2"
|
||||
new_tag = "v2"
|
||||
old_tag = "v1"
|
||||
new_tag = "v1"
|
||||
}
|
||||
|
||||
depends_on = [cloudflare_workers_deployment.sessions_bootstrap]
|
||||
}
|
||||
|
||||
resource "cloudflare_workers_deployment" "sessions" {
|
||||
@@ -137,6 +184,12 @@ resource "cloudflare_workers_deployment" "sessions" {
|
||||
version_id = cloudflare_worker_version.sessions.id
|
||||
percentage = 100
|
||||
}]
|
||||
|
||||
lifecycle {
|
||||
# Redeploying the frozen bootstrap would otherwise silently roll the Worker
|
||||
# back to the code it was first created with.
|
||||
replace_triggered_by = [cloudflare_workers_deployment.sessions_bootstrap]
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_worker" "api" {
|
||||
@@ -146,6 +199,11 @@ resource "cloudflare_worker" "api" {
|
||||
observability = {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
# Ordering only, so that destroy runs in reverse and tears this Worker down
|
||||
# first: Cloudflare refuses to delete the sessions Worker while this one still
|
||||
# binds its Durable Object namespace.
|
||||
depends_on = [cloudflare_worker.sessions]
|
||||
}
|
||||
|
||||
resource "cloudflare_worker_version" "api" {
|
||||
@@ -184,16 +242,22 @@ data "cloudflare_zone" "immich_app" {
|
||||
}
|
||||
}
|
||||
|
||||
# A Worker with no deployed version does not exist as far as the routes API is
|
||||
# concerned, and referencing only the name would let these race the deployment.
|
||||
resource "cloudflare_workers_route" "survey_api_root" {
|
||||
zone_id = data.cloudflare_zone.immich_app.zone_id
|
||||
pattern = "${module.domain.fqdn}/api"
|
||||
script = cloudflare_worker.api.name
|
||||
|
||||
depends_on = [cloudflare_workers_deployment.api]
|
||||
}
|
||||
|
||||
resource "cloudflare_workers_route" "survey_api_wildcard" {
|
||||
zone_id = data.cloudflare_zone.immich_app.zone_id
|
||||
pattern = "${module.domain.fqdn}/api/*"
|
||||
script = cloudflare_worker.api.name
|
||||
|
||||
depends_on = [cloudflare_workers_deployment.api]
|
||||
}
|
||||
|
||||
module "domain" {
|
||||
|
||||
Reference in New Issue
Block a user