@openora/core 0.4.1-canary.21 → 0.4.1-canary.25
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/dist/.tsbuildinfo +1 -1
- package/dist/casino/lobby/__tests__/lobby.cache.test.js +1 -1
- package/dist/casino/lobby/__tests__/lobby.cache.test.js.map +1 -1
- package/dist/cms/__tests__/cms.reads.test.d.ts +2 -0
- package/dist/cms/__tests__/cms.reads.test.d.ts.map +1 -0
- package/dist/cms/__tests__/{cms.cache.test.js → cms.reads.test.js} +2 -2
- package/dist/cms/__tests__/cms.reads.test.js.map +1 -0
- package/dist/cms/__tests__/cms.router.test.js +25 -22
- package/dist/cms/__tests__/cms.router.test.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.test.js +1 -1
- package/dist/engagement/chat/__tests__/chat.service.test.js.map +1 -1
- package/dist/iam/__tests__/iam.service.test.js +4 -3
- package/dist/iam/__tests__/iam.service.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.rate-limit.test.js +4 -3
- package/dist/pam/identity/__tests__/identity.rate-limit.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.service.test.js +6 -1
- package/dist/pam/identity/__tests__/identity.service.test.js.map +1 -1
- package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.js +1 -0
- package/dist/pam/identity/service/identity.service.js.map +1 -1
- package/dist/server/kernel/__tests__/cache.test.js +11 -57
- package/dist/server/kernel/__tests__/cache.test.js.map +1 -1
- package/dist/server/kernel/__tests__/event-bus.test.js +14 -1
- package/dist/server/kernel/__tests__/event-bus.test.js.map +1 -1
- package/dist/server/kernel/__tests__/rate-limiter.test.js +3 -45
- package/dist/server/kernel/__tests__/rate-limiter.test.js.map +1 -1
- package/dist/server/kernel/__tests__/redis-streams-broker.test.d.ts +2 -0
- package/dist/server/kernel/__tests__/redis-streams-broker.test.d.ts.map +1 -0
- package/dist/server/kernel/__tests__/redis-streams-broker.test.js +293 -0
- package/dist/server/kernel/__tests__/redis-streams-broker.test.js.map +1 -0
- package/dist/server/kernel/cache.d.ts +0 -12
- package/dist/server/kernel/cache.d.ts.map +1 -1
- package/dist/server/kernel/cache.js +0 -55
- package/dist/server/kernel/cache.js.map +1 -1
- package/dist/server/kernel/event-bus.d.ts +1 -16
- package/dist/server/kernel/event-bus.d.ts.map +1 -1
- package/dist/server/kernel/event-bus.js +3 -32
- package/dist/server/kernel/event-bus.js.map +1 -1
- package/dist/server/kernel/index.d.ts +4 -6
- package/dist/server/kernel/index.d.ts.map +1 -1
- package/dist/server/kernel/index.js +4 -6
- package/dist/server/kernel/index.js.map +1 -1
- package/dist/server/kernel/rate-limiter.d.ts +1 -10
- package/dist/server/kernel/rate-limiter.d.ts.map +1 -1
- package/dist/server/kernel/rate-limiter.js +0 -38
- package/dist/server/kernel/rate-limiter.js.map +1 -1
- package/dist/server/kernel/redis-streams-broker.d.ts +65 -0
- package/dist/server/kernel/redis-streams-broker.d.ts.map +1 -0
- package/dist/server/kernel/redis-streams-broker.js +208 -0
- package/dist/server/kernel/redis-streams-broker.js.map +1 -0
- package/dist/server/runtime/__tests__/create-app.test.d.ts +2 -0
- package/dist/server/runtime/__tests__/create-app.test.d.ts.map +1 -0
- package/dist/server/runtime/__tests__/create-app.test.js +60 -0
- package/dist/server/runtime/__tests__/create-app.test.js.map +1 -0
- package/dist/server/runtime/assert-durable-seams.d.ts +18 -0
- package/dist/server/runtime/assert-durable-seams.d.ts.map +1 -0
- package/dist/server/runtime/assert-durable-seams.js +32 -0
- package/dist/server/runtime/assert-durable-seams.js.map +1 -0
- package/dist/server/runtime/create-app.d.ts +15 -9
- package/dist/server/runtime/create-app.d.ts.map +1 -1
- package/dist/server/runtime/create-app.js +61 -49
- package/dist/server/runtime/create-app.js.map +1 -1
- package/dist/testing/fakes/__tests__/cache.test.d.ts +2 -0
- package/dist/testing/fakes/__tests__/cache.test.d.ts.map +1 -0
- package/dist/testing/fakes/__tests__/cache.test.js +89 -0
- package/dist/testing/fakes/__tests__/cache.test.js.map +1 -0
- package/dist/testing/fakes/__tests__/job-queue.test.d.ts.map +1 -0
- package/dist/{server/kernel → testing/fakes}/__tests__/job-queue.test.js +1 -1
- package/dist/testing/fakes/__tests__/job-queue.test.js.map +1 -0
- package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts +2 -0
- package/dist/testing/fakes/__tests__/rate-limiter.test.d.ts.map +1 -0
- package/dist/testing/fakes/__tests__/rate-limiter.test.js +83 -0
- package/dist/testing/fakes/__tests__/rate-limiter.test.js.map +1 -0
- package/dist/testing/fakes/__tests__/realtime-authorizer.test.d.ts.map +1 -0
- package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.js.map +1 -1
- package/dist/testing/fakes/__tests__/realtime-transport.test.d.ts.map +1 -0
- package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.js.map +1 -1
- package/dist/testing/fakes/cache.d.ts +14 -0
- package/dist/testing/fakes/cache.d.ts.map +1 -0
- package/dist/testing/fakes/cache.js +56 -0
- package/dist/testing/fakes/cache.js.map +1 -0
- package/dist/testing/fakes/event-bus.d.ts +17 -0
- package/dist/testing/fakes/event-bus.d.ts.map +1 -0
- package/dist/testing/fakes/event-bus.js +30 -0
- package/dist/testing/fakes/event-bus.js.map +1 -0
- package/dist/{server/kernel → testing/fakes}/job-queue.d.ts +9 -9
- package/dist/{server/kernel → testing/fakes}/job-queue.d.ts.map +1 -1
- package/dist/{server/kernel → testing/fakes}/job-queue.js +9 -9
- package/dist/{server/kernel → testing/fakes}/job-queue.js.map +1 -1
- package/dist/testing/fakes/rate-limiter.d.ts +11 -0
- package/dist/testing/fakes/rate-limiter.d.ts.map +1 -0
- package/dist/testing/fakes/rate-limiter.js +39 -0
- package/dist/testing/fakes/rate-limiter.js.map +1 -0
- package/dist/{server/kernel → testing/fakes}/realtime-authorizer.d.ts.map +1 -1
- package/dist/{server/kernel → testing/fakes}/realtime-authorizer.js.map +1 -1
- package/dist/{server/kernel → testing/fakes}/realtime-transport.d.ts.map +1 -1
- package/dist/{server/kernel → testing/fakes}/realtime-transport.js +3 -1
- package/dist/testing/fakes/realtime-transport.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/wallet/__tests__/wallet.rate-limit.test.js +1 -1
- package/dist/wallet/__tests__/wallet.rate-limit.test.js.map +1 -1
- package/package.json +6 -1
- package/turbo-generators/src/templates/module/agents.hbs +16 -57
- package/dist/cms/__tests__/cms.cache.test.d.ts +0 -2
- package/dist/cms/__tests__/cms.cache.test.d.ts.map +0 -1
- package/dist/cms/__tests__/cms.cache.test.js.map +0 -1
- package/dist/server/kernel/__tests__/job-queue.test.d.ts.map +0 -1
- package/dist/server/kernel/__tests__/job-queue.test.js.map +0 -1
- package/dist/server/kernel/__tests__/realtime-authorizer.test.d.ts.map +0 -1
- package/dist/server/kernel/__tests__/realtime-transport.test.d.ts.map +0 -1
- package/dist/server/kernel/realtime-transport.js.map +0 -1
- /package/dist/{server/kernel → testing/fakes}/__tests__/job-queue.test.d.ts +0 -0
- /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.d.ts +0 -0
- /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-authorizer.test.js +0 -0
- /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.d.ts +0 -0
- /package/dist/{server/kernel → testing/fakes}/__tests__/realtime-transport.test.js +0 -0
- /package/dist/{server/kernel → testing/fakes}/realtime-authorizer.d.ts +0 -0
- /package/dist/{server/kernel → testing/fakes}/realtime-authorizer.js +0 -0
- /package/dist/{server/kernel → testing/fakes}/realtime-transport.d.ts +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createLogger } from './logger.js';
|
|
3
|
+
const STREAM_PREFIX = 'oss:evt:';
|
|
4
|
+
const STREAM_MAXLEN = 10_000;
|
|
5
|
+
const READ_COUNT = 10;
|
|
6
|
+
const BLOCK_MS = 5_000;
|
|
7
|
+
const CLAIM_INTERVAL_MS = 30_000;
|
|
8
|
+
const CLAIM_MIN_IDLE_MS = 60_000;
|
|
9
|
+
const RETRY_DELAY_MS = 2_000;
|
|
10
|
+
function delay(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function streamKey(topic) {
|
|
14
|
+
return STREAM_PREFIX + topic;
|
|
15
|
+
}
|
|
16
|
+
function isBusyGroupError(err) {
|
|
17
|
+
return err instanceof Error && err.message.includes('BUSYGROUP');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Durable `MESSAGE_BROKER` reference driver on Redis Streams, auto-bound by
|
|
21
|
+
* `createApp` when `REDIS_URL` is set (ADR-0030 - production is distributed-only,
|
|
22
|
+
* core ships no in-process default).
|
|
23
|
+
*
|
|
24
|
+
* Every `subscribe(topic, handler)` call for the same (topic, group) - default
|
|
25
|
+
* group is the deployment's `serviceName` - coalesces into ONE Redis consumer-group
|
|
26
|
+
* loop that fans out to every registered local handler, then XACKs once: an event
|
|
27
|
+
* is delivered to exactly one replica of the service (competing consumers across
|
|
28
|
+
* replicas via the shared group), and that replica runs every local handler for
|
|
29
|
+
* the topic (matches in-process fan-out semantics, but once per cluster, not once
|
|
30
|
+
* per replica). A distinct `SERVICE_NAME` per split service gives each service its
|
|
31
|
+
* own copy of every event; `createApp` requires one as soon as `SERVICE_MANIFEST`
|
|
32
|
+
* is set, since the group name is a durable identity and can't be derived from a
|
|
33
|
+
* module list that reorders and grows.
|
|
34
|
+
*
|
|
35
|
+
* Blocking `XREADGROUP` runs on a `client.duplicate()`d connection per loop, never
|
|
36
|
+
* on the shared client - `publish` (`XADD`, non-blocking) uses the shared client,
|
|
37
|
+
* so a blocked read never stalls the cache/rate-limiter commands sharing it.
|
|
38
|
+
* `publish` awaits the XADD and lets errors propagate (the `OutboxRelay` marks a
|
|
39
|
+
* row published only after this resolves - it must be reliable, not a silent
|
|
40
|
+
* no-op). Handler errors are guarded + logged, then the entry is XACKed regardless
|
|
41
|
+
* (matches `EventBus.on` - no per-handler retry; must-not-lose events use the
|
|
42
|
+
* outbox). A crash before XACK leaves the entry pending; `XAUTOCLAIM` periodically
|
|
43
|
+
* reclaims idle-pending entries so at-least-once delivery survives a replica crash.
|
|
44
|
+
*
|
|
45
|
+
* At-least-once holds from group creation onward, NOT before it. A group is created
|
|
46
|
+
* at `startId` (default `$`) on first subscribe and persists in Redis from then on,
|
|
47
|
+
* so restarts and outages are covered: the group keeps its cursor, undelivered
|
|
48
|
+
* entries wait in the stream, and pending ones are reclaimed. The one uncovered
|
|
49
|
+
* window is a group's FIRST-EVER creation - a service extracted after events were
|
|
50
|
+
* already flowing starts from `$` and never sees the backlog. `startId: '0'` closes
|
|
51
|
+
* that window by replaying everything still retained (up to STREAM_MAXLEN), at the
|
|
52
|
+
* cost of re-running every handler for those events: safe on a fresh deploy (empty
|
|
53
|
+
* streams), a duplicate-side-effect hazard for a late-added service, since core has
|
|
54
|
+
* no dedup layer - `EventBus.on` invokes handlers directly, and `eventId` is a key
|
|
55
|
+
* consumers must dedup on themselves.
|
|
56
|
+
*
|
|
57
|
+
* `orderingKey` is NOT honoured - Redis Streams have no partitioning, so per-key
|
|
58
|
+
* ordering across concurrent consumers isn't guaranteed (same footnote as
|
|
59
|
+
* `BullMqJobQueue`'s `orderingKey`).
|
|
60
|
+
*/
|
|
61
|
+
export class RedisStreamsBroker {
|
|
62
|
+
client;
|
|
63
|
+
logger = createLogger('message-broker');
|
|
64
|
+
serviceName;
|
|
65
|
+
startId;
|
|
66
|
+
consumerId = randomUUID();
|
|
67
|
+
loops = new Map();
|
|
68
|
+
constructor(client, opts) {
|
|
69
|
+
this.client = client;
|
|
70
|
+
this.serviceName = opts.serviceName;
|
|
71
|
+
this.startId = opts.startId ?? '$';
|
|
72
|
+
}
|
|
73
|
+
async publish(envelope) {
|
|
74
|
+
await this.client.xAdd(streamKey(envelope.topic), '*', { data: JSON.stringify(envelope) }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: STREAM_MAXLEN } });
|
|
75
|
+
}
|
|
76
|
+
subscribe(topic, handler, options) {
|
|
77
|
+
const group = options?.consumerGroup ?? this.serviceName;
|
|
78
|
+
const key = streamKey(topic);
|
|
79
|
+
const loopKey = `${key}::${group}`;
|
|
80
|
+
let loop = this.loops.get(loopKey);
|
|
81
|
+
if (!loop) {
|
|
82
|
+
loop = this.startLoop(key, group);
|
|
83
|
+
this.loops.set(loopKey, loop);
|
|
84
|
+
}
|
|
85
|
+
loop.handlers.add(handler);
|
|
86
|
+
return () => {
|
|
87
|
+
const current = this.loops.get(loopKey);
|
|
88
|
+
if (!current || !current.handlers.delete(handler)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (current.handlers.size === 0) {
|
|
92
|
+
this.loops.delete(loopKey);
|
|
93
|
+
current.stop();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async close() {
|
|
98
|
+
const loops = [...this.loops.values()];
|
|
99
|
+
this.loops.clear();
|
|
100
|
+
for (const loop of loops) {
|
|
101
|
+
loop.stop();
|
|
102
|
+
}
|
|
103
|
+
await Promise.allSettled(loops.map((loop) => loop.done));
|
|
104
|
+
}
|
|
105
|
+
startLoop(key, group) {
|
|
106
|
+
const handlers = new Set();
|
|
107
|
+
const reader = this.client.duplicate();
|
|
108
|
+
reader.on('error', (err) => this.logger.error({ err, key, group }, 'stream reader connection error'));
|
|
109
|
+
let stopped = false;
|
|
110
|
+
let lastClaimAt = 0;
|
|
111
|
+
const stop = () => {
|
|
112
|
+
stopped = true;
|
|
113
|
+
// Rejects the in-flight blocking XREADGROUP (if any) immediately, instead of
|
|
114
|
+
// waiting up to BLOCK_MS for it to time out on its own.
|
|
115
|
+
reader.destroy();
|
|
116
|
+
};
|
|
117
|
+
// `ready` gates connecting + group creation, and stays false until BOTH have
|
|
118
|
+
// succeeded - so a Redis outage at loop start retries the whole handshake with
|
|
119
|
+
// backoff rather than permanently abandoning this (topic, group)'s consumption.
|
|
120
|
+
// It is NOT reset on a later error: node-redis owns reconnection from there on
|
|
121
|
+
// (socket.reconnectStrategy re-establishes the connection and re-queues the
|
|
122
|
+
// blocked XREADGROUP), and the group already exists, so there is nothing to redo.
|
|
123
|
+
// Both steps are idempotent regardless - `connect()` throws 'Socket already
|
|
124
|
+
// opened' once the socket is open, hence the isOpen guard, and a re-created group
|
|
125
|
+
// comes back BUSYGROUP, which is tolerated below.
|
|
126
|
+
const done = (async () => {
|
|
127
|
+
let ready = false;
|
|
128
|
+
while (!stopped) {
|
|
129
|
+
try {
|
|
130
|
+
if (!ready) {
|
|
131
|
+
if (!reader.isOpen) {
|
|
132
|
+
await reader.connect();
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await reader.xGroupCreate(key, group, this.startId, { MKSTREAM: true });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (!isBusyGroupError(err)) {
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ready = true;
|
|
143
|
+
}
|
|
144
|
+
if (Date.now() - lastClaimAt > CLAIM_INTERVAL_MS) {
|
|
145
|
+
lastClaimAt = Date.now();
|
|
146
|
+
await this.reclaimPending(reader, key, group, handlers);
|
|
147
|
+
}
|
|
148
|
+
const reply = (await reader.xReadGroup(group, this.consumerId, { key, id: '>' }, { COUNT: READ_COUNT, BLOCK: BLOCK_MS }));
|
|
149
|
+
if (!reply) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const stream of reply) {
|
|
153
|
+
for (const message of stream.messages) {
|
|
154
|
+
await this.dispatch(reader, key, group, message, handlers);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (stopped) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
this.logger.error({ err, key, group }, 'stream loop error, retrying');
|
|
163
|
+
await delay(RETRY_DELAY_MS);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
return { handlers, stop, done };
|
|
168
|
+
}
|
|
169
|
+
async reclaimPending(reader, key, group, handlers) {
|
|
170
|
+
try {
|
|
171
|
+
const claimed = (await reader.xAutoClaim(key, group, this.consumerId, CLAIM_MIN_IDLE_MS, '0-0', { COUNT: READ_COUNT }));
|
|
172
|
+
for (const message of claimed.messages) {
|
|
173
|
+
if (message) {
|
|
174
|
+
await this.dispatch(reader, key, group, message, handlers);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
this.logger.error({ err, key, group }, 'stream reclaim failed');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async dispatch(reader, key, group, message, handlers) {
|
|
183
|
+
const raw = message.message['data'];
|
|
184
|
+
if (raw === undefined) {
|
|
185
|
+
await reader.xAck(key, group, message.id);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let envelope;
|
|
189
|
+
try {
|
|
190
|
+
envelope = JSON.parse(raw);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
this.logger.error({ err, key, group, id: message.id }, 'stream envelope parse failed');
|
|
194
|
+
await reader.xAck(key, group, message.id);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const fn of handlers) {
|
|
198
|
+
try {
|
|
199
|
+
await fn(envelope);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
this.logger.error({ err, topic: envelope.topic }, 'message broker handler threw');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await reader.xAck(key, group, message.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=redis-streams-broker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-streams-broker.js","sourceRoot":"","sources":["../../../src/server/kernel/redis-streams-broker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-app.test.d.ts","sourceRoot":"","sources":["../../../../src/server/runtime/__tests__/create-app.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { MESSAGE_BROKER, JOB_QUEUE, CACHE, RATE_LIMITER } from '@openora/core/contracts';
|
|
3
|
+
import { InMemoryBroker, InProcessJobQueue, InProcessCache, InProcessRateLimiter, } from '@openora/core/testing';
|
|
4
|
+
import { createLogger } from '../../kernel/index.js';
|
|
5
|
+
import { createApp } from '../create-app.js';
|
|
6
|
+
// A syntactically valid but unreachable DB url - fine here because nothing in this
|
|
7
|
+
// suite ever runs a query: DrizzleService's pg.Pool connects lazily, and neither
|
|
8
|
+
// health.ping nor better-auth's getSession() touch the DB when the request carries
|
|
9
|
+
// no session cookie (getSessionFromCtx short-circuits on a missing cookie).
|
|
10
|
+
const DUMMY_DATABASE_URL = 'postgres://test:test@127.0.0.1:1/create_app_test';
|
|
11
|
+
describe('createApp - distributed-only durable seams (ADR-0030)', () => {
|
|
12
|
+
it('throws a clear, actionable error when no durable seam is bound', async () => {
|
|
13
|
+
await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } })).rejects.toThrow(/MESSAGE_BROKER.*JOB_QUEUE.*CACHE.*RATE_LIMITER/s);
|
|
14
|
+
});
|
|
15
|
+
it('boots and serves once the fakes are bound via configure', async () => {
|
|
16
|
+
const created = await createApp({
|
|
17
|
+
plugins: [],
|
|
18
|
+
databaseUrl: DUMMY_DATABASE_URL,
|
|
19
|
+
openapi: { enabled: false },
|
|
20
|
+
configure(container) {
|
|
21
|
+
container.register(MESSAGE_BROKER, () => new InMemoryBroker());
|
|
22
|
+
container.register(JOB_QUEUE, () => new InProcessJobQueue(createLogger('job-queue')));
|
|
23
|
+
container.register(CACHE, () => new InProcessCache());
|
|
24
|
+
container.register(RATE_LIMITER, () => new InProcessRateLimiter());
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const res = await created.app.request('/health');
|
|
28
|
+
expect(res.status).toBe(200);
|
|
29
|
+
expect(await res.json()).toMatchObject({ status: 'ok' });
|
|
30
|
+
await created.close();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('createApp - service name for the Redis Streams consumer group', () => {
|
|
34
|
+
const saved = { redis: process.env['REDIS_URL'], manifest: process.env['SERVICE_MANIFEST'] };
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
// Restore rather than delete: a bare delete would strip a REDIS_URL the rest of
|
|
37
|
+
// the suite (or a CI service container) legitimately set.
|
|
38
|
+
for (const [key, value] of [
|
|
39
|
+
['REDIS_URL', saved.redis],
|
|
40
|
+
['SERVICE_MANIFEST', saved.manifest],
|
|
41
|
+
]) {
|
|
42
|
+
if (value === undefined) {
|
|
43
|
+
delete process.env[key];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.env[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it('refuses to boot a split service that has no SERVICE_NAME of its own', async () => {
|
|
51
|
+
// The consumer group is a durable identity, so it can never be derived from
|
|
52
|
+
// SERVICE_MANIFEST - that is a list of module ids ('wallet,iam') which reorders
|
|
53
|
+
// and grows. Throws before any Redis client is opened.
|
|
54
|
+
process.env['REDIS_URL'] = 'redis://127.0.0.1:1';
|
|
55
|
+
process.env['SERVICE_MANIFEST'] = 'wallet,iam';
|
|
56
|
+
delete process.env['SERVICE_NAME'];
|
|
57
|
+
await expect(createApp({ plugins: [], databaseUrl: DUMMY_DATABASE_URL, openapi: { enabled: false } })).rejects.toThrow(/SERVICE_MANIFEST is set but SERVICE_NAME is not/);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=create-app.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-app.test.js","sourceRoot":"","sources":["../../../../src/server/runtime/__tests__/create-app.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AnyToken } from '@openora/core/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
|
|
4
|
+
* the in-process seam impls no longer ship on the production path, so every
|
|
5
|
+
* always-needed seam must resolve to a durable binding before `createApp` finishes
|
|
6
|
+
* booting. Call this AFTER `loadPlugins` + `configure` so a `REDIS_URL` auto-bind or
|
|
7
|
+
* an overlay's own registration has already won (Container is last-registration-wins).
|
|
8
|
+
* Modeled on `assertSealedServicesBound` (`compliance/assert.ts`) - same `has(token)`
|
|
9
|
+
* shape, same "list every gap, don't stop at the first" style.
|
|
10
|
+
*
|
|
11
|
+
* `REALTIME_TRANSPORT` is deliberately excluded: it stays lazy and throws on first
|
|
12
|
+
* use, since not every deployment serves realtime traffic.
|
|
13
|
+
*/
|
|
14
|
+
export type DurableSeamContainerView = {
|
|
15
|
+
has(token: AnyToken<unknown>): boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function assertDurableSeamsBound(container: DurableSeamContainerView): void;
|
|
18
|
+
//# sourceMappingURL=assert-durable-seams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-durable-seams.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/assert-durable-seams.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACxC,CAAC;AA2BF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,wBAAwB,GAAG,IAAI,CASjF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MESSAGE_BROKER, JOB_QUEUE, CACHE, RATE_LIMITER, } from '@openora/core/contracts';
|
|
2
|
+
const REQUIRED_DURABLE_SEAMS = [
|
|
3
|
+
{
|
|
4
|
+
token: MESSAGE_BROKER,
|
|
5
|
+
name: 'MESSAGE_BROKER',
|
|
6
|
+
fix: 'set REDIS_URL to auto-bind RedisStreamsBroker, or bind a MessageBrokerAdapter overlay (eg RabbitMQ/Kafka) via ctx.provide(MESSAGE_BROKER, ...)',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
token: JOB_QUEUE,
|
|
10
|
+
name: 'JOB_QUEUE',
|
|
11
|
+
fix: 'set REDIS_URL to auto-bind BullMqJobQueue, or bind a JOB_QUEUE overlay directly',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
token: CACHE,
|
|
15
|
+
name: 'CACHE',
|
|
16
|
+
fix: 'set REDIS_URL to auto-bind RedisCache, or bind a CACHE overlay directly',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
token: RATE_LIMITER,
|
|
20
|
+
name: 'RATE_LIMITER',
|
|
21
|
+
fix: 'set REDIS_URL to auto-bind RedisRateLimiter, or bind a RATE_LIMITER overlay directly',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
export function assertDurableSeamsBound(container) {
|
|
25
|
+
const missing = REQUIRED_DURABLE_SEAMS.filter((seam) => !container.has(seam.token));
|
|
26
|
+
if (missing.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`[create-app] production is distributed-only - the following seams have no durable binding:\n` +
|
|
30
|
+
missing.map((seam) => ` - ${seam.name}: ${seam.fix}`).join('\n'));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=assert-durable-seams.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assert-durable-seams.js","sourceRoot":"","sources":["../../../src/server/runtime/assert-durable-seams.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,SAAS,EACT,KAAK,EACL,YAAY,GAEb,MAAM,yBAAyB,CAAC;AAoBjC,MAAM,sBAAsB,GAA+B;IACzD;QACE,KAAK,EAAE,cAAc;QACrB,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE,gJAAgJ;KACtJ;IACD;QACE,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,iFAAiF;KACvF;IACD;QACE,KAAK,EAAE,KAAK;QACZ,IAAI,EAAE,OAAO;QACb,GAAG,EAAE,yEAAyE;KAC/E;IACD;QACE,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,cAAc;QACpB,GAAG,EAAE,sFAAsF;KAC5F;CACF,CAAC;AAEF,MAAM,UAAU,uBAAuB,CAAC,SAAmC;IACzE,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACpF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,IAAI,KAAK,CACb,8FAA8F;QAC5F,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACpE,CAAC;AACJ,CAAC"}
|
|
@@ -38,15 +38,21 @@ export type CreatedApp = {
|
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
40
|
* The single composition root: wires the DI container, boots every plugin in
|
|
41
|
-
* `config.plugins`, and mounts the resulting oRPC router on a Hono app.
|
|
42
|
-
*
|
|
43
|
-
* `RATE_LIMITER`/`CACHE` to their
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
41
|
+
* `config.plugins`, and mounts the resulting oRPC router on a Hono app.
|
|
42
|
+
* Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
|
|
43
|
+
* `REDIS_URL` auto-binds `JOB_QUEUE`/`RATE_LIMITER`/`CACHE`/`MESSAGE_BROKER` to their
|
|
44
|
+
* Redis-backed drivers - the broker being the `RedisStreamsBroker` reference driver,
|
|
45
|
+
* which an overlay (RabbitMQ/Kafka) or a test's `configure` callback can replace.
|
|
46
|
+
* `assertDurableSeamsBound` runs right after
|
|
47
|
+
* plugins + `configure` and throws a clear, actionable error listing every
|
|
48
|
+
* always-needed seam that still has no binding - it never falls back to an
|
|
49
|
+
* in-process impl. `REALTIME_TRANSPORT` stays lazy (throws on first use) since not
|
|
50
|
+
* every deployment serves realtime traffic. `OUTBOX_ENABLED` (or
|
|
51
|
+
* `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay.
|
|
52
|
+
* `container.dispose()` (via `close()`) runs disposers in REVERSE registration
|
|
53
|
+
* order, so DRIZZLE is registered before JOB_QUEUE to guarantee workers drain
|
|
54
|
+
* before the DB closes. A router namespace registered by more than one plugin
|
|
55
|
+
* throws at boot, not at request time.
|
|
50
56
|
*/
|
|
51
57
|
export declare function createApp(config: CreateAppConfig): Promise<CreatedApp>;
|
|
52
58
|
//# sourceMappingURL=create-app.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-app.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/create-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,EACL,SAAS,
|
|
1
|
+
{"version":3,"file":"create-app.d.ts","sourceRoot":"","sources":["../../../src/server/runtime/create-app.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,EACL,SAAS,EAYV,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAWL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA8CxE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,WAAW,EAAE,CAAC;IAOvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAErC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,IAAI,CAAC,EAAE,OAAO,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAEjD,WAAW,CAAC,EAAE,MAAM,CAAC;IAKrB,QAAQ,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC;IAE/B,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,IAAI,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,OAAO,CAAC,EAAE,aAAa,CAAC;IAIxB,SAAS,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IAEjE,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,IAAI,CAAC;IACV,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAqDF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAgR5E"}
|
|
@@ -8,12 +8,13 @@ import { HTTPException } from 'hono/http-exception';
|
|
|
8
8
|
import { serve } from '@hono/node-server';
|
|
9
9
|
import { resolve } from 'node:path';
|
|
10
10
|
import { generateOpenApiSpec } from './openapi.js';
|
|
11
|
-
import { Container, BullMqJobQueue,
|
|
11
|
+
import { Container, BullMqJobQueue, RedisCache, RedisRateLimiter, RedisStreamsBroker, createEventBus, createLogger, createRedisClient, withRequestContext, setErrorReporter, EVENT_BUS, } from '../kernel/index.js';
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
|
-
import { MESSAGE_BROKER, JOB_QUEUE,
|
|
13
|
+
import { MESSAGE_BROKER, JOB_QUEUE, OUTBOX, ADMIN_PERMISSION_RESOLVER, RATE_LIMITER, CACHE, ERROR_TRACKING, composeContract, healthContract, IGAMING_CONFIG, PLATFORM_CONFIG, } from '@openora/core/contracts';
|
|
14
14
|
import { DrizzleService, DRIZZLE, DrizzleOutboxWriter, OutboxRelay } from '../db/index.js';
|
|
15
15
|
import { AdminGuard, ADMIN_GUARD, SessionResolver, AUTH_SESSION } from '../auth/index.js';
|
|
16
16
|
import { loadPlugins } from '../plugin-host/index.js';
|
|
17
|
+
import { assertDurableSeamsBound } from './assert-durable-seams.js';
|
|
17
18
|
import { loadPlatformConfig, resolvePlatformConfigPath } from '../kernel/platform-config-loader.js';
|
|
18
19
|
// Path prefixes safe to cache at the HTTP layer: public, non-personalized reads
|
|
19
20
|
// only (lobby feeds, public CMS content, the game catalogue). NOTHING
|
|
@@ -32,6 +33,26 @@ const PUBLIC_HTTP_CACHE_PATHS = [
|
|
|
32
33
|
];
|
|
33
34
|
const DEFAULT_HTTP_CACHE_MAX_AGE_SECONDS = 30;
|
|
34
35
|
const DEFAULT_HTTP_CACHE_STALE_WHILE_REVALIDATE_SECONDS = 60;
|
|
36
|
+
// Redis Streams consumer group name for MESSAGE_BROKER. Distinct deployments/services
|
|
37
|
+
// sharing one Redis MUST use distinct names so their subscribers don't compete for
|
|
38
|
+
// each other's events. The name is a durable identity: renaming it strands the old
|
|
39
|
+
// group's pending (delivered-but-unacked) entries and starts the new one at `$`, so
|
|
40
|
+
// it can never be derived from SERVICE_MANIFEST - that is a LIST of module ids
|
|
41
|
+
// (see service-manifest.ts) whose value reorders and grows as modules move.
|
|
42
|
+
// A split deployment must therefore name itself explicitly.
|
|
43
|
+
function resolveServiceName() {
|
|
44
|
+
const serviceName = process.env['SERVICE_NAME'];
|
|
45
|
+
if (serviceName) {
|
|
46
|
+
return serviceName;
|
|
47
|
+
}
|
|
48
|
+
if (process.env['SERVICE_MANIFEST']) {
|
|
49
|
+
throw new Error('[create-app] SERVICE_MANIFEST is set but SERVICE_NAME is not. A split service needs a stable ' +
|
|
50
|
+
'name of its own: it becomes the MESSAGE_BROKER consumer group, and every service sharing a ' +
|
|
51
|
+
'group competes for the same events instead of each receiving a copy. Set SERVICE_NAME to a ' +
|
|
52
|
+
'stable identifier for this deployment (eg SERVICE_NAME=wallet).');
|
|
53
|
+
}
|
|
54
|
+
return 'monolith';
|
|
55
|
+
}
|
|
35
56
|
function headersToRecord(headers) {
|
|
36
57
|
const out = {};
|
|
37
58
|
headers.forEach((value, key) => {
|
|
@@ -83,15 +104,21 @@ async function captureRawBody(req) {
|
|
|
83
104
|
}
|
|
84
105
|
/**
|
|
85
106
|
* The single composition root: wires the DI container, boots every plugin in
|
|
86
|
-
* `config.plugins`, and mounts the resulting oRPC router on a Hono app.
|
|
87
|
-
*
|
|
88
|
-
* `RATE_LIMITER`/`CACHE` to their
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
107
|
+
* `config.plugins`, and mounts the resulting oRPC router on a Hono app.
|
|
108
|
+
* Production is distributed-only (ADR-0030, superseding ADR-0010/0014/0016/0028):
|
|
109
|
+
* `REDIS_URL` auto-binds `JOB_QUEUE`/`RATE_LIMITER`/`CACHE`/`MESSAGE_BROKER` to their
|
|
110
|
+
* Redis-backed drivers - the broker being the `RedisStreamsBroker` reference driver,
|
|
111
|
+
* which an overlay (RabbitMQ/Kafka) or a test's `configure` callback can replace.
|
|
112
|
+
* `assertDurableSeamsBound` runs right after
|
|
113
|
+
* plugins + `configure` and throws a clear, actionable error listing every
|
|
114
|
+
* always-needed seam that still has no binding - it never falls back to an
|
|
115
|
+
* in-process impl. `REALTIME_TRANSPORT` stays lazy (throws on first use) since not
|
|
116
|
+
* every deployment serves realtime traffic. `OUTBOX_ENABLED` (or
|
|
117
|
+
* `AMQP_URL`/`RABBITMQ_URL`) enables the transactional outbox relay.
|
|
118
|
+
* `container.dispose()` (via `close()`) runs disposers in REVERSE registration
|
|
119
|
+
* order, so DRIZZLE is registered before JOB_QUEUE to guarantee workers drain
|
|
120
|
+
* before the DB closes. A router namespace registered by more than one plugin
|
|
121
|
+
* throws at boot, not at request time.
|
|
95
122
|
*/
|
|
96
123
|
export async function createApp(config) {
|
|
97
124
|
if (config.databaseUrl) {
|
|
@@ -103,35 +130,31 @@ export async function createApp(config) {
|
|
|
103
130
|
container.onDispose(() => svc.dispose());
|
|
104
131
|
return svc;
|
|
105
132
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// publish. In the default in-process monolith it stays off, so emit() keeps its
|
|
114
|
-
// synchronous fan-out and emitInTransaction() guides callers to enable it.
|
|
133
|
+
// A distributed deployment wants events that survive a crash between commit and
|
|
134
|
+
// publish. Off by default: emit() keeps its best-effort fan-out and
|
|
135
|
+
// emitInTransaction() throws a guiding error until this is enabled.
|
|
136
|
+
// AMQP_URL/RABBITMQ_URL only signal the INTENT to run a RabbitMQ broker overlay -
|
|
137
|
+
// core binds no AMQP driver, so they enable the outbox and nothing else. The relay
|
|
138
|
+
// publishes to whatever MESSAGE_BROKER resolves to (the Redis Streams driver below,
|
|
139
|
+
// unless an overlay replaced it); set OUTBOX_ENABLED directly when that is the aim.
|
|
115
140
|
const outboxEnabled = !!process.env['OUTBOX_ENABLED'] || !!process.env['AMQP_URL'] || !!process.env['RABBITMQ_URL'];
|
|
116
141
|
if (outboxEnabled) {
|
|
117
142
|
container.register(OUTBOX, () => new DrizzleOutboxWriter());
|
|
118
143
|
}
|
|
119
144
|
container.register(EVENT_BUS, (c) => createEventBus(c.get(MESSAGE_BROKER), createLogger('event-bus'), outboxEnabled ? c.get(OUTBOX) : undefined));
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// distributed throttling and cross-replica cache invalidation - mirrors the AMQP_URL
|
|
129
|
-
// outbox precedent above. Otherwise the process-local in-process defaults stay (zero
|
|
130
|
-
// deps for dev/test). Either way a consumer overlay can rebind any of these tokens
|
|
131
|
-
// (plugins load after). The JOB_QUEUE disposer drains in-flight jobs before the DB
|
|
132
|
-
// closes (DRIZZLE is resolved before JOB_QUEUE below, and disposers run in reverse).
|
|
145
|
+
// When REDIS_URL is set, JOB_QUEUE, the rate limiter, the cache and the message
|
|
146
|
+
// broker bind to the shipped Redis reference adapters: BullMQ-backed durable jobs
|
|
147
|
+
// (survive restarts, real cron), distributed throttling, cross-replica cache
|
|
148
|
+
// invalidation and cross-replica events over Redis Streams. There is no in-process
|
|
149
|
+
// fallback (ADR-0030) - a deployment without REDIS_URL must bind these itself (an
|
|
150
|
+
// overlay, or a test's `configure` callback), or `assertDurableSeamsBound` below
|
|
151
|
+
// throws. The JOB_QUEUE disposer drains in-flight jobs before the DB closes (DRIZZLE
|
|
152
|
+
// is resolved before JOB_QUEUE below, and disposers run in reverse).
|
|
133
153
|
const redisUrl = process.env['REDIS_URL'];
|
|
134
154
|
if (redisUrl) {
|
|
155
|
+
// Resolved eagerly, before any client is opened: a misconfigured service name is
|
|
156
|
+
// a boot error, not a surprise on whichever code path resolves the broker first.
|
|
157
|
+
const serviceName = resolveServiceName();
|
|
135
158
|
container.register(JOB_QUEUE, () => {
|
|
136
159
|
const q = new BullMqJobQueue(redisUrl);
|
|
137
160
|
container.onDispose(() => q.close());
|
|
@@ -141,22 +164,10 @@ export async function createApp(config) {
|
|
|
141
164
|
container.onDispose(() => redis.close());
|
|
142
165
|
container.register(RATE_LIMITER, () => new RedisRateLimiter(redis));
|
|
143
166
|
container.register(CACHE, () => new RedisCache(redis));
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
container.onDispose(() => q.close());
|
|
149
|
-
return q;
|
|
150
|
-
});
|
|
151
|
-
container.register(RATE_LIMITER, () => {
|
|
152
|
-
const limiter = new InProcessRateLimiter();
|
|
153
|
-
container.onDispose(() => limiter.close());
|
|
154
|
-
return limiter;
|
|
155
|
-
});
|
|
156
|
-
container.register(CACHE, () => {
|
|
157
|
-
const cache = new InProcessCache();
|
|
158
|
-
container.onDispose(() => cache.close());
|
|
159
|
-
return cache;
|
|
167
|
+
container.register(MESSAGE_BROKER, () => {
|
|
168
|
+
const broker = new RedisStreamsBroker(redis, { serviceName });
|
|
169
|
+
container.onDispose(() => broker.close());
|
|
170
|
+
return broker;
|
|
160
171
|
});
|
|
161
172
|
}
|
|
162
173
|
// One shared better-auth instance for both the per-request middleware and AdminGuard -
|
|
@@ -176,6 +187,7 @@ export async function createApp(config) {
|
|
|
176
187
|
container.register(PLATFORM_CONFIG, () => loadPlatformConfig(resolvePlatformConfigPath()));
|
|
177
188
|
const registry = await loadPlugins(config.plugins, container);
|
|
178
189
|
await config.configure?.(container);
|
|
190
|
+
assertDurableSeamsBound(container);
|
|
179
191
|
if (container.has(ERROR_TRACKING)) {
|
|
180
192
|
const tracker = container.get(ERROR_TRACKING);
|
|
181
193
|
setErrorReporter((error, context) => tracker.captureException(error, context));
|