orpc-fn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/bound-call.d.ts +23 -0
- package/dist/bound-call.d.ts.map +1 -0
- package/dist/bound-call.js +23 -0
- package/dist/bound-call.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -0
- package/dist/create-fn.d.ts +97 -0
- package/dist/create-fn.d.ts.map +1 -0
- package/dist/create-fn.js +224 -0
- package/dist/create-fn.js.map +1 -0
- package/dist/expected-client-error.d.ts +11 -0
- package/dist/expected-client-error.d.ts.map +1 -0
- package/dist/expected-client-error.js +21 -0
- package/dist/expected-client-error.js.map +1 -0
- package/dist/expo.d.ts +66 -0
- package/dist/expo.d.ts.map +1 -0
- package/dist/expo.js +74 -0
- package/dist/expo.js.map +1 -0
- package/dist/hono.d.ts +81 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +138 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +47 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +44 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/live/codec.d.ts +9 -0
- package/dist/live/codec.d.ts.map +1 -0
- package/dist/live/codec.js +27 -0
- package/dist/live/codec.js.map +1 -0
- package/dist/live/fn-live.d.ts +155 -0
- package/dist/live/fn-live.d.ts.map +1 -0
- package/dist/live/fn-live.js +210 -0
- package/dist/live/fn-live.js.map +1 -0
- package/dist/live/index.d.ts +4 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +5 -0
- package/dist/live/index.js.map +1 -0
- package/dist/live/ioredis.d.ts +29 -0
- package/dist/live/ioredis.d.ts.map +1 -0
- package/dist/live/ioredis.js +49 -0
- package/dist/live/ioredis.js.map +1 -0
- package/dist/live/memory.d.ts +10 -0
- package/dist/live/memory.d.ts.map +1 -0
- package/dist/live/memory.js +45 -0
- package/dist/live/memory.js.map +1 -0
- package/dist/live/pub-sub.d.ts +211 -0
- package/dist/live/pub-sub.d.ts.map +1 -0
- package/dist/live/pub-sub.js +529 -0
- package/dist/live/pub-sub.js.map +1 -0
- package/dist/live/redis-bun.d.ts +26 -0
- package/dist/live/redis-bun.d.ts.map +1 -0
- package/dist/live/redis-bun.js +59 -0
- package/dist/live/redis-bun.js.map +1 -0
- package/dist/live/redis.d.ts +45 -0
- package/dist/live/redis.d.ts.map +1 -0
- package/dist/live/redis.js +214 -0
- package/dist/live/redis.js.map +1 -0
- package/dist/live/transport.d.ts +35 -0
- package/dist/live/transport.d.ts.map +1 -0
- package/dist/live/transport.js +2 -0
- package/dist/live/transport.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mastra.d.ts +44 -0
- package/dist/mastra.d.ts.map +1 -0
- package/dist/mastra.js +78 -0
- package/dist/mastra.js.map +1 -0
- package/dist/mcp.d.ts +39 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +38 -0
- package/dist/mcp.js.map +1 -0
- package/dist/meta.d.ts +35 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +30 -0
- package/dist/meta.js.map +1 -0
- package/dist/otel.d.ts +66 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +63 -0
- package/dist/otel.js.map +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +14 -0
- package/dist/router.js.map +1 -0
- package/dist/stream.d.ts +13 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +44 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +157 -0
- package/src/bound-call.ts +50 -0
- package/src/client.ts +82 -0
- package/src/create-fn.ts +461 -0
- package/src/expected-client-error.ts +23 -0
- package/src/expo.ts +127 -0
- package/src/hono.ts +247 -0
- package/src/index.ts +45 -0
- package/src/json-schema.ts +82 -0
- package/src/live/codec.ts +31 -0
- package/src/live/fn-live.ts +470 -0
- package/src/live/index.ts +31 -0
- package/src/live/ioredis.ts +74 -0
- package/src/live/memory.ts +50 -0
- package/src/live/pub-sub.ts +828 -0
- package/src/live/redis-bun.ts +76 -0
- package/src/live/redis.ts +255 -0
- package/src/live/transport.ts +41 -0
- package/src/logger.ts +28 -0
- package/src/mastra.ts +140 -0
- package/src/mcp.ts +73 -0
- package/src/meta.ts +72 -0
- package/src/otel.ts +151 -0
- package/src/router.ts +15 -0
- package/src/stream.ts +54 -0
- package/src/types.ts +389 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ORPCError } from '@orpc/server';
|
|
2
|
+
import { errorMessageOf } from '../otel.js';
|
|
3
|
+
/** Marks a coalesce window that ended before the next event arrived. */
|
|
4
|
+
const TIMEOUT = Symbol('timeout');
|
|
5
|
+
const fnLivePatchMarker = Symbol('fnLive.patch');
|
|
6
|
+
export function fnLivePatch(state, emit) {
|
|
7
|
+
return { [fnLivePatchMarker]: true, state, emit };
|
|
8
|
+
}
|
|
9
|
+
function isFnLivePatch(value) {
|
|
10
|
+
return (typeof value === 'object' && value !== null && fnLivePatchMarker in value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fail a live subscription whose initial snapshot could not be built.
|
|
14
|
+
*
|
|
15
|
+
* An expected client failure - an `ORPCError` with a 4xx status, e.g. the
|
|
16
|
+
* subscribed resource was not found or access was refused - reaches the
|
|
17
|
+
* subscriber unchanged and is NOT logged as a server fault. Only 5xx and
|
|
18
|
+
* unknown failures are logged. Unknown errors are wrapped as
|
|
19
|
+
* `INTERNAL_SERVER_ERROR`.
|
|
20
|
+
*/
|
|
21
|
+
export function throwInitialSnapshotError(err, liveName, logger,
|
|
22
|
+
/** `createFn`'s `isExpectedError`; default: a 4xx `ORPCError`. */
|
|
23
|
+
isExpected = (error) => error instanceof ORPCError && error.status < 500) {
|
|
24
|
+
if (!isExpected(err)) {
|
|
25
|
+
logger.error('fnLive initial snapshot failed', {
|
|
26
|
+
name: liveName,
|
|
27
|
+
err: errorMessageOf(err)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
throw toSnapshotError(err, liveName);
|
|
31
|
+
}
|
|
32
|
+
/** An `ORPCError` passes through; anything else becomes INTERNAL_SERVER_ERROR. */
|
|
33
|
+
function toSnapshotError(err, liveName) {
|
|
34
|
+
if (err instanceof ORPCError)
|
|
35
|
+
return err;
|
|
36
|
+
return new ORPCError('INTERNAL_SERVER_ERROR', {
|
|
37
|
+
message: `Failed to load initial snapshot for ${liveName}`,
|
|
38
|
+
cause: err
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Drives a live subscription: yields the initial snapshot, then folds each
|
|
43
|
+
* inbound event into the running snapshot via `apply`, yielding after every
|
|
44
|
+
* change. With `coalesceMs`, events arriving within that window are batched so
|
|
45
|
+
* bursty streams apply together.
|
|
46
|
+
*
|
|
47
|
+
* `apply` may return an {@link FnLivePatch} to keep the full snapshot as the
|
|
48
|
+
* internal accumulator while yielding only the lighter `emit` payload.
|
|
49
|
+
*
|
|
50
|
+
* Invariant: there is never more than one outstanding `source.next()` read. On
|
|
51
|
+
* a coalesce-window timeout the still-pending read is stashed in `pending` and
|
|
52
|
+
* reused by the next `readNext()` - issuing a second concurrent read would
|
|
53
|
+
* strand the in-flight event behind the next one (delivering it "one behind").
|
|
54
|
+
*/
|
|
55
|
+
export async function* streamLiveSnapshots(opts) {
|
|
56
|
+
const { source, initial, coalesceMs, apply } = opts;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const readNext = () => {
|
|
59
|
+
const read = pending ?? source.next();
|
|
60
|
+
pending = null;
|
|
61
|
+
return read;
|
|
62
|
+
};
|
|
63
|
+
let previous = initial;
|
|
64
|
+
yield previous;
|
|
65
|
+
while (true) {
|
|
66
|
+
const current = await readNext();
|
|
67
|
+
if (current.done)
|
|
68
|
+
break;
|
|
69
|
+
const batch = [current.value];
|
|
70
|
+
if (coalesceMs) {
|
|
71
|
+
const endAt = Date.now() + coalesceMs;
|
|
72
|
+
while (Date.now() < endAt) {
|
|
73
|
+
const remainingMs = endAt - Date.now();
|
|
74
|
+
const read = readNext();
|
|
75
|
+
let timer;
|
|
76
|
+
const result = await Promise.race([
|
|
77
|
+
read,
|
|
78
|
+
new Promise((resolve) => {
|
|
79
|
+
timer = setTimeout(resolve, remainingMs, TIMEOUT);
|
|
80
|
+
})
|
|
81
|
+
]);
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (result === TIMEOUT) {
|
|
84
|
+
// Reuse the SAME in-flight read next iteration instead of starting
|
|
85
|
+
// a new one - this is what prevents the "one behind" stranding.
|
|
86
|
+
pending = read;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (result.done) {
|
|
90
|
+
pending = Promise.resolve(result);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
batch.push(result.value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const event of batch) {
|
|
97
|
+
const next = await apply(event, previous);
|
|
98
|
+
if (next === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
if (isFnLivePatch(next)) {
|
|
101
|
+
previous = next.state;
|
|
102
|
+
yield next.emit;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
previous = next;
|
|
106
|
+
yield previous;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* @internal Wires `fnLive` onto an `fn` and channel runtime of the same
|
|
112
|
+
* instance. `createFn` returns it typed as {@link FnLive}.
|
|
113
|
+
*/
|
|
114
|
+
export function createFnLive(runtime) {
|
|
115
|
+
const { fn, createChannel, isGuard } = runtime;
|
|
116
|
+
const liveLogger = runtime.createLogger('fn-live', undefined);
|
|
117
|
+
return (options) => {
|
|
118
|
+
const { live, handler, input, output, name, ...routeConfig } = options;
|
|
119
|
+
const liveName = live.name ?? `${name}.live`;
|
|
120
|
+
const liveSummary = live.summary ?? `Live updates of ${routeConfig.summary ?? name}`;
|
|
121
|
+
const liveDescription = live.description ?? routeConfig.description;
|
|
122
|
+
// `createChannel` reads only the channel keys of the live config.
|
|
123
|
+
const channel = createChannel({ ...live, name: liveName });
|
|
124
|
+
const publish = async (event) => {
|
|
125
|
+
if (!live.safePublish)
|
|
126
|
+
return channel.publish(event);
|
|
127
|
+
try {
|
|
128
|
+
await channel.publish(event);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// Live update failures must not roll back the route that emitted them,
|
|
132
|
+
// but they should still be visible in logs.
|
|
133
|
+
liveLogger.error('fnLive publish failed', {
|
|
134
|
+
name: liveName,
|
|
135
|
+
err: errorMessageOf(err)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const runHandler = (params) => handler({ ...params, publish });
|
|
140
|
+
// Snapshots go through the output schema like the route's own result.
|
|
141
|
+
const runSnapshot = async (params) => {
|
|
142
|
+
const result = await runHandler(params);
|
|
143
|
+
return output ? output.parseAsync(result) : result;
|
|
144
|
+
};
|
|
145
|
+
const procedure = fn({
|
|
146
|
+
...routeConfig,
|
|
147
|
+
name,
|
|
148
|
+
input,
|
|
149
|
+
output,
|
|
150
|
+
handler: runHandler
|
|
151
|
+
});
|
|
152
|
+
// Guards run on the subscribe route too; route meta stays on `procedure`.
|
|
153
|
+
const guardOptions = Object.fromEntries(Object.entries(routeConfig).filter(([option]) => isGuard(option)));
|
|
154
|
+
const subscribe = fn({
|
|
155
|
+
...guardOptions,
|
|
156
|
+
procedure: routeConfig.procedure,
|
|
157
|
+
tags: routeConfig.tags,
|
|
158
|
+
name: liveName,
|
|
159
|
+
method: 'GET',
|
|
160
|
+
input,
|
|
161
|
+
summary: liveSummary,
|
|
162
|
+
description: liveDescription,
|
|
163
|
+
handler: async function* (params) {
|
|
164
|
+
// Authorize and subscribe before the snapshot: a refused subscriber
|
|
165
|
+
// gets nothing, and events published while it loads are queued.
|
|
166
|
+
const subscription = await channel.open({
|
|
167
|
+
input: params.input,
|
|
168
|
+
context: params.context,
|
|
169
|
+
signal: params.signal
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
let initial;
|
|
173
|
+
try {
|
|
174
|
+
initial = await runSnapshot(params);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// `fn.completed` logs it, at the level `isExpectedError` picks.
|
|
178
|
+
throw toSnapshotError(err, liveName);
|
|
179
|
+
}
|
|
180
|
+
const rerun = (nextInput) => runSnapshot({ ...params, input: nextInput ?? params.input });
|
|
181
|
+
const apply = async (event, previous) => {
|
|
182
|
+
if (live.shouldUpdate &&
|
|
183
|
+
!(await live.shouldUpdate({ ...params, event, previous }))) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (!live.transformerFn)
|
|
187
|
+
return runSnapshot(params);
|
|
188
|
+
return live.transformerFn({ ...params, event, previous, rerun });
|
|
189
|
+
};
|
|
190
|
+
yield* streamLiveSnapshots({
|
|
191
|
+
source: subscription.events,
|
|
192
|
+
initial,
|
|
193
|
+
coalesceMs: live.coalesceMs,
|
|
194
|
+
apply
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
subscription.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
procedure,
|
|
204
|
+
subscribe,
|
|
205
|
+
publish,
|
|
206
|
+
getChannelName: channel.getChannelName
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=fn-live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fn-live.js","sourceRoot":"","sources":["../../src/live/fn-live.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Schema } from '@orpc/contract'\nimport { type AnyProcedure, ORPCError } from '@orpc/server'\nimport type { ZodType, z } from 'zod'\nimport type { FnLogger } from '../logger.js'\nimport { errorMessageOf, type SpanLike } from '../otel.js'\nimport type {\n\tFnContext,\n\tFnDefinition,\n\tFnProcedure,\n\tFnRouteOptions,\n\tHandlerParams,\n\tMaybePromise,\n\tProcedureKey\n} from '../types.js'\nimport type {\n\tAuthFn,\n\tChannel,\n\tChannelDefinition,\n\tChannelOptions,\n\tObjectSchema,\n\tPublisherConfig\n} from './pub-sub.js'\n\n/** Marks a coalesce window that ended before the next event arrived. */\nconst TIMEOUT = Symbol('timeout')\n\nconst fnLivePatchMarker = Symbol('fnLive.patch')\n\n/**\n * Transformer result that splits the internal accumulator from the wire\n * payload: `state` becomes the `previous` snapshot for later events while only\n * `emit` is sent to the subscriber. Lets high-frequency streams avoid\n * re-serializing the full snapshot per event.\n */\nexport type FnLivePatch<TOutput, TEmit> = {\n\t[fnLivePatchMarker]: true\n\tstate: TOutput\n\temit: TEmit\n}\n\nexport function fnLivePatch<TOutput, TEmit>(\n\tstate: TOutput,\n\temit: TEmit\n): FnLivePatch<TOutput, TEmit> {\n\treturn { [fnLivePatchMarker]: true, state, emit }\n}\n\nfunction isFnLivePatch(value: unknown): value is FnLivePatch<unknown, unknown> {\n\treturn (\n\t\ttypeof value === 'object' && value !== null && fnLivePatchMarker in value\n\t)\n}\n\nexport type FnLiveConfig<\n\tTDef extends FnDefinition,\n\tTInput,\n\tTEventSchema extends ZodType,\n\tTOutput,\n\tTKey extends ProcedureKey<TDef>,\n\tTEmit = never\n> = Omit<PublisherConfig, 'name'> & {\n\t/** Subscribe route name; default `${name}.live`. */\n\tname?: string\n\teventSchema: TEventSchema\n\tchannel: ChannelDefinition<\n\t\tTInput,\n\t\tz.output<TEventSchema>,\n\t\tFnContext<TDef, TKey>\n\t>\n\t/** Fold an event into the snapshot; without it every event re-runs the handler. */\n\ttransformerFn?: (\n\t\tparams: HandlerParams<TDef, TInput, TKey> & {\n\t\t\tevent: z.output<TEventSchema>\n\t\t\tprevious: TOutput | undefined\n\t\t\trerun: (input?: TInput) => Promise<TOutput>\n\t\t}\n\t) => MaybePromise<TOutput | FnLivePatch<TOutput, TEmit> | undefined>\n\t/** Return false to skip an event. */\n\tshouldUpdate?: (\n\t\tparams: HandlerParams<TDef, TInput, TKey> & {\n\t\t\tevent: z.output<TEventSchema>\n\t\t\tprevious: TOutput | undefined\n\t\t}\n\t) => MaybePromise<boolean>\n\tauthFn?: AuthFn<TInput, FnContext<TDef, TKey>>\n\t/** Log publish failures instead of throwing them into the publishing route. */\n\tsafePublish?: boolean\n\t/** Batch events arriving within this window into one snapshot update. */\n\tcoalesceMs?: number\n\t/**\n\t * When a slow subscriber's queue overflows (drop-oldest), this marker is\n\t * enqueued INSTEAD of the dropped event so the client resyncs. Return\n\t * `null` to skip.\n\t */\n\toverflowMarker?: (input: TInput) => z.output<TEventSchema> | null\n\tsummary?: string\n\tdescription?: string\n}\n\nexport type FnLiveReturn<\n\tTDef extends FnDefinition,\n\tTKey extends ProcedureKey<TDef>,\n\tTInput extends ObjectSchema,\n\tTOutputSchema extends Schema<unknown, unknown>,\n\tTOutput,\n\tTEventSchema extends ZodType,\n\tTEmit = never\n> = {\n\tprocedure: FnProcedure<TDef, TKey, TInput, TOutputSchema>\n\tsubscribe: FnProcedure<\n\t\tTDef,\n\t\tTKey,\n\t\tTInput,\n\t\tSchema<AsyncGenerator<TOutput | TEmit>, AsyncGenerator<TOutput | TEmit>>\n\t>\n\tpublish: (event: z.input<TEventSchema>) => Promise<void>\n\tgetChannelName: (\n\t\tparams: Partial<z.output<TInput>> | Partial<z.output<TEventSchema>>,\n\t\tcontext?: FnContext<TDef, TKey>\n\t) => string\n}\n\n/**\n * A route plus a subscribe route that streams its result: the initial\n * snapshot, then an update for every event published on the channel.\n */\nexport interface FnLive<TDef extends FnDefinition> {\n\t<\n\t\tTInput extends ObjectSchema,\n\t\tTOutputSchema extends ZodType,\n\t\tTEventSchema extends ZodType,\n\t\tTKey extends ProcedureKey<TDef> = TDef['default'],\n\t\tTEmit = never\n\t>(\n\t\toptions: {\n\t\t\tinput: TInput\n\t\t\toutput: TOutputSchema\n\t\t\thandler: (\n\t\t\t\tparams: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>> & {\n\t\t\t\t\tpublish: (event: z.input<TEventSchema>) => Promise<void>\n\t\t\t\t}\n\t\t\t\t// The output schema parses what the handler returns: its input type.\n\t\t\t) => MaybePromise<z.input<TOutputSchema>>\n\t\t\tlive: FnLiveConfig<\n\t\t\t\tTDef,\n\t\t\t\tz.output<TInput>,\n\t\t\t\tTEventSchema,\n\t\t\t\tz.output<TOutputSchema>,\n\t\t\t\tNoInfer<TKey>,\n\t\t\t\tTEmit\n\t\t\t>\n\t\t} & FnRouteOptions<TDef, TKey>\n\t): FnLiveReturn<\n\t\tTDef,\n\t\tTKey,\n\t\tTInput,\n\t\tTOutputSchema,\n\t\tz.output<TOutputSchema>,\n\t\tTEventSchema,\n\t\tTEmit\n\t>\n\n\t<\n\t\tTInput extends ObjectSchema,\n\t\tTOutput,\n\t\tTEventSchema extends ZodType,\n\t\tTKey extends ProcedureKey<TDef> = TDef['default'],\n\t\tTEmit = never\n\t>(\n\t\toptions: {\n\t\t\tinput: TInput\n\t\t\toutput?: undefined\n\t\t\thandler: (\n\t\t\t\tparams: HandlerParams<TDef, z.output<TInput>, NoInfer<TKey>> & {\n\t\t\t\t\tpublish: (event: z.input<TEventSchema>) => Promise<void>\n\t\t\t\t}\n\t\t\t) => MaybePromise<TOutput>\n\t\t\tlive: FnLiveConfig<\n\t\t\t\tTDef,\n\t\t\t\tz.output<TInput>,\n\t\t\t\tTEventSchema,\n\t\t\t\tTOutput,\n\t\t\t\tNoInfer<TKey>,\n\t\t\t\tTEmit\n\t\t\t>\n\t\t} & FnRouteOptions<TDef, TKey>\n\t): FnLiveReturn<\n\t\tTDef,\n\t\tTKey,\n\t\tTInput,\n\t\tSchema<TOutput, TOutput>,\n\t\tTOutput,\n\t\tTEventSchema,\n\t\tTEmit\n\t>\n}\n\n/**\n * Fail a live subscription whose initial snapshot could not be built.\n *\n * An expected client failure - an `ORPCError` with a 4xx status, e.g. the\n * subscribed resource was not found or access was refused - reaches the\n * subscriber unchanged and is NOT logged as a server fault. Only 5xx and\n * unknown failures are logged. Unknown errors are wrapped as\n * `INTERNAL_SERVER_ERROR`.\n */\nexport function throwInitialSnapshotError(\n\terr: unknown,\n\tliveName: string,\n\tlogger: Pick<FnLogger, 'error'>,\n\t/** `createFn`'s `isExpectedError`; default: a 4xx `ORPCError`. */\n\tisExpected: (error: unknown) => boolean = (error) =>\n\t\terror instanceof ORPCError && error.status < 500\n): never {\n\tif (!isExpected(err)) {\n\t\tlogger.error('fnLive initial snapshot failed', {\n\t\t\tname: liveName,\n\t\t\terr: errorMessageOf(err)\n\t\t})\n\t}\n\tthrow toSnapshotError(err, liveName)\n}\n\n/** An `ORPCError` passes through; anything else becomes INTERNAL_SERVER_ERROR. */\nfunction toSnapshotError(\n\terr: unknown,\n\tliveName: string\n): ORPCError<string, unknown> {\n\tif (err instanceof ORPCError) return err\n\treturn new ORPCError('INTERNAL_SERVER_ERROR', {\n\t\tmessage: `Failed to load initial snapshot for ${liveName}`,\n\t\tcause: err\n\t})\n}\n\n/**\n * Drives a live subscription: yields the initial snapshot, then folds each\n * inbound event into the running snapshot via `apply`, yielding after every\n * change. With `coalesceMs`, events arriving within that window are batched so\n * bursty streams apply together.\n *\n * `apply` may return an {@link FnLivePatch} to keep the full snapshot as the\n * internal accumulator while yielding only the lighter `emit` payload.\n *\n * Invariant: there is never more than one outstanding `source.next()` read. On\n * a coalesce-window timeout the still-pending read is stashed in `pending` and\n * reused by the next `readNext()` - issuing a second concurrent read would\n * strand the in-flight event behind the next one (delivering it \"one behind\").\n */\nexport async function* streamLiveSnapshots<\n\tTEvent,\n\tTOutput,\n\tTEmit = never\n>(opts: {\n\tsource: AsyncIterator<TEvent>\n\tinitial: TOutput\n\tcoalesceMs?: number | undefined\n\tapply: (\n\t\tevent: TEvent,\n\t\tprevious: TOutput\n\t) => MaybePromise<TOutput | FnLivePatch<TOutput, TEmit> | undefined>\n}): AsyncGenerator<TOutput | TEmit> {\n\tconst { source, initial, coalesceMs, apply } = opts\n\n\tlet pending: Promise<IteratorResult<TEvent>> | null = null\n\tconst readNext = () => {\n\t\tconst read = pending ?? source.next()\n\t\tpending = null\n\t\treturn read\n\t}\n\n\tlet previous = initial\n\tyield previous\n\n\twhile (true) {\n\t\tconst current = await readNext()\n\t\tif (current.done) break\n\n\t\tconst batch: TEvent[] = [current.value]\n\n\t\tif (coalesceMs) {\n\t\t\tconst endAt = Date.now() + coalesceMs\n\t\t\twhile (Date.now() < endAt) {\n\t\t\t\tconst remainingMs = endAt - Date.now()\n\t\t\t\tconst read = readNext()\n\t\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined\n\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\tread,\n\t\t\t\t\tnew Promise<typeof TIMEOUT>((resolve) => {\n\t\t\t\t\t\ttimer = setTimeout(resolve, remainingMs, TIMEOUT)\n\t\t\t\t\t})\n\t\t\t\t])\n\t\t\t\tclearTimeout(timer)\n\n\t\t\t\tif (result === TIMEOUT) {\n\t\t\t\t\t// Reuse the SAME in-flight read next iteration instead of starting\n\t\t\t\t\t// a new one - this is what prevents the \"one behind\" stranding.\n\t\t\t\t\tpending = read\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif (result.done) {\n\t\t\t\t\tpending = Promise.resolve(result)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tbatch.push(result.value)\n\t\t\t}\n\t\t}\n\n\t\tfor (const event of batch) {\n\t\t\tconst next = await apply(event, previous)\n\t\t\tif (next === undefined) continue\n\t\t\tif (isFnLivePatch(next)) {\n\t\t\t\tprevious = next.state as TOutput\n\t\t\t\tyield next.emit as TEmit\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tprevious = next\n\t\t\tyield previous\n\t\t}\n\t}\n}\n\ntype RuntimeParams = Record<string, unknown> & {\n\tinput: Record<string, unknown>\n\tcontext: unknown\n\tsignal: AbortSignal | undefined\n\tlogger: FnLogger\n}\n\n/** `FnLiveConfig` with the generics erased: what the implementation reads. */\ntype RuntimeLiveConfig = Omit<ChannelOptions, 'name'> & {\n\tname?: string\n\ttransformerFn?: (params: RuntimeParams & Record<string, unknown>) => unknown\n\tshouldUpdate?: (params: RuntimeParams & Record<string, unknown>) => unknown\n\tsafePublish?: boolean\n\tcoalesceMs?: number\n\tsummary?: string\n\tdescription?: string\n}\n\n/**\n * @internal Wires `fnLive` onto an `fn` and channel runtime of the same\n * instance. `createFn` returns it typed as {@link FnLive}.\n */\nexport function createFnLive(runtime: {\n\t// biome-ignore lint/suspicious/noExplicitAny: runtime half of the typed `Fn`\n\tfn: (options: any) => AnyProcedure\n\tcreateChannel: (options: ChannelOptions) => Channel\n\tcreateLogger: (scope: string, span: SpanLike | undefined) => FnLogger\n\tisGuard: (option: string) => boolean\n}) {\n\tconst { fn, createChannel, isGuard } = runtime\n\tconst liveLogger = runtime.createLogger('fn-live', undefined)\n\n\treturn (\n\t\toptions: {\n\t\t\tlive: RuntimeLiveConfig\n\t\t\thandler: (params: RuntimeParams) => unknown\n\t\t\tinput: ZodType\n\t\t\toutput?: ZodType\n\t\t\tname: string\n\t\t\tprocedure?: string\n\t\t\tsummary?: string\n\t\t\tdescription?: string\n\t\t\ttags?: string[]\n\t\t} & Record<string, unknown>\n\t) => {\n\t\tconst { live, handler, input, output, name, ...routeConfig } = options\n\t\tconst liveName = live.name ?? `${name}.live`\n\t\tconst liveSummary =\n\t\t\tlive.summary ?? `Live updates of ${routeConfig.summary ?? name}`\n\t\tconst liveDescription = live.description ?? routeConfig.description\n\n\t\t// `createChannel` reads only the channel keys of the live config.\n\t\tconst channel = createChannel({ ...live, name: liveName })\n\n\t\tconst publish = async (event: unknown) => {\n\t\t\tif (!live.safePublish) return channel.publish(event)\n\t\t\ttry {\n\t\t\t\tawait channel.publish(event)\n\t\t\t} catch (err) {\n\t\t\t\t// Live update failures must not roll back the route that emitted them,\n\t\t\t\t// but they should still be visible in logs.\n\t\t\t\tliveLogger.error('fnLive publish failed', {\n\t\t\t\t\tname: liveName,\n\t\t\t\t\terr: errorMessageOf(err)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tconst runHandler = (params: RuntimeParams) =>\n\t\t\thandler({ ...params, publish })\n\t\t// Snapshots go through the output schema like the route's own result.\n\t\tconst runSnapshot = async (params: RuntimeParams) => {\n\t\t\tconst result = await runHandler(params)\n\t\t\treturn output ? output.parseAsync(result) : result\n\t\t}\n\n\t\tconst procedure = fn({\n\t\t\t...routeConfig,\n\t\t\tname,\n\t\t\tinput,\n\t\t\toutput,\n\t\t\thandler: runHandler\n\t\t})\n\n\t\t// Guards run on the subscribe route too; route meta stays on `procedure`.\n\t\tconst guardOptions = Object.fromEntries(\n\t\t\tObject.entries(routeConfig).filter(([option]) => isGuard(option))\n\t\t)\n\t\tconst subscribe = fn({\n\t\t\t...guardOptions,\n\t\t\tprocedure: routeConfig.procedure,\n\t\t\ttags: routeConfig.tags,\n\t\t\tname: liveName,\n\t\t\tmethod: 'GET',\n\t\t\tinput,\n\t\t\tsummary: liveSummary,\n\t\t\tdescription: liveDescription,\n\t\t\thandler: async function* (params: RuntimeParams) {\n\t\t\t\t// Authorize and subscribe before the snapshot: a refused subscriber\n\t\t\t\t// gets nothing, and events published while it loads are queued.\n\t\t\t\tconst subscription = await channel.open({\n\t\t\t\t\tinput: params.input,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tsignal: params.signal\n\t\t\t\t})\n\t\t\t\ttry {\n\t\t\t\t\tlet initial: unknown\n\t\t\t\t\ttry {\n\t\t\t\t\t\tinitial = await runSnapshot(params)\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t// `fn.completed` logs it, at the level `isExpectedError` picks.\n\t\t\t\t\t\tthrow toSnapshotError(err, liveName)\n\t\t\t\t\t}\n\n\t\t\t\t\tconst rerun = (nextInput?: Record<string, unknown>) =>\n\t\t\t\t\t\trunSnapshot({ ...params, input: nextInput ?? params.input })\n\t\t\t\t\tconst apply = async (event: unknown, previous: unknown) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tlive.shouldUpdate &&\n\t\t\t\t\t\t\t!(await live.shouldUpdate({ ...params, event, previous }))\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\treturn undefined\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!live.transformerFn) return runSnapshot(params)\n\t\t\t\t\t\treturn live.transformerFn({ ...params, event, previous, rerun })\n\t\t\t\t\t}\n\n\t\t\t\t\tyield* streamLiveSnapshots({\n\t\t\t\t\t\tsource: subscription.events,\n\t\t\t\t\t\tinitial,\n\t\t\t\t\t\tcoalesceMs: live.coalesceMs,\n\t\t\t\t\t\tapply\n\t\t\t\t\t})\n\t\t\t\t} finally {\n\t\t\t\t\tsubscription.close()\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\treturn {\n\t\t\tprocedure,\n\t\t\tsubscribe,\n\t\t\tpublish,\n\t\t\tgetChannelName: channel.getChannelName\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { type FnLive, type FnLiveConfig, type FnLivePatch, type FnLiveReturn, fnLivePatch, streamLiveSnapshots, throwInitialSnapshotError } from './fn-live.js';
|
|
2
|
+
export { type AuthFn, type ChannelDefinition, type CreatePublisher, type CreatePubSub, createBoundedEventQueue, type FilterFn, type ObjectSchema, type Publisher, type PublisherConfig, type PublisherOptions, type PubSub, type PubSubOptions, type PubSubRuntimeOptions } from './pub-sub.js';
|
|
3
|
+
export { type BacklogOptions, backlogKey, type PubSubMessage, type PubSubTransport } from './transport.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/live/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EACN,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,uBAAuB,EACvB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACX,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EACN,KAAK,cAAc,EACnB,UAAU,EACV,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// `fnLive`, `createPubSub` and `createPublisher` come typed from `createFn`.
|
|
2
|
+
export { fnLivePatch, streamLiveSnapshots, throwInitialSnapshotError } from './fn-live.js';
|
|
3
|
+
export { createBoundedEventQueue } from './pub-sub.js';
|
|
4
|
+
export { backlogKey } from './transport.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/live/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,EAKN,WAAW,EACX,mBAAmB,EACnB,yBAAyB,EACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EAKN,uBAAuB,EASvB,MAAM,cAAc,CAAA;AACrB,OAAO,EAEN,UAAU,EAGV,MAAM,gBAAgB,CAAA","sourcesContent":["// `fnLive`, `createPubSub` and `createPublisher` come typed from `createFn`.\nexport {\n\ttype FnLive,\n\ttype FnLiveConfig,\n\ttype FnLivePatch,\n\ttype FnLiveReturn,\n\tfnLivePatch,\n\tstreamLiveSnapshots,\n\tthrowInitialSnapshotError\n} from './fn-live.js'\nexport {\n\ttype AuthFn,\n\ttype ChannelDefinition,\n\ttype CreatePublisher,\n\ttype CreatePubSub,\n\tcreateBoundedEventQueue,\n\ttype FilterFn,\n\ttype ObjectSchema,\n\ttype Publisher,\n\ttype PublisherConfig,\n\ttype PublisherOptions,\n\ttype PubSub,\n\ttype PubSubOptions,\n\ttype PubSubRuntimeOptions\n} from './pub-sub.js'\nexport {\n\ttype BacklogOptions,\n\tbacklogKey,\n\ttype PubSubMessage,\n\ttype PubSubTransport\n} from './transport.js'\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The part of an ioredis `Redis` client this transport uses. */
|
|
2
|
+
export interface IORedisLike {
|
|
3
|
+
call(command: string, ...args: string[]): Promise<unknown>;
|
|
4
|
+
subscribe(...channels: string[]): Promise<unknown>;
|
|
5
|
+
unsubscribe(...channels: string[]): Promise<unknown>;
|
|
6
|
+
duplicate(): IORedisLike;
|
|
7
|
+
disconnect(): void;
|
|
8
|
+
on(event: 'message', listener: (channel: string, message: string) => void): unknown;
|
|
9
|
+
on(event: 'end', listener: () => void): unknown;
|
|
10
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
11
|
+
off(event: 'message', listener: (channel: string, message: string) => void): unknown;
|
|
12
|
+
off(event: 'end', listener: () => void): unknown;
|
|
13
|
+
off(event: 'error', listener: (error: Error) => void): unknown;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `PubSubTransport` for ioredis. Subscriptions use a dedicated connection
|
|
17
|
+
* (`client.duplicate()` unless `subscriber` is given); ioredis re-subscribes
|
|
18
|
+
* after reconnects itself (`autoResubscribe`), so no `resubscribe` is needed. `onError` receives connection
|
|
19
|
+
* errors that ioredis is already retrying.
|
|
20
|
+
*
|
|
21
|
+
* Call `close()` on shutdown to disconnect the subscriber it created.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ioredisTransport(client: IORedisLike, options?: {
|
|
24
|
+
subscriber?: IORedisLike;
|
|
25
|
+
onError?: (error: Error) => void;
|
|
26
|
+
}): import("./transport.js").PubSubTransport & {
|
|
27
|
+
close(): void;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=ioredis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ioredis.d.ts","sourceRoot":"","sources":["../../src/live/ioredis.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,SAAS,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClD,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACpD,SAAS,IAAI,WAAW,CAAA;IACxB,UAAU,IAAI,IAAI,CAAA;IAClB,EAAE,CACD,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAClD,OAAO,CAAA;IACV,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IAC/C,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAA;IAC7D,GAAG,CACF,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAClD,OAAO,CAAA;IACV,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IAChD,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAA;CAC9D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,WAAW,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CAAO;;EAwC5E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createRedisTransport } from './redis.js';
|
|
2
|
+
/**
|
|
3
|
+
* `PubSubTransport` for ioredis. Subscriptions use a dedicated connection
|
|
4
|
+
* (`client.duplicate()` unless `subscriber` is given); ioredis re-subscribes
|
|
5
|
+
* after reconnects itself (`autoResubscribe`), so no `resubscribe` is needed. `onError` receives connection
|
|
6
|
+
* errors that ioredis is already retrying.
|
|
7
|
+
*
|
|
8
|
+
* Call `close()` on shutdown to disconnect the subscriber it created.
|
|
9
|
+
*/
|
|
10
|
+
export function ioredisTransport(client, options = {}) {
|
|
11
|
+
return createRedisTransport({
|
|
12
|
+
send: (command, args) => client.call(command, ...args),
|
|
13
|
+
async connectSubscriber(handlers) {
|
|
14
|
+
const owned = !options.subscriber;
|
|
15
|
+
const subscriber = options.subscriber ?? client.duplicate();
|
|
16
|
+
const onMessage = (channel, message) => handlers.onMessage(channel, message);
|
|
17
|
+
// Without a listener an ioredis 'error' event would crash the process.
|
|
18
|
+
const onError = (error) => options.onError?.(error);
|
|
19
|
+
// 'end' means ioredis stopped reconnecting.
|
|
20
|
+
const onEnd = () => handlers.onLost(new Error('Redis subscriber connection ended'));
|
|
21
|
+
const ours = new Set();
|
|
22
|
+
subscriber.on('message', onMessage);
|
|
23
|
+
subscriber.on('error', onError);
|
|
24
|
+
subscriber.on('end', onEnd);
|
|
25
|
+
return {
|
|
26
|
+
subscribe: (channel) => {
|
|
27
|
+
ours.add(channel);
|
|
28
|
+
return subscriber.subscribe(channel);
|
|
29
|
+
},
|
|
30
|
+
unsubscribe: (channel) => {
|
|
31
|
+
ours.delete(channel);
|
|
32
|
+
return subscriber.unsubscribe(channel);
|
|
33
|
+
},
|
|
34
|
+
close() {
|
|
35
|
+
// Detach first: a given subscriber outlives this transport's use.
|
|
36
|
+
subscriber.off('message', onMessage);
|
|
37
|
+
subscriber.off('end', onEnd);
|
|
38
|
+
if (owned)
|
|
39
|
+
return subscriber.disconnect();
|
|
40
|
+
subscriber.off('error', onError);
|
|
41
|
+
if (ours.size > 0) {
|
|
42
|
+
void subscriber.unsubscribe(...ours).catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=ioredis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ioredis.js","sourceRoot":"","sources":["../../src/live/ioredis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAuBjD;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC/B,MAAmB,EACnB,OAAO,GAAmE,EAAE;IAE5E,OAAO,oBAAoB,CAAC;QAC3B,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;QACtD,KAAK,CAAC,iBAAiB,CAAC,QAAQ;YAC/B,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,UAAU,CAAA;YACjC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC,SAAS,EAAE,CAAA;YAC3D,MAAM,SAAS,GAAG,CAAC,OAAe,EAAE,OAAe,EAAE,EAAE,CACtD,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YACrC,uEAAuE;YACvE,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;YAC1D,4CAA4C;YAC5C,MAAM,KAAK,GAAG,GAAG,EAAE,CAClB,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC,CAAA;YAChE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;YAC9B,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YACnC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC/B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YAC3B,OAAO;gBACN,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;oBACtB,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;oBACjB,OAAO,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;gBACrC,CAAC;gBACD,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE;oBACxB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;oBACpB,OAAO,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;gBACvC,CAAC;gBACD,KAAK;oBACJ,kEAAkE;oBAClE,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;oBACpC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;oBAC5B,IAAI,KAAK;wBAAE,OAAO,UAAU,CAAC,UAAU,EAAE,CAAA;oBACzC,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;oBAChC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;wBACnB,KAAK,UAAU,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBACrD,CAAC;gBACF,CAAC;aACD,CAAA;QACF,CAAC;KACD,CAAC,CAAA;AACH,CAAC","sourcesContent":["import { createRedisTransport } from './redis.js'\n\n/** The part of an ioredis `Redis` client this transport uses. */\nexport interface IORedisLike {\n\tcall(command: string, ...args: string[]): Promise<unknown>\n\tsubscribe(...channels: string[]): Promise<unknown>\n\tunsubscribe(...channels: string[]): Promise<unknown>\n\tduplicate(): IORedisLike\n\tdisconnect(): void\n\ton(\n\t\tevent: 'message',\n\t\tlistener: (channel: string, message: string) => void\n\t): unknown\n\ton(event: 'end', listener: () => void): unknown\n\ton(event: 'error', listener: (error: Error) => void): unknown\n\toff(\n\t\tevent: 'message',\n\t\tlistener: (channel: string, message: string) => void\n\t): unknown\n\toff(event: 'end', listener: () => void): unknown\n\toff(event: 'error', listener: (error: Error) => void): unknown\n}\n\n/**\n * `PubSubTransport` for ioredis. Subscriptions use a dedicated connection\n * (`client.duplicate()` unless `subscriber` is given); ioredis re-subscribes\n * after reconnects itself (`autoResubscribe`), so no `resubscribe` is needed. `onError` receives connection\n * errors that ioredis is already retrying.\n *\n * Call `close()` on shutdown to disconnect the subscriber it created.\n */\nexport function ioredisTransport(\n\tclient: IORedisLike,\n\toptions: { subscriber?: IORedisLike; onError?: (error: Error) => void } = {}\n) {\n\treturn createRedisTransport({\n\t\tsend: (command, args) => client.call(command, ...args),\n\t\tasync connectSubscriber(handlers) {\n\t\t\tconst owned = !options.subscriber\n\t\t\tconst subscriber = options.subscriber ?? client.duplicate()\n\t\t\tconst onMessage = (channel: string, message: string) =>\n\t\t\t\thandlers.onMessage(channel, message)\n\t\t\t// Without a listener an ioredis 'error' event would crash the process.\n\t\t\tconst onError = (error: Error) => options.onError?.(error)\n\t\t\t// 'end' means ioredis stopped reconnecting.\n\t\t\tconst onEnd = () =>\n\t\t\t\thandlers.onLost(new Error('Redis subscriber connection ended'))\n\t\t\tconst ours = new Set<string>()\n\t\t\tsubscriber.on('message', onMessage)\n\t\t\tsubscriber.on('error', onError)\n\t\t\tsubscriber.on('end', onEnd)\n\t\t\treturn {\n\t\t\t\tsubscribe: (channel) => {\n\t\t\t\t\tours.add(channel)\n\t\t\t\t\treturn subscriber.subscribe(channel)\n\t\t\t\t},\n\t\t\t\tunsubscribe: (channel) => {\n\t\t\t\t\tours.delete(channel)\n\t\t\t\t\treturn subscriber.unsubscribe(channel)\n\t\t\t\t},\n\t\t\t\tclose() {\n\t\t\t\t\t// Detach first: a given subscriber outlives this transport's use.\n\t\t\t\t\tsubscriber.off('message', onMessage)\n\t\t\t\t\tsubscriber.off('end', onEnd)\n\t\t\t\t\tif (owned) return subscriber.disconnect()\n\t\t\t\t\tsubscriber.off('error', onError)\n\t\t\t\t\tif (ours.size > 0) {\n\t\t\t\t\t\tvoid subscriber.unsubscribe(...ours).catch(() => {})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PubSubTransport } from './transport.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-process transport for tests and single-process development. Delivery is
|
|
4
|
+
* asynchronous (a microtask), like a real broker.
|
|
5
|
+
*/
|
|
6
|
+
export declare function memoryTransport(): PubSubTransport & {
|
|
7
|
+
/** Active listeners per channel, for assertions. */
|
|
8
|
+
listenerCount(channel: string): number;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/live/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAErD;;;GAGG;AACH,wBAAgB,eAAe,IAAI,eAAe,GAAG;IACpD,oDAAoD;IACpD,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;CACtC,CAwCA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process transport for tests and single-process development. Delivery is
|
|
3
|
+
* asynchronous (a microtask), like a real broker.
|
|
4
|
+
*/
|
|
5
|
+
export function memoryTransport() {
|
|
6
|
+
const listeners = new Map();
|
|
7
|
+
const backlogs = new Map();
|
|
8
|
+
return {
|
|
9
|
+
async publish(messages, backlog) {
|
|
10
|
+
for (const { channel, payload } of messages) {
|
|
11
|
+
if (backlog) {
|
|
12
|
+
const current = backlogs.get(channel);
|
|
13
|
+
const items = current && current.expiresAt > Date.now() ? current.items : [];
|
|
14
|
+
items.push(payload);
|
|
15
|
+
items.splice(0, Math.max(0, items.length - backlog.size));
|
|
16
|
+
backlogs.set(channel, {
|
|
17
|
+
items,
|
|
18
|
+
expiresAt: Date.now() + backlog.ttlSeconds * 1000
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const listener of [...(listeners.get(channel) ?? [])]) {
|
|
22
|
+
queueMicrotask(() => listener(payload));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
async readBacklog(channel) {
|
|
27
|
+
const backlog = backlogs.get(channel);
|
|
28
|
+
return backlog && backlog.expiresAt > Date.now() ? [...backlog.items] : [];
|
|
29
|
+
},
|
|
30
|
+
async subscribe(channel, listener) {
|
|
31
|
+
const set = listeners.get(channel) ?? new Set();
|
|
32
|
+
listeners.set(channel, set);
|
|
33
|
+
// Wrap so the same listener can subscribe twice and unsubscribe once.
|
|
34
|
+
const entry = (payload) => listener(payload);
|
|
35
|
+
set.add(entry);
|
|
36
|
+
return async () => {
|
|
37
|
+
set.delete(entry);
|
|
38
|
+
if (set.size === 0)
|
|
39
|
+
listeners.delete(channel);
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
listenerCount: (channel) => listeners.get(channel)?.size ?? 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/live/memory.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,eAAe;IAI9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0C,CAAA;IACnE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkD,CAAA;IAE1E,OAAO;QACN,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO;YAC9B,KAAK,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC7C,IAAI,OAAO,EAAE,CAAC;oBACb,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;oBACrC,MAAM,KAAK,GACV,OAAO,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;oBAC/D,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;oBACnB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;oBACzD,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE;wBACrB,KAAK;wBACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI;qBACjD,CAAC,CAAA;gBACH,CAAC;gBACD,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC5D,cAAc,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;gBACxC,CAAC;YACF,CAAC;QACF,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,OAAO;YACxB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACrC,OAAO,OAAO,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAC3E,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ;YAChC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;YAC/C,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;YAC3B,sEAAsE;YACtE,MAAM,KAAK,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;YACpD,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACd,OAAO,KAAK,IAAI,EAAE;gBACjB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBACjB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;oBAAE,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;YAC9C,CAAC,CAAA;QACF,CAAC;QACD,aAAa,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,IAAI,CAAC;KAC7D,CAAA;AACF,CAAC","sourcesContent":["import type { PubSubTransport } from './transport.js'\n\n/**\n * In-process transport for tests and single-process development. Delivery is\n * asynchronous (a microtask), like a real broker.\n */\nexport function memoryTransport(): PubSubTransport & {\n\t/** Active listeners per channel, for assertions. */\n\tlistenerCount(channel: string): number\n} {\n\tconst listeners = new Map<string, Set<(payload: string) => void>>()\n\tconst backlogs = new Map<string, { items: string[]; expiresAt: number }>()\n\n\treturn {\n\t\tasync publish(messages, backlog) {\n\t\t\tfor (const { channel, payload } of messages) {\n\t\t\t\tif (backlog) {\n\t\t\t\t\tconst current = backlogs.get(channel)\n\t\t\t\t\tconst items =\n\t\t\t\t\t\tcurrent && current.expiresAt > Date.now() ? current.items : []\n\t\t\t\t\titems.push(payload)\n\t\t\t\t\titems.splice(0, Math.max(0, items.length - backlog.size))\n\t\t\t\t\tbacklogs.set(channel, {\n\t\t\t\t\t\titems,\n\t\t\t\t\t\texpiresAt: Date.now() + backlog.ttlSeconds * 1000\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tfor (const listener of [...(listeners.get(channel) ?? [])]) {\n\t\t\t\t\tqueueMicrotask(() => listener(payload))\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tasync readBacklog(channel) {\n\t\t\tconst backlog = backlogs.get(channel)\n\t\t\treturn backlog && backlog.expiresAt > Date.now() ? [...backlog.items] : []\n\t\t},\n\t\tasync subscribe(channel, listener) {\n\t\t\tconst set = listeners.get(channel) ?? new Set()\n\t\t\tlisteners.set(channel, set)\n\t\t\t// Wrap so the same listener can subscribe twice and unsubscribe once.\n\t\t\tconst entry = (payload: string) => listener(payload)\n\t\t\tset.add(entry)\n\t\t\treturn async () => {\n\t\t\t\tset.delete(entry)\n\t\t\t\tif (set.size === 0) listeners.delete(channel)\n\t\t\t}\n\t\t},\n\t\tlistenerCount: (channel) => listeners.get(channel)?.size ?? 0\n\t}\n}\n"]}
|