@getstrata/bootstrap 2.0.3 → 2.0.5
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/CHANGELOG.md +8 -0
- package/dist/bootstrap/buildModuleRoutes.d.ts +2 -1
- package/dist/bootstrap/contracts.d.ts +4 -0
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +11 -2
- package/dist/entries/buildModuleRoutes.js +22 -7
- package/dist/entries/buildWebModuleRoutes.js +22 -7
- package/dist/entries/context.js +86 -7
- package/dist/entries/createWebRoutes.js +22 -7
- package/dist/entries/dependencies.js +86 -7
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +84 -7
- package/dist/entries/providers.js +86 -7
- package/dist/index.js +93 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/bootstrap changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5
|
|
4
|
+
|
|
5
|
+
- Allow explicitly approved application headers in CORS
|
|
6
|
+
|
|
7
|
+
## 2.0.4
|
|
8
|
+
|
|
9
|
+
- Place cache rollout guidance beside outbox docs without overlapping API additions
|
|
10
|
+
|
|
3
11
|
## 2.0.3
|
|
4
12
|
|
|
5
13
|
- Persist transactional events with leased tenant-aware SQL outbox workers
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { OpenApiRouteMap } from "@getstrata/core/openapi/registeredRoute";
|
|
1
2
|
import type { AppDependencies, AppModule, AppRouteMap } from "./contracts";
|
|
2
3
|
interface BuildModuleRoutesOptions {
|
|
3
4
|
apiPrefix?: string;
|
|
4
5
|
modules?: AppModule[];
|
|
5
6
|
clearRegistry?: boolean;
|
|
6
7
|
}
|
|
7
|
-
declare function registerOpenApiRouteMap(routes: Record<string, unknown>, middleware: string[]): Record<string, unknown>;
|
|
8
|
+
declare function registerOpenApiRouteMap(routes: Record<string, unknown>, middleware: string[], metadata?: OpenApiRouteMap): Record<string, unknown>;
|
|
8
9
|
declare function buildModuleRoutes(dependencies: AppDependencies, options?: BuildModuleRoutesOptions): AppRouteMap;
|
|
9
10
|
export type { BuildModuleRoutesOptions };
|
|
10
11
|
export { buildModuleRoutes, registerOpenApiRouteMap };
|
|
@@ -8,7 +8,10 @@ interface ModuleRouteContext {
|
|
|
8
8
|
cachedJson: CachedJson;
|
|
9
9
|
kernel: HttpKernel;
|
|
10
10
|
}
|
|
11
|
+
import type { OpenApiRouteMap } from "@getstrata/core/openapi/registeredRoute";
|
|
11
12
|
interface AppModule {
|
|
13
|
+
/** Paths match this module's API route map before apiPrefix is applied. */
|
|
14
|
+
openApi?: OpenApiRouteMap;
|
|
12
15
|
name: string;
|
|
13
16
|
order?: number;
|
|
14
17
|
tableName?: string;
|
|
@@ -18,4 +21,5 @@ interface AppModule {
|
|
|
18
21
|
routes?(context: ModuleRouteContext): AppRouteMap;
|
|
19
22
|
webRoutes?(context: ModuleRouteContext): AppRouteMap;
|
|
20
23
|
}
|
|
24
|
+
export type { OpenApiOperation, OpenApiRouteMap, OpenApiSchema, } from "@getstrata/core/openapi/registeredRoute";
|
|
21
25
|
export type { AppModule, ModuleRouteContext };
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import { type EventBus } from "@getstrata/core/events";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import type { DurableListener, SqlOutbox } from "@getstrata/core/events/outbox";
|
|
3
|
+
import type { CacheLike } from "../../types/services";
|
|
4
|
+
interface ModelCacheInvalidationOptions {
|
|
5
|
+
outbox?: SqlOutbox;
|
|
6
|
+
}
|
|
7
|
+
/** Include in the application's one outbox registry; never dispatch through Redis queues. */
|
|
8
|
+
declare function createModelCacheInvalidationListener(resolveCache?: () => CacheLike): DurableListener;
|
|
9
|
+
/** Durable mode requires the outbox schema and an explicit business transaction. */
|
|
10
|
+
declare function registerInvalidateCacheOnModelWriteListeners(bus?: EventBus, options?: ModelCacheInvalidationOptions): () => void;
|
|
11
|
+
export type { ModelCacheInvalidationOptions };
|
|
12
|
+
export { createModelCacheInvalidationListener, registerInvalidateCacheOnModelWriteListeners };
|
|
@@ -428,22 +428,22 @@ function readSharedRouteRegistry() {
|
|
|
428
428
|
var routeRegistry = readSharedRouteRegistry();
|
|
429
429
|
|
|
430
430
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
431
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
432
|
-
routeRegistry.register({ method, path, middleware });
|
|
431
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
432
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
433
433
|
}
|
|
434
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
434
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
435
435
|
const registered = {};
|
|
436
436
|
for (const [path, handler] of Object.entries(routes)) {
|
|
437
437
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
438
438
|
const methodMap = handler;
|
|
439
439
|
registered[path] = methodMap;
|
|
440
440
|
for (const method of Object.keys(methodMap)) {
|
|
441
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
441
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
442
442
|
}
|
|
443
443
|
continue;
|
|
444
444
|
}
|
|
445
445
|
registered[path] = handler;
|
|
446
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
446
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
447
447
|
}
|
|
448
448
|
return registered;
|
|
449
449
|
}
|
|
@@ -462,14 +462,29 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
462
462
|
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
463
463
|
const cachedJson = createCachedJson(dependencies);
|
|
464
464
|
const moduleRoutes = {};
|
|
465
|
+
const metadata = {};
|
|
465
466
|
for (const module of modules) {
|
|
466
467
|
if (!module.routes) {
|
|
467
468
|
continue;
|
|
468
469
|
}
|
|
469
|
-
|
|
470
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
471
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
472
|
+
if (!(path in routes))
|
|
473
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
474
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
475
|
+
for (const method of Object.keys(operations))
|
|
476
|
+
if (!methods.includes(method))
|
|
477
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
478
|
+
}
|
|
479
|
+
for (const path of Object.keys(routes)) {
|
|
480
|
+
delete metadata[path];
|
|
481
|
+
if (module.openApi?.[path])
|
|
482
|
+
metadata[path] = module.openApi[path];
|
|
483
|
+
}
|
|
484
|
+
Object.assign(moduleRoutes, routes);
|
|
470
485
|
}
|
|
471
486
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
472
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
487
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
473
488
|
}
|
|
474
489
|
export {
|
|
475
490
|
buildModuleRoutes,
|
|
@@ -431,22 +431,22 @@ function readSharedRouteRegistry() {
|
|
|
431
431
|
var routeRegistry = readSharedRouteRegistry();
|
|
432
432
|
|
|
433
433
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
434
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
435
|
-
routeRegistry.register({ method, path, middleware });
|
|
434
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
435
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
436
436
|
}
|
|
437
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
437
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
438
438
|
const registered = {};
|
|
439
439
|
for (const [path, handler] of Object.entries(routes)) {
|
|
440
440
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
441
441
|
const methodMap = handler;
|
|
442
442
|
registered[path] = methodMap;
|
|
443
443
|
for (const method of Object.keys(methodMap)) {
|
|
444
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
444
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
445
445
|
}
|
|
446
446
|
continue;
|
|
447
447
|
}
|
|
448
448
|
registered[path] = handler;
|
|
449
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
449
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
450
450
|
}
|
|
451
451
|
return registered;
|
|
452
452
|
}
|
|
@@ -465,14 +465,29 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
465
465
|
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
466
466
|
const cachedJson = createCachedJson(dependencies);
|
|
467
467
|
const moduleRoutes = {};
|
|
468
|
+
const metadata = {};
|
|
468
469
|
for (const module of modules) {
|
|
469
470
|
if (!module.routes) {
|
|
470
471
|
continue;
|
|
471
472
|
}
|
|
472
|
-
|
|
473
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
474
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
475
|
+
if (!(path in routes))
|
|
476
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
477
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
478
|
+
for (const method of Object.keys(operations))
|
|
479
|
+
if (!methods.includes(method))
|
|
480
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
481
|
+
}
|
|
482
|
+
for (const path of Object.keys(routes)) {
|
|
483
|
+
delete metadata[path];
|
|
484
|
+
if (module.openApi?.[path])
|
|
485
|
+
metadata[path] = module.openApi[path];
|
|
486
|
+
}
|
|
487
|
+
Object.assign(moduleRoutes, routes);
|
|
473
488
|
}
|
|
474
489
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
475
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
490
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
476
491
|
}
|
|
477
492
|
|
|
478
493
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
package/dist/entries/context.js
CHANGED
|
@@ -285,6 +285,9 @@ var appEnvSchema = defineEnvSchema({
|
|
|
285
285
|
API_PREFIX: {
|
|
286
286
|
default: "/api/v1"
|
|
287
287
|
},
|
|
288
|
+
CORS_ADDITIONAL_ALLOWED_HEADERS: {
|
|
289
|
+
default: ""
|
|
290
|
+
},
|
|
288
291
|
CORS_ALLOWED_ORIGINS: {
|
|
289
292
|
default: "*"
|
|
290
293
|
},
|
|
@@ -437,14 +440,88 @@ function discoverModelTableNames() {
|
|
|
437
440
|
|
|
438
441
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
439
442
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
444
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
445
|
+
function registrations() {
|
|
446
|
+
const global = globalThis;
|
|
447
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
448
|
+
return global[STATE_KEY];
|
|
449
|
+
}
|
|
450
|
+
function requireCacheTags(value) {
|
|
451
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
452
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
456
|
+
return {
|
|
457
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
458
|
+
event: CACHE_EVENT,
|
|
459
|
+
async handle(event, { signal }) {
|
|
460
|
+
const payload = event.payload;
|
|
461
|
+
if (event.version !== 1)
|
|
462
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
463
|
+
const tags = requireCacheTags(payload?.tags);
|
|
464
|
+
signal.throwIfAborted();
|
|
465
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
466
|
+
signal.throwIfAborted();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
471
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
472
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
473
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
474
|
+
tableName,
|
|
475
|
+
action,
|
|
476
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
477
|
+
})));
|
|
478
|
+
if (options.outbox) {
|
|
479
|
+
for (const write of writes)
|
|
480
|
+
if (write.tags.length)
|
|
481
|
+
requireCacheTags(write.tags);
|
|
482
|
+
}
|
|
483
|
+
const state = registrations();
|
|
484
|
+
const previous = state.get(bus);
|
|
485
|
+
if (previous?.durable && !options.outbox)
|
|
486
|
+
return () => {};
|
|
487
|
+
previous?.suspend();
|
|
488
|
+
const unsubscribe = [];
|
|
489
|
+
const subscribe = [];
|
|
490
|
+
const registration = {
|
|
491
|
+
durable: !!options.outbox,
|
|
492
|
+
suspend() {
|
|
493
|
+
for (const stop of unsubscribe.splice(0))
|
|
494
|
+
stop();
|
|
495
|
+
},
|
|
496
|
+
resume() {
|
|
497
|
+
for (const start of subscribe)
|
|
498
|
+
unsubscribe.push(start());
|
|
499
|
+
state.set(bus, registration);
|
|
500
|
+
},
|
|
501
|
+
dispose() {
|
|
502
|
+
if (state.get(bus) !== registration)
|
|
503
|
+
return;
|
|
504
|
+
registration.suspend();
|
|
505
|
+
if (previous)
|
|
506
|
+
previous.resume();
|
|
507
|
+
else
|
|
508
|
+
state.delete(bus);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
512
|
+
const event = modelEventName(tableName, action);
|
|
513
|
+
if (options.outbox) {
|
|
514
|
+
if (!durableTags.length)
|
|
515
|
+
continue;
|
|
516
|
+
const outbox = options.outbox;
|
|
517
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
518
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
519
|
+
}));
|
|
520
|
+
} else {
|
|
521
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
444
522
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
445
|
-
if (tags.length
|
|
523
|
+
if (!tags.length)
|
|
446
524
|
return;
|
|
447
|
-
}
|
|
448
525
|
let cache;
|
|
449
526
|
let queue;
|
|
450
527
|
try {
|
|
@@ -455,9 +532,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
455
532
|
}
|
|
456
533
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
457
534
|
await queue.dispatch(job, { tags });
|
|
458
|
-
});
|
|
535
|
+
}));
|
|
459
536
|
}
|
|
460
537
|
}
|
|
538
|
+
registration.resume();
|
|
539
|
+
return registration.dispose;
|
|
461
540
|
}
|
|
462
541
|
|
|
463
542
|
// ../../src/bootstrap/providers/listeners.ts
|
|
@@ -437,22 +437,22 @@ function readSharedRouteRegistry() {
|
|
|
437
437
|
var routeRegistry = readSharedRouteRegistry();
|
|
438
438
|
|
|
439
439
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
440
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
441
|
-
routeRegistry.register({ method, path, middleware });
|
|
440
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
441
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
442
442
|
}
|
|
443
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
443
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
444
444
|
const registered = {};
|
|
445
445
|
for (const [path, handler] of Object.entries(routes)) {
|
|
446
446
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
447
447
|
const methodMap = handler;
|
|
448
448
|
registered[path] = methodMap;
|
|
449
449
|
for (const method of Object.keys(methodMap)) {
|
|
450
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
450
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
451
451
|
}
|
|
452
452
|
continue;
|
|
453
453
|
}
|
|
454
454
|
registered[path] = handler;
|
|
455
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
455
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
456
456
|
}
|
|
457
457
|
return registered;
|
|
458
458
|
}
|
|
@@ -471,14 +471,29 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
471
471
|
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
472
472
|
const cachedJson = createCachedJson(dependencies);
|
|
473
473
|
const moduleRoutes = {};
|
|
474
|
+
const metadata = {};
|
|
474
475
|
for (const module of modules) {
|
|
475
476
|
if (!module.routes) {
|
|
476
477
|
continue;
|
|
477
478
|
}
|
|
478
|
-
|
|
479
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
480
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
481
|
+
if (!(path in routes))
|
|
482
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
483
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
484
|
+
for (const method of Object.keys(operations))
|
|
485
|
+
if (!methods.includes(method))
|
|
486
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
487
|
+
}
|
|
488
|
+
for (const path of Object.keys(routes)) {
|
|
489
|
+
delete metadata[path];
|
|
490
|
+
if (module.openApi?.[path])
|
|
491
|
+
metadata[path] = module.openApi[path];
|
|
492
|
+
}
|
|
493
|
+
Object.assign(moduleRoutes, routes);
|
|
479
494
|
}
|
|
480
495
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
481
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
496
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
482
497
|
}
|
|
483
498
|
|
|
484
499
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
@@ -285,6 +285,9 @@ var appEnvSchema = defineEnvSchema({
|
|
|
285
285
|
API_PREFIX: {
|
|
286
286
|
default: "/api/v1"
|
|
287
287
|
},
|
|
288
|
+
CORS_ADDITIONAL_ALLOWED_HEADERS: {
|
|
289
|
+
default: ""
|
|
290
|
+
},
|
|
288
291
|
CORS_ALLOWED_ORIGINS: {
|
|
289
292
|
default: "*"
|
|
290
293
|
},
|
|
@@ -437,14 +440,88 @@ function discoverModelTableNames() {
|
|
|
437
440
|
|
|
438
441
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
439
442
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
444
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
445
|
+
function registrations() {
|
|
446
|
+
const global = globalThis;
|
|
447
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
448
|
+
return global[STATE_KEY];
|
|
449
|
+
}
|
|
450
|
+
function requireCacheTags(value) {
|
|
451
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
452
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
456
|
+
return {
|
|
457
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
458
|
+
event: CACHE_EVENT,
|
|
459
|
+
async handle(event, { signal }) {
|
|
460
|
+
const payload = event.payload;
|
|
461
|
+
if (event.version !== 1)
|
|
462
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
463
|
+
const tags = requireCacheTags(payload?.tags);
|
|
464
|
+
signal.throwIfAborted();
|
|
465
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
466
|
+
signal.throwIfAborted();
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
471
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
472
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
473
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
474
|
+
tableName,
|
|
475
|
+
action,
|
|
476
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
477
|
+
})));
|
|
478
|
+
if (options.outbox) {
|
|
479
|
+
for (const write of writes)
|
|
480
|
+
if (write.tags.length)
|
|
481
|
+
requireCacheTags(write.tags);
|
|
482
|
+
}
|
|
483
|
+
const state = registrations();
|
|
484
|
+
const previous = state.get(bus);
|
|
485
|
+
if (previous?.durable && !options.outbox)
|
|
486
|
+
return () => {};
|
|
487
|
+
previous?.suspend();
|
|
488
|
+
const unsubscribe = [];
|
|
489
|
+
const subscribe = [];
|
|
490
|
+
const registration = {
|
|
491
|
+
durable: !!options.outbox,
|
|
492
|
+
suspend() {
|
|
493
|
+
for (const stop of unsubscribe.splice(0))
|
|
494
|
+
stop();
|
|
495
|
+
},
|
|
496
|
+
resume() {
|
|
497
|
+
for (const start of subscribe)
|
|
498
|
+
unsubscribe.push(start());
|
|
499
|
+
state.set(bus, registration);
|
|
500
|
+
},
|
|
501
|
+
dispose() {
|
|
502
|
+
if (state.get(bus) !== registration)
|
|
503
|
+
return;
|
|
504
|
+
registration.suspend();
|
|
505
|
+
if (previous)
|
|
506
|
+
previous.resume();
|
|
507
|
+
else
|
|
508
|
+
state.delete(bus);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
512
|
+
const event = modelEventName(tableName, action);
|
|
513
|
+
if (options.outbox) {
|
|
514
|
+
if (!durableTags.length)
|
|
515
|
+
continue;
|
|
516
|
+
const outbox = options.outbox;
|
|
517
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
518
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
519
|
+
}));
|
|
520
|
+
} else {
|
|
521
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
444
522
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
445
|
-
if (tags.length
|
|
523
|
+
if (!tags.length)
|
|
446
524
|
return;
|
|
447
|
-
}
|
|
448
525
|
let cache;
|
|
449
526
|
let queue;
|
|
450
527
|
try {
|
|
@@ -455,9 +532,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
455
532
|
}
|
|
456
533
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
457
534
|
await queue.dispatch(job, { tags });
|
|
458
|
-
});
|
|
535
|
+
}));
|
|
459
536
|
}
|
|
460
537
|
}
|
|
538
|
+
registration.resume();
|
|
539
|
+
return registration.dispose;
|
|
461
540
|
}
|
|
462
541
|
|
|
463
542
|
// ../../src/bootstrap/providers/listeners.ts
|
|
@@ -102,14 +102,88 @@ function discoverModelTableNames() {
|
|
|
102
102
|
|
|
103
103
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
104
104
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
106
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
107
|
+
function registrations() {
|
|
108
|
+
const global = globalThis;
|
|
109
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
110
|
+
return global[STATE_KEY];
|
|
111
|
+
}
|
|
112
|
+
function requireCacheTags(value) {
|
|
113
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
114
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
118
|
+
return {
|
|
119
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
120
|
+
event: CACHE_EVENT,
|
|
121
|
+
async handle(event, { signal }) {
|
|
122
|
+
const payload = event.payload;
|
|
123
|
+
if (event.version !== 1)
|
|
124
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
125
|
+
const tags = requireCacheTags(payload?.tags);
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus, options = {}) {
|
|
133
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
134
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
135
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
136
|
+
tableName,
|
|
137
|
+
action,
|
|
138
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
139
|
+
})));
|
|
140
|
+
if (options.outbox) {
|
|
141
|
+
for (const write of writes)
|
|
142
|
+
if (write.tags.length)
|
|
143
|
+
requireCacheTags(write.tags);
|
|
144
|
+
}
|
|
145
|
+
const state = registrations();
|
|
146
|
+
const previous = state.get(bus);
|
|
147
|
+
if (previous?.durable && !options.outbox)
|
|
148
|
+
return () => {};
|
|
149
|
+
previous?.suspend();
|
|
150
|
+
const unsubscribe = [];
|
|
151
|
+
const subscribe = [];
|
|
152
|
+
const registration = {
|
|
153
|
+
durable: !!options.outbox,
|
|
154
|
+
suspend() {
|
|
155
|
+
for (const stop of unsubscribe.splice(0))
|
|
156
|
+
stop();
|
|
157
|
+
},
|
|
158
|
+
resume() {
|
|
159
|
+
for (const start of subscribe)
|
|
160
|
+
unsubscribe.push(start());
|
|
161
|
+
state.set(bus, registration);
|
|
162
|
+
},
|
|
163
|
+
dispose() {
|
|
164
|
+
if (state.get(bus) !== registration)
|
|
165
|
+
return;
|
|
166
|
+
registration.suspend();
|
|
167
|
+
if (previous)
|
|
168
|
+
previous.resume();
|
|
169
|
+
else
|
|
170
|
+
state.delete(bus);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
174
|
+
const event = modelEventName(tableName, action);
|
|
175
|
+
if (options.outbox) {
|
|
176
|
+
if (!durableTags.length)
|
|
177
|
+
continue;
|
|
178
|
+
const outbox = options.outbox;
|
|
179
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
180
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
181
|
+
}));
|
|
182
|
+
} else {
|
|
183
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
109
184
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
110
|
-
if (tags.length
|
|
185
|
+
if (!tags.length)
|
|
111
186
|
return;
|
|
112
|
-
}
|
|
113
187
|
let cache;
|
|
114
188
|
let queue;
|
|
115
189
|
try {
|
|
@@ -120,10 +194,13 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
120
194
|
}
|
|
121
195
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
122
196
|
await queue.dispatch(job, { tags });
|
|
123
|
-
});
|
|
197
|
+
}));
|
|
124
198
|
}
|
|
125
199
|
}
|
|
200
|
+
registration.resume();
|
|
201
|
+
return registration.dispose;
|
|
126
202
|
}
|
|
127
203
|
export {
|
|
204
|
+
createModelCacheInvalidationListener,
|
|
128
205
|
registerInvalidateCacheOnModelWriteListeners
|
|
129
206
|
};
|
|
@@ -191,6 +191,9 @@ var appEnvSchema = defineEnvSchema({
|
|
|
191
191
|
API_PREFIX: {
|
|
192
192
|
default: "/api/v1"
|
|
193
193
|
},
|
|
194
|
+
CORS_ADDITIONAL_ALLOWED_HEADERS: {
|
|
195
|
+
default: ""
|
|
196
|
+
},
|
|
194
197
|
CORS_ALLOWED_ORIGINS: {
|
|
195
198
|
default: "*"
|
|
196
199
|
},
|
|
@@ -427,14 +430,88 @@ function discoverModelTableNames() {
|
|
|
427
430
|
|
|
428
431
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
429
432
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
433
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
434
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
435
|
+
function registrations() {
|
|
436
|
+
const global = globalThis;
|
|
437
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
438
|
+
return global[STATE_KEY];
|
|
439
|
+
}
|
|
440
|
+
function requireCacheTags(value) {
|
|
441
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
442
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
function createModelCacheInvalidationListener(resolveCache = resolveApplicationCache) {
|
|
446
|
+
return {
|
|
447
|
+
name: "strata.cache.invalidate-tags.v1",
|
|
448
|
+
event: CACHE_EVENT,
|
|
449
|
+
async handle(event, { signal }) {
|
|
450
|
+
const payload = event.payload;
|
|
451
|
+
if (event.version !== 1)
|
|
452
|
+
throw new Error("Invalid durable cache invalidation version.");
|
|
453
|
+
const tags = requireCacheTags(payload?.tags);
|
|
454
|
+
signal.throwIfAborted();
|
|
455
|
+
await new InvalidateCacheTagsJob(resolveCache()).handle({ tags });
|
|
456
|
+
signal.throwIfAborted();
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
461
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
462
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
463
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
464
|
+
tableName,
|
|
465
|
+
action,
|
|
466
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
467
|
+
})));
|
|
468
|
+
if (options.outbox) {
|
|
469
|
+
for (const write of writes)
|
|
470
|
+
if (write.tags.length)
|
|
471
|
+
requireCacheTags(write.tags);
|
|
472
|
+
}
|
|
473
|
+
const state = registrations();
|
|
474
|
+
const previous = state.get(bus);
|
|
475
|
+
if (previous?.durable && !options.outbox)
|
|
476
|
+
return () => {};
|
|
477
|
+
previous?.suspend();
|
|
478
|
+
const unsubscribe = [];
|
|
479
|
+
const subscribe = [];
|
|
480
|
+
const registration = {
|
|
481
|
+
durable: !!options.outbox,
|
|
482
|
+
suspend() {
|
|
483
|
+
for (const stop of unsubscribe.splice(0))
|
|
484
|
+
stop();
|
|
485
|
+
},
|
|
486
|
+
resume() {
|
|
487
|
+
for (const start of subscribe)
|
|
488
|
+
unsubscribe.push(start());
|
|
489
|
+
state.set(bus, registration);
|
|
490
|
+
},
|
|
491
|
+
dispose() {
|
|
492
|
+
if (state.get(bus) !== registration)
|
|
493
|
+
return;
|
|
494
|
+
registration.suspend();
|
|
495
|
+
if (previous)
|
|
496
|
+
previous.resume();
|
|
497
|
+
else
|
|
498
|
+
state.delete(bus);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
502
|
+
const event = modelEventName(tableName, action);
|
|
503
|
+
if (options.outbox) {
|
|
504
|
+
if (!durableTags.length)
|
|
505
|
+
continue;
|
|
506
|
+
const outbox = options.outbox;
|
|
507
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
508
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
509
|
+
}));
|
|
510
|
+
} else {
|
|
511
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
434
512
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
435
|
-
if (tags.length
|
|
513
|
+
if (!tags.length)
|
|
436
514
|
return;
|
|
437
|
-
}
|
|
438
515
|
let cache;
|
|
439
516
|
let queue;
|
|
440
517
|
try {
|
|
@@ -445,9 +522,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
445
522
|
}
|
|
446
523
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
447
524
|
await queue.dispatch(job, { tags });
|
|
448
|
-
});
|
|
525
|
+
}));
|
|
449
526
|
}
|
|
450
527
|
}
|
|
528
|
+
registration.resume();
|
|
529
|
+
return registration.dispose;
|
|
451
530
|
}
|
|
452
531
|
|
|
453
532
|
// ../../src/bootstrap/providers/listeners.ts
|
package/dist/index.js
CHANGED
|
@@ -436,22 +436,22 @@ function readSharedRouteRegistry() {
|
|
|
436
436
|
var routeRegistry = readSharedRouteRegistry();
|
|
437
437
|
|
|
438
438
|
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
439
|
-
function registerOpenApiRoute(method, path, middleware) {
|
|
440
|
-
routeRegistry.register({ method, path, middleware });
|
|
439
|
+
function registerOpenApiRoute(method, path, middleware, openApi) {
|
|
440
|
+
routeRegistry.register({ method, path, middleware, ...openApi ? { openApi } : {} });
|
|
441
441
|
}
|
|
442
|
-
function registerOpenApiRouteMap(routes, middleware) {
|
|
442
|
+
function registerOpenApiRouteMap(routes, middleware, metadata = {}) {
|
|
443
443
|
const registered = {};
|
|
444
444
|
for (const [path, handler] of Object.entries(routes)) {
|
|
445
445
|
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
446
446
|
const methodMap = handler;
|
|
447
447
|
registered[path] = methodMap;
|
|
448
448
|
for (const method of Object.keys(methodMap)) {
|
|
449
|
-
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
449
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware, metadata[path]?.[method.toUpperCase()]);
|
|
450
450
|
}
|
|
451
451
|
continue;
|
|
452
452
|
}
|
|
453
453
|
registered[path] = handler;
|
|
454
|
-
registerOpenApiRoute("GET", path, middleware);
|
|
454
|
+
registerOpenApiRoute("GET", path, middleware, metadata[path]?.GET);
|
|
455
455
|
}
|
|
456
456
|
return registered;
|
|
457
457
|
}
|
|
@@ -470,14 +470,29 @@ function buildModuleRoutes(dependencies, options = {}) {
|
|
|
470
470
|
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
471
471
|
const cachedJson = createCachedJson(dependencies);
|
|
472
472
|
const moduleRoutes = {};
|
|
473
|
+
const metadata = {};
|
|
473
474
|
for (const module of modules) {
|
|
474
475
|
if (!module.routes) {
|
|
475
476
|
continue;
|
|
476
477
|
}
|
|
477
|
-
|
|
478
|
+
const routes = module.routes({ dependencies, cachedJson, kernel });
|
|
479
|
+
for (const [path, operations] of Object.entries(module.openApi ?? {})) {
|
|
480
|
+
if (!(path in routes))
|
|
481
|
+
throw new Error(`OpenAPI metadata has no API route: ${module.name} ${path}`);
|
|
482
|
+
const methods = typeof routes[path] === "function" ? ["GET"] : Object.keys(routes[path]).map((method) => method.toUpperCase());
|
|
483
|
+
for (const method of Object.keys(operations))
|
|
484
|
+
if (!methods.includes(method))
|
|
485
|
+
throw new Error(`OpenAPI metadata has no API handler: ${method} ${path}`);
|
|
486
|
+
}
|
|
487
|
+
for (const path of Object.keys(routes)) {
|
|
488
|
+
delete metadata[path];
|
|
489
|
+
if (module.openApi?.[path])
|
|
490
|
+
metadata[path] = module.openApi[path];
|
|
491
|
+
}
|
|
492
|
+
Object.assign(moduleRoutes, routes);
|
|
478
493
|
}
|
|
479
494
|
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
480
|
-
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
495
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"], prefixRouteMap(apiPrefix, metadata)), middleware);
|
|
481
496
|
}
|
|
482
497
|
// ../../src/bootstrap/buildWebModuleRoutes.ts
|
|
483
498
|
import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/core/http/middleware";
|
|
@@ -689,6 +704,9 @@ var appEnvSchema = defineEnvSchema({
|
|
|
689
704
|
API_PREFIX: {
|
|
690
705
|
default: "/api/v1"
|
|
691
706
|
},
|
|
707
|
+
CORS_ADDITIONAL_ALLOWED_HEADERS: {
|
|
708
|
+
default: ""
|
|
709
|
+
},
|
|
692
710
|
CORS_ALLOWED_ORIGINS: {
|
|
693
711
|
default: "*"
|
|
694
712
|
},
|
|
@@ -823,14 +841,73 @@ import { eventBus as eventBus2, modelEventName } from "@getstrata/core/events";
|
|
|
823
841
|
import { InvalidateCacheTagsJob } from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
824
842
|
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
825
843
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
844
|
+
var CACHE_EVENT = "strata.cache.invalidate-tags";
|
|
845
|
+
var STATE_KEY = Symbol.for("@getstrata/modelCacheInvalidationRegistrations");
|
|
846
|
+
function registrations() {
|
|
847
|
+
const global = globalThis;
|
|
848
|
+
global[STATE_KEY] ??= new WeakMap;
|
|
849
|
+
return global[STATE_KEY];
|
|
850
|
+
}
|
|
851
|
+
function requireCacheTags(value) {
|
|
852
|
+
if (!Array.isArray(value) || !value.length || value.length > 128 || !value.every((tag) => typeof tag === "string" && tag.length > 0 && tag.length <= 256))
|
|
853
|
+
throw new Error("Invalid durable cache invalidation payload.");
|
|
854
|
+
return value;
|
|
855
|
+
}
|
|
856
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2, options = {}) {
|
|
857
|
+
if (options.outbox && !options.outbox.hasListener("strata.cache.invalidate-tags.v1", CACHE_EVENT))
|
|
858
|
+
throw new Error("The application outbox must include createModelCacheInvalidationListener().");
|
|
859
|
+
const writes = discoverModelTableNames().flatMap((tableName) => MODEL_WRITE_ACTIONS.map((action) => ({
|
|
860
|
+
tableName,
|
|
861
|
+
action,
|
|
862
|
+
tags: cacheTagsForModelWrite(tableName, action)
|
|
863
|
+
})));
|
|
864
|
+
if (options.outbox) {
|
|
865
|
+
for (const write of writes)
|
|
866
|
+
if (write.tags.length)
|
|
867
|
+
requireCacheTags(write.tags);
|
|
868
|
+
}
|
|
869
|
+
const state = registrations();
|
|
870
|
+
const previous = state.get(bus);
|
|
871
|
+
if (previous?.durable && !options.outbox)
|
|
872
|
+
return () => {};
|
|
873
|
+
previous?.suspend();
|
|
874
|
+
const unsubscribe = [];
|
|
875
|
+
const subscribe = [];
|
|
876
|
+
const registration = {
|
|
877
|
+
durable: !!options.outbox,
|
|
878
|
+
suspend() {
|
|
879
|
+
for (const stop of unsubscribe.splice(0))
|
|
880
|
+
stop();
|
|
881
|
+
},
|
|
882
|
+
resume() {
|
|
883
|
+
for (const start of subscribe)
|
|
884
|
+
unsubscribe.push(start());
|
|
885
|
+
state.set(bus, registration);
|
|
886
|
+
},
|
|
887
|
+
dispose() {
|
|
888
|
+
if (state.get(bus) !== registration)
|
|
889
|
+
return;
|
|
890
|
+
registration.suspend();
|
|
891
|
+
if (previous)
|
|
892
|
+
previous.resume();
|
|
893
|
+
else
|
|
894
|
+
state.delete(bus);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
for (const { tableName, action, tags: durableTags } of writes) {
|
|
898
|
+
const event = modelEventName(tableName, action);
|
|
899
|
+
if (options.outbox) {
|
|
900
|
+
if (!durableTags.length)
|
|
901
|
+
continue;
|
|
902
|
+
const outbox = options.outbox;
|
|
903
|
+
subscribe.push(() => bus.listenTransactional(event, async () => {
|
|
904
|
+
await outbox.publish(CACHE_EVENT, { tags: durableTags });
|
|
905
|
+
}));
|
|
906
|
+
} else {
|
|
907
|
+
subscribe.push(() => bus.listen(event, async () => {
|
|
830
908
|
const tags = cacheTagsForModelWrite(tableName, action);
|
|
831
|
-
if (tags.length
|
|
909
|
+
if (!tags.length)
|
|
832
910
|
return;
|
|
833
|
-
}
|
|
834
911
|
let cache;
|
|
835
912
|
let queue;
|
|
836
913
|
try {
|
|
@@ -841,9 +918,11 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
|
841
918
|
}
|
|
842
919
|
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
843
920
|
await queue.dispatch(job, { tags });
|
|
844
|
-
});
|
|
921
|
+
}));
|
|
845
922
|
}
|
|
846
923
|
}
|
|
924
|
+
registration.resume();
|
|
925
|
+
return registration.dispose;
|
|
847
926
|
}
|
|
848
927
|
|
|
849
928
|
// ../../src/bootstrap/providers/listeners.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/bootstrap",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "Strata application bootstrap: HttpKernel, DI, web session helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"access": "public"
|
|
186
186
|
},
|
|
187
187
|
"peerDependencies": {
|
|
188
|
-
"@getstrata/core": "^2.0.
|
|
188
|
+
"@getstrata/core": "^2.0.5",
|
|
189
189
|
"typescript": "^5.9.0 || ^6.0.0 || ^7.0.0"
|
|
190
190
|
},
|
|
191
191
|
"engines": {
|