@chaos-overlords/storage 0.9.9 → 0.9.11
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/node/index.d.ts +7 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -1
- package/dist/postgres/index.d.ts +1 -0
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +1 -0
- package/dist/postgres/index.js.map +1 -1
- package/dist/postgres/players.d.ts +4 -0
- package/dist/postgres/players.d.ts.map +1 -0
- package/dist/postgres/players.js +193 -0
- package/dist/postgres/players.js.map +1 -0
- package/dist/postgres/rateLimits.d.ts +19 -0
- package/dist/postgres/rateLimits.d.ts.map +1 -0
- package/dist/postgres/rateLimits.js +73 -0
- package/dist/postgres/rateLimits.js.map +1 -0
- package/dist/postgres/repositories.d.ts.map +1 -1
- package/dist/postgres/repositories.js +2 -147
- package/dist/postgres/repositories.js.map +1 -1
- package/dist/postgres/schema.d.ts +99 -0
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/schema.js +22 -2
- package/dist/postgres/schema.js.map +1 -1
- package/dist/sqlite/players.d.ts +4 -0
- package/dist/sqlite/players.d.ts.map +1 -0
- package/dist/sqlite/players.js +199 -0
- package/dist/sqlite/players.js.map +1 -0
- package/dist/sqlite/repositories.d.ts.map +1 -1
- package/dist/sqlite/repositories.js +2 -133
- package/dist/sqlite/repositories.js.map +1 -1
- package/migrations/postgres/0007_cultured_mongu.sql +9 -0
- package/migrations/postgres/meta/0007_snapshot.json +1001 -0
- package/migrations/postgres/meta/_journal.json +7 -0
- package/package.json +6 -6
package/dist/node/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MultiplayerStorage } from '@chaos-overlords/kernel';
|
|
2
|
+
import { type SweepableRateLimitStore } from '../postgres/rateLimits';
|
|
2
3
|
/** Migration folders, resolved from the package so a runtime never guesses a relative path. */
|
|
3
4
|
export declare const SQLITE_MIGRATIONS_DIR: string;
|
|
4
5
|
export declare const POSTGRES_MIGRATIONS_DIR: string;
|
|
@@ -10,6 +11,11 @@ export interface OpenedStorage {
|
|
|
10
11
|
* (the retention batch above all) by this.
|
|
11
12
|
*/
|
|
12
13
|
dialect: 'sqlite' | 'postgres';
|
|
14
|
+
/**
|
|
15
|
+
* Rate limit windows every process on this database shares, when the database can be shared.
|
|
16
|
+
* Postgres has one; a SQLite file belongs to one process, which counts in memory.
|
|
17
|
+
*/
|
|
18
|
+
rateLimits?: SweepableRateLimitStore;
|
|
13
19
|
close(): Promise<void>;
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
@@ -27,6 +33,7 @@ export declare function openSqliteStorage(filename: string): OpenedStorage;
|
|
|
27
33
|
* the pool would have recovered from on the next query.
|
|
28
34
|
*/
|
|
29
35
|
export declare function openPostgresStorage(connectionString: string, onPoolError?: (error: unknown) => void): Promise<OpenedStorage>;
|
|
36
|
+
export type { SweepableRateLimitStore };
|
|
30
37
|
export type StorageTarget = {
|
|
31
38
|
kind: 'sqlite';
|
|
32
39
|
filename: string;
|
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAOjE,OAAO,EAA0B,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAM7F,+FAA+F;AAC/F,eAAO,MAAM,qBAAqB,QAEjC,CAAA;AACD,eAAO,MAAM,uBAAuB,QAEnC,CAAA;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,kBAAkB,CAAA;IAC3B;;;;OAIG;IACH,OAAO,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,uBAAuB,CAAA;IACpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AASD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAiBjE;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,gBAAgB,EAAE,MAAM,EACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACrC,OAAO,CAAC,aAAa,CAAC,CAyBxB;AAED,YAAY,EAAE,uBAAuB,EAAE,CAAA;AAEvC,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAA;AAElD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAUrE;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACrC,OAAO,CAAC,aAAa,CAAC,CAIxB"}
|
package/dist/node/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { migrate as migrateSqlite } from 'drizzle-orm/better-sqlite3/migrator';
|
|
|
6
6
|
import { drizzle as drizzlePostgres } from 'drizzle-orm/node-postgres';
|
|
7
7
|
import { migrate as migratePostgres } from 'drizzle-orm/node-postgres/migrator';
|
|
8
8
|
import pg from 'pg';
|
|
9
|
+
import { postgresRateLimitStore } from '../postgres/rateLimits';
|
|
9
10
|
import { createPostgresStorage } from '../postgres/repositories';
|
|
10
11
|
import * as postgresSchema from '../postgres/schema';
|
|
11
12
|
import { createSqliteStorage } from '../sqlite/repositories';
|
|
@@ -73,6 +74,7 @@ export async function openPostgresStorage(connectionString, onPoolError) {
|
|
|
73
74
|
return {
|
|
74
75
|
storage: createPostgresStorage(db),
|
|
75
76
|
dialect: 'postgres',
|
|
77
|
+
rateLimits: postgresRateLimitStore(db),
|
|
76
78
|
close: () => pool.end(),
|
|
77
79
|
};
|
|
78
80
|
}
|
package/dist/node/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAE7C,OAAO,aAAa,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,oCAAoC,CAAA;AAC/E,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,KAAK,cAAc,MAAM,oBAAoB,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,YAAY,MAAM,kBAAkB,CAAA;AAEhD,+FAA+F;AAC/F,MAAM,CAAC,MAAM,qBAAqB,GAAG,aAAa,CAChD,IAAI,GAAG,CAAC,yBAAyB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CACpD,CAAA;AACD,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAClD,IAAI,GAAG,CAAC,2BAA2B,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CACtD,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"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"}
|
package/dist/postgres/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/postgres/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,KAAK,cAAc,MAAM,UAAU,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/postgres/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,KAAK,cAAc,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,sBAAsB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/postgres/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/postgres/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAyB,MAAM,gBAAgB,CAAA;AAC7E,OAAO,KAAK,cAAc,MAAM,UAAU,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/postgres/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAyB,MAAM,gBAAgB,CAAA;AAC7E,OAAO,KAAK,cAAc,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,sBAAsB,EAAgC,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"players.d.ts","sourceRoot":"","sources":["../../src/postgres/players.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAGvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAwClD,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,gBAAgB,GAAG,gBAAgB,CAyL/E"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { and, asc, eq, exists, inArray, isNotNull, ne, notExists, or, sql } from 'drizzle-orm';
|
|
2
|
+
import { firstOrNull, toPlayer } from '../shared/mappers';
|
|
3
|
+
import * as schema from './schema';
|
|
4
|
+
/** Players and their seats in the Postgres dialect; see the SQLite twin. */
|
|
5
|
+
/** Thrown inside the late-claim transaction to roll it back when the claim is refused. */
|
|
6
|
+
const LATE_CLAIM_REFUSED = new Error('late seat claim refused');
|
|
7
|
+
/**
|
|
8
|
+
* The rows that still hold a claim on their seat: every row but a computer-controlled one whose
|
|
9
|
+
* token is gone. The SQL twin of `isVacated`, negated.
|
|
10
|
+
*/
|
|
11
|
+
function holdsClaim() {
|
|
12
|
+
const { players } = schema;
|
|
13
|
+
return or(ne(players.status, 'computer'), isNotNull(players.tokenHash));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The select list `create` and `createLate` feed their insert from: the player's own values, each
|
|
17
|
+
* under the column name it is inserted as.
|
|
18
|
+
*
|
|
19
|
+
* The list is written out, which means it does NOT get Drizzle's column mapping: a column added to
|
|
20
|
+
* `players` later has to be added here too, in the storage form the column expects. The
|
|
21
|
+
* conformance suite compares a created player against its fixture field by field, so a dropped
|
|
22
|
+
* column fails there rather than going unnoticed.
|
|
23
|
+
*/
|
|
24
|
+
function playerValues(player) {
|
|
25
|
+
return {
|
|
26
|
+
id: sql `${player.id}`.as('id'),
|
|
27
|
+
matchId: sql `${player.matchId}`.as('match_id'),
|
|
28
|
+
slot: sql `${player.slot}`.as('slot'),
|
|
29
|
+
joinOrder: sql `${player.joinOrder}`.as('join_order'),
|
|
30
|
+
displayName: sql `${player.displayName}`.as('display_name'),
|
|
31
|
+
portraitId: sql `${player.portraitId}`.as('portrait_id'),
|
|
32
|
+
tokenHash: sql `${player.tokenHash}`.as('token_hash'),
|
|
33
|
+
status: sql `${player.status}`.as('status'),
|
|
34
|
+
joinedAt: sql `${player.joinedAt}`.as('joined_at'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function postgresPlayerRepository(db) {
|
|
38
|
+
const { matches, players } = schema;
|
|
39
|
+
return {
|
|
40
|
+
/**
|
|
41
|
+
* An insert fed by a select over the match row, so "the match is still in the lobby" is tested
|
|
42
|
+
* by the same statement that writes the player. A row lock makes this guard hold through the
|
|
43
|
+
* insert under READ COMMITTED, including when the match starts concurrently.
|
|
44
|
+
*/
|
|
45
|
+
async create(player) {
|
|
46
|
+
const rows = await db
|
|
47
|
+
.insert(players)
|
|
48
|
+
.select(db
|
|
49
|
+
.select(playerValues(player))
|
|
50
|
+
.from(matches)
|
|
51
|
+
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'lobby')))
|
|
52
|
+
.for('share'))
|
|
53
|
+
.returning({ id: players.id });
|
|
54
|
+
return rows.length === 1;
|
|
55
|
+
},
|
|
56
|
+
async createLate(player) {
|
|
57
|
+
// Lock first, then release, then run the guarded insert as a new READ COMMITTED statement.
|
|
58
|
+
// Putting the count in the locking SELECT would still let two joins evaluate it against the
|
|
59
|
+
// same old snapshot. `no key update` serializes late joins without blocking the foreign-key
|
|
60
|
+
// checks every other insert under this match takes.
|
|
61
|
+
//
|
|
62
|
+
// The release comes before the insert because `rejoin` does not take the match lock. Its
|
|
63
|
+
// update and the release meet on the former player's row: one waits for the other, and a
|
|
64
|
+
// `rejoin` that commits first leaves a human on the seat for the insert's fresh snapshot to
|
|
65
|
+
// see. Released after the insert instead, a `rejoin` committed between the two would have
|
|
66
|
+
// been skipped by the release and missed by the insert, and both players would hold the
|
|
67
|
+
// seat. A refused insert rolls the release back.
|
|
68
|
+
try {
|
|
69
|
+
return await db.transaction(async (tx) => {
|
|
70
|
+
const locked = await tx
|
|
71
|
+
.select({ id: matches.id })
|
|
72
|
+
.from(matches)
|
|
73
|
+
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'running')))
|
|
74
|
+
.for('no key update');
|
|
75
|
+
if (locked.length === 0)
|
|
76
|
+
throw LATE_CLAIM_REFUSED;
|
|
77
|
+
const released = await tx
|
|
78
|
+
.update(players)
|
|
79
|
+
.set({ tokenHash: null })
|
|
80
|
+
.where(and(eq(players.matchId, player.matchId), eq(players.slot, player.slot), eq(players.status, 'computer'), isNotNull(players.tokenHash)))
|
|
81
|
+
.returning({ id: players.id });
|
|
82
|
+
const occupied = tx
|
|
83
|
+
.select({ id: players.id })
|
|
84
|
+
.from(players)
|
|
85
|
+
.where(and(eq(players.matchId, player.matchId), eq(players.slot, player.slot), holdsClaim()));
|
|
86
|
+
const rows = await tx
|
|
87
|
+
.insert(players)
|
|
88
|
+
.select(tx
|
|
89
|
+
.select(playerValues(player))
|
|
90
|
+
.from(matches)
|
|
91
|
+
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'running'), notExists(occupied),
|
|
92
|
+
// Capacity in the same statement as the insert; see the SQLite twin.
|
|
93
|
+
sql `(select count(*) from ${players} where ${players.matchId} = ${player.matchId} and ${holdsClaim()}) < ${matches.maxPlayers}`)))
|
|
94
|
+
.onConflictDoNothing()
|
|
95
|
+
.returning({ id: players.id });
|
|
96
|
+
if (rows.length !== 1)
|
|
97
|
+
throw LATE_CLAIM_REFUSED;
|
|
98
|
+
return { released: released.map((row) => row.id) };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error === LATE_CLAIM_REFUSED)
|
|
103
|
+
return null;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async get(id) {
|
|
108
|
+
return firstOrNull((await db.select().from(players).where(eq(players.id, id))).map(toPlayer));
|
|
109
|
+
},
|
|
110
|
+
async getByTokenHash(tokenHash) {
|
|
111
|
+
return firstOrNull((await db.select().from(players).where(eq(players.tokenHash, tokenHash))).map(toPlayer));
|
|
112
|
+
},
|
|
113
|
+
async listByMatch(matchId) {
|
|
114
|
+
const rows = await db
|
|
115
|
+
.select()
|
|
116
|
+
.from(players)
|
|
117
|
+
.where(eq(players.matchId, matchId))
|
|
118
|
+
.orderBy(asc(players.slot), asc(players.joinOrder), asc(players.id));
|
|
119
|
+
return rows.map(toPlayer);
|
|
120
|
+
},
|
|
121
|
+
async listSeats(matchIds) {
|
|
122
|
+
if (matchIds.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const rows = await db
|
|
125
|
+
.select({
|
|
126
|
+
matchId: players.matchId,
|
|
127
|
+
slot: players.slot,
|
|
128
|
+
status: players.status,
|
|
129
|
+
tokenHash: players.tokenHash,
|
|
130
|
+
})
|
|
131
|
+
.from(players)
|
|
132
|
+
.where(inArray(players.matchId, [...matchIds]));
|
|
133
|
+
return rows.map((row) => ({
|
|
134
|
+
matchId: row.matchId,
|
|
135
|
+
slot: row.slot,
|
|
136
|
+
computer: row.status === 'computer',
|
|
137
|
+
vacated: row.status === 'computer' && row.tokenHash === null,
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
140
|
+
async setStatus(playerId, status) {
|
|
141
|
+
await db.update(players).set({ status }).where(eq(players.id, playerId));
|
|
142
|
+
},
|
|
143
|
+
async transitionStatus(playerId, from, status, options) {
|
|
144
|
+
const rows = await db
|
|
145
|
+
.update(players)
|
|
146
|
+
.set({ status })
|
|
147
|
+
.where(and(eq(players.id, playerId), inArray(players.status, from), options?.holdingToken === true ? isNotNull(players.tokenHash) : undefined))
|
|
148
|
+
.returning({ id: players.id });
|
|
149
|
+
return rows.length === 1;
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* "The match is still in the lobby" is tested by the statement that writes the profile, so an
|
|
153
|
+
* update racing `start` either lands before the roster is seated or not at all.
|
|
154
|
+
* The share lock on the match row is what makes that hold under READ COMMITTED: a `start`
|
|
155
|
+
* racing this either waits for it, or makes it re-read the row and find the match running.
|
|
156
|
+
*/
|
|
157
|
+
async updateProfile(playerId, profile) {
|
|
158
|
+
const inLobby = exists(db
|
|
159
|
+
.select({ one: sql `1` })
|
|
160
|
+
.from(matches)
|
|
161
|
+
.where(and(eq(matches.id, players.matchId), eq(matches.status, 'lobby')))
|
|
162
|
+
.for('share'));
|
|
163
|
+
const rows = await db
|
|
164
|
+
.update(players)
|
|
165
|
+
.set({ displayName: profile.displayName, portraitId: profile.portraitId })
|
|
166
|
+
.where(and(eq(players.id, playerId), eq(players.status, 'active'), inLobby))
|
|
167
|
+
.returning({ id: players.id });
|
|
168
|
+
return rows.length === 1;
|
|
169
|
+
},
|
|
170
|
+
async revokeToken(playerId) {
|
|
171
|
+
await db.update(players).set({ tokenHash: null }).where(eq(players.id, playerId));
|
|
172
|
+
},
|
|
173
|
+
async assignSlots(assignments) {
|
|
174
|
+
if (assignments.length === 0)
|
|
175
|
+
return;
|
|
176
|
+
// One statement: a CASE that maps each id to its slot. A loop of updates could commit some
|
|
177
|
+
// seats and not others, and the transition that made the roster final has already landed.
|
|
178
|
+
const cases = assignments.map(({ playerId, slot }) => sql `when ${players.id} = ${playerId} then ${slot}`);
|
|
179
|
+
await db
|
|
180
|
+
.update(players)
|
|
181
|
+
.set({ slot: sql `case ${sql.join(cases, sql ` `)} else ${players.slot} end` })
|
|
182
|
+
.where(inArray(players.id, assignments.map(({ playerId }) => playerId)));
|
|
183
|
+
},
|
|
184
|
+
async delete(playerId) {
|
|
185
|
+
const rows = await db
|
|
186
|
+
.delete(players)
|
|
187
|
+
.where(eq(players.id, playerId))
|
|
188
|
+
.returning({ id: players.id });
|
|
189
|
+
return rows.length === 1;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=players.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"players.js","sourceRoot":"","sources":["../../src/postgres/players.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RateLimitStore } from '@chaos-overlords/kernel';
|
|
2
|
+
import type { PostgresDatabase } from './database';
|
|
3
|
+
/** A {@link RateLimitStore} that can also delete the windows that have rolled. */
|
|
4
|
+
export interface SweepableRateLimitStore extends RateLimitStore {
|
|
5
|
+
/** Deletes up to `limit` windows that rolled at or before `now`; returns how many went. */
|
|
6
|
+
sweep(now: number, limit?: number): Promise<number>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rate limit windows in a Postgres table, so every Node instance on one database spends the same
|
|
10
|
+
* budgets.
|
|
11
|
+
*
|
|
12
|
+
* `consume` is one `INSERT … ON CONFLICT DO UPDATE`: Postgres locks the conflicting row for the
|
|
13
|
+
* update, so concurrent calls from any number of instances are counted one after another, and the
|
|
14
|
+
* decision comes back in the same round trip. Every expression in the `SET` list reads the row as it
|
|
15
|
+
* was before the statement, which is what lets `allowed` record whether this call found room. The
|
|
16
|
+
* rules are the kernel's `consumeWindow`, written in SQL.
|
|
17
|
+
*/
|
|
18
|
+
export declare function postgresRateLimitStore(db: PostgresDatabase): SweepableRateLimitStore;
|
|
19
|
+
//# sourceMappingURL=rateLimits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimits.d.ts","sourceRoot":"","sources":["../../src/postgres/rateLimits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,cAAc,EAAmB,MAAM,yBAAyB,CAAA;AAE/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAGlD,kFAAkF;AAClF,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,2FAA2F;IAC3F,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,gBAAgB,GAAG,uBAAuB,CA6DpF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { and, eq, gt, inArray, lte, sql } from 'drizzle-orm';
|
|
2
|
+
import { rateLimitWindows } from './schema';
|
|
3
|
+
/**
|
|
4
|
+
* Rate limit windows in a Postgres table, so every Node instance on one database spends the same
|
|
5
|
+
* budgets.
|
|
6
|
+
*
|
|
7
|
+
* `consume` is one `INSERT … ON CONFLICT DO UPDATE`: Postgres locks the conflicting row for the
|
|
8
|
+
* update, so concurrent calls from any number of instances are counted one after another, and the
|
|
9
|
+
* decision comes back in the same round trip. Every expression in the `SET` list reads the row as it
|
|
10
|
+
* was before the statement, which is what lets `allowed` record whether this call found room. The
|
|
11
|
+
* rules are the kernel's `consumeWindow`, written in SQL.
|
|
12
|
+
*/
|
|
13
|
+
export function postgresRateLimitStore(db) {
|
|
14
|
+
const t = rateLimitWindows;
|
|
15
|
+
return {
|
|
16
|
+
async consume(key, policy, now) {
|
|
17
|
+
const rolled = sql `${t.resetAt} <= ${now}`;
|
|
18
|
+
const rows = await db
|
|
19
|
+
.insert(t)
|
|
20
|
+
.values({ key, windowStart: now, resetAt: now + policy.windowMs, count: 1, allowed: true })
|
|
21
|
+
.onConflictDoUpdate({
|
|
22
|
+
target: t.key,
|
|
23
|
+
set: {
|
|
24
|
+
allowed: sql `(${rolled} or ${t.count} < ${policy.limit})`,
|
|
25
|
+
windowStart: sql `case when ${rolled} then ${now} else ${t.windowStart} end`,
|
|
26
|
+
resetAt: sql `case when ${rolled} then ${now + policy.windowMs} else ${t.resetAt} end`,
|
|
27
|
+
count: sql `case when ${rolled} then 1 when ${t.count} < ${policy.limit} then ${t.count} + 1 else ${t.count} end`,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
.returning({
|
|
31
|
+
allowed: t.allowed,
|
|
32
|
+
count: t.count,
|
|
33
|
+
windowStart: t.windowStart,
|
|
34
|
+
resetAt: t.resetAt,
|
|
35
|
+
});
|
|
36
|
+
const row = rows[0];
|
|
37
|
+
if (!row)
|
|
38
|
+
throw new Error('rate limit upsert returned no row');
|
|
39
|
+
return row;
|
|
40
|
+
},
|
|
41
|
+
async inspect(key, now) {
|
|
42
|
+
const rows = await db
|
|
43
|
+
.select({ count: t.count, resetAt: t.resetAt })
|
|
44
|
+
.from(t)
|
|
45
|
+
.where(and(eq(t.key, key), gt(t.resetAt, now)));
|
|
46
|
+
return rows[0] ?? null;
|
|
47
|
+
},
|
|
48
|
+
async refund(key, windowStart) {
|
|
49
|
+
await db
|
|
50
|
+
.update(t)
|
|
51
|
+
.set({ count: sql `${t.count} - 1` })
|
|
52
|
+
.where(and(eq(t.key, key), eq(t.windowStart, windowStart), gt(t.count, 0)));
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Batched, so a sweep after a flood of distinct addresses is many short statements rather than
|
|
56
|
+
* one that holds row locks every `consume` is waiting on.
|
|
57
|
+
*/
|
|
58
|
+
async sweep(now, limit = 1000) {
|
|
59
|
+
const doomed = db
|
|
60
|
+
.select({ key: t.key })
|
|
61
|
+
.from(t)
|
|
62
|
+
.where(lte(t.resetAt, now))
|
|
63
|
+
.orderBy(t.resetAt)
|
|
64
|
+
.limit(limit);
|
|
65
|
+
const deleted = await db
|
|
66
|
+
.delete(t)
|
|
67
|
+
.where(and(inArray(t.key, doomed), lte(t.resetAt, now)))
|
|
68
|
+
.returning({ key: t.key });
|
|
69
|
+
return deleted.length;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=rateLimits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimits.js","sourceRoot":"","sources":["../../src/postgres/rateLimits.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repositories.d.ts","sourceRoot":"","sources":["../../src/postgres/repositories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,
|
|
1
|
+
{"version":3,"file":"repositories.d.ts","sourceRoot":"","sources":["../../src/postgres/repositories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAInB,MAAM,yBAAyB,CAAA;AAgChC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAMlD;;;;GAIG;AACH,YAAY,EAAE,gBAAgB,EAAE,CAAA;AAEhC,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,gBAAgB,GAAG,kBAAkB,CAS9E"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { and, asc, desc, eq, exists, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, or, sql, } from 'drizzle-orm';
|
|
2
2
|
import { insertUnlessTaken } from '../shared/constraints';
|
|
3
|
-
import { firstOrNull, toMatch, toMatchInsert,
|
|
3
|
+
import { firstOrNull, toMatch, toMatchInsert, toPublicLobbyRow, toSnapshot, toSnapshotSummary, toTurn, toTurnOrders, toTurnReport, } from '../shared/mappers';
|
|
4
4
|
import { postgresEventRepository } from './events';
|
|
5
|
+
import { postgresPlayerRepository } from './players';
|
|
5
6
|
import * as schema from './schema';
|
|
6
7
|
import { postgresTakeoverRepository } from './takeovers';
|
|
7
8
|
export function createPostgresStorage(db) {
|
|
@@ -179,152 +180,6 @@ function postgresMatchRepository(db) {
|
|
|
179
180
|
},
|
|
180
181
|
};
|
|
181
182
|
}
|
|
182
|
-
/**
|
|
183
|
-
* The select list `create` and `createLate` feed their insert from: the player's own values, each
|
|
184
|
-
* under the column name it is inserted as.
|
|
185
|
-
*
|
|
186
|
-
* The list is written out, which means it does NOT get Drizzle's column mapping: a column added to
|
|
187
|
-
* `players` later has to be added here too, in the storage form the column expects. The
|
|
188
|
-
* conformance suite compares a created player against its fixture field by field, so a dropped
|
|
189
|
-
* column fails there rather than going unnoticed.
|
|
190
|
-
*/
|
|
191
|
-
function playerValues(player) {
|
|
192
|
-
return {
|
|
193
|
-
id: sql `${player.id}`.as('id'),
|
|
194
|
-
matchId: sql `${player.matchId}`.as('match_id'),
|
|
195
|
-
slot: sql `${player.slot}`.as('slot'),
|
|
196
|
-
joinOrder: sql `${player.joinOrder}`.as('join_order'),
|
|
197
|
-
displayName: sql `${player.displayName}`.as('display_name'),
|
|
198
|
-
portraitId: sql `${player.portraitId}`.as('portrait_id'),
|
|
199
|
-
tokenHash: sql `${player.tokenHash}`.as('token_hash'),
|
|
200
|
-
status: sql `${player.status}`.as('status'),
|
|
201
|
-
joinedAt: sql `${player.joinedAt}`.as('joined_at'),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
function postgresPlayerRepository(db) {
|
|
205
|
-
const { matches, players } = schema;
|
|
206
|
-
return {
|
|
207
|
-
/**
|
|
208
|
-
* An insert fed by a select over the match row, so "the match is still in the lobby" is tested
|
|
209
|
-
* by the same statement that writes the player. A row lock makes this guard hold through the
|
|
210
|
-
* insert under READ COMMITTED, including when the match starts concurrently.
|
|
211
|
-
*/
|
|
212
|
-
async create(player) {
|
|
213
|
-
const rows = await db
|
|
214
|
-
.insert(players)
|
|
215
|
-
.select(db
|
|
216
|
-
.select(playerValues(player))
|
|
217
|
-
.from(matches)
|
|
218
|
-
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'lobby')))
|
|
219
|
-
.for('share'))
|
|
220
|
-
.returning({ id: players.id });
|
|
221
|
-
return rows.length === 1;
|
|
222
|
-
},
|
|
223
|
-
async createLate(player) {
|
|
224
|
-
// Lock first, then run the guarded insert as a new READ COMMITTED statement. Putting the
|
|
225
|
-
// count in the locking SELECT would still let two joins evaluate it against the same old
|
|
226
|
-
// snapshot. `no key update` serializes late joins without blocking the foreign-key checks
|
|
227
|
-
// every other insert under this match takes.
|
|
228
|
-
return db.transaction(async (tx) => {
|
|
229
|
-
const locked = await tx
|
|
230
|
-
.select({ id: matches.id })
|
|
231
|
-
.from(matches)
|
|
232
|
-
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'running')))
|
|
233
|
-
.for('no key update');
|
|
234
|
-
if (locked.length === 0)
|
|
235
|
-
return false;
|
|
236
|
-
const occupied = tx
|
|
237
|
-
.select({ id: players.id })
|
|
238
|
-
.from(players)
|
|
239
|
-
.where(and(eq(players.matchId, player.matchId), eq(players.slot, player.slot)));
|
|
240
|
-
const rows = await tx
|
|
241
|
-
.insert(players)
|
|
242
|
-
.select(tx
|
|
243
|
-
.select(playerValues(player))
|
|
244
|
-
.from(matches)
|
|
245
|
-
.where(and(eq(matches.id, player.matchId), eq(matches.status, 'running'), notExists(occupied),
|
|
246
|
-
// Capacity in the same statement as the insert; see the SQLite twin.
|
|
247
|
-
sql `(select count(*) from ${players} where ${players.matchId} = ${player.matchId}) < ${matches.maxPlayers}`)))
|
|
248
|
-
.onConflictDoNothing()
|
|
249
|
-
.returning({ id: players.id });
|
|
250
|
-
return rows.length === 1;
|
|
251
|
-
});
|
|
252
|
-
},
|
|
253
|
-
async get(id) {
|
|
254
|
-
return firstOrNull((await db.select().from(players).where(eq(players.id, id))).map(toPlayer));
|
|
255
|
-
},
|
|
256
|
-
async getByTokenHash(tokenHash) {
|
|
257
|
-
return firstOrNull((await db.select().from(players).where(eq(players.tokenHash, tokenHash))).map(toPlayer));
|
|
258
|
-
},
|
|
259
|
-
async listByMatch(matchId) {
|
|
260
|
-
const rows = await db
|
|
261
|
-
.select()
|
|
262
|
-
.from(players)
|
|
263
|
-
.where(eq(players.matchId, matchId))
|
|
264
|
-
.orderBy(asc(players.slot), asc(players.joinOrder), asc(players.id));
|
|
265
|
-
return rows.map(toPlayer);
|
|
266
|
-
},
|
|
267
|
-
async listSeats(matchIds) {
|
|
268
|
-
if (matchIds.length === 0)
|
|
269
|
-
return [];
|
|
270
|
-
return db
|
|
271
|
-
.select({ matchId: players.matchId, slot: players.slot })
|
|
272
|
-
.from(players)
|
|
273
|
-
.where(inArray(players.matchId, [...matchIds]));
|
|
274
|
-
},
|
|
275
|
-
async setStatus(playerId, status) {
|
|
276
|
-
await db.update(players).set({ status }).where(eq(players.id, playerId));
|
|
277
|
-
},
|
|
278
|
-
async transitionStatus(playerId, from, status) {
|
|
279
|
-
const rows = await db
|
|
280
|
-
.update(players)
|
|
281
|
-
.set({ status })
|
|
282
|
-
.where(and(eq(players.id, playerId), inArray(players.status, from)))
|
|
283
|
-
.returning({ id: players.id });
|
|
284
|
-
return rows.length === 1;
|
|
285
|
-
},
|
|
286
|
-
/**
|
|
287
|
-
* "The match is still in the lobby" is tested by the statement that writes the profile, so an
|
|
288
|
-
* update racing `start` either lands before the roster is seated or not at all.
|
|
289
|
-
* The share lock on the match row is what makes that hold under READ COMMITTED: a `start`
|
|
290
|
-
* racing this either waits for it, or makes it re-read the row and find the match running.
|
|
291
|
-
*/
|
|
292
|
-
async updateProfile(playerId, profile) {
|
|
293
|
-
const inLobby = exists(db
|
|
294
|
-
.select({ one: sql `1` })
|
|
295
|
-
.from(matches)
|
|
296
|
-
.where(and(eq(matches.id, players.matchId), eq(matches.status, 'lobby')))
|
|
297
|
-
.for('share'));
|
|
298
|
-
const rows = await db
|
|
299
|
-
.update(players)
|
|
300
|
-
.set({ displayName: profile.displayName, portraitId: profile.portraitId })
|
|
301
|
-
.where(and(eq(players.id, playerId), eq(players.status, 'active'), inLobby))
|
|
302
|
-
.returning({ id: players.id });
|
|
303
|
-
return rows.length === 1;
|
|
304
|
-
},
|
|
305
|
-
async revokeToken(playerId) {
|
|
306
|
-
await db.update(players).set({ tokenHash: null }).where(eq(players.id, playerId));
|
|
307
|
-
},
|
|
308
|
-
async assignSlots(assignments) {
|
|
309
|
-
if (assignments.length === 0)
|
|
310
|
-
return;
|
|
311
|
-
// One statement: a CASE that maps each id to its slot. A loop of updates could commit some
|
|
312
|
-
// seats and not others, and the transition that made the roster final has already landed.
|
|
313
|
-
const cases = assignments.map(({ playerId, slot }) => sql `when ${players.id} = ${playerId} then ${slot}`);
|
|
314
|
-
await db
|
|
315
|
-
.update(players)
|
|
316
|
-
.set({ slot: sql `case ${sql.join(cases, sql ` `)} else ${players.slot} end` })
|
|
317
|
-
.where(inArray(players.id, assignments.map(({ playerId }) => playerId)));
|
|
318
|
-
},
|
|
319
|
-
async delete(playerId) {
|
|
320
|
-
const rows = await db
|
|
321
|
-
.delete(players)
|
|
322
|
-
.where(eq(players.id, playerId))
|
|
323
|
-
.returning({ id: players.id });
|
|
324
|
-
return rows.length === 1;
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
183
|
/** The orders half of the turn repository; see the SQLite twin. */
|
|
329
184
|
function postgresTurnOrderMethods(db) {
|
|
330
185
|
const { turns, turnOrders } = schema;
|