@moneypot/hub 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # @moneypot/hub
2
2
 
3
- This library implements a controller server that manages users and balances across any number of Moneypot casinos.
3
+ @moneypot/hub is our official game server that integrates with any number of Moneypot casinos.
4
4
 
5
- You can use it to quickly create your own controller.
5
+ - Extend it with custom tables and game logic.
6
+ - Give it an api key for each controller you've registered on each casino.
7
+ - It will automatically sync users, their balances, deposits, and withdrawals.
6
8
 
7
9
  Example implementations:
8
10
 
@@ -57,3 +59,13 @@ startAndListen(options)
57
59
  })
58
60
  .catch(console.error);
59
61
  ```
62
+
63
+ ## Dashboard
64
+
65
+ When the server is running, visit its admin dashboard at the `/dashboard` route.
66
+
67
+ You'll need an api key from your hub database:
68
+
69
+ ```sql
70
+ insert into hub.api_key default values returning key;
71
+ ```
package/dist/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import PgUpgradeSchema, { DatabaseAheadError, } from "@moneypot/pg-upgrade-schema";
2
2
  import * as db from "./db/index.js";
3
3
  import config from "./config.js";
4
- import * as server from "./server/index.js";
4
+ import { createHubServer } from "./server/index.js";
5
5
  import { initializeTransferProcessors } from "./process-transfers.js";
6
6
  import { join } from "path";
7
7
  import { logger, setLogger } from "./logger.js";
@@ -10,6 +10,10 @@ async function initialize(options) {
10
10
  if (options.logger) {
11
11
  setLogger(options.logger);
12
12
  }
13
+ if (options.signal.aborted) {
14
+ logger.info("Initialization aborted by graceful shutdown");
15
+ return;
16
+ }
13
17
  const pgClient = db.getPgClient(config.SUPERUSER_DATABASE_URL);
14
18
  await pgClient.connect();
15
19
  try {
@@ -28,6 +32,10 @@ async function initialize(options) {
28
32
  process.exit(1);
29
33
  }
30
34
  }
35
+ if (options.signal.aborted) {
36
+ logger.info("Initialization aborted by graceful shutdown");
37
+ return;
38
+ }
31
39
  if (options.userDatabaseMigrationsPath) {
32
40
  await PgUpgradeSchema.default({
33
41
  pgClient,
@@ -36,7 +44,13 @@ async function initialize(options) {
36
44
  });
37
45
  }
38
46
  await pgClient.end();
39
- initializeTransferProcessors();
47
+ if (options.signal.aborted) {
48
+ logger.info("Initialization aborted by graceful shutdown");
49
+ return;
50
+ }
51
+ initializeTransferProcessors({
52
+ signal: options.signal,
53
+ });
40
54
  }
41
55
  export async function startAndListen(options) {
42
56
  if (options.userDatabaseMigrationsPath &&
@@ -46,18 +60,46 @@ export async function startAndListen(options) {
46
60
  if (!options.exportSchemaSDLPath.startsWith("/")) {
47
61
  throw new Error(`exportSchemaSDLPath must be an absolute path, got ${options.exportSchemaSDLPath}`);
48
62
  }
49
- await initialize({
50
- userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
51
- logger: options.logger,
52
- });
53
- return server
54
- .listen({
63
+ const abortController = new AbortController();
64
+ let isShuttingDown = false;
65
+ const hubServer = createHubServer({
55
66
  configureApp: options.configureApp,
56
67
  plugins: options.plugins,
57
68
  exportSchemaSDLPath: options.exportSchemaSDLPath,
58
69
  extraPgSchemas: options.extraPgSchemas,
59
- })
60
- .then(() => {
70
+ abortSignal: abortController.signal,
71
+ });
72
+ const gracefulShutdown = async () => {
73
+ if (isShuttingDown) {
74
+ console.warn("Already shutting down.");
75
+ return;
76
+ }
77
+ isShuttingDown = true;
78
+ console.log("Shutting down gracefully...");
79
+ abortController.abort();
80
+ const cleanupTasks = [
81
+ db.postgraphilePool.end(),
82
+ db.superuserPool.end(),
83
+ db.notifier.close(),
84
+ hubServer.shutdown(),
85
+ ];
86
+ try {
87
+ await Promise.all(cleanupTasks);
88
+ console.log("All cleanup tasks completed. Shutdown successful.");
89
+ }
90
+ catch (err) {
91
+ console.error("Error during cleanup:", err);
92
+ process.exit(1);
93
+ }
94
+ };
95
+ process.on("SIGINT", gracefulShutdown);
96
+ process.on("SIGTERM", gracefulShutdown);
97
+ await initialize({
98
+ userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
99
+ logger: options.logger,
100
+ signal: abortController.signal,
101
+ });
102
+ return hubServer.listen().then(() => {
61
103
  return {
62
104
  port: config.PORT,
63
105
  };
@@ -47,7 +47,8 @@ export const HubAddCasinoPlugin = makeExtendSchemaPlugin((build) => {
47
47
  Mutation: {
48
48
  hubAddCasino(_, { $input }) {
49
49
  const $identity = context().get("identity");
50
- const $casinoId = sideEffect([$input, $identity], ([input, identity]) => {
50
+ const $abortSignal = context().get("abortSignal");
51
+ const $casinoId = sideEffect([$input, $identity, $abortSignal], ([input, identity, abortSignal]) => {
51
52
  return withPgPoolTransaction(superuserPool, async (pgClient) => {
52
53
  if (identity?.kind !== "operator") {
53
54
  throw new GraphQLError("Unauthorized");
@@ -140,7 +141,10 @@ export const HubAddCasinoPlugin = makeExtendSchemaPlugin((build) => {
140
141
  graphqlClient,
141
142
  casinoId: casino.id,
142
143
  });
143
- startTransferProcessor({ casinoId: casino.id });
144
+ startTransferProcessor({
145
+ casinoId: casino.id,
146
+ signal: abortSignal,
147
+ });
144
148
  return casino.id;
145
149
  });
146
150
  });
@@ -1,7 +1,10 @@
1
1
  import * as db from "./db/index.js";
2
2
  export declare function casinoIdsInProcess(): string[];
3
- export declare function startTransferProcessor({ casinoId, }: {
3
+ export declare function startTransferProcessor({ casinoId, signal, }: {
4
4
  casinoId: db.DbCasino["id"];
5
+ signal: AbortSignal;
5
6
  }): void;
6
7
  export declare function stopTransferProcessor(casinoId: string): void;
7
- export declare function initializeTransferProcessors(): void;
8
+ export declare function initializeTransferProcessors({ signal, }: {
9
+ signal: AbortSignal;
10
+ }): void;
@@ -63,7 +63,7 @@ const MP_CLAIM_TRANSFER = gql(`
63
63
  const casinoMap = new Map();
64
64
  const MIN_BACKOFF_TIME = 5000;
65
65
  const MAX_BACKOFF_TIME = 30 * 1000;
66
- async function listenForNewCasinos() {
66
+ async function listenForNewCasinos({ signal }) {
67
67
  const pgClient = new pg.Client(config.SUPERUSER_DATABASE_URL);
68
68
  await pgClient.connect();
69
69
  const NewCasinoPayload = z.object({
@@ -89,11 +89,15 @@ async function listenForNewCasinos() {
89
89
  logger.error("Error parsing new casino notification:", result.error);
90
90
  return;
91
91
  }
92
- startTransferProcessor({ casinoId: result.data.id });
92
+ startTransferProcessor({ casinoId: result.data.id, signal });
93
93
  break;
94
94
  }
95
95
  }
96
96
  });
97
+ signal.addEventListener("abort", () => {
98
+ pgClient.removeAllListeners("notification");
99
+ pgClient.end();
100
+ });
97
101
  await pgClient.query("LISTEN new_casino");
98
102
  }
99
103
  async function processTransfer({ casinoId, controllerId, transfer, graphqlClient, }) {
@@ -191,14 +195,18 @@ async function processTransfer({ casinoId, controllerId, transfer, graphqlClient
191
195
  }
192
196
  }
193
197
  }
194
- async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoInfo, }) {
198
+ async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoInfo, signal, }) {
195
199
  let hasNextPage = true;
196
200
  const timeout = (ms) => new Promise((res) => setTimeout(res, ms));
197
- while (hasNextPage) {
201
+ while (hasNextPage && !signal.aborted) {
198
202
  await processWithdrawalRequests({
199
203
  casinoId: casinoInfo.id,
200
204
  graphqlClient,
201
205
  });
206
+ if (signal.aborted) {
207
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
208
+ break;
209
+ }
202
210
  const data = await graphqlClient.request(PAGINATE_TRANSFERS, {
203
211
  controllerId: casinoInfo.controller_id,
204
212
  limit: 10,
@@ -219,6 +227,10 @@ async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoIn
219
227
  logger.error(`transfer ${transfer.id} is not an ExperienceTransfer, skipping`);
220
228
  continue;
221
229
  }
230
+ if (signal.aborted) {
231
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
232
+ break;
233
+ }
222
234
  await processTransfer({
223
235
  casinoId: casinoInfo.id,
224
236
  controllerId: casinoInfo.controller_id,
@@ -232,6 +244,10 @@ async function processTransfersUntilEmpty({ afterCursor, graphqlClient, casinoIn
232
244
  }
233
245
  hasNextPage = data.transfersByHolder.pageInfo.hasNextPage;
234
246
  afterCursor = data.transfersByHolder.pageInfo.endCursor || undefined;
247
+ if (signal.aborted) {
248
+ logger.info(`[processTransfersUntilEmpty] Aborted by graceful shutdown.`);
249
+ break;
250
+ }
235
251
  await timeout(1000);
236
252
  }
237
253
  return afterCursor;
@@ -292,7 +308,11 @@ async function processWithdrawals({ casinoId, graphqlClient, }) {
292
308
  export function casinoIdsInProcess() {
293
309
  return Array.from(casinoMap.keys());
294
310
  }
295
- export function startTransferProcessor({ casinoId, }) {
311
+ export function startTransferProcessor({ casinoId, signal, }) {
312
+ if (signal.aborted) {
313
+ logger.info(`[startTransferProcessor] AbortSignal aborted. Not starting processor for casino ${casinoId}`);
314
+ return;
315
+ }
296
316
  logger.info(`starting processor for casino ${casinoId}`);
297
317
  if (casinoMap.has(casinoId)) {
298
318
  throw new Error(`processor already running for casino ${casinoId}`);
@@ -321,7 +341,7 @@ export function startTransferProcessor({ casinoId, }) {
321
341
  processorState.emitter.on("once", () => {
322
342
  shouldStop = true;
323
343
  });
324
- while (!shouldStop) {
344
+ while (!shouldStop && !signal.aborted) {
325
345
  try {
326
346
  const now = Date.now();
327
347
  const timeToWait = Math.max(0, processorState.backoffTime - (now - processorState.lastAttempt));
@@ -344,11 +364,19 @@ export function startTransferProcessor({ casinoId, }) {
344
364
  graphqlUrl: casino.graphql_url,
345
365
  apiKey: casinoSecret.api_key,
346
366
  });
367
+ if (signal.aborted) {
368
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
369
+ break;
370
+ }
347
371
  await jwtService.refreshCasinoJwksTask(superuserPool, {
348
372
  casinoId: casino.id,
349
373
  graphqlClient,
350
374
  });
351
375
  if (!upsertedCurrencies) {
376
+ if (signal.aborted) {
377
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
378
+ break;
379
+ }
352
380
  const currencies = await graphqlClient
353
381
  .request(GET_CURRENCIES)
354
382
  .then((res) => {
@@ -362,11 +390,20 @@ export function startTransferProcessor({ casinoId, }) {
362
390
  }
363
391
  upsertedCurrencies = true;
364
392
  }
393
+ if (signal.aborted) {
394
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
395
+ break;
396
+ }
365
397
  cursor = await processTransfersUntilEmpty({
366
398
  afterCursor: cursor,
367
399
  graphqlClient,
368
400
  casinoInfo: { ...casino, ...casinoSecret },
401
+ signal,
369
402
  });
403
+ if (signal.aborted) {
404
+ logger.info(`[startTransferProcessor] Aborted by graceful shutdown.`);
405
+ break;
406
+ }
370
407
  await processWithdrawals({ casinoId: casino.id, graphqlClient });
371
408
  processorState.backoffTime = MIN_BACKOFF_TIME;
372
409
  }
@@ -376,7 +413,7 @@ export function startTransferProcessor({ casinoId, }) {
376
413
  logger.debug(`Next retry for casino ${casinoId} in ${processorState.backoffTime}ms`);
377
414
  }
378
415
  }
379
- logger.debug(`processor stopped for casino ${casinoId}`);
416
+ logger.info(`processor stopped for casino ${casinoId}`);
380
417
  casinoMap.delete(casinoId);
381
418
  })();
382
419
  }
@@ -388,7 +425,7 @@ export function stopTransferProcessor(casinoId) {
388
425
  }
389
426
  processorState.emitter.emit("stop");
390
427
  }
391
- export function initializeTransferProcessors() {
428
+ export function initializeTransferProcessors({ signal, }) {
392
429
  (async () => {
393
430
  try {
394
431
  const casinos = await db.listCasinos(superuserPool);
@@ -402,9 +439,9 @@ export function initializeTransferProcessors() {
402
439
  logger.warn(`${casino.id} has localhost endpoint "${casino.graphql_url}" while NODE_ENV=production.`);
403
440
  }
404
441
  logger.info(`Starting casino processor for "${casino.name}" at "${casino.graphql_url}"`);
405
- startTransferProcessor({ casinoId: casino.id });
442
+ startTransferProcessor({ casinoId: casino.id, signal });
406
443
  }
407
- await listenForNewCasinos();
444
+ await listenForNewCasinos({ signal });
408
445
  }
409
446
  catch (e) {
410
447
  logger.error(`Error initializing transfer processors:`, e);
@@ -15,6 +15,7 @@ type PluginIdentity = {
15
15
  };
16
16
  export type PluginContext = Grafast.Context & {
17
17
  identity?: PluginIdentity;
18
+ abortSignal: AbortSignal;
18
19
  };
19
20
  export declare const requiredPlugins: readonly GraphileConfig.Plugin[];
20
21
  declare global {
@@ -25,9 +26,10 @@ declare global {
25
26
  }
26
27
  }
27
28
  export declare const defaultPlugins: readonly GraphileConfig.Plugin[];
28
- export declare function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, }: {
29
+ export declare function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, }: {
29
30
  plugins: readonly GraphileConfig.Plugin[];
30
31
  exportSchemaSDLPath: string;
31
32
  extraPgSchemas: string[];
33
+ abortSignal: AbortSignal;
32
34
  }): GraphileConfig.Preset;
33
35
  export {};
@@ -38,7 +38,7 @@ export const defaultPlugins = [
38
38
  HubClaimFaucetPlugin,
39
39
  customPgOmitArchivedPlugin("deleted"),
40
40
  ];
41
- export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, }) {
41
+ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, }) {
42
42
  if (!exportSchemaSDLPath.startsWith("/")) {
43
43
  throw new Error("exportSchemaSDLPath must be an absolute path");
44
44
  }
@@ -120,6 +120,7 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, })
120
120
  return {
121
121
  pgSettings,
122
122
  identity: pluginIdentity,
123
+ abortSignal,
123
124
  };
124
125
  },
125
126
  },
@@ -1,2 +1,8 @@
1
1
  import { ServerOptions } from "../index.js";
2
- export declare function listen({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp">): Promise<void>;
2
+ export type HubServer = {
3
+ listen: () => Promise<void>;
4
+ shutdown: () => Promise<void>;
5
+ };
6
+ export declare function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp"> & {
7
+ abortSignal: AbortSignal;
8
+ }): HubServer;
@@ -1,4 +1,4 @@
1
- import { createServer } from "node:http";
1
+ import { createServer as createNodeServer } from "node:http";
2
2
  import { grafserv } from "grafserv/express/v4";
3
3
  import postgraphile from "postgraphile";
4
4
  import { createPreset, defaultPlugins } from "./graphile.config.js";
@@ -43,27 +43,37 @@ function createExpressServer() {
43
43
  });
44
44
  return app;
45
45
  }
46
- export async function listen({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, }) {
46
+ export function createHubServer({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, abortSignal, }) {
47
47
  const expressServer = createExpressServer();
48
48
  const preset = createPreset({
49
49
  plugins: plugins ?? defaultPlugins,
50
50
  exportSchemaSDLPath,
51
51
  extraPgSchemas: extraPgSchemas ?? [],
52
+ abortSignal,
52
53
  });
53
54
  const pgl = postgraphile.default(preset);
54
55
  const serv = pgl.createServ(grafserv);
55
56
  if (configureApp) {
56
57
  configureApp(expressServer);
57
58
  }
58
- const server = createServer(expressServer);
59
- server.on("error", (e) => {
59
+ const nodeServer = createNodeServer(expressServer);
60
+ nodeServer.on("error", (e) => {
60
61
  logger.error(e);
61
62
  });
62
- serv.addTo(expressServer, server).catch((e) => {
63
+ serv.addTo(expressServer, nodeServer).catch((e) => {
63
64
  logger.error(e);
64
65
  process.exit(1);
65
66
  });
66
- return new Promise((resolve) => {
67
- server.listen(config.PORT, resolve);
68
- });
67
+ return {
68
+ listen: () => {
69
+ return new Promise((resolve) => {
70
+ nodeServer.listen(config.PORT, resolve);
71
+ });
72
+ },
73
+ shutdown: () => {
74
+ return new Promise((resolve) => {
75
+ nodeServer.close(() => resolve());
76
+ });
77
+ },
78
+ };
69
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [