bunsane 0.2.8 → 0.2.10
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/CLAUDE.md +26 -0
- package/core/App.ts +97 -0
- package/core/remote/CircuitBreaker.ts +115 -0
- package/core/remote/OutboxWorker.ts +176 -0
- package/core/remote/RemoteManager.ts +400 -0
- package/core/remote/RpcCaller.ts +310 -0
- package/core/remote/StreamConsumer.ts +535 -0
- package/core/remote/decorators.ts +121 -0
- package/core/remote/health.ts +139 -0
- package/core/remote/index.ts +37 -0
- package/core/remote/metrics.ts +99 -0
- package/core/remote/outboxSchema.ts +41 -0
- package/core/remote/types.ts +151 -0
- package/core/scheduler/DistributedLock.ts +309 -266
- package/docs/SCALABILITY_PLAN.md +3 -3
- package/package.json +1 -1
- package/query/FilterBuilder.ts +25 -0
- package/query/Query.ts +5 -1
- package/query/builders/JsonbArrayBuilder.ts +116 -0
- package/query/index.ts +28 -2
- package/tests/helpers/MockRedisClient.ts +113 -0
- package/tests/helpers/MockRedisStreamServer.ts +448 -0
- package/tests/integration/query/Query.exec.test.ts +67 -14
- package/tests/integration/query/Query.jsonbArray.test.ts +214 -0
- package/tests/integration/remote/dlq.test.ts +175 -0
- package/tests/integration/remote/event-dispatch.test.ts +114 -0
- package/tests/integration/remote/outbox.test.ts +130 -0
- package/tests/integration/remote/rpc.test.ts +177 -0
- package/tests/pglite-setup.ts +1 -0
- package/tests/unit/query/JsonbArrayBuilder.test.ts +178 -0
- package/tests/unit/remote/CircuitBreaker.test.ts +159 -0
- package/tests/unit/remote/RemoteError.test.ts +55 -0
- package/tests/unit/remote/decorators.test.ts +195 -0
- package/tests/unit/remote/metrics.test.ts +115 -0
- package/tests/unit/remote/mockRedisStreamServer.test.ts +104 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Communication: Health check
|
|
3
|
+
*
|
|
4
|
+
* Aggregates health signals from Redis, the consumer group PEL, the outbox
|
|
5
|
+
* table, the DLQ, and the circuit breaker. Exposed via `/health/remote`
|
|
6
|
+
* and callable directly through `RemoteManager.health()`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type Redis from "ioredis";
|
|
10
|
+
import type { SQL } from "bun";
|
|
11
|
+
import type { CircuitBreaker } from "./CircuitBreaker";
|
|
12
|
+
|
|
13
|
+
export interface RemoteHealthCheck {
|
|
14
|
+
healthy: boolean;
|
|
15
|
+
checks: {
|
|
16
|
+
redis: { ok: boolean; latencyMs?: number; error?: string };
|
|
17
|
+
consumer: {
|
|
18
|
+
streamKey: string;
|
|
19
|
+
pelCount?: number;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
dlq: { stream: string; length?: number; error?: string };
|
|
23
|
+
outbox?: { pendingCount?: number; error?: string };
|
|
24
|
+
circuitBreaker: {
|
|
25
|
+
state: string;
|
|
26
|
+
failures: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HealthInputs {
|
|
33
|
+
publisher: Redis | null;
|
|
34
|
+
consumerRedis: Redis | null;
|
|
35
|
+
streamKey: string;
|
|
36
|
+
consumerGroup: string;
|
|
37
|
+
dlqStream: string;
|
|
38
|
+
outboxEnabled: boolean;
|
|
39
|
+
db: SQL;
|
|
40
|
+
breaker: CircuitBreaker;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function collectRemoteHealth(
|
|
44
|
+
inputs: HealthInputs
|
|
45
|
+
): Promise<RemoteHealthCheck> {
|
|
46
|
+
const result: RemoteHealthCheck = {
|
|
47
|
+
healthy: true,
|
|
48
|
+
checks: {
|
|
49
|
+
redis: { ok: false },
|
|
50
|
+
consumer: { streamKey: inputs.streamKey },
|
|
51
|
+
dlq: { stream: inputs.dlqStream },
|
|
52
|
+
circuitBreaker: {
|
|
53
|
+
state: inputs.breaker.getState(),
|
|
54
|
+
failures: inputs.breaker.getStats().failures,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Redis ping via publisher connection
|
|
61
|
+
if (inputs.publisher) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
try {
|
|
64
|
+
await inputs.publisher.ping();
|
|
65
|
+
result.checks.redis = {
|
|
66
|
+
ok: true,
|
|
67
|
+
latencyMs: Date.now() - start,
|
|
68
|
+
};
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
result.checks.redis = {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: error?.message ?? String(error),
|
|
73
|
+
};
|
|
74
|
+
result.healthy = false;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
result.checks.redis = { ok: false, error: "publisher not started" };
|
|
78
|
+
result.healthy = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PEL count (pending entries in consumer group)
|
|
82
|
+
if (inputs.publisher) {
|
|
83
|
+
try {
|
|
84
|
+
const pending: any = await inputs.publisher.xpending(
|
|
85
|
+
inputs.streamKey,
|
|
86
|
+
inputs.consumerGroup
|
|
87
|
+
);
|
|
88
|
+
// XPENDING summary: [total, smallest-id, largest-id, consumers]
|
|
89
|
+
const count = Array.isArray(pending)
|
|
90
|
+
? (pending[0] as number) ?? 0
|
|
91
|
+
: 0;
|
|
92
|
+
result.checks.consumer.pelCount = count;
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
result.checks.consumer.error = error?.message ?? String(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// DLQ length
|
|
99
|
+
if (inputs.publisher) {
|
|
100
|
+
try {
|
|
101
|
+
const len = await inputs.publisher.xlen(inputs.dlqStream);
|
|
102
|
+
result.checks.dlq.length = len ?? 0;
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
const msg = error?.message ?? String(error);
|
|
105
|
+
// ERR no such key = stream hasn't been created yet, that's fine
|
|
106
|
+
if (String(msg).toLowerCase().includes("no such key")) {
|
|
107
|
+
result.checks.dlq.length = 0;
|
|
108
|
+
} else {
|
|
109
|
+
result.checks.dlq.error = msg;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Outbox pending count
|
|
115
|
+
if (inputs.outboxEnabled) {
|
|
116
|
+
try {
|
|
117
|
+
const db = inputs.db as any;
|
|
118
|
+
const rows = await db`
|
|
119
|
+
SELECT COUNT(*)::int AS pending
|
|
120
|
+
FROM remote_outbox
|
|
121
|
+
WHERE published_at IS NULL
|
|
122
|
+
`;
|
|
123
|
+
result.checks.outbox = {
|
|
124
|
+
pendingCount: rows?.[0]?.pending ?? 0,
|
|
125
|
+
};
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
result.checks.outbox = {
|
|
128
|
+
error: error?.message ?? String(error),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Circuit breaker open -> degrade health
|
|
134
|
+
if (result.checks.circuitBreaker.state === "open") {
|
|
135
|
+
result.healthy = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export {
|
|
2
|
+
RemoteManager,
|
|
3
|
+
getRemoteManager,
|
|
4
|
+
setRemoteManager,
|
|
5
|
+
} from "./RemoteManager";
|
|
6
|
+
export { StreamConsumer } from "./StreamConsumer";
|
|
7
|
+
export { RpcCaller } from "./RpcCaller";
|
|
8
|
+
export { OutboxWorker } from "./OutboxWorker";
|
|
9
|
+
export { ensureOutboxSchema } from "./outboxSchema";
|
|
10
|
+
export { CircuitBreaker, CircuitOpenError } from "./CircuitBreaker";
|
|
11
|
+
export type { CircuitState, CircuitBreakerConfig } from "./CircuitBreaker";
|
|
12
|
+
export { RemoteMetrics } from "./metrics";
|
|
13
|
+
export type { RemoteMetricsSnapshot } from "./metrics";
|
|
14
|
+
export { collectRemoteHealth } from "./health";
|
|
15
|
+
export type { RemoteHealthCheck } from "./health";
|
|
16
|
+
export {
|
|
17
|
+
RemoteEvent,
|
|
18
|
+
RemoteRpc,
|
|
19
|
+
registerRemoteHandlers,
|
|
20
|
+
} from "./decorators";
|
|
21
|
+
export type { RemoteEventOptions, RemoteRpcOptions } from "./decorators";
|
|
22
|
+
export { RemoteError } from "./types";
|
|
23
|
+
export type {
|
|
24
|
+
RemoteContext,
|
|
25
|
+
RemoteHandler,
|
|
26
|
+
RemoteHandlerInfo,
|
|
27
|
+
RemoteEnvelope,
|
|
28
|
+
RemoteErrorOptions,
|
|
29
|
+
RemoteManagerConfig,
|
|
30
|
+
RemoteKind,
|
|
31
|
+
RpcHandler,
|
|
32
|
+
RpcResponse,
|
|
33
|
+
RpcSuccessResponse,
|
|
34
|
+
RpcErrorResponse,
|
|
35
|
+
CallOptions,
|
|
36
|
+
EmitOptions,
|
|
37
|
+
} from "./types";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Communication: Metrics
|
|
3
|
+
*
|
|
4
|
+
* In-memory counters for the remote subsystem. Exposed via
|
|
5
|
+
* `RemoteManager.getMetrics()` and the `/metrics` HTTP endpoint.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface RemoteMetricsSnapshot {
|
|
9
|
+
emit: {
|
|
10
|
+
direct: number;
|
|
11
|
+
outbox: number;
|
|
12
|
+
failed: number;
|
|
13
|
+
};
|
|
14
|
+
events: {
|
|
15
|
+
received: number;
|
|
16
|
+
handled: number;
|
|
17
|
+
handlerFailed: number;
|
|
18
|
+
noHandler: number;
|
|
19
|
+
dlq: number;
|
|
20
|
+
};
|
|
21
|
+
rpc: {
|
|
22
|
+
called: number;
|
|
23
|
+
succeeded: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
timedOut: number;
|
|
26
|
+
handlerExecuted: number;
|
|
27
|
+
handlerFailed: number;
|
|
28
|
+
pastDeadline: number;
|
|
29
|
+
};
|
|
30
|
+
outbox: {
|
|
31
|
+
claimed: number;
|
|
32
|
+
published: number;
|
|
33
|
+
publishFailed: number;
|
|
34
|
+
};
|
|
35
|
+
circuitBreaker: {
|
|
36
|
+
trips: number;
|
|
37
|
+
rejected: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function emptySnapshot(): RemoteMetricsSnapshot {
|
|
42
|
+
return {
|
|
43
|
+
emit: { direct: 0, outbox: 0, failed: 0 },
|
|
44
|
+
events: { received: 0, handled: 0, handlerFailed: 0, noHandler: 0, dlq: 0 },
|
|
45
|
+
rpc: {
|
|
46
|
+
called: 0,
|
|
47
|
+
succeeded: 0,
|
|
48
|
+
failed: 0,
|
|
49
|
+
timedOut: 0,
|
|
50
|
+
handlerExecuted: 0,
|
|
51
|
+
handlerFailed: 0,
|
|
52
|
+
pastDeadline: 0,
|
|
53
|
+
},
|
|
54
|
+
outbox: { claimed: 0, published: 0, publishFailed: 0 },
|
|
55
|
+
circuitBreaker: { trips: 0, rejected: 0 },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class RemoteMetrics {
|
|
60
|
+
private snapshot: RemoteMetricsSnapshot = emptySnapshot();
|
|
61
|
+
|
|
62
|
+
// Emit
|
|
63
|
+
emitDirect(): void { this.snapshot.emit.direct++; }
|
|
64
|
+
emitOutbox(): void { this.snapshot.emit.outbox++; }
|
|
65
|
+
emitFailed(): void { this.snapshot.emit.failed++; }
|
|
66
|
+
|
|
67
|
+
// Events
|
|
68
|
+
eventReceived(): void { this.snapshot.events.received++; }
|
|
69
|
+
eventHandled(): void { this.snapshot.events.handled++; }
|
|
70
|
+
eventHandlerFailed(): void { this.snapshot.events.handlerFailed++; }
|
|
71
|
+
eventNoHandler(): void { this.snapshot.events.noHandler++; }
|
|
72
|
+
eventDlq(): void { this.snapshot.events.dlq++; }
|
|
73
|
+
|
|
74
|
+
// RPC
|
|
75
|
+
rpcCalled(): void { this.snapshot.rpc.called++; }
|
|
76
|
+
rpcSucceeded(): void { this.snapshot.rpc.succeeded++; }
|
|
77
|
+
rpcFailed(): void { this.snapshot.rpc.failed++; }
|
|
78
|
+
rpcTimedOut(): void { this.snapshot.rpc.timedOut++; }
|
|
79
|
+
rpcHandlerExecuted(): void { this.snapshot.rpc.handlerExecuted++; }
|
|
80
|
+
rpcHandlerFailed(): void { this.snapshot.rpc.handlerFailed++; }
|
|
81
|
+
rpcPastDeadline(): void { this.snapshot.rpc.pastDeadline++; }
|
|
82
|
+
|
|
83
|
+
// Outbox
|
|
84
|
+
outboxClaimed(n: number): void { this.snapshot.outbox.claimed += n; }
|
|
85
|
+
outboxPublished(n: number): void { this.snapshot.outbox.published += n; }
|
|
86
|
+
outboxPublishFailed(): void { this.snapshot.outbox.publishFailed++; }
|
|
87
|
+
|
|
88
|
+
// Circuit Breaker
|
|
89
|
+
cbTripped(): void { this.snapshot.circuitBreaker.trips++; }
|
|
90
|
+
cbRejected(): void { this.snapshot.circuitBreaker.rejected++; }
|
|
91
|
+
|
|
92
|
+
getSnapshot(): RemoteMetricsSnapshot {
|
|
93
|
+
return JSON.parse(JSON.stringify(this.snapshot));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reset(): void {
|
|
97
|
+
this.snapshot = emptySnapshot();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Communication: Transactional Outbox schema
|
|
3
|
+
*
|
|
4
|
+
* The outbox table records `emit()` calls made inside a DB transaction.
|
|
5
|
+
* A background worker picks pending rows up and publishes them to Redis.
|
|
6
|
+
* This guarantees that the event is only released to consumers if the
|
|
7
|
+
* transaction that produced it committed — no "committed write without
|
|
8
|
+
* matching event" after a crash.
|
|
9
|
+
*
|
|
10
|
+
* Schema is intentionally minimal (Gall's Law): id, target, event, data,
|
|
11
|
+
* created_at, published_at. Retry counts, DLQ tracking, and leases can be
|
|
12
|
+
* added in later phases when there's a concrete reason.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { SQL } from "bun";
|
|
16
|
+
import { logger } from "../Logger";
|
|
17
|
+
|
|
18
|
+
const loggerInstance = logger.child({ scope: "OutboxSchema" });
|
|
19
|
+
|
|
20
|
+
export async function ensureOutboxSchema(db: SQL): Promise<void> {
|
|
21
|
+
await db`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS remote_outbox (
|
|
23
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
24
|
+
target VARCHAR(255) NOT NULL,
|
|
25
|
+
event VARCHAR(255) NOT NULL,
|
|
26
|
+
data JSONB NOT NULL,
|
|
27
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
28
|
+
published_at TIMESTAMPTZ
|
|
29
|
+
)
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
// Partial index: only unpublished rows. Keeps the index small even as
|
|
33
|
+
// the table accumulates historical sent messages.
|
|
34
|
+
await db`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_remote_outbox_pending
|
|
36
|
+
ON remote_outbox (created_at)
|
|
37
|
+
WHERE published_at IS NULL
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
loggerInstance.info("remote_outbox schema ensured");
|
|
41
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Communication: Types
|
|
3
|
+
*
|
|
4
|
+
* Standalone types for cross-app events over Redis Streams.
|
|
5
|
+
* RemoteContext is NOT derived from GraphQLContext — remote handlers run
|
|
6
|
+
* outside request scope.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface RemoteContext {
|
|
10
|
+
sourceApp: string;
|
|
11
|
+
messageId: string;
|
|
12
|
+
timestamp: Date;
|
|
13
|
+
attempt: number;
|
|
14
|
+
correlationId?: string;
|
|
15
|
+
deadline?: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type RemoteHandler<T = unknown> = (
|
|
19
|
+
data: T,
|
|
20
|
+
ctx: RemoteContext
|
|
21
|
+
) => Promise<void> | void;
|
|
22
|
+
|
|
23
|
+
export type RemoteKind = "event" | "rpc_request";
|
|
24
|
+
|
|
25
|
+
export interface RemoteHandlerInfo {
|
|
26
|
+
event: string;
|
|
27
|
+
methodName: string;
|
|
28
|
+
handlerId: string;
|
|
29
|
+
/** "event" for @RemoteEvent, "rpc_request" for @RemoteRpc */
|
|
30
|
+
kind: RemoteKind;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RemoteEnvelope {
|
|
34
|
+
/** Discriminator — absent/`"event"` = fire-and-forget, `"rpc_request"` = RPC */
|
|
35
|
+
kind?: RemoteKind;
|
|
36
|
+
sourceApp: string;
|
|
37
|
+
event: string;
|
|
38
|
+
data: unknown;
|
|
39
|
+
emittedAt: number;
|
|
40
|
+
|
|
41
|
+
/** RPC-only */
|
|
42
|
+
correlationId?: string;
|
|
43
|
+
replyTo?: string;
|
|
44
|
+
deadline?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type RpcHandler<TIn = unknown, TOut = unknown> = (
|
|
48
|
+
data: TIn,
|
|
49
|
+
ctx: RemoteContext
|
|
50
|
+
) => Promise<TOut> | TOut;
|
|
51
|
+
|
|
52
|
+
export interface RpcSuccessResponse {
|
|
53
|
+
correlationId: string;
|
|
54
|
+
sourceApp: string;
|
|
55
|
+
success: true;
|
|
56
|
+
result: unknown;
|
|
57
|
+
respondedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RpcErrorResponse {
|
|
61
|
+
correlationId: string;
|
|
62
|
+
sourceApp: string;
|
|
63
|
+
success: false;
|
|
64
|
+
error: {
|
|
65
|
+
code: string;
|
|
66
|
+
message: string;
|
|
67
|
+
extensions?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
respondedAt: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type RpcResponse = RpcSuccessResponse | RpcErrorResponse;
|
|
73
|
+
|
|
74
|
+
export interface CallOptions {
|
|
75
|
+
/** Timeout in ms (default: 5000) */
|
|
76
|
+
timeout?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* emit() options. Passing `trx` routes the event through the transactional
|
|
81
|
+
* outbox — the row is inserted within the caller's transaction and
|
|
82
|
+
* published by the OutboxWorker after commit.
|
|
83
|
+
*/
|
|
84
|
+
export interface EmitOptions {
|
|
85
|
+
/** Transaction handle from `db.begin()` / `db.transaction()`. */
|
|
86
|
+
trx?: import("bun").SQL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RemoteErrorOptions {
|
|
90
|
+
code: string;
|
|
91
|
+
sourceApp?: string;
|
|
92
|
+
extensions?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class RemoteError extends Error {
|
|
96
|
+
public readonly code: string;
|
|
97
|
+
public readonly sourceApp?: string;
|
|
98
|
+
public readonly extensions?: Record<string, unknown>;
|
|
99
|
+
|
|
100
|
+
constructor(message: string, options: RemoteErrorOptions) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = "RemoteError";
|
|
103
|
+
this.code = options.code;
|
|
104
|
+
this.sourceApp = options.sourceApp;
|
|
105
|
+
this.extensions = options.extensions;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface RemoteManagerConfig {
|
|
110
|
+
/** This app's identity — used as stream name and sourceApp field */
|
|
111
|
+
appName: string;
|
|
112
|
+
/** Consumer group (defaults to appName) */
|
|
113
|
+
consumerGroup?: string;
|
|
114
|
+
/** Unique consumer id within the group (defaults to pid + timestamp) */
|
|
115
|
+
consumerId?: string;
|
|
116
|
+
/** Stream key prefix (default: "remote:") */
|
|
117
|
+
streamPrefix?: string;
|
|
118
|
+
/** Enable verbose logging */
|
|
119
|
+
enableLogging?: boolean;
|
|
120
|
+
/** Max messages per XREADGROUP batch (default: 10) */
|
|
121
|
+
batchSize?: number;
|
|
122
|
+
/** XREADGROUP BLOCK timeout in ms (default: 2000) */
|
|
123
|
+
blockMs?: number;
|
|
124
|
+
/** XAUTOCLAIM idle threshold in ms on startup (default: 60000). 0 disables */
|
|
125
|
+
autoClaimIdleMs?: number;
|
|
126
|
+
/** Max response stream length cap per XADD MAXLEN ~ (default: 1000) */
|
|
127
|
+
responseStreamMaxLen?: number;
|
|
128
|
+
/** Default RPC call timeout in ms (default: 5000) */
|
|
129
|
+
defaultCallTimeout?: number;
|
|
130
|
+
/** Grace window for pending RPC calls during shutdown (default: 2000) */
|
|
131
|
+
shutdownDrainMs?: number;
|
|
132
|
+
/** Enable transactional outbox (default: false) */
|
|
133
|
+
enableOutbox?: boolean;
|
|
134
|
+
/** Outbox polling interval in ms (default: 1000) */
|
|
135
|
+
outboxPollIntervalMs?: number;
|
|
136
|
+
/** Max rows processed per outbox tick (default: 100) */
|
|
137
|
+
outboxBatchSize?: number;
|
|
138
|
+
/** Circuit breaker failure threshold before opening (default: 5) */
|
|
139
|
+
circuitBreakerThreshold?: number;
|
|
140
|
+
/** Circuit breaker reset timeout in ms (default: 30000) */
|
|
141
|
+
circuitBreakerResetMs?: number;
|
|
142
|
+
/** Max deliveries before routing a message to DLQ (default: 3, 0 disables) */
|
|
143
|
+
dlqMaxDeliveries?: number;
|
|
144
|
+
/**
|
|
145
|
+
* Test-only: override how Redis clients are constructed. Return a
|
|
146
|
+
* connected client compatible with the ioredis `Redis` interface.
|
|
147
|
+
* `blocking` is `true` for connections that will issue BLOCK commands
|
|
148
|
+
* (consumer + RPC listener), `false` for the publisher.
|
|
149
|
+
*/
|
|
150
|
+
redisFactory?: (blocking: boolean) => any;
|
|
151
|
+
}
|