@preventive/triage 1.0.0-alpha.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/LICENSE +21 -0
- package/common/save-error-reason.ts +53 -0
- package/common/utf8.d.ts +13 -0
- package/common/utf8.js +57 -0
- package/out/brotli-fallback.js +3 -0
- package/out/client-sync.js +15 -0
- package/out/graph.js +4 -0
- package/out/icon-maskable.svg +5 -0
- package/out/icon.svg +5 -0
- package/out/index.html +78 -0
- package/out/manifest.webmanifest +30 -0
- package/out/prism.js +14 -0
- package/out/terminal.js +39 -0
- package/out/view.css +1 -0
- package/out/view.html +12 -0
- package/out/view.js +138 -0
- package/package.json +129 -0
- package/server/auth.ts +99 -0
- package/server/config.example.json +3 -0
- package/server/config.ts +196 -0
- package/server/db-neon.ts +374 -0
- package/server/db-revision-sql.ts +152 -0
- package/server/db-stmt.ts +53 -0
- package/server/db.ts +577 -0
- package/server/http.ts +142 -0
- package/server/hub.ts +98 -0
- package/server/index.ts +353 -0
- package/server/lifecycle.ts +177 -0
- package/server/neon-driver.ts +26 -0
- package/server/objstore/blob-fs.ts +164 -0
- package/server/objstore/blob-vercel.ts +508 -0
- package/server/objstore/blob.ts +169 -0
- package/server/objstore/fs.ts +67 -0
- package/server/objstore/handlers.ts +235 -0
- package/server/objstore/init.ts +118 -0
- package/server/objstore/reaper.ts +199 -0
- package/server/objstore/rest.ts +484 -0
- package/server/objstore/sign.ts +164 -0
- package/server/objstore/store-neon.ts +351 -0
- package/server/objstore/store.ts +799 -0
- package/server/objstore/tokens.ts +168 -0
- package/server/origin.ts +68 -0
- package/server/peer.ts +38 -0
- package/server/sign.ts +231 -0
- package/server/static.ts +374 -0
- package/server/sync-handlers.ts +311 -0
- package/server/util.ts +27 -0
- package/server/validation.ts +36 -0
- package/server/ws-server.ts +245 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Neon Postgres backend for the v1.objstore tables. Mirrors the
|
|
2
|
+
// SQLite-backed `openObjstore` in ./store.ts — same `Handle` shape
|
|
3
|
+
// (with `db: DatabaseSync` unset), same queries by intent, Postgres
|
|
4
|
+
// dialect on the wire.
|
|
5
|
+
//
|
|
6
|
+
// `@neondatabase/serverless` is an OPTIONAL peer dep — selected by
|
|
7
|
+
// the `DATABASE_URL` branch in `server/index.ts`. The peer dep
|
|
8
|
+
// itself is loaded lazily inside `openNeonObjstore` below. A single
|
|
9
|
+
// Neon project / database holds both `workspace_revision` and the
|
|
10
|
+
// two objstore tables; each plane opens its own stateless
|
|
11
|
+
// `neon(url)` HTTP callable independently — there is no shared
|
|
12
|
+
// connection. Calling `neon()` is cheap; no resource is held to
|
|
13
|
+
// release.
|
|
14
|
+
//
|
|
15
|
+
// Byte plane: passed in as a `BlobBackend` (./blob.ts). The
|
|
16
|
+
// supported pairings selected by server/index.ts are:
|
|
17
|
+
// - Neon + Vercel Blob Private Storage (multi-replica, the only
|
|
18
|
+
// pairing that survives a replica restart without local-disk
|
|
19
|
+
// coordination). Activated by setting BLOB_READ_WRITE_TOKEN
|
|
20
|
+
// alongside DATABASE_URL.
|
|
21
|
+
// - Neon + local FS (development / operator escape hatch — bytes
|
|
22
|
+
// still need to live on a shared filesystem if you run more
|
|
23
|
+
// than one replica).
|
|
24
|
+
|
|
25
|
+
import type { BlobBackend } from './blob.ts'
|
|
26
|
+
import type { AllStmt, GetStmt, RunStmt } from '../db-stmt.ts'
|
|
27
|
+
import type { Handle } from './store.ts'
|
|
28
|
+
import { type NeonSql, assertDurableSyncCommit, getRowStmt, num, numOrNull, runStmt } from '../db-neon.ts'
|
|
29
|
+
|
|
30
|
+
// Advisory-lock keys (two int32s for the two-arg form). Distinct
|
|
31
|
+
// from the workspace_revision DDL keys in `db-neon.ts` so the two
|
|
32
|
+
// bootstraps don't serialize against each other. See db-neon.ts for
|
|
33
|
+
// the choice rationale.
|
|
34
|
+
const DDL_LOCK_KEY_OBJSTORE = 0x6465_7670 // 'depv'
|
|
35
|
+
const DDL_LOCK_KEY_OBJSTORE_SUB = 0x6f62_6a73 // 'objs'
|
|
36
|
+
|
|
37
|
+
// Statement-per-array-element because Neon's HTTP transport runs
|
|
38
|
+
// one statement per request — there's no multi-statement support
|
|
39
|
+
// like SQLite's `db.exec`. The DDL bootstrap in `openNeonObjstore`
|
|
40
|
+
// wraps the full array in a pipelined `sql.transaction` so the
|
|
41
|
+
// whole schema either applies or rolls back, AND grabs a
|
|
42
|
+
// transaction-scoped advisory lock so concurrent boots serialize.
|
|
43
|
+
//
|
|
44
|
+
// `CHECK (version >= 0)` / `CHECK (content_length >= 0)` (plus the
|
|
45
|
+
// staging table's `expected_length >= 0` and `prev_version` guards)
|
|
46
|
+
// defend the commitPut conflict arithmetic — a manual `UPDATE
|
|
47
|
+
// workspace_object SET version = -1` would otherwise round-trip
|
|
48
|
+
// through `num()` (which only rejects non-safe-integers) and corrupt
|
|
49
|
+
// the version monotonicity invariant. The SQLite schema carries the
|
|
50
|
+
// identical CHECKs (see `store.ts`) — STRICT there enforces the column
|
|
51
|
+
// TYPE but NOT this value domain (`version = -1` is a valid integer
|
|
52
|
+
// STRICT accepts), so both backends need the explicit CHECKs.
|
|
53
|
+
const SCHEMA_PG = [
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS workspace_object (
|
|
55
|
+
workspace_tag TEXT NOT NULL,
|
|
56
|
+
resource_tag TEXT NOT NULL,
|
|
57
|
+
version BIGINT NOT NULL CHECK (version >= 0),
|
|
58
|
+
incarnation TEXT NOT NULL,
|
|
59
|
+
content_hash TEXT NOT NULL,
|
|
60
|
+
content_length BIGINT NOT NULL CHECK (content_length >= 0),
|
|
61
|
+
signature TEXT NOT NULL,
|
|
62
|
+
put_at BIGINT NOT NULL,
|
|
63
|
+
PRIMARY KEY (workspace_tag, resource_tag)
|
|
64
|
+
)`,
|
|
65
|
+
`CREATE TABLE IF NOT EXISTS workspace_object_staging (
|
|
66
|
+
workspace_tag TEXT NOT NULL,
|
|
67
|
+
resource_tag TEXT NOT NULL,
|
|
68
|
+
staging_id TEXT NOT NULL,
|
|
69
|
+
prev_version BIGINT CHECK (prev_version IS NULL OR prev_version >= 0),
|
|
70
|
+
prev_incarnation TEXT,
|
|
71
|
+
expected_length BIGINT NOT NULL CHECK (expected_length >= 0),
|
|
72
|
+
content_hash TEXT NOT NULL,
|
|
73
|
+
signature TEXT NOT NULL,
|
|
74
|
+
begun_at BIGINT NOT NULL,
|
|
75
|
+
PRIMARY KEY (workspace_tag, resource_tag, staging_id)
|
|
76
|
+
)`,
|
|
77
|
+
`CREATE INDEX IF NOT EXISTS workspace_object_staging_begun_at_idx
|
|
78
|
+
ON workspace_object_staging (begun_at)`,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// `num` / `numOrNull` (safe-integer BIGINT coercion) are shared with
|
|
82
|
+
// the workspace_revision Neon plane — imported from ../db-neon.ts so
|
|
83
|
+
// the two planes can't drift on the coercion / range-check rules.
|
|
84
|
+
|
|
85
|
+
type LiveDbRow = {
|
|
86
|
+
resource_tag: string; version: number; incarnation: string; content_hash: string; content_length: number
|
|
87
|
+
signature: string; put_at: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mapLiveRow(r: Record<string, unknown>): LiveDbRow {
|
|
91
|
+
return {
|
|
92
|
+
resource_tag: String(r['resource_tag']),
|
|
93
|
+
version: num(r['version']),
|
|
94
|
+
incarnation: String(r['incarnation']),
|
|
95
|
+
content_hash: String(r['content_hash']),
|
|
96
|
+
content_length: num(r['content_length']),
|
|
97
|
+
signature: String(r['signature']),
|
|
98
|
+
put_at: num(r['put_at']),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Per-statement builders — extracted so `openNeonObjstore` stays
|
|
103
|
+
// within the max-lines-per-function budget. Each closes over the
|
|
104
|
+
// Neon `sql` callable.
|
|
105
|
+
|
|
106
|
+
function buildInsertStaging(sql: NeonSql): RunStmt<[string, string, string, number | null, string | null, number, string, string, number]> {
|
|
107
|
+
return runStmt(sql, `INSERT INTO workspace_object_staging
|
|
108
|
+
(workspace_tag, resource_tag, staging_id, prev_version, prev_incarnation,
|
|
109
|
+
expected_length, content_hash, signature, begun_at)
|
|
110
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type StagingRow = {
|
|
114
|
+
prev_version: number | null
|
|
115
|
+
prev_incarnation: string | null
|
|
116
|
+
expected_length: number
|
|
117
|
+
content_hash: string
|
|
118
|
+
signature: string
|
|
119
|
+
begun_at: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildSelectStaging(sql: NeonSql): GetStmt<[string, string, string], StagingRow> {
|
|
123
|
+
return { get: async (tag, resourceTag, stagingId) => {
|
|
124
|
+
const rows = await sql(
|
|
125
|
+
`SELECT prev_version, prev_incarnation, expected_length, content_hash, signature, begun_at
|
|
126
|
+
FROM workspace_object_staging
|
|
127
|
+
WHERE workspace_tag = $1 AND resource_tag = $2 AND staging_id = $3`,
|
|
128
|
+
[tag, resourceTag, stagingId],
|
|
129
|
+
) as Array<Record<string, unknown>>
|
|
130
|
+
const r = rows[0]
|
|
131
|
+
if (!r) return undefined
|
|
132
|
+
return {
|
|
133
|
+
prev_version: numOrNull(r['prev_version']),
|
|
134
|
+
prev_incarnation: r['prev_incarnation'] == null ? null : String(r['prev_incarnation']),
|
|
135
|
+
expected_length: num(r['expected_length']),
|
|
136
|
+
content_hash: String(r['content_hash']),
|
|
137
|
+
signature: String(r['signature']),
|
|
138
|
+
begun_at: num(r['begun_at']),
|
|
139
|
+
}
|
|
140
|
+
} }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildSelectStagingByWsSid(sql: NeonSql): GetStmt<[string, string], unknown> {
|
|
144
|
+
return getRowStmt(sql, `SELECT 1 AS one FROM workspace_object_staging WHERE workspace_tag = $1 AND staging_id = $2`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildRefreshStagingBegunAt(sql: NeonSql): RunStmt<[number, string, string, string]> {
|
|
148
|
+
return runStmt(sql, `UPDATE workspace_object_staging SET begun_at = $1
|
|
149
|
+
WHERE workspace_tag = $2 AND resource_tag = $3 AND staging_id = $4`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildDeleteStaging(sql: NeonSql): RunStmt<[string, string, string]> {
|
|
153
|
+
return runStmt(sql, `DELETE FROM workspace_object_staging
|
|
154
|
+
WHERE workspace_tag = $1 AND resource_tag = $2 AND staging_id = $3`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Atomic conditional stale-row delete for the reaper. Deletes the row
|
|
158
|
+
// IFF its `begun_at < $4`, returning `{ ok: 1 }` only when a row was
|
|
159
|
+
// actually removed (a concurrent `refreshStagingBegunAt` that bumped
|
|
160
|
+
// `begun_at` fresh makes the predicate fail → empty RETURNING →
|
|
161
|
+
// caller skips). Mirrors the SQLite `deleteStagingIfStale` statement.
|
|
162
|
+
// Bind order: (tag, res, sid, staleBefore).
|
|
163
|
+
function buildDeleteStagingIfStale(sql: NeonSql): GetStmt<[string, string, string, number], { ok: number }> {
|
|
164
|
+
return { get: async (tag, resourceTag, stagingId, staleBefore) => {
|
|
165
|
+
const rows = await sql(
|
|
166
|
+
`DELETE FROM workspace_object_staging
|
|
167
|
+
WHERE workspace_tag = $1 AND resource_tag = $2 AND staging_id = $3 AND begun_at < $4
|
|
168
|
+
RETURNING 1 AS ok`,
|
|
169
|
+
[tag, resourceTag, stagingId, staleBefore],
|
|
170
|
+
) as Array<{ ok: number | string }>
|
|
171
|
+
const r = rows[0]
|
|
172
|
+
return r ? { ok: num(r.ok) } : undefined
|
|
173
|
+
} }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildSelectLive(sql: NeonSql): AllStmt<[string], LiveDbRow> {
|
|
177
|
+
return { all: async (tag) => {
|
|
178
|
+
const rows = await sql(
|
|
179
|
+
`SELECT resource_tag, version, incarnation, content_hash, content_length,
|
|
180
|
+
signature, put_at
|
|
181
|
+
FROM workspace_object
|
|
182
|
+
WHERE workspace_tag = $1
|
|
183
|
+
ORDER BY resource_tag ASC`,
|
|
184
|
+
[tag],
|
|
185
|
+
) as Array<Record<string, unknown>>
|
|
186
|
+
return rows.map(mapLiveRow)
|
|
187
|
+
} }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildSelectLiveOne(sql: NeonSql): GetStmt<[string, string], LiveDbRow> {
|
|
191
|
+
return { get: async (tag, resourceTag) => {
|
|
192
|
+
const rows = await sql(
|
|
193
|
+
`SELECT resource_tag, version, incarnation, content_hash, content_length,
|
|
194
|
+
signature, put_at
|
|
195
|
+
FROM workspace_object
|
|
196
|
+
WHERE workspace_tag = $1 AND resource_tag = $2`,
|
|
197
|
+
[tag, resourceTag],
|
|
198
|
+
) as Array<Record<string, unknown>>
|
|
199
|
+
const r = rows[0]
|
|
200
|
+
return r ? mapLiveRow(r) : undefined
|
|
201
|
+
} }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// First-write CAS: insert the live row at version 1 IF ABSENT.
|
|
205
|
+
// `ON CONFLICT … DO NOTHING RETURNING 1` returns a row only when our
|
|
206
|
+
// insert won; a racing first-write that landed first makes this a
|
|
207
|
+
// no-op (empty result) → caller maps to conflict. Bind order:
|
|
208
|
+
// (tag, res, hash, len, sig, put_at); version is the literal 1.
|
|
209
|
+
function buildInsertLiveIfAbsent(sql: NeonSql): GetStmt<[string, string, string, string, number, string, number], { ok: number }> {
|
|
210
|
+
return { get: async (tag, resourceTag, incarnation, contentHash, contentLength, signature, putAt) => {
|
|
211
|
+
const rows = await sql(
|
|
212
|
+
`INSERT INTO workspace_object
|
|
213
|
+
(workspace_tag, resource_tag, version, incarnation, content_hash, content_length,
|
|
214
|
+
signature, put_at)
|
|
215
|
+
VALUES ($1, $2, 1, $3, $4, $5, $6, $7)
|
|
216
|
+
ON CONFLICT (workspace_tag, resource_tag) DO NOTHING
|
|
217
|
+
RETURNING 1 AS ok`,
|
|
218
|
+
[tag, resourceTag, incarnation, contentHash, contentLength, signature, putAt],
|
|
219
|
+
) as Array<{ ok: number | string }>
|
|
220
|
+
const r = rows[0]
|
|
221
|
+
return r ? { ok: num(r.ok) } : undefined
|
|
222
|
+
} }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Re-upload CAS: bump the row to `$3` (next version) only when the
|
|
226
|
+
// live version still equals `$8` (the precondition the caller read).
|
|
227
|
+
// `RETURNING 1` comes back only when the WHERE matched — exactly one
|
|
228
|
+
// of N racing re-uploads against the same base version wins; the
|
|
229
|
+
// losers get an empty result → conflict + rebase. Bind order:
|
|
230
|
+
// (tag, res, nextVersion, hash, len, sig, put_at, expectedVersion).
|
|
231
|
+
function buildUpdateLiveCAS(sql: NeonSql): GetStmt<[string, string, number, string, number, string, number, number, string], { ok: number }> {
|
|
232
|
+
return { get: async (tag, resourceTag, nextVersion, contentHash, contentLength, signature, putAt, expectedVersion, expectedIncarnation) => {
|
|
233
|
+
const rows = await sql(
|
|
234
|
+
`UPDATE workspace_object
|
|
235
|
+
SET version = $3,
|
|
236
|
+
content_hash = $4,
|
|
237
|
+
content_length = $5,
|
|
238
|
+
signature = $6,
|
|
239
|
+
put_at = $7
|
|
240
|
+
WHERE workspace_tag = $1 AND resource_tag = $2 AND version = $8 AND incarnation = $9
|
|
241
|
+
RETURNING 1 AS ok`,
|
|
242
|
+
[tag, resourceTag, nextVersion, contentHash, contentLength, signature, putAt, expectedVersion, expectedIncarnation],
|
|
243
|
+
) as Array<{ ok: number | string }>
|
|
244
|
+
const r = rows[0]
|
|
245
|
+
return r ? { ok: num(r.ok) } : undefined
|
|
246
|
+
} }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Version-CAS delete: drop the row only while its version still matches
|
|
250
|
+
// the precondition (mirrors buildUpdateLiveCAS). RETURNING a row iff we
|
|
251
|
+
// removed it; deleteObject treats 0 rows as a lost race → conflict /
|
|
252
|
+
// not-found, never a lost update.
|
|
253
|
+
function buildDeleteLiveCAS(sql: NeonSql): GetStmt<[string, string, number, string], { ok: number }> {
|
|
254
|
+
return { get: async (tag, resourceTag, expectedVersion, expectedIncarnation) => {
|
|
255
|
+
const rows = await sql(
|
|
256
|
+
`DELETE FROM workspace_object
|
|
257
|
+
WHERE workspace_tag = $1 AND resource_tag = $2 AND version = $3 AND incarnation = $4
|
|
258
|
+
RETURNING 1 AS ok`,
|
|
259
|
+
[tag, resourceTag, expectedVersion, expectedIncarnation],
|
|
260
|
+
) as Array<{ ok: number | string }>
|
|
261
|
+
const r = rows[0]
|
|
262
|
+
return r ? { ok: num(r.ok) } : undefined
|
|
263
|
+
} }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildListAllStaging(sql: NeonSql): AllStmt<[number], { workspace_tag: string; resource_tag: string; staging_id: string; begun_at: number }> {
|
|
267
|
+
return { all: async (staleBefore) => {
|
|
268
|
+
// `WHERE begun_at < $1` uses workspace_object_staging_begun_at_idx
|
|
269
|
+
// so the reaper sweep is O(stale-rows) cluster-wide. DB-layout
|
|
270
|
+
// audit follow-up.
|
|
271
|
+
const rows = await sql(
|
|
272
|
+
`SELECT workspace_tag, resource_tag, staging_id, begun_at
|
|
273
|
+
FROM workspace_object_staging
|
|
274
|
+
WHERE begun_at < $1`,
|
|
275
|
+
[staleBefore],
|
|
276
|
+
) as Array<Record<string, unknown>>
|
|
277
|
+
return rows.map((r) => ({
|
|
278
|
+
workspace_tag: String(r['workspace_tag']),
|
|
279
|
+
resource_tag: String(r['resource_tag']),
|
|
280
|
+
staging_id: String(r['staging_id']),
|
|
281
|
+
begun_at: num(r['begun_at']),
|
|
282
|
+
}))
|
|
283
|
+
} }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildListLiveTags(sql: NeonSql): AllStmt<[], { workspace_tag: string }> {
|
|
287
|
+
return { all: async () => {
|
|
288
|
+
const rows = await sql(
|
|
289
|
+
`SELECT DISTINCT workspace_tag FROM workspace_object`,
|
|
290
|
+
[],
|
|
291
|
+
) as Array<Record<string, unknown>>
|
|
292
|
+
return rows.map((r) => ({ workspace_tag: String(r['workspace_tag']) }))
|
|
293
|
+
} }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildCountLive(sql: NeonSql): GetStmt<[string], { c: number }> {
|
|
297
|
+
return { get: async (tag) => {
|
|
298
|
+
const rows = await sql(
|
|
299
|
+
`SELECT COUNT(*) AS c FROM workspace_object WHERE workspace_tag = $1`,
|
|
300
|
+
[tag],
|
|
301
|
+
) as Array<{ c: number | string | bigint }>
|
|
302
|
+
const r = rows[0]
|
|
303
|
+
return r ? { c: num(r.c) } : undefined
|
|
304
|
+
} }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function openNeonObjstore(connectionString: string, blob: BlobBackend): Promise<Handle> {
|
|
308
|
+
// Same dynamic-import pattern as db-neon.ts — routed through the
|
|
309
|
+
// local `../neon-driver.ts` re-export wrapper so the peer dep stays
|
|
310
|
+
// optional AND tests can mock the driver (a local path is always
|
|
311
|
+
// resolvable; the bare specifier isn't when the dep is absent). See
|
|
312
|
+
// `server/neon-driver.ts`. Cast through `unknown` because the
|
|
313
|
+
// wrapper's `export *` re-exports a `@ts-ignore`'d module.
|
|
314
|
+
const mod = (await import('../neon-driver.ts')) as unknown as { neon: (url: string) => NeonSql }
|
|
315
|
+
const sql: NeonSql = mod.neon(connectionString)
|
|
316
|
+
// Boot-time durability gate (see db-neon.ts) — `openNeonDb` runs
|
|
317
|
+
// the same assertion when it opens, but `openNeonObjstore` is
|
|
318
|
+
// independently invoked for the objstore plane and an operator
|
|
319
|
+
// who runs only this plane (future stand-alone deployment) must
|
|
320
|
+
// be gated too.
|
|
321
|
+
await assertDurableSyncCommit(sql)
|
|
322
|
+
// DDL bootstrap under one pipelined transaction so a transient
|
|
323
|
+
// network failure mid-batch rolls back, AND a transaction-scoped
|
|
324
|
+
// advisory lock so two replicas booting concurrently serialize
|
|
325
|
+
// their DDL (the advisory lock releases at COMMIT).
|
|
326
|
+
await sql.transaction([
|
|
327
|
+
sql(`SELECT pg_advisory_xact_lock($1, $2)`, [DDL_LOCK_KEY_OBJSTORE, DDL_LOCK_KEY_OBJSTORE_SUB]),
|
|
328
|
+
...SCHEMA_PG.map((stmt) => sql(stmt, [])),
|
|
329
|
+
])
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
// `db` intentionally unset — Neon has no `DatabaseSync`.
|
|
333
|
+
// `dir` intentionally unset — the byte plane goes through the
|
|
334
|
+
// `blob` backend, which may not have an on-disk layout at all.
|
|
335
|
+
blob,
|
|
336
|
+
insertStaging: buildInsertStaging(sql),
|
|
337
|
+
selectStaging: buildSelectStaging(sql),
|
|
338
|
+
selectStagingByWsSid: buildSelectStagingByWsSid(sql),
|
|
339
|
+
refreshStagingBegunAt: buildRefreshStagingBegunAt(sql),
|
|
340
|
+
deleteStaging: buildDeleteStaging(sql),
|
|
341
|
+
deleteStagingIfStale: buildDeleteStagingIfStale(sql),
|
|
342
|
+
selectLive: buildSelectLive(sql),
|
|
343
|
+
selectLiveOne: buildSelectLiveOne(sql),
|
|
344
|
+
insertLiveIfAbsent: buildInsertLiveIfAbsent(sql),
|
|
345
|
+
updateLiveCAS: buildUpdateLiveCAS(sql),
|
|
346
|
+
deleteLiveCAS: buildDeleteLiveCAS(sql),
|
|
347
|
+
listAllStaging: buildListAllStaging(sql),
|
|
348
|
+
listLiveTags: buildListLiveTags(sql),
|
|
349
|
+
countLive: buildCountLive(sql),
|
|
350
|
+
}
|
|
351
|
+
}
|