@getstrata/bootstrap 0.2.25 → 0.2.28

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.
@@ -10,7 +10,7 @@ import {
10
10
  resolveApplicationPolicyGate,
11
11
  resolveApplicationQueue,
12
12
  setActiveApplicationContext
13
- } from "@getstrata/core/runtime/applicationRegistry.ts";
13
+ } from "@getstrata/core/runtime/applicationRegistry";
14
14
 
15
15
  // ../../src/bootstrap/contracts.ts
16
16
  import {
@@ -135,7 +135,7 @@ import {
135
135
  CORE_POLICY_GATE_TOKEN,
136
136
  CORE_QUEUE_TOKEN,
137
137
  CORE_TOKEN_SERVICE_TOKEN
138
- } from "@getstrata/core/contracts/serviceTokens.ts";
138
+ } from "@getstrata/core/contracts/serviceTokens";
139
139
  var APP_PORT_CONFIG_KEY = "app.port";
140
140
  var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
141
141
  var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
@@ -284,6 +284,25 @@ function getDb() {
284
284
  getDatabase();
285
285
  return getDefaultDatabaseQuery();
286
286
  }
287
+ async function pingDatabase(connection = getDatabase()) {
288
+ try {
289
+ await connection`SELECT 1`;
290
+ return true;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ async function ensureDatabaseConnection() {
296
+ if (await pingDatabase()) {
297
+ return getDatabase();
298
+ }
299
+ await getDatabase().close().catch(() => {
300
+ return;
301
+ });
302
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
303
+ registerDefaultDatabasePool(connectionHolder.connection);
304
+ return getDatabase();
305
+ }
287
306
  var db = new Proxy(function database() {}, {
288
307
  apply(_target, _thisArg, args) {
289
308
  return getDb()(...args);
@@ -1393,25 +1412,117 @@ function discoverListeners() {
1393
1412
  return appListeners;
1394
1413
  }
1395
1414
 
1396
- // ../../src/core/queue/index.ts
1397
- class Job {
1398
- maxAttempts;
1399
- backoffMs;
1400
- priority;
1415
+ // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
1416
+ import { eventBus as eventBus2, modelEventName as modelEventName2 } from "@getstrata/core/events";
1417
+ import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
1418
+ import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
1419
+
1420
+ // ../../src/bootstrap/cache/modelCacheTags.ts
1421
+ function cacheTagsForModelWrite(tableName, action) {
1422
+ const module = discoverModules().find((entry) => entry.tableName === tableName);
1423
+ const baseTags = module?.cacheTags ?? [`${tableName}s`];
1424
+ const isDelete = action === "deleted" || action === "force-deleted";
1425
+ const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
1426
+ return [...new Set([...baseTags, ...extraTags])];
1427
+ }
1428
+ function discoverModelTableNames() {
1429
+ return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
1401
1430
  }
1402
1431
 
1403
- // ../../src/core/jobs/invalidateCacheTagsJob.ts
1404
- class InvalidateCacheTagsJob extends Job {
1405
- cache;
1406
- constructor(cache) {
1407
- super();
1408
- this.cache = cache;
1432
+ // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
1433
+ var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
1434
+ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
1435
+ for (const tableName of discoverModelTableNames()) {
1436
+ for (const action of MODEL_WRITE_ACTIONS) {
1437
+ bus.listen(modelEventName2(tableName, action), async () => {
1438
+ const tags = cacheTagsForModelWrite(tableName, action);
1439
+ if (tags.length === 0) {
1440
+ return;
1441
+ }
1442
+ let cache;
1443
+ let queue;
1444
+ try {
1445
+ cache = resolveApplicationCache();
1446
+ queue = resolveApplicationQueue();
1447
+ } catch {
1448
+ return;
1449
+ }
1450
+ const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
1451
+ await queue.dispatch(job, { tags });
1452
+ });
1453
+ }
1409
1454
  }
1410
- async handle(payload) {
1411
- await this.cache.tags(...payload.tags).flush();
1455
+ }
1456
+
1457
+ // ../../src/bootstrap/providers/listeners.ts
1458
+ var registeredListenerGroups = new Set;
1459
+ function registerListenerGroup(name, register) {
1460
+ if (registeredListenerGroups.has(name)) {
1461
+ return;
1412
1462
  }
1463
+ registeredListenerGroups.add(name);
1464
+ register();
1413
1465
  }
1414
- var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
1466
+ var listenersProvider = {
1467
+ name: "core.listeners",
1468
+ boot() {
1469
+ registerListenerGroup("cache.invalidate-on-model-write", () => {
1470
+ registerInvalidateCacheOnModelWriteListeners();
1471
+ });
1472
+ for (const [index, registerListener] of discoverListeners().entries()) {
1473
+ registerListenerGroup(`app.listener.${index}`, registerListener);
1474
+ }
1475
+ }
1476
+ };
1477
+ var listeners_default = listenersProvider;
1478
+
1479
+ // ../../src/core/auth/policy.ts
1480
+ var BLOCKED_POLICY_ACTIONS = new Set([
1481
+ "constructor",
1482
+ "toString",
1483
+ "valueOf",
1484
+ "hasOwnProperty",
1485
+ "isPrototypeOf",
1486
+ "propertyIsEnumerable",
1487
+ "__proto__"
1488
+ ]);
1489
+
1490
+ class PolicyGate {
1491
+ constructor() {}
1492
+ policies = new Map;
1493
+ register(resource, policy) {
1494
+ this.policies.set(resource, policy);
1495
+ }
1496
+ allows(resource, action, user, model) {
1497
+ const policy = this.policies.get(resource);
1498
+ if (!policy) {
1499
+ return false;
1500
+ }
1501
+ if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
1502
+ return false;
1503
+ }
1504
+ const handler = policy[action];
1505
+ if (typeof handler !== "function") {
1506
+ return false;
1507
+ }
1508
+ const resolvedUser = user === undefined ? currentAuthUser() : user;
1509
+ return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
1510
+ }
1511
+ authorize(resource, action, user, model) {
1512
+ if (!this.allows(resource, action, user, model)) {
1513
+ throw new ForbiddenError2;
1514
+ }
1515
+ }
1516
+ }
1517
+
1518
+ // ../../src/bootstrap/providers/policy.ts
1519
+ var policyProvider = {
1520
+ name: "core.policy",
1521
+ register({ container }) {
1522
+ container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
1523
+ }
1524
+ };
1525
+ var policy_default = policyProvider;
1415
1526
 
1416
1527
  // ../../src/core/pagination/index.ts
1417
1528
  function buildPaginationMeta(input) {
@@ -3299,9 +3410,6 @@ class ResilientQueue {
3299
3410
  function createFailedJobService() {
3300
3411
  return new failedJobService_default(new failedJobRepository_default);
3301
3412
  }
3302
- function createTrackedJob(name, job) {
3303
- return jobRegistry.track(name, job);
3304
- }
3305
3413
  function createProductionQueue(driver, options = {}) {
3306
3414
  options.registerJobs?.();
3307
3415
  const failedJobs = options.failedJobs ?? createFailedJobService();
@@ -3324,116 +3432,16 @@ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(),
3324
3432
  });
3325
3433
  }
3326
3434
 
3327
- // ../../src/bootstrap/cache/modelCacheTags.ts
3328
- function cacheTagsForModelWrite(tableName, action) {
3329
- const module = discoverModules().find((entry) => entry.tableName === tableName);
3330
- const baseTags = module?.cacheTags ?? [`${tableName}s`];
3331
- const isDelete = action === "deleted" || action === "force-deleted";
3332
- const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
3333
- return [...new Set([...baseTags, ...extraTags])];
3334
- }
3335
- function discoverModelTableNames() {
3336
- return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
3337
- }
3338
-
3339
- // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
3340
- var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
3341
- function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
3342
- for (const tableName of discoverModelTableNames()) {
3343
- for (const action of MODEL_WRITE_ACTIONS) {
3344
- bus.listen(modelEventName(tableName, action), async () => {
3345
- const tags = cacheTagsForModelWrite(tableName, action);
3346
- if (tags.length === 0) {
3347
- return;
3348
- }
3349
- let cache;
3350
- let queue;
3351
- try {
3352
- cache = resolveApplicationCache();
3353
- queue = resolveApplicationQueue();
3354
- } catch {
3355
- return;
3356
- }
3357
- const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
3358
- await queue.dispatch(job, { tags });
3359
- });
3360
- }
3361
- }
3362
- }
3363
-
3364
- // ../../src/bootstrap/providers/listeners.ts
3365
- var registeredListenerGroups = new Set;
3366
- function registerListenerGroup(name, register) {
3367
- if (registeredListenerGroups.has(name)) {
3368
- return;
3369
- }
3370
- registeredListenerGroups.add(name);
3371
- register();
3372
- }
3373
- var listenersProvider = {
3374
- name: "core.listeners",
3375
- boot() {
3376
- registerListenerGroup("cache.invalidate-on-model-write", () => {
3377
- registerInvalidateCacheOnModelWriteListeners();
3378
- });
3379
- for (const [index, registerListener] of discoverListeners().entries()) {
3380
- registerListenerGroup(`app.listener.${index}`, registerListener);
3381
- }
3382
- }
3383
- };
3384
- var listeners_default = listenersProvider;
3385
-
3386
- // ../../src/core/auth/policy.ts
3387
- var BLOCKED_POLICY_ACTIONS = new Set([
3388
- "constructor",
3389
- "toString",
3390
- "valueOf",
3391
- "hasOwnProperty",
3392
- "isPrototypeOf",
3393
- "propertyIsEnumerable",
3394
- "__proto__"
3395
- ]);
3396
-
3397
- class PolicyGate {
3398
- constructor() {}
3399
- policies = new Map;
3400
- register(resource, policy) {
3401
- this.policies.set(resource, policy);
3402
- }
3403
- allows(resource, action, user, model) {
3404
- const policy = this.policies.get(resource);
3405
- if (!policy) {
3406
- return false;
3407
- }
3408
- if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
3409
- return false;
3410
- }
3411
- const handler = policy[action];
3412
- if (typeof handler !== "function") {
3413
- return false;
3414
- }
3415
- const resolvedUser = user === undefined ? currentAuthUser() : user;
3416
- return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
3417
- }
3418
- authorize(resource, action, user, model) {
3419
- if (!this.allows(resource, action, user, model)) {
3420
- throw new ForbiddenError2;
3421
- }
3422
- }
3423
- }
3424
-
3425
- // ../../src/bootstrap/providers/policy.ts
3426
- var policyProvider = {
3427
- name: "core.policy",
3428
- register({ container }) {
3429
- container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
3430
- }
3431
- };
3432
- var policy_default = policyProvider;
3433
-
3434
3435
  // ../../src/core/jobs/dispatchWebhookJob.ts
3435
3436
  import { createHmac as createHmac2 } from "crypto";
3436
3437
 
3438
+ // ../../src/core/queue/index.ts
3439
+ class Job {
3440
+ maxAttempts;
3441
+ backoffMs;
3442
+ priority;
3443
+ }
3444
+
3437
3445
  // ../../src/core/security/safeUrl.ts
3438
3446
  import { lookup as dnsLookupImpl } from "dns/promises";
3439
3447
  var dnsLookup = dnsLookupImpl;
@@ -3618,6 +3626,19 @@ class DispatchWebhookJob extends Job {
3618
3626
  }
3619
3627
  var dispatchWebhookJob_default = DispatchWebhookJob;
3620
3628
 
3629
+ // ../../src/core/jobs/invalidateCacheTagsJob.ts
3630
+ class InvalidateCacheTagsJob2 extends Job {
3631
+ cache;
3632
+ constructor(cache) {
3633
+ super();
3634
+ this.cache = cache;
3635
+ }
3636
+ async handle(payload) {
3637
+ await this.cache.tags(...payload.tags).flush();
3638
+ }
3639
+ }
3640
+ var invalidateCacheTagsJob_default = InvalidateCacheTagsJob2;
3641
+
3621
3642
  // ../../src/core/contracts/di.ts
3622
3643
  function getRequiredDependency2(dependencies, key) {
3623
3644
  const dependency = dependencies[key];
@@ -3855,6 +3876,9 @@ function readFrontendMode() {
3855
3876
  function isViewsEnabled() {
3856
3877
  return readFrontendMode() === "server-htmx";
3857
3878
  }
3879
+ function isSpaEnabled() {
3880
+ return readFrontendMode() === "spa-react";
3881
+ }
3858
3882
 
3859
3883
  // ../../src/core/http/requestMetaContext.ts
3860
3884
  var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
@@ -0,0 +1,212 @@
1
+ // @bun
2
+ // ../../src/bootstrap/health.ts
3
+ import { jsonResponse } from "@getstrata/core/http";
4
+ var {RedisClient } = globalThis.Bun;
5
+
6
+ // ../../src/config/database.ts
7
+ function readInteger(name, fallback) {
8
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
9
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
10
+ }
11
+ var databaseConfig = {
12
+ url: process.env.DATABASE_URL ?? "",
13
+ poolMax: readInteger("DB_POOL_MAX", 10),
14
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
15
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
16
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
17
+ };
18
+
19
+ // ../../src/core/runtime/asyncContextStore.ts
20
+ import { AsyncLocalStorage } from "async_hooks";
21
+ function createAsyncContextStore(key) {
22
+ const symbol = Symbol.for(key);
23
+ const globalRecord = globalThis;
24
+ const existing = globalRecord[symbol];
25
+ if (existing) {
26
+ return existing;
27
+ }
28
+ const store = new AsyncLocalStorage;
29
+ globalRecord[symbol] = store;
30
+ return store;
31
+ }
32
+
33
+ // ../../src/core/database/connectionContext.ts
34
+ var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
35
+ function getActiveDatabaseConnection(fallback) {
36
+ return activeConnection.getStore() ?? fallback;
37
+ }
38
+
39
+ // ../../src/core/database/queryProxy.ts
40
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
41
+ function createDatabaseQueryProxy(pool) {
42
+ function resolveDatabase() {
43
+ return getActiveDatabaseConnection(pool);
44
+ }
45
+ function resolveDatabaseForProperty(property) {
46
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
47
+ return pool;
48
+ }
49
+ return resolveDatabase();
50
+ }
51
+ return new Proxy(function database() {}, {
52
+ apply(_target, _thisArg, args) {
53
+ return resolveDatabase()(...args);
54
+ },
55
+ get(_target, property) {
56
+ const connection = resolveDatabaseForProperty(property);
57
+ const value = connection[property];
58
+ return typeof value === "function" ? value.bind(connection) : value;
59
+ }
60
+ });
61
+ }
62
+
63
+ // ../../src/core/database/defaultConnection.ts
64
+ var defaultPool = {
65
+ connection: null
66
+ };
67
+ var defaultQuery = {
68
+ connection: null
69
+ };
70
+ function registerDefaultDatabasePool(connection) {
71
+ defaultPool.connection = connection;
72
+ defaultQuery.connection = createDatabaseQueryProxy(connection);
73
+ }
74
+ function getDefaultDatabaseQuery() {
75
+ if (!defaultQuery.connection) {
76
+ throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
77
+ }
78
+ return defaultQuery.connection;
79
+ }
80
+
81
+ // ../../src/db/connection/createConnection.ts
82
+ var {SQL } = globalThis.Bun;
83
+ function createDatabaseConnection(config) {
84
+ if (!config.url) {
85
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
86
+ }
87
+ return new SQL({
88
+ url: config.url,
89
+ max: config.poolMax,
90
+ idleTimeout: config.idleTimeoutSeconds,
91
+ maxLifetime: config.maxLifetimeSeconds,
92
+ connectionTimeout: config.connectionTimeoutSeconds
93
+ });
94
+ }
95
+
96
+ // ../../src/db/connection/index.ts
97
+ var connectionHolder = {
98
+ connection: null
99
+ };
100
+ function getDatabase() {
101
+ if (!connectionHolder.connection) {
102
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
103
+ registerDefaultDatabasePool(connectionHolder.connection);
104
+ }
105
+ return connectionHolder.connection;
106
+ }
107
+ function getDb() {
108
+ getDatabase();
109
+ return getDefaultDatabaseQuery();
110
+ }
111
+ async function pingDatabase(connection = getDatabase()) {
112
+ try {
113
+ await connection`SELECT 1`;
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+ async function ensureDatabaseConnection() {
120
+ if (await pingDatabase()) {
121
+ return getDatabase();
122
+ }
123
+ await getDatabase().close().catch(() => {
124
+ return;
125
+ });
126
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
127
+ registerDefaultDatabasePool(connectionHolder.connection);
128
+ return getDatabase();
129
+ }
130
+ var db = new Proxy(function database() {}, {
131
+ apply(_target, _thisArg, args) {
132
+ return getDb()(...args);
133
+ },
134
+ get(_target, property) {
135
+ const connection = getDb();
136
+ const value = connection[property];
137
+ return typeof value === "function" ? value.bind(connection) : value;
138
+ }
139
+ });
140
+
141
+ // ../../src/bootstrap/config.ts
142
+ import {
143
+ CORE_AUTH_TOKEN,
144
+ CORE_CACHE_TOKEN,
145
+ CORE_CONFIG_TOKEN,
146
+ CORE_EVENT_BUS_TOKEN,
147
+ CORE_POLICY_GATE_TOKEN,
148
+ CORE_QUEUE_TOKEN,
149
+ CORE_TOKEN_SERVICE_TOKEN
150
+ } from "@getstrata/core/contracts/serviceTokens";
151
+ var APP_PORT_CONFIG_KEY = "app.port";
152
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
153
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
154
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
155
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
156
+ var DATABASE_URL_CONFIG_KEY = "database.url";
157
+ var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
158
+ var DEFAULT_APP_PORT = 3000;
159
+ var DEFAULT_CACHE_TTL_MS = 3600000;
160
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
161
+ var DEFAULT_CACHE_DRIVER = "array";
162
+ var DEFAULT_API_TOKEN = "";
163
+ var DEFAULT_QUEUE_DRIVER = "sync";
164
+
165
+ // ../../src/bootstrap/health.ts
166
+ function resolveRedisUrl(dependencies) {
167
+ if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
168
+ return process.env.REDIS_URL?.trim() || undefined;
169
+ }
170
+ const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
171
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
172
+ return redisUrl || undefined;
173
+ }
174
+ async function checkDatabase() {
175
+ await ensureDatabaseConnection();
176
+ return await pingDatabase();
177
+ }
178
+ async function checkRedis(redisUrl) {
179
+ try {
180
+ const client = new RedisClient(redisUrl);
181
+ const response = await client.ping();
182
+ return response === "PONG";
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+ function createHealthRoutes(dependencies) {
188
+ return {
189
+ "/health": async () => jsonResponse({ status: "ok" }),
190
+ "/ready": async () => {
191
+ const checks = {
192
+ database: await checkDatabase() ? "ok" : "error"
193
+ };
194
+ const redisUrl = resolveRedisUrl(dependencies);
195
+ if (redisUrl) {
196
+ checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
197
+ } else {
198
+ checks.redis = "skipped";
199
+ }
200
+ const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
201
+ return jsonResponse({
202
+ status: ready ? "ready" : "not_ready",
203
+ checks
204
+ }, { status: ready ? 200 : 503 });
205
+ }
206
+ };
207
+ }
208
+ export {
209
+ createHealthRoutes,
210
+ checkRedis,
211
+ checkDatabase
212
+ };
@@ -39,6 +39,9 @@ function readFrontendMode() {
39
39
  function isViewsEnabled() {
40
40
  return readFrontendMode() === "server-htmx";
41
41
  }
42
+ function isSpaEnabled() {
43
+ return readFrontendMode() === "spa-react";
44
+ }
42
45
 
43
46
  // ../../src/config/rateLimit.ts
44
47
  var LOCAL_LOGIN_RATE_LIMIT = {
@@ -83,7 +86,7 @@ import {
83
86
  CORE_POLICY_GATE_TOKEN,
84
87
  CORE_QUEUE_TOKEN,
85
88
  CORE_TOKEN_SERVICE_TOKEN
86
- } from "@getstrata/core/contracts/serviceTokens.ts";
89
+ } from "@getstrata/core/contracts/serviceTokens";
87
90
  var APP_PORT_CONFIG_KEY = "app.port";
88
91
  var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
89
92
  var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
@@ -0,0 +1,118 @@
1
+ // @bun
2
+ // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
3
+ import { eventBus, modelEventName } from "@getstrata/core/events";
4
+ import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
5
+ import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
6
+
7
+ // ../../src/bootstrap/applicationRegistry.ts
8
+ import {
9
+ resolveApplicationAuth,
10
+ resolveApplicationCache,
11
+ resolveApplicationConfig,
12
+ resolveApplicationDependencies,
13
+ resolveApplicationEventBus,
14
+ resolveApplicationLogger,
15
+ resolveApplicationPolicyGate,
16
+ resolveApplicationQueue,
17
+ setActiveApplicationContext
18
+ } from "@getstrata/core/runtime/applicationRegistry";
19
+
20
+ // ../../src/bootstrap/discoverModules.ts
21
+ import { readdirSync } from "fs";
22
+ import { join } from "path";
23
+ import { pathToFileURL } from "url";
24
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
25
+ function readDiscoverModulesState() {
26
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
27
+ if (existing) {
28
+ return existing;
29
+ }
30
+ const state = { appModules: [] };
31
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
32
+ return state;
33
+ }
34
+ function configureModulesDirectory(modulesDir) {
35
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
36
+ }
37
+ function resolveModulesDirectory(options) {
38
+ const state = readDiscoverModulesState();
39
+ if (options?.modulesDir) {
40
+ return options.modulesDir;
41
+ }
42
+ if (state.configuredModulesDir) {
43
+ return state.configuredModulesDir;
44
+ }
45
+ return join(import.meta.dir, "../modules");
46
+ }
47
+ async function loadDiscoveredModules(options) {
48
+ const modulesDirectory = resolveModulesDirectory(options);
49
+ let moduleNames;
50
+ try {
51
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
52
+ } catch (error) {
53
+ if (error.code === "ENOENT") {
54
+ return [];
55
+ }
56
+ throw error;
57
+ }
58
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
59
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
60
+ const loaded = await import(moduleUrl);
61
+ return loaded.default;
62
+ }));
63
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
64
+ }
65
+ async function ensureModulesLoaded(options) {
66
+ const state = readDiscoverModulesState();
67
+ if (state.appModules.length > 0) {
68
+ return state.appModules;
69
+ }
70
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
71
+ state.appModules.splice(0, state.appModules.length, ...modules);
72
+ return state.appModules;
73
+ });
74
+ return state.modulesReady;
75
+ }
76
+ function discoverModules() {
77
+ return readDiscoverModulesState().appModules;
78
+ }
79
+
80
+ // ../../src/bootstrap/cache/modelCacheTags.ts
81
+ function cacheTagsForModelWrite(tableName, action) {
82
+ const module = discoverModules().find((entry) => entry.tableName === tableName);
83
+ const baseTags = module?.cacheTags ?? [`${tableName}s`];
84
+ const isDelete = action === "deleted" || action === "force-deleted";
85
+ const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
86
+ return [...new Set([...baseTags, ...extraTags])];
87
+ }
88
+ function discoverModelTableNames() {
89
+ return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
90
+ }
91
+
92
+ // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
93
+ var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
94
+ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
95
+ for (const tableName of discoverModelTableNames()) {
96
+ for (const action of MODEL_WRITE_ACTIONS) {
97
+ bus.listen(modelEventName(tableName, action), async () => {
98
+ const tags = cacheTagsForModelWrite(tableName, action);
99
+ if (tags.length === 0) {
100
+ return;
101
+ }
102
+ let cache;
103
+ let queue;
104
+ try {
105
+ cache = resolveApplicationCache();
106
+ queue = resolveApplicationQueue();
107
+ } catch {
108
+ return;
109
+ }
110
+ const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
111
+ await queue.dispatch(job, { tags });
112
+ });
113
+ }
114
+ }
115
+ }
116
+ export {
117
+ registerInvalidateCacheOnModelWriteListeners
118
+ };
@@ -0,0 +1,16 @@
1
+ // @bun
2
+ // ../../src/bootstrap/metricsRoutes.ts
3
+ import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
4
+ function createMetricsRoutes() {
5
+ return {
6
+ "/metrics": async () => new Response(prometheusRegistry.renderMetrics(), {
7
+ status: 200,
8
+ headers: {
9
+ "content-type": "text/plain; version=0.0.4; charset=utf-8"
10
+ }
11
+ })
12
+ };
13
+ }
14
+ export {
15
+ createMetricsRoutes
16
+ };