Repository navigation
Expand file tree
/
Copy pathworker.ts
More file actions
140 lines (128 loc) · 7.25 KB
/
Copy pathworker.ts
File metadata and controls
140 lines (128 loc) · 7.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// Cloudflare Worker entrypoint (hosted target).
//
// @opennextjs/cloudflare compiles the Next.js app to `.open-next/worker.js`,
// whose DEFAULT export is the fetch handler. Cloudflare also requires every
// Durable Object class the Worker uses to be EXPORTED from the same entry module
// (this is the documented OpenNext convention — its own cache DOs, DOQueueHandler
// etc., are re-exported from the generated worker exactly this way). So this
// wrapper:
// 1. re-exports OpenNext's fetch handler as the default export, and
// 2. exports `UserDataObject`, the per-user data Durable Object that
// wrangler.toml's `[[durable_objects.bindings]] class_name` reserves.
//
// wrangler.toml's `main` points here instead of straight at the generated
// worker, so both the app handler and the DO class are registered.
//
// The DO class binds the real `cloudflare:workers` DurableObject base (only
// available in the Workers runtime / build) around the runtime-agnostic
// UserDataObject implementation in lib/repo/user-data-object.ts. Keeping the base
// import out of that lib file means the rest of the app (and Node tests) never
// try to resolve the `cloudflare:workers` virtual module.
// @ts-expect-error — resolved by the wrangler/OpenNext build, not by tsc/Node.
import { DurableObject } from "cloudflare:workers";
import { UserDataObject as UserDataImpl } from "./lib/repo/user-data-object";
import type { AnyRepoCall } from "./lib/repo/repo-rpc";
// OpenNext's generated default export is `{ fetch }`. To run a Cloudflare Queue
// consumer on the SAME Worker, the `queue` handler must be a property of the
// default export object alongside `fetch` (the documented OpenNext custom-worker
// pattern — a `queue`/`scheduled` handler can't be a separate top-level export;
// the runtime only looks at the default export's methods). So we import OpenNext's
// handler, re-export its `fetch`, and add our P4 queue consumer.
// `.open-next/worker.js` exists only after `opennextjs-cloudflare build`, so
// this import may or may not resolve depending on build order — @ts-ignore
// (not @ts-expect-error) because a plain `next build` run AFTER a cf:build
// finds the file and would flag the expect-error as unused.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import openNextHandler from "./.open-next/worker.js";
import { queueHandler, type QueueConsumerEnv } from "./lib/queue/consumer";
import type { ParseJobMessage, QueueMessageBatchLike } from "./lib/queue/types";
import {
handleEmailMessage,
type EmailMessageLike,
type EmailWorkerEnv,
type EmailCtxLike,
} from "./cloud/ingest/email-worker";
import * as Sentry from "@sentry/cloudflare";
import { sentryOptions } from "./lib/sentry";
// MATCHED PAIR with wrangler.toml — change the two together. Every handler and
// exported Durable Object class below must have a corresponding declaration in
// wrangler.toml ([[queues.consumers]], [[containers]], [[durable_objects]],
// [triggers]); exporting a DO class the config never declares breaks the deploy.
//
// `main` and `deploy/full-app` now carry the SAME config and the SAME entry
// module. They used to differ — main had a trimmed config with these handlers
// removed — and that split drifted unnoticed for 38 commits before breaking a
// deploy merge. Don't reintroduce it.
//
// Handlers only run when their trigger is configured, so this file is safe on a
// deploy whose resources aren't provisioned: no queue consumer means `queue`
// never fires, no Email Routing rule means `email` never fires, no [triggers]
// cron means `scheduled` never fires.
const handler = {
// The Next.js app's fetch handler, untouched.
fetch: (openNextHandler as { fetch: (...args: unknown[]) => Promise<Response> }).fetch,
// The P4 async parse pipeline. Cloudflare delivers a MessageBatch to this
// handler for each batch pulled off the PARSE_QUEUE consumer (wired in P6). It
// acks/retries per message; a throw from a message's processing redelivers only
// that message (we use per-message ack/retry, not ackAll/retryAll).
async queue(batch: QueueMessageBatchLike<ParseJobMessage>, env: QueueConsumerEnv) {
return queueHandler(batch, env);
},
// Email ingest: statements forwarded to *@in.pare.money land here via Email
// Routing (routing rule configured in the dashboard, not wrangler.toml). The
// handler never throws on bad mail; without a routing rule it simply never runs.
async email(message: EmailMessageLike, env: EmailWorkerEnv, ctx: EmailCtxLike) {
return handleEmailMessage(message, env, ctx);
},
// Daily SimpleFIN sync (wrangler `[triggers] crons`). Same env-parameter
// discipline as the queue consumer: everything resolves off `env` (D1 via
// env.DB, the per-user DO namespace via env.USER_DATA) — getCloudflareContext
// is not reliably available inside a scheduled() invocation. Without a cron
// trigger configured this handler simply never runs, so deploys whose
// wrangler config has no [triggers] (e.g. the trimmed one) are unaffected.
async scheduled(_event: unknown, env: unknown) {
const { scheduledSimplefinSync } = await import("./cloud/simplefin/scheduled");
await scheduledSimplefinSync(env as never);
},
};
// PHASE 4 — error tracking. withSentry wraps BOTH the fetch and queue handlers,
// capturing unhandled errors with request context. The options come from the
// per-Worker env (SENTRY_DSN secret); when it's unset, Sentry is a no-op (nothing
// sent), so dev/self-host/un-provisioned deploys behave exactly as before. PII is
// stripped in lib/sentry.ts's beforeSend. The Durable Object classes are exported
// separately below and are unaffected by the wrap.
export default Sentry.withSentry(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(env: any) => sentryOptions(env),
handler
);
// The PDF parser runs in a Cloudflare Container (Python + poppler — unavailable in
// the Workers runtime). Like UserDataObject, the Container-backed Durable Object
// class must be exported from the entry module so wrangler can register it
// (wrangler.toml [[containers]] + [[durable_objects.bindings]] class_name = "ParserContainer").
export { ParserContainer } from "./lib/parser/parser-container-do";
// The registered Durable Object. Extends the Workers DurableObject base (so the
// platform recognises it as a DO with storage + an input gate) and delegates all
// data work to UserDataImpl, which owns the SqliteRepo/DoBackend over ctx.storage.
// One instance per user (the Worker addresses it by id derived from userId), so
// tenant isolation is by construction.
export class UserDataObject extends DurableObject {
private impl: UserDataImpl;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(ctx: any, env: any) {
super(ctx, env);
this.impl = new UserDataImpl(ctx, env);
}
// RPC method called on the DO stub by the request-side DoRepoClient transport
// (lib/repo/index.ts: `stub.call(call)`). The envelope is structured-clone-safe.
async call(req: AnyRepoCall): Promise<unknown> {
return this.impl.call(req);
}
// Account-deletion RPC: hard-delete this user's entire database (drop all SQL
// tables/views + clear KV storage). Called by destroyUserData() via the stub.
// Idempotent.
async destroy(): Promise<void> {
return this.impl.destroy();
}
}