mirror of https://github.com/requarks/wiki
parent
ff4a5bc6e6
commit
1c6e71eebf
@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "pageRenderQueue" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
"allowScripts" boolean DEFAULT false NOT NULL,
|
||||||
|
"allowStyles" boolean DEFAULT false NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"pageId" uuid NOT NULL UNIQUE,
|
||||||
|
"siteId" uuid NOT NULL,
|
||||||
|
"requestedById" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "pageRenderQueue_createdAt_idx" ON "pageRenderQueue" ("createdAt");--> statement-breakpoint
|
||||||
|
ALTER TABLE "pageRenderQueue" ADD CONSTRAINT "pageRenderQueue_pageId_pages_id_fkey" FOREIGN KEY ("pageId") REFERENCES "pages"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "pageRenderQueue" ADD CONSTRAINT "pageRenderQueue_siteId_sites_id_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id");--> statement-breakpoint
|
||||||
|
ALTER TABLE "pageRenderQueue" ADD CONSTRAINT "pageRenderQueue_requestedById_users_id_fkey" FOREIGN KEY ("requestedById") REFERENCES "users"("id") ON DELETE SET NULL;
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,53 @@
|
|||||||
|
import type { PoolClient } from 'pg'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `pg_notify` sender for one LISTEN/NOTIFY client.
|
||||||
|
*
|
||||||
|
* Sending is fire-and-forget by design — see `createNotifier` for why that has to be arranged rather
|
||||||
|
* than simply left unawaited.
|
||||||
|
*/
|
||||||
|
export interface Notifier {
|
||||||
|
/** Queue a notification behind whatever is already going out. Never throws. */
|
||||||
|
send(channel: string, payload: string): void
|
||||||
|
/** Resolves once everything queued so far has gone out, for an orderly shutdown. */
|
||||||
|
drained(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the notifications sent on a dedicated LISTEN/NOTIFY client.
|
||||||
|
*
|
||||||
|
* Three modules hold such a client — the event bus (`core/db.ts`), the scheduler and collaborative
|
||||||
|
* editing — and all three publish from places that cannot wait for a round trip to postgres: an
|
||||||
|
* Emittery listener, a job being picked up, a Yjs handler reacting to a keystroke. So none of them
|
||||||
|
* awaits the `pg_notify`.
|
||||||
|
*
|
||||||
|
* Handing an unawaited query to a client that is already running one is exactly what `pg` deprecated
|
||||||
|
* in 8.x and removes in 9.0. It queues them internally today, which is why this went unnoticed: the
|
||||||
|
* only symptom is a `DeprecationWarning`, and `util.deprecate` emits it once per process however often
|
||||||
|
* it happens. Queueing them here instead costs nothing — the round trips were already serialized, only
|
||||||
|
* silently and on the way out.
|
||||||
|
*
|
||||||
|
* Every notification carries its own `catch`, rather than one at the end of the chain: a failure to
|
||||||
|
* publish belongs to the message that failed, and must not stop the ones behind it from going out.
|
||||||
|
*
|
||||||
|
* @param client Read on each send, since the client is opened after this is built and dropped at
|
||||||
|
* shutdown. A notification sent while there is none is discarded.
|
||||||
|
* @param label What these notifications are, for the log line when one cannot be sent
|
||||||
|
*/
|
||||||
|
export function createNotifier(client: () => PoolClient | null, label: string): Notifier {
|
||||||
|
let tail: Promise<void> = Promise.resolve()
|
||||||
|
return {
|
||||||
|
send(channel: string, payload: string): void {
|
||||||
|
tail = tail.then(async () => {
|
||||||
|
try {
|
||||||
|
await client()?.query('SELECT pg_notify($1, $2)', [channel, payload])
|
||||||
|
} catch (err: any) {
|
||||||
|
WIKI.logger.warn(`Failed to publish a ${label} notification: ${err.message}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
drained(): Promise<void> {
|
||||||
|
return tail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Render every page waiting in the render queue.
|
||||||
|
*
|
||||||
|
* Queued whenever a page is added to `pageRenderQueue` — by an explicit re-render, or by an approved
|
||||||
|
* suggestion that arrived without its HTML. The work is deliberately not split across jobs: rendering
|
||||||
|
* means driving a headless browser, and the whole point of draining the queue in one task is that
|
||||||
|
* there is one browser and it renders one page at a time. A run that finds the queue empty (a second
|
||||||
|
* job for a batch this one already swept) returns without launching anything.
|
||||||
|
*/
|
||||||
|
export async function task(): Promise<void> {
|
||||||
|
await WIKI.models.rendering.drainQueue()
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* The message a failed API request should be reported with.
|
||||||
|
*
|
||||||
|
* The server's own, when it sent one — every `/_api` failure comes back as
|
||||||
|
* `{ ok, error, statusCode, message }` — and ky's description of the request otherwise, which is all
|
||||||
|
* there is for a failure that never reached a route.
|
||||||
|
*
|
||||||
|
* Read off `err.data`, never `err.response`: ky parses the body itself to fill `data` before it throws,
|
||||||
|
* and that consumes it, so `err.response.json()` fails with "Body has already been read". Caught and
|
||||||
|
* discarded — as it was everywhere this replaces — that failure is indistinguishable from a response
|
||||||
|
* with no message, and the server's explanation gets quietly replaced by ky's generic "Request failed
|
||||||
|
* with status code 503". Which is why a wrong password, a name already taken and a missing extension
|
||||||
|
* all used to read the same.
|
||||||
|
*
|
||||||
|
* Synchronous, unlike the per-file helpers it replaces: with the body already parsed there is nothing
|
||||||
|
* left to wait for, so callers read it straight out of the catch.
|
||||||
|
*
|
||||||
|
* @param {Error} err The thrown error — ky's `HTTPError`, or anything else that reached the catch
|
||||||
|
* @param {string} [fallback] Shown when neither the server nor ky offered anything
|
||||||
|
* @returns {string|undefined} What to put in front of the user
|
||||||
|
*/
|
||||||
|
export function apiErrorMessage(err, fallback) {
|
||||||
|
return err?.data?.message || err?.message || fallback
|
||||||
|
}
|
||||||
Loading…
Reference in new issue