@moneypot/hub 1.19.16 → 1.20.0-dev.1

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.
Files changed (44) hide show
  1. package/README.md +26 -0
  2. package/dist/dashboard/assets/index-Gvpn1nS9.js +350 -0
  3. package/dist/dashboard/index.html +1 -1
  4. package/dist/src/audit-log.d.ts +9 -9
  5. package/dist/src/audit-log.js +7 -1
  6. package/dist/src/config.js +4 -8
  7. package/dist/src/db/public.d.ts +8 -8
  8. package/dist/src/db/public.js +29 -7
  9. package/dist/src/db/transaction.d.ts +2 -5
  10. package/dist/src/db/transaction.js +0 -1
  11. package/dist/src/db/types.d.ts +11 -2
  12. package/dist/src/db/util.d.ts +1 -3
  13. package/dist/src/deprecated.d.ts +6 -0
  14. package/dist/src/deprecated.js +2 -0
  15. package/dist/src/hash-chain/plugins/hub-create-hash-chain.js +12 -12
  16. package/dist/src/index.d.ts +7 -2
  17. package/dist/src/index.js +4 -0
  18. package/dist/src/liability/internal/db.d.ts +5 -0
  19. package/dist/src/liability/internal/db.js +12 -0
  20. package/dist/src/liability/internal/index.d.ts +2 -0
  21. package/dist/src/liability/internal/index.js +2 -0
  22. package/dist/src/liability/internal/processor.d.ts +16 -0
  23. package/dist/src/liability/internal/processor.js +122 -0
  24. package/dist/src/liability/public/config.d.ts +37 -0
  25. package/dist/src/liability/public/config.js +15 -0
  26. package/dist/src/liability/public/db.d.ts +10 -0
  27. package/dist/src/liability/public/db.js +53 -0
  28. package/dist/src/liability/public/index.d.ts +2 -0
  29. package/dist/src/liability/public/index.js +1 -0
  30. package/dist/src/liability/types.d.ts +24 -0
  31. package/dist/src/liability/types.js +1 -0
  32. package/dist/src/migrations.js +2 -2
  33. package/dist/src/pg-versions/017-liability.sql +48 -0
  34. package/dist/src/plugins/hub-make-outcome-bet.js +4 -4
  35. package/dist/src/plugins/hub-risk-limits.js +1 -1
  36. package/dist/src/risk-policy.d.ts +2 -2
  37. package/dist/src/risk-policy.js +3 -3
  38. package/dist/src/server/index.d.ts +4 -2
  39. package/dist/src/server/index.js +14 -3
  40. package/dist/src/test/index.d.ts +7 -6
  41. package/dist/src/test/index.js +7 -2
  42. package/dist/src/util.d.ts +5 -0
  43. package/package.json +10 -11
  44. package/dist/dashboard/assets/index-QbfKgHTj.js +0 -350
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Dashboard</title>
7
- <script type="module" crossorigin src="/dashboard/assets/index-QbfKgHTj.js"></script>
7
+ <script type="module" crossorigin src="/dashboard/assets/index-Gvpn1nS9.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/dashboard/assets/index-Bl5lLFHa.css">
9
9
  </head>
10
10
  <body>
@@ -1,4 +1,4 @@
1
- import { DbBalance, DbBankroll } from "./db/types.js";
1
+ import { DbPlayerBalance, DbHouseBankroll } from "./db/types.js";
2
2
  import { QueryExecutor } from "./db/util.js";
3
3
  export type AuditLogType = "player-balance" | "house-bankroll" | "both";
4
4
  export type HubAuditAction = "hub:init" | "hub:deposit" | "hub:take_request:pending" | "hub:take_request:refund" | "hub:outcome_bet";
@@ -16,16 +16,16 @@ export type BaseAudit = {
16
16
  refId?: null;
17
17
  });
18
18
  export type NewBalanceAudit = {
19
- balanceId: DbBalance["id"];
20
- balanceOld: DbBalance["amount"];
21
- balanceNew: DbBalance["amount"];
22
- balanceDelta: DbBalance["amount"];
19
+ balanceId: DbPlayerBalance["id"];
20
+ balanceOld: DbPlayerBalance["amount"];
21
+ balanceNew: DbPlayerBalance["amount"];
22
+ balanceDelta: DbPlayerBalance["amount"];
23
23
  };
24
24
  export type NewBankrollAudit = {
25
- bankrollId: DbBankroll["id"];
26
- bankrollOld: DbBankroll["amount"];
27
- bankrollNew: DbBankroll["amount"];
28
- bankrollDelta: DbBankroll["amount"];
25
+ bankrollId: DbHouseBankroll["id"];
26
+ bankrollOld: DbHouseBankroll["amount"];
27
+ bankrollNew: DbHouseBankroll["amount"];
28
+ bankrollDelta: DbHouseBankroll["amount"];
29
29
  };
30
30
  export type NewBothAudit = NewBalanceAudit & NewBankrollAudit;
31
31
  export declare function insertAuditLog(pgClient: QueryExecutor, type: "player-balance", audit: NewBalanceAudit & BaseAudit): Promise<void>;
@@ -5,7 +5,13 @@ export async function insertAuditLog(pgClient, type, audit) {
5
5
  const { balanceOld, balanceNew, balanceDelta } = audit;
6
6
  const expected = balanceOld + balanceDelta;
7
7
  if (Math.abs(balanceNew - expected) > DELTA_TOLERANCE) {
8
- logger.warn({ balanceOld, balanceDelta, balanceNew, expected, action: audit.action }, "Audit balance delta mismatch");
8
+ logger.warn({
9
+ balanceOld,
10
+ balanceDelta,
11
+ balanceNew,
12
+ expected,
13
+ action: audit.action,
14
+ }, "Audit balance delta mismatch");
9
15
  }
10
16
  }
11
17
  if (type === "house-bankroll" || type === "both") {
@@ -30,9 +30,6 @@ const logLevels = [
30
30
  ];
31
31
  export const LOG_LEVEL = getEnvVariable("LOG_LEVEL", (value) => {
32
32
  if (logLevels.includes(value)) {
33
- if (value === "silent") {
34
- console.log("LOG_LEVEL is set to silent. No logging will be done.");
35
- }
36
33
  return value;
37
34
  }
38
35
  if (value === "") {
@@ -101,15 +98,14 @@ export const SUPERUSER_DATABASE_URL = getEnvVariable("SUPERUSER_DATABASE_URL", (
101
98
  export const HASHCHAINSERVER_URL = getEnvVariable("HASHCHAINSERVER_URL", (value) => {
102
99
  value = value || "mock-server";
103
100
  if (value === "mock-server") {
104
- console.warn("Using mock-server for HASHCHAINSERVER_URL. This is only allowed in development.");
101
+ if (NODE_ENV !== "development" && NODE_ENV !== "test") {
102
+ console.warn("Using mock-server for HASHCHAINSERVER_URL. This is only allowed in development.", { NODE_ENV, foo: 32 });
103
+ }
105
104
  }
106
- else if (!URL.parse(value)) {
105
+ else if (!URL.canParse(value)) {
107
106
  console.warn("HASHCHAINSERVER_URL is not a valid URL. It can either be empty, 'mock-server', or URL but it was: " +
108
107
  value);
109
108
  }
110
- if (NODE_ENV !== "development" && value === "mock-server") {
111
- console.warn("Using mock-server for HASHCHAINSERVER_URL. This is only allowed in development.");
112
- }
113
109
  return value;
114
110
  });
115
111
  export const HASHCHAINSERVER_MAX_ITERATIONS = getEnvVariable("HASHCHAINSERVER_MAX_ITERATIONS", (value) => {
@@ -1,20 +1,20 @@
1
- import { DbBalance, DbBankroll, DbCasino, DbCurrency, DbSession } from "./types.js";
1
+ import { DbCasino, DbCurrency, DbSession, DbLockedPlayerBalance, DbLockedHouseBankrollWithAvailable, DbHouseBankrollWithAvailable } from "./types.js";
2
2
  import { PgClientInTransaction, QueryExecutor } from "./index.js";
3
3
  export declare function dbGetActiveSessionById(pgClient: QueryExecutor, sessionId: string): Promise<DbSession | undefined>;
4
- export declare function dbGetCasinoCurrencyByKey(pgClient: QueryExecutor, { currencyKey, casinoId }: {
5
- currencyKey: string;
6
- casinoId: string;
7
- }): Promise<DbCurrency | undefined>;
4
+ export declare function dbGetCurrencyByKey(pgClient: QueryExecutor, { currencyKey, casinoId, }: {
5
+ currencyKey: DbCurrency["key"];
6
+ casinoId: DbCasino["id"];
7
+ }): Promise<DbCurrency | null>;
8
8
  export declare function dbGetHouseBankrolls(pgClient: QueryExecutor, { casinoId }: {
9
9
  casinoId: DbCasino["id"];
10
- }): Promise<Pick<DbBankroll, "id" | "currency_key" | "amount">[]>;
10
+ }): Promise<Pick<DbHouseBankrollWithAvailable, "id" | "currency_key" | "grossAmount" | "available">[]>;
11
11
  export declare function dbLockPlayerBalance(pgClient: PgClientInTransaction, { userId, casinoId, experienceId, currencyKey, }: {
12
12
  userId: string;
13
13
  casinoId: string;
14
14
  experienceId: string;
15
15
  currencyKey: string;
16
- }): Promise<Pick<DbBalance, "id" | "amount"> | null>;
16
+ }): Promise<Pick<DbLockedPlayerBalance, "id" | "amount"> | null>;
17
17
  export declare function dbLockHouseBankroll(pgClient: PgClientInTransaction, { casinoId, currencyKey }: {
18
18
  casinoId: string;
19
19
  currencyKey: string;
20
- }): Promise<Pick<DbBankroll, "id" | "amount"> | null>;
20
+ }): Promise<DbLockedHouseBankrollWithAvailable | null>;
@@ -8,7 +8,7 @@ export async function dbGetActiveSessionById(pgClient, sessionId) {
8
8
  `, [sessionId])
9
9
  .then(maybeOneRow);
10
10
  }
11
- export async function dbGetCasinoCurrencyByKey(pgClient, { currencyKey, casinoId }) {
11
+ export async function dbGetCurrencyByKey(pgClient, { currencyKey, casinoId, }) {
12
12
  return pgClient
13
13
  .query(`
14
14
  SELECT *
@@ -16,16 +16,23 @@ export async function dbGetCasinoCurrencyByKey(pgClient, { currencyKey, casinoId
16
16
  WHERE key = $1
17
17
  AND casino_id = $2
18
18
  `, [currencyKey, casinoId])
19
- .then(maybeOneRow);
19
+ .then(maybeOneRow)
20
+ .then((row) => row ?? null);
20
21
  }
21
22
  export async function dbGetHouseBankrolls(pgClient, { casinoId }) {
22
- return pgClient
23
+ const y = pgClient
23
24
  .query(`
24
- SELECT id, currency_key, amount
25
+ SELECT id, currency_key, amount, liability, (amount - liability) AS available
25
26
  FROM hub.bankroll
26
27
  WHERE casino_id = $1
27
28
  `, [casinoId])
28
- .then((res) => res.rows);
29
+ .then((res) => res.rows.map((row) => ({
30
+ id: row.id,
31
+ currency_key: row.currency_key,
32
+ grossAmount: row.amount,
33
+ available: row.available,
34
+ })));
35
+ return y;
29
36
  }
30
37
  export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experienceId, currencyKey, }) {
31
38
  return pgClient
@@ -44,12 +51,27 @@ export async function dbLockPlayerBalance(pgClient, { userId, casinoId, experien
44
51
  export async function dbLockHouseBankroll(pgClient, { casinoId, currencyKey }) {
45
52
  return pgClient
46
53
  .query(`
47
- SELECT *
54
+ SELECT
55
+ id,
56
+ casino_id,
57
+ currency_key,
58
+ amount,
59
+ liability,
60
+ (amount - liability) AS available
48
61
  FROM hub.bankroll
49
62
  WHERE casino_id = $1
50
63
  AND currency_key = $2
51
64
  FOR UPDATE
52
65
  `, [casinoId, currencyKey])
53
66
  .then(maybeOneRow)
54
- .then((row) => row ?? null);
67
+ .then((row) => row
68
+ ? {
69
+ id: row.id,
70
+ casino_id: row.casino_id,
71
+ currency_key: row.currency_key,
72
+ liability: row.liability,
73
+ grossAmount: row.amount,
74
+ available: row.available,
75
+ }
76
+ : null);
55
77
  }
@@ -1,13 +1,11 @@
1
1
  import * as pg from "pg";
2
- declare const PgClientInTransactionBrand: unique symbol;
2
+ import { Branded } from "../util.js";
3
3
  export declare class RollbackFailedError extends Error {
4
4
  readonly originalError: unknown;
5
5
  readonly rollbackError: unknown;
6
6
  constructor(originalError: unknown, rollbackError: unknown);
7
7
  }
8
- export type PgClientInTransaction = pg.ClientBase & {
9
- readonly [PgClientInTransactionBrand]: true;
10
- };
8
+ export type PgClientInTransaction = Branded<"PgClientInTransaction", pg.ClientBase>;
11
9
  export declare function isInTransaction(pgClient: pg.ClientBase): pgClient is PgClientInTransaction;
12
10
  export declare function assertInTransaction(pgClient: pg.ClientBase): asserts pgClient is PgClientInTransaction;
13
11
  export declare function getIsolationLevel(pgClient: PgClientInTransaction): IsolationLevel | null;
@@ -21,4 +19,3 @@ export declare function withPgClientTransaction<T>(pgClient: pg.ClientBase, call
21
19
  export declare function withPgClientTransaction<T>(pgClient: pg.ClientBase, isolationLevel: IsolationLevel, callback: (pgClient: PgClientInTransaction) => Promise<T>): Promise<T>;
22
20
  export declare function withPgPoolTransaction<T>(pool: pg.Pool, callback: (pgClient: PgClientInTransaction) => Promise<T>, retryCount?: number, maxRetries?: number): Promise<T>;
23
21
  export declare function withPgPoolTransaction<T>(pool: pg.Pool, isolationLevel: IsolationLevel, callback: (pgClient: PgClientInTransaction) => Promise<T>, retryCount?: number, maxRetries?: number): Promise<T>;
24
- export {};
@@ -1,7 +1,6 @@
1
1
  import * as pg from "pg";
2
2
  import { logger } from "../logger.js";
3
3
  import { setTimeout } from "node:timers/promises";
4
- const PgClientInTransactionBrand = Symbol("PgClientInTransaction");
5
4
  export class RollbackFailedError extends Error {
6
5
  originalError;
7
6
  rollbackError;
@@ -1,4 +1,5 @@
1
1
  import * as jose from "jose";
2
+ import { Branded } from "../util.js";
2
3
  export type DbSession = {
3
4
  id: string;
4
5
  user_id: string;
@@ -75,12 +76,20 @@ export type DbCasinoJwks = {
75
76
  updated_at: Date;
76
77
  forced_at: Date | null;
77
78
  };
78
- export type DbBankroll = {
79
+ export type DbHouseBankroll = {
79
80
  id: string;
80
81
  casino_id: string;
81
82
  currency_key: string;
82
83
  amount: number;
84
+ liability: number;
83
85
  };
86
+ export type DbLockedHouseBankroll = Branded<"DbLockedHouseBankroll", DbHouseBankroll>;
87
+ export type DbLockedPlayerBalance = Branded<"DbLockedPlayerBalance", DbPlayerBalance>;
88
+ export type DbHouseBankrollWithAvailable = Omit<DbHouseBankroll, "amount"> & {
89
+ grossAmount: number;
90
+ available: number;
91
+ };
92
+ export type DbLockedHouseBankrollWithAvailable = Branded<"DbLockedHouseBankrollWithAvailable", DbHouseBankrollWithAvailable>;
84
93
  export type DbProcessedTakeRequest = {
85
94
  id: string;
86
95
  mp_take_request_id: string;
@@ -91,7 +100,7 @@ export type DbProcessedTakeRequest = {
91
100
  requested_amount: number | null;
92
101
  transferred_amount: number;
93
102
  };
94
- export type DbBalance = {
103
+ export type DbPlayerBalance = {
95
104
  id: string;
96
105
  casino_id: string;
97
106
  user_id: string;
@@ -1,9 +1,7 @@
1
1
  import { QueryResult, QueryResultRow } from "pg";
2
2
  import { PgClientResult } from "postgraphile/@dataplan/pg";
3
3
  import * as pg from "pg";
4
- export interface QueryExecutor {
5
- query<T extends pg.QueryResultRow = any>(queryText: string, values?: any[]): Promise<pg.QueryResult<T>>;
6
- }
4
+ export type QueryExecutor = Pick<pg.Pool, "query"> | Pick<pg.ClientBase, "query">;
7
5
  type ResultType<T> = PgClientResult<T> | QueryResult<T extends QueryResultRow ? T : never>;
8
6
  export declare function maybeOneRow<T>(result: ResultType<T>): T | undefined;
9
7
  export declare function exactlyOneRow<T>(result: ResultType<T>): T;
@@ -0,0 +1,6 @@
1
+ import type { DbPlayerBalance, DbHouseBankroll, DbHouseBankrollWithAvailable } from "./db/types.js";
2
+ export type DbBalance = DbPlayerBalance;
3
+ export type DbBankroll = DbHouseBankroll;
4
+ export type DbBankrollWithAvailable = DbHouseBankrollWithAvailable;
5
+ import { dbGetCurrencyByKey } from "./db/public.js";
6
+ export declare const dbGetCasinoCurrencyByKey: typeof dbGetCurrencyByKey;
@@ -0,0 +1,2 @@
1
+ import { dbGetCurrencyByKey } from "./db/public.js";
2
+ export const dbGetCasinoCurrencyByKey = dbGetCurrencyByKey;
@@ -1,8 +1,8 @@
1
- import { context, object, sideEffect } from "@moneypot/hub/grafast";
2
- import { gql, makeExtendSchemaPlugin } from "@moneypot/hub/graphile";
1
+ import { context, object, sideEffect } from "../../grafast.js";
2
+ import { gql, makeExtendSchemaPlugin } from "../../graphile.js";
3
3
  import { GraphQLError } from "graphql";
4
4
  import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
5
- import { exactlyOneRow, withPgPoolTransaction } from "@moneypot/hub/db";
5
+ import { exactlyOneRow, withPgPoolTransaction } from "../../db/index.js";
6
6
  import * as HashCommon from "../get-hash.js";
7
7
  import { DbHashKind } from "../../db/types.js";
8
8
  import * as config from "../../config.js";
@@ -37,7 +37,7 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
37
37
  await pgClient.query(`
38
38
  UPDATE hub.hash_chain
39
39
  SET active = false
40
- WHERE user_id = $1
40
+ WHERE user_id = $1
41
41
  AND experience_id = $2
42
42
  AND casino_id = $3
43
43
  AND active = true
@@ -49,11 +49,11 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
49
49
  const dbHashChain = await pgClient
50
50
  .query(`
51
51
  INSERT INTO hub.hash_chain (
52
- user_id,
53
- experience_id,
54
- casino_id,
55
- active,
56
- max_iteration,
52
+ user_id,
53
+ experience_id,
54
+ casino_id,
55
+ active,
56
+ max_iteration,
57
57
  current_iteration
58
58
  )
59
59
  VALUES ($1, $2, $3, true, $4, $4)
@@ -73,9 +73,9 @@ export const HubCreateHashChainPlugin = makeExtendSchemaPlugin((build) => {
73
73
  }
74
74
  await pgClient.query(`
75
75
  INSERT INTO hub.hash (
76
- hash_chain_id,
77
- kind,
78
- digest,
76
+ hash_chain_id,
77
+ kind,
78
+ digest,
79
79
  iteration
80
80
  )
81
81
  VALUES ($1, $2, $3, $4)
@@ -20,13 +20,16 @@ declare global {
20
20
  export { HubGameConfigPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-game-config-plugin.js";
21
21
  export { type CustomGameConfig, type CustomGameConfigMap, } from "./custom-game-config.js";
22
22
  export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
23
+ export { type DbLiability, type NewLiability, type LiabilityConfig, type LiabilityHandler, type LiabilityResolved, type DbLockedLiability, LIABILITY_RESOLVED, createLiabilityConfig, } from "./liability/public/index.js";
23
24
  export type PluginContext = Grafast.Context;
25
+ export * from "./db/types.js";
24
26
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, type HubPlugin, } from "./server/graphile.config.js";
25
27
  export type { ConfigureAppArgs } from "./server/index.js";
26
28
  import { runMigrations } from "./migrations.js";
27
29
  export { runMigrations };
28
30
  import type { HubPlugin } from "./server/graphile.config.js";
29
- export type ServerOptions = {
31
+ import type { LiabilityConfig } from "./liability/public/index.js";
32
+ export type ServerOptions<LiabilityRefType extends string = string> = {
30
33
  configureApp?: (args: ConfigureAppArgs) => void;
31
34
  plugins?: readonly HubPlugin[];
32
35
  extraPgSchemas?: string[];
@@ -37,8 +40,10 @@ export type ServerOptions = {
37
40
  port?: number;
38
41
  superuserDatabaseUrl?: string;
39
42
  postgraphileDatabaseUrl?: string;
43
+ liabilityConfig?: LiabilityConfig<LiabilityRefType>;
40
44
  };
41
- export declare function startAndListen(options: ServerOptions): Promise<{
45
+ export declare function startAndListen<LiabilityRefType extends string = string>(options: ServerOptions<LiabilityRefType>): Promise<{
42
46
  port: number;
43
47
  stop: () => Promise<void>;
44
48
  }>;
49
+ export * from "./deprecated.js";
package/dist/src/index.js CHANGED
@@ -4,6 +4,8 @@ import { getPgClient } from "./db/index.js";
4
4
  import { createHubServer } from "./server/index.js";
5
5
  export { HubGameConfigPlugin, } from "./plugins/hub-game-config-plugin.js";
6
6
  export { validateRisk, } from "./risk-policy.js";
7
+ export { LIABILITY_RESOLVED, createLiabilityConfig, } from "./liability/public/index.js";
8
+ export * from "./db/types.js";
7
9
  export { defaultPlugins, } from "./server/graphile.config.js";
8
10
  import { runMigrations } from "./migrations.js";
9
11
  export { runMigrations };
@@ -44,6 +46,7 @@ export async function startAndListen(options) {
44
46
  enablePlayground: options.enablePlayground ?? true,
45
47
  port: options.port ?? PORT,
46
48
  runProcessors: NODE_ENV !== "test",
49
+ liabilityConfig: options.liabilityConfig,
47
50
  });
48
51
  if (process.env.NODE_ENV !== "test") {
49
52
  const handler = () => {
@@ -64,3 +67,4 @@ export async function startAndListen(options) {
64
67
  stop: () => hubServer.shutdown(),
65
68
  };
66
69
  }
70
+ export * from "./deprecated.js";
@@ -0,0 +1,5 @@
1
+ import { PgClientInTransaction } from "../../db/transaction.js";
2
+ import { DbLiability } from "../types.js";
3
+ export declare function dbLockLiability(pool: PgClientInTransaction, { id }: {
4
+ id: string;
5
+ }): Promise<DbLiability | null>;
@@ -0,0 +1,12 @@
1
+ export async function dbLockLiability(pool, { id }) {
2
+ const result = await pool.query({
3
+ text: `
4
+ SELECT *
5
+ FROM hub.liability
6
+ WHERE id = $1
7
+ FOR UPDATE SKIP LOCKED
8
+ `,
9
+ values: [id],
10
+ });
11
+ return result.rows[0] ?? null;
12
+ }
@@ -0,0 +1,2 @@
1
+ export { dbLockLiability } from "./db.js";
2
+ export { startLiabilityExpirationProcessor, processExpiredLiabilities, } from "./processor.js";
@@ -0,0 +1,2 @@
1
+ export { dbLockLiability } from "./db.js";
2
+ export { startLiabilityExpirationProcessor, processExpiredLiabilities, } from "./processor.js";
@@ -0,0 +1,16 @@
1
+ import * as pg from "pg";
2
+ import { LiabilityConfigInternal } from "../public/config.js";
3
+ import { QueryExecutor } from "../../db/util.js";
4
+ export declare function dbGetLiabilityIdsPastDeadline(pgClient: QueryExecutor, { limit }?: {
5
+ limit?: number;
6
+ }): Promise<string[]>;
7
+ export declare function startLiabilityExpirationProcessor<RefType extends string>({ signal, pool, liabilityConfig, }: {
8
+ signal: AbortSignal;
9
+ pool: pg.Pool;
10
+ liabilityConfig: LiabilityConfigInternal<RefType>;
11
+ }): void;
12
+ export declare function processExpiredLiabilities<RefType extends string>({ pool, liabilityConfig, signal, }: {
13
+ pool: pg.Pool;
14
+ liabilityConfig: LiabilityConfigInternal<RefType>;
15
+ signal: AbortSignal;
16
+ }): Promise<void>;
@@ -0,0 +1,122 @@
1
+ import { logger } from "../../logger.js";
2
+ import { withPgPoolTransaction, } from "../../db/transaction.js";
3
+ import { dbLockHouseBankroll, dbLockPlayerBalance } from "../../db/public.js";
4
+ const BATCH_SIZE = 100;
5
+ export async function dbGetLiabilityIdsPastDeadline(pgClient, { limit = 100 } = {}) {
6
+ const result = await pgClient.query({
7
+ text: `
8
+ SELECT id
9
+ FROM hub.liability
10
+ WHERE resolved_at IS NULL
11
+ AND deadline < now()
12
+ ORDER BY deadline ASC
13
+ LIMIT $1
14
+ `,
15
+ values: [limit],
16
+ });
17
+ return result.rows.map((r) => r.id);
18
+ }
19
+ async function dbTryLockLiabilityById(pgClient, id) {
20
+ const result = await pgClient.query({
21
+ text: `
22
+ SELECT *
23
+ FROM hub.liability
24
+ WHERE id = $1
25
+ AND resolved_at IS NULL
26
+ FOR UPDATE SKIP LOCKED
27
+ `,
28
+ values: [id],
29
+ });
30
+ return result.rows[0] ?? null;
31
+ }
32
+ export function startLiabilityExpirationProcessor({ signal, pool, liabilityConfig, }) {
33
+ if (signal.aborted) {
34
+ logger.info(`[startLiabilityExpirationProcessor] AbortSignal aborted. Not starting processor.`);
35
+ return;
36
+ }
37
+ logger.info(`Starting liability expiration processor`);
38
+ (async () => {
39
+ let shouldStop = false;
40
+ signal.addEventListener("abort", () => {
41
+ shouldStop = true;
42
+ });
43
+ while (!shouldStop && !signal.aborted) {
44
+ try {
45
+ await processExpiredLiabilities({ pool, liabilityConfig, signal });
46
+ }
47
+ catch (e) {
48
+ logger.error(e, `Error processing expired liabilities`);
49
+ }
50
+ if (!shouldStop && !signal.aborted) {
51
+ await new Promise((resolve) => setTimeout(resolve, liabilityConfig._options.pollIntervalMs));
52
+ }
53
+ }
54
+ logger.info(`Liability expiration processor stopped`);
55
+ })();
56
+ }
57
+ export async function processExpiredLiabilities({ pool, liabilityConfig, signal, }) {
58
+ const liabilityIds = await dbGetLiabilityIdsPastDeadline(pool, {
59
+ limit: BATCH_SIZE,
60
+ });
61
+ if (liabilityIds.length === 0) {
62
+ return;
63
+ }
64
+ logger.info(`Processor found ${liabilityIds.length} liability candidates...`);
65
+ for (const liabilityId of liabilityIds) {
66
+ if (signal.aborted) {
67
+ logger.info(`[processExpiredLiabilities] Aborted by graceful shutdown.`);
68
+ break;
69
+ }
70
+ await withPgPoolTransaction(pool, async (pgClient) => {
71
+ const dbLockedLiability = await dbTryLockLiabilityById(pgClient, liabilityId);
72
+ if (!dbLockedLiability) {
73
+ return;
74
+ }
75
+ const refTypeConfig = liabilityConfig._refTypes[dbLockedLiability.ref_type];
76
+ if (!refTypeConfig?.handleDeadline) {
77
+ logger.error(`No handler configured for liability ref_type "${dbLockedLiability.ref_type}" (id: ${dbLockedLiability.id}). Skipping.`);
78
+ return;
79
+ }
80
+ const dbLockedPlayerBalance = await dbLockPlayerBalance(pgClient, {
81
+ userId: dbLockedLiability.user_id,
82
+ casinoId: dbLockedLiability.casino_id,
83
+ experienceId: dbLockedLiability.experience_id,
84
+ currencyKey: dbLockedLiability.currency_key,
85
+ });
86
+ if (!dbLockedPlayerBalance) {
87
+ logger.error(`No player balance found for liability ${dbLockedLiability.id}. Skipping.`);
88
+ return;
89
+ }
90
+ const dbLockedHouseBankroll = await dbLockHouseBankroll(pgClient, {
91
+ casinoId: dbLockedLiability.casino_id,
92
+ currencyKey: dbLockedLiability.currency_key,
93
+ });
94
+ if (!dbLockedHouseBankroll) {
95
+ logger.error(`No house bankroll found for liability ${dbLockedLiability.id}.`);
96
+ return;
97
+ }
98
+ try {
99
+ await refTypeConfig.handleDeadline({
100
+ pgClientInTransaction: pgClient,
101
+ lockedLiability: dbLockedLiability,
102
+ lockedPlayerBalance: dbLockedPlayerBalance,
103
+ lockedHouseBankroll: dbLockedHouseBankroll,
104
+ });
105
+ const { resolved_at } = await pgClient
106
+ .query(`SELECT resolved_at FROM hub.liability WHERE id = $1`, [
107
+ dbLockedLiability.id,
108
+ ])
109
+ .then((r) => r.rows[0]);
110
+ if (!resolved_at) {
111
+ throw new Error(`Handler returned LIABILITY_RESOLVED but did not resolve liability ${dbLockedLiability.id} (ref_type: ${dbLockedLiability.ref_type}, ref_id: ${dbLockedLiability.ref_id}). ` +
112
+ `Handler must call dbResolveLiabilityByRef before returning.`);
113
+ }
114
+ logger.debug(`Resolved liability ${dbLockedLiability.id}`);
115
+ }
116
+ catch (e) {
117
+ logger.error(e, `Error handling expiration for liability ${dbLockedLiability.id}`);
118
+ throw e;
119
+ }
120
+ });
121
+ }
122
+ }
@@ -0,0 +1,37 @@
1
+ import { DbLockedHouseBankrollWithAvailable, DbLockedPlayerBalance, PgClientInTransaction } from "../../db/index.js";
2
+ import { Branded } from "../../util.js";
3
+ import type { DbLiability } from "../types.js";
4
+ import { dbCreateLiability, dbResolveLiabilityByRef } from "./db.js";
5
+ export type DbLockedLiability<RefType extends string = string> = DbLiability & {
6
+ ref_type: RefType;
7
+ };
8
+ export type LiabilityResolved = Branded<"LiabilityResolved", {
9
+ readonly resolved: true;
10
+ }>;
11
+ export declare const LIABILITY_RESOLVED: LiabilityResolved;
12
+ export type HandleDeadlineArgs<RefType extends string> = {
13
+ pgClientInTransaction: PgClientInTransaction;
14
+ lockedLiability: DbLockedLiability<RefType>;
15
+ lockedPlayerBalance: Pick<DbLockedPlayerBalance, "id" | "amount">;
16
+ lockedHouseBankroll: Pick<DbLockedHouseBankrollWithAvailable, "id" | "grossAmount" | "available">;
17
+ };
18
+ export type HandleDeadlineFn<RefType extends string> = (args: HandleDeadlineArgs<RefType>) => Promise<LiabilityResolved>;
19
+ export type LiabilityRefTypeConfig<RefType extends string> = {
20
+ handleDeadline: HandleDeadlineFn<RefType>;
21
+ };
22
+ export declare function createLiabilityConfig<RefType extends string>(refTypes: {
23
+ [K in RefType]: LiabilityRefTypeConfig<K>;
24
+ }, options?: Partial<LiabilityConfigOptions>): LiabilityConfig<RefType>;
25
+ export type LiabilityConfig<RefType extends string> = {
26
+ dbCreateLiability: typeof dbCreateLiability<RefType>;
27
+ dbResolveLiabilityByRef: typeof dbResolveLiabilityByRef<RefType>;
28
+ };
29
+ export type LiabilityConfigOptions = {
30
+ pollIntervalMs: number;
31
+ };
32
+ export type LiabilityConfigInternal<RefType extends string> = LiabilityConfig<RefType> & {
33
+ _refTypes: {
34
+ [K in RefType]: LiabilityRefTypeConfig<K>;
35
+ };
36
+ _options: LiabilityConfigOptions;
37
+ };
@@ -0,0 +1,15 @@
1
+ import { dbCreateLiability, dbResolveLiabilityByRef } from "./db.js";
2
+ export const LIABILITY_RESOLVED = {
3
+ resolved: true,
4
+ };
5
+ const DEFAULT_OPTIONS = {
6
+ pollIntervalMs: 30_000,
7
+ };
8
+ export function createLiabilityConfig(refTypes, options) {
9
+ return {
10
+ _refTypes: refTypes,
11
+ _options: { ...DEFAULT_OPTIONS, ...options },
12
+ dbCreateLiability,
13
+ dbResolveLiabilityByRef,
14
+ };
15
+ }
@@ -0,0 +1,10 @@
1
+ import { PgClientInTransaction } from "../../db/transaction.js";
2
+ import { DbLiability, NewLiability } from "../types.js";
3
+ export declare function dbCreateLiability<RefType extends string>(pgClient: PgClientInTransaction, { lockedBankrollId, casinoId, experienceId, userId, currencyKey, refId, refType, maxPayout, deadline, }: NewLiability<RefType>): Promise<DbLiability & {
4
+ ref_type: RefType;
5
+ }>;
6
+ export declare function dbResolveLiabilityByRef<RefType extends string>(pgClient: PgClientInTransaction, { refType, refId, lockedBankrollId, }: {
7
+ refType: RefType;
8
+ refId: string;
9
+ lockedBankrollId: string;
10
+ }): Promise<boolean>;