@meith/db 0.17.2 → 0.19.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.
@@ -400,6 +400,13 @@
400
400
  "when": 1787524084640,
401
401
  "tag": "0056_marketplace_catalog",
402
402
  "breakpoints": true
403
+ },
404
+ {
405
+ "idx": 57,
406
+ "version": "7",
407
+ "when": 1787650466674,
408
+ "tag": "0057_promotion_scan_cursor",
409
+ "breakpoints": true
403
410
  }
404
411
  ]
405
412
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meith/db",
3
- "version": "0.17.2",
4
- "license": "LGPL-3.0-or-later",
3
+ "version": "0.19.0",
4
+ "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/meith-dev/meith.git",
@@ -22,35 +22,35 @@
22
22
  "dependencies": {
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "postgres": "^3.4.7",
25
- "@meith/accounts": "0.17.2",
26
- "@meith/antispam": "0.17.2",
27
- "@meith/admin": "0.17.2",
28
- "@meith/authorization": "0.17.2",
29
- "@meith/attachments": "0.17.2",
30
- "@meith/api": "0.17.2",
31
- "@meith/avatars": "0.17.2",
32
- "@meith/core": "0.17.2",
33
- "@meith/drafts": "0.17.2",
34
- "@meith/events": "0.17.2",
35
- "@meith/forums": "0.17.2",
36
- "@meith/i18n": "0.17.2",
37
- "@meith/groups": "0.17.2",
38
- "@meith/marketplace": "0.17.2",
39
- "@meith/messages": "0.17.2",
40
- "@meith/markdown": "0.17.2",
41
- "@meith/moderation": "0.17.2",
42
- "@meith/notifications": "0.17.2",
43
- "@meith/plugin-kit": "0.17.2",
44
- "@meith/polls": "0.17.2",
45
- "@meith/relations": "0.17.2",
46
- "@meith/profile-fields": "0.17.2",
47
- "@meith/reputation": "0.17.2",
48
- "@meith/search": "0.17.2",
49
- "@meith/subscriptions": "0.17.2",
50
- "@meith/settings": "0.17.2",
51
- "@meith/tasks": "0.17.2",
52
- "@meith/threads": "0.17.2",
53
- "@meith/signatures": "0.17.2"
25
+ "@meith/accounts": "0.19.0",
26
+ "@meith/admin": "0.19.0",
27
+ "@meith/api": "0.19.0",
28
+ "@meith/antispam": "0.19.0",
29
+ "@meith/attachments": "0.19.0",
30
+ "@meith/authorization": "0.19.0",
31
+ "@meith/avatars": "0.19.0",
32
+ "@meith/drafts": "0.19.0",
33
+ "@meith/core": "0.19.0",
34
+ "@meith/events": "0.19.0",
35
+ "@meith/forums": "0.19.0",
36
+ "@meith/groups": "0.19.0",
37
+ "@meith/i18n": "0.19.0",
38
+ "@meith/markdown": "0.19.0",
39
+ "@meith/moderation": "0.19.0",
40
+ "@meith/messages": "0.19.0",
41
+ "@meith/marketplace": "0.19.0",
42
+ "@meith/plugin-kit": "0.19.0",
43
+ "@meith/notifications": "0.19.0",
44
+ "@meith/polls": "0.19.0",
45
+ "@meith/profile-fields": "0.19.0",
46
+ "@meith/relations": "0.19.0",
47
+ "@meith/reputation": "0.19.0",
48
+ "@meith/search": "0.19.0",
49
+ "@meith/settings": "0.19.0",
50
+ "@meith/signatures": "0.19.0",
51
+ "@meith/threads": "0.19.0",
52
+ "@meith/subscriptions": "0.19.0",
53
+ "@meith/tasks": "0.19.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "drizzle-kit": "^0.31.6",
package/src/index.ts CHANGED
@@ -146,6 +146,7 @@ export {
146
146
  INSTALL_LOCK_KEY,
147
147
  isInstalled,
148
148
  markInstalled,
149
+ tryInstallLock,
149
150
  withInstallLock,
150
151
  } from './install-repo'
151
152
  export {
@@ -50,17 +50,28 @@ export async function markInstalled(db: Database, version: string): Promise<bool
50
50
  return rows.length > 0
51
51
  }
52
52
 
53
- export async function withInstallLock<T>(url: string, body: () => Promise<T>): Promise<T | null> {
54
- const client = postgres(url, { max: 1, prepare: false, onnotice: () => {} })
53
+ export async function tryInstallLock<T>(
54
+ sql: ReturnType<typeof postgres>,
55
+ body: () => Promise<T>,
56
+ ): Promise<T | null> {
57
+ const rows = await sql`
58
+ select pg_try_advisory_lock(${INSTALL_LOCK_KEY}::bigint) as locked
59
+ `
60
+
61
+ if (rows[0]?.locked !== true) return null
55
62
 
56
63
  try {
57
- const rows = await client`
58
- select pg_try_advisory_lock(${INSTALL_LOCK_KEY}::bigint) as locked
59
- `
64
+ return await body()
65
+ } finally {
66
+ await sql`select pg_advisory_unlock(${INSTALL_LOCK_KEY}::bigint)`.catch(() => undefined)
67
+ }
68
+ }
60
69
 
61
- if (rows[0]?.locked !== true) return null
70
+ export async function withInstallLock<T>(url: string, body: () => Promise<T>): Promise<T | null> {
71
+ const client = postgres(url, { max: 1, prepare: false, onnotice: () => {} })
62
72
 
63
- return await body()
73
+ try {
74
+ return await tryInstallLock(client, body)
64
75
  } finally {
65
76
  await client.end({ timeout: 5 })
66
77
  }
@@ -13,9 +13,11 @@ import { promotionRuleProblem } from '@meith/groups'
13
13
  import { msg } from '@meith/i18n'
14
14
 
15
15
  import type { Database } from './client'
16
- import { groupPromotions, users } from './schema'
16
+ import { groupPromotions, promotionScanState, users } from './schema'
17
17
  import { keptDisplayGroupSql } from './staff-groups'
18
18
 
19
+ const SCAN_STATE_ID = 'default'
20
+
19
21
  function optionalNumber(value: number | null): number | undefined {
20
22
  return value === null ? undefined : value
21
23
  }
@@ -120,6 +122,28 @@ export class PostgresPromotionRepository implements PromotionRepository, Promoti
120
122
  return rows
121
123
  }
122
124
 
125
+ async scanCursor(): Promise<number> {
126
+ const rows = await this.db
127
+ .select({ cursor: promotionScanState.cursor })
128
+ .from(promotionScanState)
129
+ .where(eq(promotionScanState.id, SCAN_STATE_ID))
130
+
131
+ return Number(rows[0]?.cursor ?? 0)
132
+ }
133
+
134
+ async advanceScanCursor(afterUserId: number): Promise<void> {
135
+ const completedPass = afterUserId === 0
136
+
137
+ await this.db.execute(sql`
138
+ insert into promotion_scan_state (id, cursor, passes, updated_at)
139
+ values (${SCAN_STATE_ID}, ${afterUserId}, ${completedPass ? 1 : 0}, now())
140
+ on conflict (id) do update
141
+ set cursor = ${afterUserId},
142
+ passes = promotion_scan_state.passes + ${completedPass ? 1 : 0},
143
+ updated_at = now()
144
+ `)
145
+ }
146
+
123
147
  async applyPromotions(outcomes: readonly PromotionOutcome[]): Promise<void> {
124
148
  if (outcomes.length === 0) return
125
149
 
@@ -248,6 +248,13 @@ export const counterRecountState = pgTable('counter_recount_state', {
248
248
  updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
249
249
  })
250
250
 
251
+ export const promotionScanState = pgTable('promotion_scan_state', {
252
+ id: text('id').primaryKey(),
253
+ cursor: integer('cursor').notNull().default(0),
254
+ passes: integer('passes').notNull().default(0),
255
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
256
+ })
257
+
251
258
  export const massMails = pgTable('mass_mails', {
252
259
  id: integer('id').primaryKey().generatedByDefaultAsIdentity(),
253
260
  subject: text('subject').notNull(),