@coffre/db 0.1.9 → 0.1.10
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/hyperdrive.d.ts +22 -6
- package/dist/hyperdrive.js +85 -14
- package/package.json +2 -2
- package/src/hyperdrive.ts +95 -14
package/dist/hyperdrive.d.ts
CHANGED
|
@@ -13,20 +13,36 @@ export declare function postgres(hyperdrive: {
|
|
|
13
13
|
readonly connectionString: string;
|
|
14
14
|
}): PostgresDatabase;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* node-postgres for one call on Workers, through Hyperdrive: one client,
|
|
17
|
+
* connected on the first query, which every query and transaction of the
|
|
18
|
+
* call takes its turn on.
|
|
17
19
|
*
|
|
18
|
-
* Hyperdrive
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Hyperdrive keeps the pool to the database; a Worker's connections belong
|
|
21
|
+
* to the call that opened them, so each call opens its own. One, not one per
|
|
22
|
+
* query: on Cloudflare, a call that opened several Hyperdrive connections at
|
|
23
|
+
* once was now and then cancelled as hung, with nothing left pending, and
|
|
24
|
+
* every connection costs a round trip before the query can be sent.
|
|
25
|
+
*
|
|
26
|
+
* The turns keep a transaction whole: queries go to the client in the order
|
|
27
|
+
* they are asked, a transaction holds it from BEGIN until it is released,
|
|
28
|
+
* and a query asked meanwhile waits until then, so nothing runs inside a
|
|
29
|
+
* transaction it is not part of. So code inside a transaction must query
|
|
30
|
+
* through the transaction: a query on the database there would wait for the
|
|
31
|
+
* transaction to end, which waits for it. Neither the vault nor the server
|
|
32
|
+
* does.
|
|
33
|
+
*
|
|
34
|
+
* The class name must end in `Pool`: that is how Drizzle knows to check out
|
|
35
|
+
* a client for each transaction.
|
|
23
36
|
*/
|
|
24
37
|
export declare class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
|
|
25
38
|
#private;
|
|
26
39
|
constructor(connectionString: string);
|
|
27
40
|
readonly query: pg.Pool['query'];
|
|
41
|
+
/** The client, for a transaction: held from now until `release`. */
|
|
28
42
|
connect(): Promise<Pick<pg.PoolClient, 'query'> & {
|
|
29
43
|
release: () => void;
|
|
30
44
|
}>;
|
|
45
|
+
/** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
|
|
46
|
+
end(): Promise<void>;
|
|
31
47
|
}
|
|
32
48
|
//#endregion
|
package/dist/hyperdrive.js
CHANGED
|
@@ -10,44 +10,115 @@ function postgres(hyperdrive) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* node-postgres for one call on Workers, through Hyperdrive: one client,
|
|
14
|
+
* connected on the first query, which every query and transaction of the
|
|
15
|
+
* call takes its turn on.
|
|
14
16
|
*
|
|
15
|
-
* Hyperdrive
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Hyperdrive keeps the pool to the database; a Worker's connections belong
|
|
18
|
+
* to the call that opened them, so each call opens its own. One, not one per
|
|
19
|
+
* query: on Cloudflare, a call that opened several Hyperdrive connections at
|
|
20
|
+
* once was now and then cancelled as hung, with nothing left pending, and
|
|
21
|
+
* every connection costs a round trip before the query can be sent.
|
|
22
|
+
*
|
|
23
|
+
* The turns keep a transaction whole: queries go to the client in the order
|
|
24
|
+
* they are asked, a transaction holds it from BEGIN until it is released,
|
|
25
|
+
* and a query asked meanwhile waits until then, so nothing runs inside a
|
|
26
|
+
* transaction it is not part of. So code inside a transaction must query
|
|
27
|
+
* through the transaction: a query on the database there would wait for the
|
|
28
|
+
* transaction to end, which waits for it. Neither the vault nor the server
|
|
29
|
+
* does.
|
|
30
|
+
*
|
|
31
|
+
* The class name must end in `Pool`: that is how Drizzle knows to check out
|
|
32
|
+
* a client for each transaction.
|
|
20
33
|
*/
|
|
21
34
|
var HyperdrivePool = class {
|
|
22
35
|
#connectionString;
|
|
36
|
+
/** The client, connecting or connected; null before the first query, and once it has failed. */
|
|
37
|
+
#client = null;
|
|
38
|
+
/** Whether a transaction holds the client. */
|
|
39
|
+
#held = false;
|
|
40
|
+
/** What waits for its turn, in the order it asked. */
|
|
41
|
+
#waiting = [];
|
|
42
|
+
/** Queries and transactions asked and not done: waiting for the client, their turn, or an answer. */
|
|
43
|
+
#busy = 0;
|
|
44
|
+
/** What waits for the client to be idle: `end`. */
|
|
45
|
+
#idle = [];
|
|
23
46
|
constructor(connectionString) {
|
|
24
47
|
if (connectionString.trim().length === 0) throw new Error("Hyperdrive returned an empty connection string");
|
|
25
48
|
this.#connectionString = connectionString;
|
|
26
49
|
}
|
|
27
50
|
query = (async (...args) => {
|
|
28
|
-
|
|
51
|
+
this.#busy += 1;
|
|
29
52
|
try {
|
|
30
|
-
|
|
53
|
+
const client = await this.#connected();
|
|
54
|
+
return await new Promise((resolve, reject) => {
|
|
55
|
+
this.#turn(() => client.query(...args).then(resolve, reject));
|
|
56
|
+
});
|
|
31
57
|
} finally {
|
|
32
|
-
|
|
58
|
+
this.#done();
|
|
33
59
|
}
|
|
34
60
|
});
|
|
61
|
+
/** The client, for a transaction: held from now until `release`. */
|
|
35
62
|
async connect() {
|
|
36
|
-
|
|
63
|
+
this.#busy += 1;
|
|
64
|
+
let client;
|
|
65
|
+
try {
|
|
66
|
+
client = await this.#connected();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.#done();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve) => this.#turn(() => {
|
|
72
|
+
this.#held = true;
|
|
73
|
+
resolve();
|
|
74
|
+
}));
|
|
37
75
|
let released = false;
|
|
38
76
|
return {
|
|
39
77
|
query: client.query.bind(client),
|
|
40
78
|
release: () => {
|
|
41
79
|
if (released) return;
|
|
42
80
|
released = true;
|
|
43
|
-
|
|
81
|
+
this.#held = false;
|
|
82
|
+
this.#next();
|
|
83
|
+
this.#done();
|
|
44
84
|
}
|
|
45
85
|
};
|
|
46
86
|
}
|
|
47
|
-
|
|
87
|
+
/** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
|
|
88
|
+
async end() {
|
|
89
|
+
while (this.#busy > 0) await new Promise((resolve) => this.#idle.push(resolve));
|
|
90
|
+
const client = this.#client;
|
|
91
|
+
this.#client = null;
|
|
92
|
+
if (client === null) return;
|
|
93
|
+
await client.then((connected) => connected.end()).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
/** Run `go` now if nothing holds the client or waits before it, otherwise at its turn. */
|
|
96
|
+
#turn(go) {
|
|
97
|
+
if (!this.#held && this.#waiting.length === 0) go();
|
|
98
|
+
else this.#waiting.push(go);
|
|
99
|
+
}
|
|
100
|
+
/** Give the next turns: queries until one is a transaction, which holds the client. */
|
|
101
|
+
#next() {
|
|
102
|
+
while (!this.#held && this.#waiting.length > 0) this.#waiting.shift()();
|
|
103
|
+
}
|
|
104
|
+
/** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
|
|
105
|
+
#done() {
|
|
106
|
+
this.#busy -= 1;
|
|
107
|
+
if (this.#busy === 0) for (const resolve of this.#idle.splice(0)) resolve();
|
|
108
|
+
}
|
|
109
|
+
/** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
|
|
110
|
+
#connected() {
|
|
111
|
+
if (this.#client !== null) return this.#client;
|
|
48
112
|
const client = new pg.Client(postgresConnection(this.#connectionString));
|
|
49
|
-
|
|
50
|
-
|
|
113
|
+
const connecting = client.connect().then(() => client);
|
|
114
|
+
const drop = () => {
|
|
115
|
+
if (this.#client === connecting) this.#client = null;
|
|
116
|
+
};
|
|
117
|
+
client.on("error", drop);
|
|
118
|
+
client.on("end", drop);
|
|
119
|
+
connecting.catch(drop);
|
|
120
|
+
this.#client = connecting;
|
|
121
|
+
return connecting;
|
|
51
122
|
}
|
|
52
123
|
};
|
|
53
124
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"drizzle-orm": "0.45.2",
|
|
75
75
|
"pg": "8.16.3",
|
|
76
76
|
"pg-connection-string": "2.14.0",
|
|
77
|
-
"@coffre/core": "0.1.
|
|
77
|
+
"@coffre/core": "0.1.10"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@types/node": "26.1.1",
|
package/src/hyperdrive.ts
CHANGED
|
@@ -15,16 +15,39 @@ export function postgres(hyperdrive: { readonly connectionString: string }): Pos
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* node-postgres for one call on Workers, through Hyperdrive: one client,
|
|
19
|
+
* connected on the first query, which every query and transaction of the
|
|
20
|
+
* call takes its turn on.
|
|
19
21
|
*
|
|
20
|
-
* Hyperdrive
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Hyperdrive keeps the pool to the database; a Worker's connections belong
|
|
23
|
+
* to the call that opened them, so each call opens its own. One, not one per
|
|
24
|
+
* query: on Cloudflare, a call that opened several Hyperdrive connections at
|
|
25
|
+
* once was now and then cancelled as hung, with nothing left pending, and
|
|
26
|
+
* every connection costs a round trip before the query can be sent.
|
|
27
|
+
*
|
|
28
|
+
* The turns keep a transaction whole: queries go to the client in the order
|
|
29
|
+
* they are asked, a transaction holds it from BEGIN until it is released,
|
|
30
|
+
* and a query asked meanwhile waits until then, so nothing runs inside a
|
|
31
|
+
* transaction it is not part of. So code inside a transaction must query
|
|
32
|
+
* through the transaction: a query on the database there would wait for the
|
|
33
|
+
* transaction to end, which waits for it. Neither the vault nor the server
|
|
34
|
+
* does.
|
|
35
|
+
*
|
|
36
|
+
* The class name must end in `Pool`: that is how Drizzle knows to check out
|
|
37
|
+
* a client for each transaction.
|
|
25
38
|
*/
|
|
26
39
|
export class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
|
|
27
40
|
readonly #connectionString: string;
|
|
41
|
+
/** The client, connecting or connected; null before the first query, and once it has failed. */
|
|
42
|
+
#client: Promise<pg.Client> | null = null;
|
|
43
|
+
/** Whether a transaction holds the client. */
|
|
44
|
+
#held = false;
|
|
45
|
+
/** What waits for its turn, in the order it asked. */
|
|
46
|
+
readonly #waiting: (() => void)[] = [];
|
|
47
|
+
/** Queries and transactions asked and not done: waiting for the client, their turn, or an answer. */
|
|
48
|
+
#busy = 0;
|
|
49
|
+
/** What waits for the client to be idle: `end`. */
|
|
50
|
+
readonly #idle: (() => void)[] = [];
|
|
28
51
|
|
|
29
52
|
constructor(connectionString: string) {
|
|
30
53
|
if (connectionString.trim().length === 0) {
|
|
@@ -34,30 +57,88 @@ export class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
|
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
readonly query: pg.Pool['query'] = (async (...args: unknown[]) => {
|
|
37
|
-
|
|
60
|
+
this.#busy += 1;
|
|
38
61
|
try {
|
|
39
|
-
|
|
62
|
+
const client = await this.#connected();
|
|
63
|
+
return await new Promise((resolve, reject) => {
|
|
64
|
+
// Sent at its turn, at once: pg answers its queries in the order they are sent.
|
|
65
|
+
this.#turn(() => (client.query as (...queryArgs: unknown[]) => Promise<unknown>)(...args).then(resolve, reject));
|
|
66
|
+
});
|
|
40
67
|
} finally {
|
|
41
|
-
|
|
68
|
+
this.#done();
|
|
42
69
|
}
|
|
43
70
|
}) as pg.Pool['query'];
|
|
44
71
|
|
|
72
|
+
/** The client, for a transaction: held from now until `release`. */
|
|
45
73
|
async connect(): Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }> {
|
|
46
|
-
|
|
74
|
+
this.#busy += 1;
|
|
75
|
+
let client: pg.Client;
|
|
76
|
+
try {
|
|
77
|
+
client = await this.#connected();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.#done();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise<void>((resolve) =>
|
|
83
|
+
this.#turn(() => {
|
|
84
|
+
this.#held = true;
|
|
85
|
+
resolve();
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
47
88
|
let released = false;
|
|
48
89
|
return {
|
|
49
90
|
query: client.query.bind(client) as pg.PoolClient['query'],
|
|
50
91
|
release: () => {
|
|
51
92
|
if (released) return;
|
|
52
93
|
released = true;
|
|
53
|
-
|
|
94
|
+
this.#held = false;
|
|
95
|
+
this.#next();
|
|
96
|
+
this.#done();
|
|
54
97
|
},
|
|
55
98
|
};
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
|
|
101
|
+
/** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
|
|
102
|
+
async end(): Promise<void> {
|
|
103
|
+
while (this.#busy > 0) {
|
|
104
|
+
await new Promise<void>((resolve) => this.#idle.push(resolve));
|
|
105
|
+
}
|
|
106
|
+
const client = this.#client;
|
|
107
|
+
this.#client = null;
|
|
108
|
+
if (client === null) return;
|
|
109
|
+
await client.then((connected) => connected.end()).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Run `go` now if nothing holds the client or waits before it, otherwise at its turn. */
|
|
113
|
+
#turn(go: () => void): void {
|
|
114
|
+
if (!this.#held && this.#waiting.length === 0) go();
|
|
115
|
+
else this.#waiting.push(go);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Give the next turns: queries until one is a transaction, which holds the client. */
|
|
119
|
+
#next(): void {
|
|
120
|
+
while (!this.#held && this.#waiting.length > 0) this.#waiting.shift()!();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
|
|
124
|
+
#done(): void {
|
|
125
|
+
this.#busy -= 1;
|
|
126
|
+
if (this.#busy === 0) for (const resolve of this.#idle.splice(0)) resolve();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
|
|
130
|
+
#connected(): Promise<pg.Client> {
|
|
131
|
+
if (this.#client !== null) return this.#client;
|
|
59
132
|
const client = new pg.Client(postgresConnection(this.#connectionString));
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
const connecting: Promise<pg.Client> = client.connect().then(() => client);
|
|
134
|
+
const drop = () => {
|
|
135
|
+
if (this.#client === connecting) this.#client = null;
|
|
136
|
+
};
|
|
137
|
+
// A connection lost between queries: pg fails what was waiting on it, and the next query connects again.
|
|
138
|
+
client.on('error', drop);
|
|
139
|
+
client.on('end', drop);
|
|
140
|
+
connecting.catch(drop);
|
|
141
|
+
this.#client = connecting;
|
|
142
|
+
return connecting;
|
|
62
143
|
}
|
|
63
144
|
}
|