@coffre/db 0.14.4 → 0.14.5
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/dist/connect.d.ts +1 -1
- package/dist/{database-CFhTnnzM.d.ts → database-wq1AVRAP.d.ts} +1 -1
- package/dist/dialect.d.ts +1 -1
- package/dist/grants.d.ts +1 -1
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +27 -1
- package/dist/log.js +50 -1
- package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/dist/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/dist/migrations/postgres/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-z8GcN_Xu.d.ts → schema-BfQEBqFv.d.ts} +3 -2
- package/dist/schema-sqlite.d.ts +2 -2
- package/dist/schema-sqlite.js +2 -2
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +7 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -2
- package/package.json +2 -2
- package/src/log.ts +52 -0
- package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- package/src/migrations/postgres/meta/0021_snapshot.json +3334 -0
- package/src/migrations/postgres/meta/_journal.json +7 -0
- package/src/schema.sqlite.ts +2 -2
- package/src/schema.ts +3 -2
package/dist/connect.d.ts
CHANGED
package/dist/dialect.d.ts
CHANGED
package/dist/grants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
1
|
+
import { i as Queryable } from "./database-wq1AVRAP.js";
|
|
2
2
|
import { Authenticity, Newest } from "./log.js";
|
|
3
3
|
import { StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
import { Conversion, ConversionInput, InstanceRole, Scope } from "@coffre/core/access";
|
package/dist/hyperdrive.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { g as schema_d_exports } from "./schema-
|
|
2
|
-
import { a as Transaction, c as tablesOf, i as Queryable, l as prepared, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-
|
|
1
|
+
import { g as schema_d_exports } from "./schema-BfQEBqFv.js";
|
|
2
|
+
import { a as Transaction, c as tablesOf, i as Queryable, l as prepared, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-wq1AVRAP.js";
|
|
3
3
|
export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, prepared, schema_d_exports as schema, tablesOf };
|
package/dist/log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as Transaction, i as Queryable } from "./database-
|
|
1
|
+
import { a as Transaction, i as Queryable } from "./database-wq1AVRAP.js";
|
|
2
2
|
import { Placeholder, SQL } from "drizzle-orm";
|
|
3
3
|
import { LogKey, StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
//#region src/log.d.ts
|
|
@@ -48,6 +48,16 @@ export type Appended = {
|
|
|
48
48
|
export declare class LogHeadMismatch extends Error {
|
|
49
49
|
constructor(message: string);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* As many transactions as may already wait for the log's head do
|
|
53
|
+
* (`lockLogHeadQueued`): this one waits for nothing, and holds nothing.
|
|
54
|
+
* Its code is Postgres's for a lock not available, so every caller already
|
|
55
|
+
* answers it as one that gave up waiting: unavailable, ask again shortly.
|
|
56
|
+
*/
|
|
57
|
+
export declare class HeadBusy extends Error {
|
|
58
|
+
readonly code = "55P03";
|
|
59
|
+
constructor();
|
|
60
|
+
}
|
|
51
61
|
/**
|
|
52
62
|
* The log's head is behind one this process wrote: the database was rolled
|
|
53
63
|
* back, or entries were cut off its end. That cannot be told from a restore,
|
|
@@ -92,6 +102,22 @@ export declare function lockLogHead(tx: Transaction): Promise<{
|
|
|
92
102
|
* SQLite's transactions hold the whole file, so the head is always free.
|
|
93
103
|
*/
|
|
94
104
|
export declare function lockLogHeadIfFree(tx: Transaction): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Lock the log's head, waiting for it as `lockLogHead` does, if fewer than
|
|
107
|
+
* `waiters` other transactions hold one of its places in line; otherwise
|
|
108
|
+
* refuse at once (`HeadBusy`), having waited for nothing. A place is an
|
|
109
|
+
* advisory lock, tried, never waited for, and held to the end of the
|
|
110
|
+
* transaction, which holds the head by then: so at most `waiters`
|
|
111
|
+
* connections wait for the head, or hold it, through here, and a burst
|
|
112
|
+
* beyond them answers in one round trip rather than seconds later, holding
|
|
113
|
+
* a connection all the while. One statement: the place, then, only if one
|
|
114
|
+
* was free, the head; refused, one more says whether the head is there at
|
|
115
|
+
* all. SQLite's transactions hold the whole file.
|
|
116
|
+
*/
|
|
117
|
+
export declare function lockLogHeadQueued(tx: Transaction, waiters: number): Promise<{
|
|
118
|
+
nextSeq: bigint;
|
|
119
|
+
headHash: Buffer;
|
|
120
|
+
}>;
|
|
95
121
|
/**
|
|
96
122
|
* Append `entries` under `key`. `check`, when given, runs once the head is
|
|
97
123
|
* locked and before anything is written: a writer's own condition on the
|
package/dist/log.js
CHANGED
|
@@ -12,6 +12,19 @@ var LogHeadMismatch = class extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
|
+
* As many transactions as may already wait for the log's head do
|
|
16
|
+
* (`lockLogHeadQueued`): this one waits for nothing, and holds nothing.
|
|
17
|
+
* Its code is Postgres's for a lock not available, so every caller already
|
|
18
|
+
* answers it as one that gave up waiting: unavailable, ask again shortly.
|
|
19
|
+
*/
|
|
20
|
+
var HeadBusy = class extends Error {
|
|
21
|
+
code = "55P03";
|
|
22
|
+
constructor() {
|
|
23
|
+
super("too many requests wait for the audit log's head: ask again shortly");
|
|
24
|
+
this.name = "HeadBusy";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
15
28
|
* The log's head is behind one this process wrote: the database was rolled
|
|
16
29
|
* back, or entries were cut off its end. That cannot be told from a restore,
|
|
17
30
|
* so the append refuses and someone has to look.
|
|
@@ -99,6 +112,42 @@ async function lockLogHeadIfFree(tx) {
|
|
|
99
112
|
if (rows.length > 0) holding.add(tx);
|
|
100
113
|
return rows.length > 0;
|
|
101
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Lock the log's head, waiting for it as `lockLogHead` does, if fewer than
|
|
117
|
+
* `waiters` other transactions hold one of its places in line; otherwise
|
|
118
|
+
* refuse at once (`HeadBusy`), having waited for nothing. A place is an
|
|
119
|
+
* advisory lock, tried, never waited for, and held to the end of the
|
|
120
|
+
* transaction, which holds the head by then: so at most `waiters`
|
|
121
|
+
* connections wait for the head, or hold it, through here, and a burst
|
|
122
|
+
* beyond them answers in one round trip rather than seconds later, holding
|
|
123
|
+
* a connection all the while. One statement: the place, then, only if one
|
|
124
|
+
* was free, the head; refused, one more says whether the head is there at
|
|
125
|
+
* all. SQLite's transactions hold the whole file.
|
|
126
|
+
*/
|
|
127
|
+
async function lockLogHeadQueued(tx, waiters) {
|
|
128
|
+
if (engineOf(tx) === "sqlite") return lockLogHead(tx);
|
|
129
|
+
const { rows } = await tx.execute(sql`
|
|
130
|
+
SELECT next_seq, head_hash FROM audit_chain_head
|
|
131
|
+
WHERE only_row AND (SELECT true FROM generate_series(1, ${waiters}::int) AS place WHERE pg_try_advisory_xact_lock(${HEAD_LINE}, place) LIMIT 1)
|
|
132
|
+
FOR UPDATE`);
|
|
133
|
+
const [head] = rows;
|
|
134
|
+
if (head === void 0) {
|
|
135
|
+
const { rows: [found] } = await tx.execute(sql`SELECT EXISTS (SELECT 1 FROM audit_chain_head WHERE only_row) AS found`);
|
|
136
|
+
if (found?.found !== true) throw new Error("audit_chain_head is missing; refusing to write an unchained entry");
|
|
137
|
+
throw new HeadBusy();
|
|
138
|
+
}
|
|
139
|
+
holding.add(tx);
|
|
140
|
+
return {
|
|
141
|
+
nextSeq: BigInt(head.next_seq),
|
|
142
|
+
headHash: head.head_hash
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The first key of the head's places in line, Postgres's advisory locks:
|
|
147
|
+
* the second is the place. A space apart from the members' queues and the
|
|
148
|
+
* migrator's key.
|
|
149
|
+
*/
|
|
150
|
+
const HEAD_LINE = 1668245096;
|
|
102
151
|
const lockedHead = prepared((db) => {
|
|
103
152
|
const { auditChainHead } = tablesOf(db);
|
|
104
153
|
return forUpdate(db, db.select({
|
|
@@ -388,4 +437,4 @@ async function newestAuthentic(page, check) {
|
|
|
388
437
|
return "unreached";
|
|
389
438
|
}
|
|
390
439
|
//#endregion
|
|
391
|
-
export { BATCH_BYTES, KeyRetired, LOOK_BACK, LogHeadMismatch, LogRewound, MOVES_FROM, appendEntries, entryColumns, forgetLogHeads, lockLogHead, lockLogHeadIfFree, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
440
|
+
export { BATCH_BYTES, HeadBusy, KeyRetired, LOOK_BACK, LogHeadMismatch, LogRewound, MOVES_FROM, appendEntries, entryColumns, forgetLogHeads, lockLogHead, lockLogHeadIfFree, lockLogHeadQueued, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Releases pruned (W199, T111): the vault's checkpoint forgets the releases no window can count again, those over a day and an hour old by the database's clock, at most at_most a call. A bulk limit's window is a day at most (the vault's config), and the hour is a margin for the clock stepping back: no caller chooses what is kept, so the vault's login, which still deletes no row itself, cannot forget a release a window counts. The release before never calls it.
|
|
2
|
+
CREATE FUNCTION prune_vault_releases(at_most integer) RETURNS integer
|
|
3
|
+
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET lock_timeout = '3s' AS $$
|
|
4
|
+
-- The bound in the DELETE's own WHERE too: a row a read moved on since the subquery's snapshot is checked again,
|
|
5
|
+
-- as it is now, and kept.
|
|
6
|
+
WITH bound AS (SELECT (extract(epoch from clock_timestamp()) * 1000)::bigint - 25 * 3600000 AS before),
|
|
7
|
+
pruned AS (
|
|
8
|
+
DELETE FROM public.vault_releases
|
|
9
|
+
WHERE released_at < (SELECT before FROM bound)
|
|
10
|
+
AND ctid IN (
|
|
11
|
+
-- Any of them, each past every window; a row an append holds is left to the next run, rather than waited for.
|
|
12
|
+
SELECT ctid FROM public.vault_releases WHERE released_at < (SELECT before FROM bound) LIMIT at_most FOR UPDATE SKIP LOCKED
|
|
13
|
+
)
|
|
14
|
+
RETURNING 1
|
|
15
|
+
)
|
|
16
|
+
SELECT count(*)::integer FROM pruned;
|
|
17
|
+
$$;--> statement-breakpoint
|
|
18
|
+
REVOKE ALL ON FUNCTION prune_vault_releases(integer) FROM PUBLIC;--> statement-breakpoint
|
|
19
|
+
GRANT EXECUTE ON FUNCTION prune_vault_releases(integer) TO coffre_vault;
|