@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.
@@ -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;
@@ -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;AAYjE,+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,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,CAwBxB;AAED,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"}
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"}
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
1
  export { createPostgresStorage, type PostgresDatabase } from './repositories';
2
2
  export * as postgresSchema from './schema';
3
+ export { postgresRateLimitStore, type SweepableRateLimitStore } from './rateLimits';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -1,3 +1,4 @@
1
1
  export { createPostgresStorage } from './repositories';
2
2
  export * as postgresSchema from './schema';
3
+ export { postgresRateLimitStore } from './rateLimits';
3
4
  //# sourceMappingURL=index.js.map
@@ -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,4 @@
1
+ import type { PlayerRepository } from '@chaos-overlords/kernel';
2
+ import type { PostgresDatabase } from './database';
3
+ export declare function postgresPlayerRepository(db: PostgresDatabase): PlayerRepository;
4
+ //# sourceMappingURL=players.d.ts.map
@@ -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,EAMnB,MAAM,yBAAyB,CAAA;AAiChC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAKlD;;;;GAIG;AACH,YAAY,EAAE,gBAAgB,EAAE,CAAA;AAEhC,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,gBAAgB,GAAG,kBAAkB,CAS9E"}
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, toPlayer, toPublicLobbyRow, toSnapshot, toSnapshotSummary, toTurn, toTurnOrders, toTurnReport, } from '../shared/mappers';
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;