orpc-fn 0.3.0 → 0.5.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/README.md +146 -5
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -1
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +2 -8
- package/dist/create-fn.js.map +1 -1
- package/dist/hono.d.ts +12 -1
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +15 -1
- package/dist/hono.js.map +1 -1
- package/dist/live/fn-live.d.ts +1 -6
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js +2 -18
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/index.d.ts +3 -1
- package/dist/live/index.d.ts.map +1 -1
- package/dist/live/index.js +2 -0
- package/dist/live/index.js.map +1 -1
- package/dist/live/invalidation-stream.d.ts +52 -0
- package/dist/live/invalidation-stream.d.ts.map +1 -0
- package/dist/live/invalidation-stream.js +98 -0
- package/dist/live/invalidation-stream.js.map +1 -0
- package/dist/live/ioredis.d.ts +1 -0
- package/dist/live/ioredis.d.ts.map +1 -1
- package/dist/live/ioredis.js +1 -0
- package/dist/live/ioredis.js.map +1 -1
- package/dist/live/memory.d.ts +2 -1
- package/dist/live/memory.d.ts.map +1 -1
- package/dist/live/memory.js +6 -1
- package/dist/live/memory.js.map +1 -1
- package/dist/live/pub-sub.d.ts +5 -0
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +36 -15
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/live/redis-bun.d.ts +3 -0
- package/dist/live/redis-bun.d.ts.map +1 -1
- package/dist/live/redis-bun.js +1 -0
- package/dist/live/redis-bun.js.map +1 -1
- package/dist/live/redis.d.ts +4 -1
- package/dist/live/redis.d.ts.map +1 -1
- package/dist/live/redis.js +14 -2
- package/dist/live/redis.js.map +1 -1
- package/dist/live/scoped-change-bus.d.ts +45 -0
- package/dist/live/scoped-change-bus.d.ts.map +1 -0
- package/dist/live/scoped-change-bus.js +38 -0
- package/dist/live/scoped-change-bus.js.map +1 -0
- package/dist/live/transport.d.ts +11 -0
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js.map +1 -1
- package/dist/otel.d.ts +5 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +7 -0
- package/dist/otel.js.map +1 -1
- package/dist/procedure-cli.d.ts +25 -0
- package/dist/procedure-cli.d.ts.map +1 -0
- package/dist/procedure-cli.js +67 -0
- package/dist/procedure-cli.js.map +1 -0
- package/dist/react.d.ts +116 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +156 -0
- package/dist/react.js.map +1 -0
- package/dist/realtime.d.ts +114 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +316 -0
- package/dist/realtime.js.map +1 -0
- package/dist/vue/testing.d.ts +67 -0
- package/dist/vue/testing.d.ts.map +1 -0
- package/dist/vue/testing.js +225 -0
- package/dist/vue/testing.js.map +1 -0
- package/dist/vue.d.ts +211 -0
- package/dist/vue.d.ts.map +1 -0
- package/dist/vue.js +312 -0
- package/dist/vue.js.map +1 -0
- package/package.json +51 -4
- package/src/client.ts +57 -0
- package/src/create-fn.ts +2 -7
- package/src/hono.ts +22 -2
- package/src/live/fn-live.ts +3 -22
- package/src/live/index.ts +12 -1
- package/src/live/invalidation-stream.ts +142 -0
- package/src/live/ioredis.ts +2 -0
- package/src/live/memory.ts +11 -2
- package/src/live/pub-sub.ts +48 -17
- package/src/live/redis-bun.ts +3 -0
- package/src/live/redis.ts +33 -6
- package/src/live/scoped-change-bus.ts +98 -0
- package/src/live/transport.ts +12 -0
- package/src/otel.ts +8 -0
- package/src/procedure-cli.ts +99 -0
- package/src/react.ts +355 -0
- package/src/realtime.ts +416 -0
- package/src/vue/testing.ts +296 -0
- package/src/vue.ts +783 -0
package/dist/live/redis-bun.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface BunRedisClientLike {
|
|
|
6
6
|
unsubscribe(channel: string, listener?: Listener): Promise<unknown>;
|
|
7
7
|
duplicate(): Promise<BunRedisClientLike>;
|
|
8
8
|
close(): void;
|
|
9
|
+
/** Bun's `RedisClient.connected`; read by `stats()` when present. */
|
|
10
|
+
readonly connected?: boolean;
|
|
9
11
|
onconnect: ((this: never) => void) | null;
|
|
10
12
|
onclose: ((this: never, error: Error) => void) | null;
|
|
11
13
|
}
|
|
@@ -21,6 +23,7 @@ export declare function bunRedisTransport(client: BunRedisClientLike, options?:
|
|
|
21
23
|
subscriber?: BunRedisClientLike;
|
|
22
24
|
}): import("./transport.js").PubSubTransport & {
|
|
23
25
|
close(): Promise<void>;
|
|
26
|
+
stats(): import("./transport.js").PubSubTransportStats;
|
|
24
27
|
};
|
|
25
28
|
export {};
|
|
26
29
|
//# sourceMappingURL=redis-bun.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-bun.d.ts","sourceRoot":"","sources":["../../src/live/redis-bun.ts"],"names":[],"mappings":"AAEA,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAE1D,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnE,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;CACrD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,kBAAkB,CAAA;CAAO
|
|
1
|
+
{"version":3,"file":"redis-bun.d.ts","sourceRoot":"","sources":["../../src/live/redis-bun.ts"],"names":[],"mappings":"AAEA,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAE1D,2DAA2D;AAC3D,MAAM,WAAW,kBAAkB;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACvD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAChE,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnE,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxC,KAAK,IAAI,IAAI,CAAA;IACb,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;CACrD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,kBAAkB,CAAA;CAAO;;;EAqDjD"}
|
package/dist/live/redis-bun.js
CHANGED
|
@@ -39,6 +39,7 @@ export function bunRedisTransport(client, options = {}) {
|
|
|
39
39
|
ours.delete(channel);
|
|
40
40
|
return subscriber.unsubscribe(channel, dispatch);
|
|
41
41
|
},
|
|
42
|
+
connected: () => subscriber.connected !== false,
|
|
42
43
|
close() {
|
|
43
44
|
// No-ops, not null: Bun calls `onclose` without checking it.
|
|
44
45
|
subscriber.onconnect = () => { };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-bun.js","sourceRoot":"","sources":["../../src/live/redis-bun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"redis-bun.js","sourceRoot":"","sources":["../../src/live/redis-bun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAiBjD;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAChC,MAA0B,EAC1B,OAAO,GAAwC,EAAE;IAEjD,OAAO,oBAAoB,CAAC;QAC3B,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC;QACnD,KAAK,CAAC,iBAAiB,CAAC,QAAQ;YAC/B,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,UAAU,CAAA;YACjC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;YACnE,MAAM,QAAQ,GAAa,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAC/C,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;YAC9B,wEAAwE;YACxE,2DAA2D;YAC3D,UAAU,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;YACnD,mEAAmE;YACnE,sBAAsB;YACtB,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE,CAC9B,QAAQ,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAA;YAC/D,OAAO;gBACN,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;oBACtB,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;oBACjB,OAAO,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;gBAC/C,CAAC;gBACD,sEAAsE;gBACtE,kEAAkE;gBAClE,gDAAgD;gBAChD,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;oBAC9B,MAAM,OAAO,CAAC,OAAO,EAAE;yBACrB,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;yBAC3C,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBACjB,OAAO,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;gBAC/C,CAAC;gBACD,WAAW,EAAE,CAAC,OAAO,EAAE,EAAE;oBACxB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;oBACpB,OAAO,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;gBACjD,CAAC;gBACD,SAAS,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,KAAK,KAAK;gBAC/C,KAAK;oBACJ,6DAA6D;oBAC7D,UAAU,CAAC,SAAS,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;oBAC/B,UAAU,CAAC,OAAO,GAAG,GAAG,EAAE,GAAE,CAAC,CAAA;oBAC7B,IAAI,KAAK;wBAAE,OAAO,UAAU,CAAC,KAAK,EAAE,CAAA;oBACpC,yEAAyE;oBACzE,OAAO,OAAO,CAAC,GAAG,CACjB,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CACzB,OAAO,CAAC,OAAO,EAAE;yBACf,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;yBACrD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CACjB,CACD,CAAC,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;gBACjB,CAAC;aACD,CAAA;QACF,CAAC;KACD,CAAC,CAAA;AACH,CAAC","sourcesContent":["import { createRedisTransport } from './redis.js'\n\ntype Listener = (message: string, channel: string) => void\n\n/** The part of Bun's `RedisClient` this transport uses. */\nexport interface BunRedisClientLike {\n\tsend(command: string, args: string[]): Promise<unknown>\n\tsubscribe(channel: string, listener: Listener): Promise<unknown>\n\tunsubscribe(channel: string, listener?: Listener): Promise<unknown>\n\tduplicate(): Promise<BunRedisClientLike>\n\tclose(): void\n\t/** Bun's `RedisClient.connected`; read by `stats()` when present. */\n\treadonly connected?: boolean\n\tonconnect: ((this: never) => void) | null\n\tonclose: ((this: never, error: Error) => void) | null\n}\n\n/**\n * `PubSubTransport` for Bun's built-in `RedisClient`. Subscriptions use a\n * dedicated connection (`client.duplicate()` unless `subscriber` is given)\n * that re-subscribes its channels after every reconnect; configure retries on\n * the client (`maxRetries`) since the duplicate copies its options.\n *\n * Call `close()` on shutdown to close the subscriber connection it created.\n */\nexport function bunRedisTransport(\n\tclient: BunRedisClientLike,\n\toptions: { subscriber?: BunRedisClientLike } = {}\n) {\n\treturn createRedisTransport({\n\t\tsend: (command, args) => client.send(command, args),\n\t\tasync connectSubscriber(handlers) {\n\t\t\tconst owned = !options.subscriber\n\t\t\tconst subscriber = options.subscriber ?? (await client.duplicate())\n\t\t\tconst dispatch: Listener = (message, channel) =>\n\t\t\t\thandlers.onMessage(channel, message)\n\t\t\tconst ours = new Set<string>()\n\t\t\t// Bun reconnects on its own but drops every subscription. Also fires on\n\t\t\t// the first connect, when there is nothing to restore yet.\n\t\t\tsubscriber.onconnect = () => handlers.onReconnect()\n\t\t\t// Fires only when Bun gives up reconnecting (and on close(), which\n\t\t\t// detaches it first).\n\t\t\tsubscriber.onclose = (error) =>\n\t\t\t\thandlers.onLost(error ?? new Error('Redis subscriber closed'))\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, dispatch)\n\t\t\t\t},\n\t\t\t\t// Bun keeps listeners across a reconnect and does not dedupe them, so\n\t\t\t\t// clear the channel before adding its listener again. unsubscribe\n\t\t\t\t// throws synchronously outside subscriber mode.\n\t\t\t\tresubscribe: async (channel) => {\n\t\t\t\t\tawait Promise.resolve()\n\t\t\t\t\t\t.then(() => subscriber.unsubscribe(channel))\n\t\t\t\t\t\t.catch(() => {})\n\t\t\t\t\treturn subscriber.subscribe(channel, dispatch)\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, dispatch)\n\t\t\t\t},\n\t\t\t\tconnected: () => subscriber.connected !== false,\n\t\t\t\tclose() {\n\t\t\t\t\t// No-ops, not null: Bun calls `onclose` without checking it.\n\t\t\t\t\tsubscriber.onconnect = () => {}\n\t\t\t\t\tsubscriber.onclose = () => {}\n\t\t\t\t\tif (owned) return subscriber.close()\n\t\t\t\t\t// A given subscriber stays open: remove only this transport's listeners.\n\t\t\t\t\treturn Promise.all(\n\t\t\t\t\t\t[...ours].map((channel) =>\n\t\t\t\t\t\t\tPromise.resolve()\n\t\t\t\t\t\t\t\t.then(() => subscriber.unsubscribe(channel, dispatch))\n\t\t\t\t\t\t\t\t.catch(() => {})\n\t\t\t\t\t\t)\n\t\t\t\t\t).then(() => {})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n"]}
|
package/dist/live/redis.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PubSubTransport } from './transport.js';
|
|
1
|
+
import { type PubSubTransport, type PubSubTransportStats } from './transport.js';
|
|
2
2
|
export type RedisSend = (command: string, args: string[]) => Promise<unknown>;
|
|
3
3
|
/**
|
|
4
4
|
* Ship a Lua script by SHA instead of resending its source on every call.
|
|
@@ -17,6 +17,8 @@ export type SubscriberConnection = {
|
|
|
17
17
|
resubscribe?(channel: string): Promise<unknown>;
|
|
18
18
|
/** Stop delivering and release the connection (if the driver created it). */
|
|
19
19
|
close(): void | Promise<void>;
|
|
20
|
+
/** The driver reports the connection open; omitted means open until lost. */
|
|
21
|
+
connected?(): boolean;
|
|
20
22
|
};
|
|
21
23
|
/** Callbacks for one connection; they do nothing once it is replaced. */
|
|
22
24
|
export type SubscriberHandlers = {
|
|
@@ -41,5 +43,6 @@ export declare function createRedisTransport(driver: {
|
|
|
41
43
|
connectSubscriber: (handlers: SubscriberHandlers) => Promise<SubscriberConnection>;
|
|
42
44
|
}): PubSubTransport & {
|
|
43
45
|
close(): Promise<void>;
|
|
46
|
+
stats(): PubSubTransportStats;
|
|
44
47
|
};
|
|
45
48
|
//# sourceMappingURL=redis.d.ts.map
|
package/dist/live/redis.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/live/redis.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/live/redis.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAM7E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,UAavB,MAAM,EAAE,QAAQ,MAAM,EAAE,sBAW/D;AA8BD,MAAM,MAAM,oBAAoB,GAAG;IAClC,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9C;;;OAGG;IACH,WAAW,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/C,6EAA6E;IAC7E,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,IAAI,OAAO,CAAA;CACrB,CAAA;AAED,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAAG;IAChC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACjD,kEAAkE;IAClE,WAAW,IAAI,IAAI,CAAA;IACnB,gEAAgE;IAChE,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CAC1B,CAAA;AAcD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC5C,IAAI,EAAE,SAAS,CAAA;IACf,iBAAiB,EAAE,CAClB,QAAQ,EAAE,kBAAkB,KACxB,OAAO,CAAC,oBAAoB,CAAC,CAAA;CAClC,GAAG,eAAe,GAAG;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,KAAK,IAAI,oBAAoB,CAAA;CAC7B,CAkLA"}
|
package/dist/live/redis.js
CHANGED
|
@@ -77,6 +77,8 @@ export function createRedisTransport(driver) {
|
|
|
77
77
|
// An UNSUBSCRIBE still in flight would cancel a SUBSCRIBE sent after it.
|
|
78
78
|
const unsubscribing = new Map();
|
|
79
79
|
let current = null;
|
|
80
|
+
// The open connection of the current generation, once it resolves.
|
|
81
|
+
let open = null;
|
|
80
82
|
const connect = () => {
|
|
81
83
|
let retired = false;
|
|
82
84
|
let closing;
|
|
@@ -84,8 +86,10 @@ export function createRedisTransport(driver) {
|
|
|
84
86
|
if (retired)
|
|
85
87
|
return closing ?? Promise.resolve();
|
|
86
88
|
retired = true;
|
|
87
|
-
if (current?.connection === connection)
|
|
89
|
+
if (current?.connection === connection) {
|
|
88
90
|
current = null;
|
|
91
|
+
open = null;
|
|
92
|
+
}
|
|
89
93
|
closing = connection.then((open) => open.close()).catch(() => { });
|
|
90
94
|
if (!error)
|
|
91
95
|
return closing;
|
|
@@ -141,7 +145,10 @@ export function createRedisTransport(driver) {
|
|
|
141
145
|
onLost: (error) => retire(error)
|
|
142
146
|
};
|
|
143
147
|
const connection = driver.connectSubscriber(handlers);
|
|
144
|
-
connection.
|
|
148
|
+
connection.then((resolved) => {
|
|
149
|
+
if (!retired)
|
|
150
|
+
open = resolved;
|
|
151
|
+
}, () => {
|
|
145
152
|
if (current?.connection === connection)
|
|
146
153
|
current = null;
|
|
147
154
|
});
|
|
@@ -229,6 +236,11 @@ export function createRedisTransport(driver) {
|
|
|
229
236
|
await done;
|
|
230
237
|
};
|
|
231
238
|
},
|
|
239
|
+
stats: () => ({
|
|
240
|
+
channels: channels.size,
|
|
241
|
+
listeners: [...channels.values()].reduce((sum, state) => sum + state.entries.size, 0),
|
|
242
|
+
subscriberConnected: open ? (open.connected?.() ?? true) : false
|
|
243
|
+
}),
|
|
232
244
|
async close() {
|
|
233
245
|
const closing = current?.retire();
|
|
234
246
|
channels.clear();
|
package/dist/live/redis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/live/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,UAAU,EAAwB,MAAM,gBAAgB,CAAA;AAIjE,SAAS,eAAe,CAAC,KAAc;IACtC,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAAe,EAAE,MAAc;IAC9D,IAAI,GAAG,GAAkB,IAAI,CAAA;IAC7B,IAAI,OAAO,GAA2B,IAAI,CAAA;IAE1C,SAAS,IAAI;QACZ,OAAO,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;aAC1C,IAAI,CAAC,MAAM,CAAC;aACZ,OAAO,CAAC,GAAG,EAAE;YACb,OAAO,GAAG,IAAI,CAAA;QACf,CAAC,CAAC,CAAA;QACH,OAAO,OAAO,CAAA;IACf,CAAC;IAED,OAAO,KAAK,UAAU,UAAU,CAAC,IAAc,EAAE,IAAc;QAC9D,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAA;QACpD,GAAG,KAAK,MAAM,IAAI,EAAE,CAAA;QACpB,IAAI,CAAC;YACJ,OAAO,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAA;YACxC,GAAG,GAAG,IAAI,CAAA;YACV,OAAO,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;QAC7C,CAAC;IACF,CAAC,CAAA;AACF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;CAiBnB,CAAA;AAmCD;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAKpC;IACA,MAAM,aAAa,GAAG,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;IAC/D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;IAChD,yEAAyE;IACzE,MAAM,aAAa,GAAG,IAAI,GAAG,EAA4B,CAAA;IACzD,IAAI,OAAO,GAGA,IAAI,CAAA;IAEf,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,IAAI,OAAkC,CAAA;QACtC,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;YAChC,IAAI,OAAO;gBAAE,OAAO,OAAO,IAAI,OAAO,CAAC,OAAO,EAAE,CAAA;YAChD,OAAO,GAAG,IAAI,CAAA;YACd,IAAI,OAAO,EAAE,UAAU,KAAK,UAAU;gBAAE,OAAO,GAAG,IAAI,CAAA;YACtD,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;YACjE,IAAI,CAAC,KAAK;gBAAE,OAAO,OAAO,CAAA;YAC1B,MAAM,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;YAC1E,QAAQ,CAAC,KAAK,EAAE,CAAA;YAChB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;gBAC1B,IAAI,CAAC;oBACJ,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACR,+CAA+C;gBAChD,CAAC;YACF,CAAC;YACD,OAAO,OAAO,CAAA;QACf,CAAC,CAAA;QACD,MAAM,QAAQ,GAAuB;YACpC,SAAS,CAAC,OAAO,EAAE,OAAO;gBACzB,IAAI,OAAO;oBAAE,OAAM;gBACnB,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;oBACjE,IAAI,CAAC;wBACJ,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;oBACxB,CAAC;oBAAC,MAAM,CAAC;wBACR,2CAA2C;oBAC5C,CAAC;gBACF,CAAC;YACF,CAAC;YACD,WAAW;gBACV,IAAI,OAAO;oBAAE,OAAM;gBACnB,mEAAmE;gBACnE,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC;qBAC3B,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC;qBACpC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAA;gBAC7B,KAAK,UAAU;qBACb,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CACd,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAClE;qBACA,IAAI,CAAC,GAAG,EAAE;oBACV,IAAI,OAAO;wBAAE,OAAM;oBACnB,KAAK,MAAM,OAAO,IAAI,OAAO;wBAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;4BACjE,IAAI,CAAC;gCACJ,KAAK,CAAC,WAAW,EAAE,EAAE,CAAA;4BACtB,CAAC;4BAAC,MAAM,CAAC;gCACR,mCAAmC;4BACpC,CAAC;wBACF,CAAC;gBACH,CAAC,CAAC;qBACD,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC3C,CAAC;YACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;SAChC,CAAA;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAA;QACrD,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;YACrB,IAAI,OAAO,EAAE,UAAU,KAAK,UAAU;gBAAE,OAAO,GAAG,IAAI,CAAA;QACvD,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,EAAE,CAAA;QAChD,OAAO,UAAU,CAAA;IAClB,CAAC,CAAA;IAED,OAAO;QACN,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO;YAC9B,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAA;YACxB,IAAI,CAAC,KAAK;gBAAE,OAAM;YAClB,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvC,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;gBAC5D,OAAM;YACP,CAAC;YACD,MAAM,IAAI,GAAa,EAAE,CAAA;YACzB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;YACvC,MAAM,IAAI,GAAG;gBACZ,MAAM,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,CAAC;gBAC1B,MAAM,CAAC,OAAO,EAAE,UAAU,IAAI,CAAC,CAAC;aAChC,CAAA;YACD,KAAK,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC7C,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;gBAC7B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;oBACxB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;oBACvC,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAA;oBACtB,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBACzB,CAAC;gBACD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;YACjC,CAAC;YACD,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QAChC,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,OAAO;YACxB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;gBACzC,UAAU,CAAC,OAAO,CAAC;gBACnB,GAAG;gBACH,IAAI;aACJ,CAAC,CAAA;YACF,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACrD,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO;YACjD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,UAAU,IAAI,OAAO,EAAE,CAAC,CAAA;YAC3D,MAAM,KAAK,GAAU;gBACpB,QAAQ;gBACR,MAAM;gBACN,WAAW,EAAE,OAAO,EAAE,WAAW;aACjC,CAAA;YACD,MAAM,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAChC,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACjC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACZ,MAAM,OAAO,GAAiB;oBAC7B,OAAO,EAAE,IAAI,GAAG,EAAE;oBAClB,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC;oBACpC,OAAO,EAAE,KAAK;iBACd,CAAA;gBACD,OAAO,CAAC,KAAK,CAAC,IAAI,CACjB,GAAG,EAAE;oBACJ,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;gBACvB,CAAC,EACD,GAAG,EAAE;oBACJ,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,OAAO;wBAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBAChE,CAAC,CACD,CAAA;gBACD,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;gBAC9B,KAAK,GAAG,OAAO,CAAA;YAChB,CAAC;YACD,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACxB,IAAI,CAAC;gBACJ,MAAM,KAAK,CAAC,KAAK,CAAA;YAClB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAC3B,MAAM,KAAK,CAAA;YACZ,CAAC;YACD,MAAM,UAAU,GAAG,KAAK,CAAA;YACxB,OAAO,KAAK,IAAI,EAAE;gBACjB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;oBAAE,OAAM;gBAC7C,IAAI,UAAU,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,UAAU;oBACtE,OAAM;gBACP,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBACxB,MAAM,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;oBACzD,IAAI,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI;wBAAE,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;gBACvE,CAAC,CAAC,CAAA;gBACF,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;gBAChC,MAAM,IAAI,CAAA;YACX,CAAC,CAAA;QACF,CAAC;QACD,KAAK,CAAC,KAAK;YACV,MAAM,OAAO,GAAG,OAAO,EAAE,MAAM,EAAE,CAAA;YACjC,QAAQ,CAAC,KAAK,EAAE,CAAA;YAChB,MAAM,OAAO,CAAA;YACb,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QACtD,CAAC;KACD,CAAA;AACF,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE,CAClC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA","sourcesContent":["import { errorMessageOf } from '../otel.js'\nimport { backlogKey, type PubSubTransport } from './transport.js'\n\nexport type RedisSend = (command: string, args: string[]) => Promise<unknown>\n\nfunction isNoScriptError(error: unknown) {\n\treturn errorMessageOf(error).includes('NOSCRIPT')\n}\n\n/**\n * Ship a Lua script by SHA instead of resending its source on every call.\n * Redis forgets its script cache on restart and on `SCRIPT FLUSH`, so a\n * `NOSCRIPT` reply falls back to an inline `EVAL` (which repopulates the\n * cache) and drops the stale digest.\n */\nexport function createLuaScript(send: RedisSend, source: string) {\n\tlet sha: string | null = null\n\tlet loading: Promise<string> | null = null\n\n\tfunction load() {\n\t\tloading ??= send('SCRIPT', ['LOAD', source])\n\t\t\t.then(String)\n\t\t\t.finally(() => {\n\t\t\t\tloading = null\n\t\t\t})\n\t\treturn loading\n\t}\n\n\treturn async function evalScript(keys: string[], args: string[]) {\n\t\tconst tail = [String(keys.length), ...keys, ...args]\n\t\tsha ??= await load()\n\t\ttry {\n\t\t\treturn await send('EVALSHA', [sha, ...tail])\n\t\t} catch (error) {\n\t\t\tif (!isNoScriptError(error)) throw error\n\t\t\tsha = null\n\t\t\treturn await send('EVAL', [source, ...tail])\n\t\t}\n\t}\n}\n\n/**\n * Publish payloads and maintain each channel's capped, TTL'd backlog in one\n * atomic round-trip. Separate PUBLISH/RPUSH/LTRIM/EXPIRE calls can tear (a\n * message published but missing from the backlog, or a backlog without TTL).\n *\n * KEYS = channel1, backlog1, channel2, backlog2, ...\n * ARGV[1] = max backlog size (0 = no backlog), ARGV[2] = TTL seconds,\n * ARGV[3..] = pairs of (channel slot, payload)\n */\nconst PUBLISH_LUA = `\nlocal maxSize = tonumber(ARGV[1])\nlocal ttl = tonumber(ARGV[2])\nfor i = 3, #ARGV, 2 do\n local slot = tonumber(ARGV[i]) * 2\n redis.call('PUBLISH', KEYS[slot - 1], ARGV[i + 1])\n if maxSize > 0 then\n redis.call('RPUSH', KEYS[slot], ARGV[i + 1])\n end\nend\nif maxSize > 0 then\n for slot = 2, #KEYS, 2 do\n redis.call('LTRIM', KEYS[slot], -maxSize, -1)\n redis.call('EXPIRE', KEYS[slot], ttl)\n end\nend\nreturn (#ARGV - 2) / 2\n`\n\nexport type SubscriberConnection = {\n\tsubscribe(channel: string): Promise<unknown>\n\tunsubscribe(channel: string): Promise<unknown>\n\t/**\n\t * Restore one channel after the driver reconnected on its own and lost its\n\t * subscriptions. Omit when the client re-subscribes itself (ioredis).\n\t */\n\tresubscribe?(channel: string): Promise<unknown>\n\t/** Stop delivering and release the connection (if the driver created it). */\n\tclose(): void | Promise<void>\n}\n\n/** Callbacks for one connection; they do nothing once it is replaced. */\nexport type SubscriberHandlers = {\n\tonMessage(channel: string, message: string): void\n\t/** The driver reconnected; its subscriptions must be restored. */\n\tonReconnect(): void\n\t/** The connection is gone for good; a fresh one replaces it. */\n\tonLost(error: Error): void\n}\n\ntype Entry = {\n\tlistener: (payload: string) => void\n\tonLost: ((error: Error) => void) | undefined\n\tonReconnect: (() => void) | undefined\n}\n\ntype ChannelState = {\n\tentries: Set<Entry>\n\tready: Promise<unknown>\n\tsettled: boolean\n}\n\n/**\n * Shared core of the Redis transports: one SUBSCRIBE per channel no matter\n * how many listeners, and one dedicated subscriber connection at a time.\n *\n * Each connection is a generation. Losing it (or failing to restore its\n * channels after a reconnect) retires it: the core closes it, ignores\n * anything it still emits, and tells every listener, whose resubscribe then\n * opens the next generation. Nothing from an old connection can reach the\n * channels of a new one.\n */\nexport function createRedisTransport(driver: {\n\tsend: RedisSend\n\tconnectSubscriber: (\n\t\thandlers: SubscriberHandlers\n\t) => Promise<SubscriberConnection>\n}): PubSubTransport & { close(): Promise<void> } {\n\tconst publishScript = createLuaScript(driver.send, PUBLISH_LUA)\n\tconst channels = new Map<string, ChannelState>()\n\t// An UNSUBSCRIBE still in flight would cancel a SUBSCRIBE sent after it.\n\tconst unsubscribing = new Map<string, Promise<unknown>>()\n\tlet current: {\n\t\tconnection: Promise<SubscriberConnection>\n\t\tretire: () => Promise<void>\n\t} | null = null\n\n\tconst connect = () => {\n\t\tlet retired = false\n\t\tlet closing: Promise<void> | undefined\n\t\tconst retire = (error?: Error) => {\n\t\t\tif (retired) return closing ?? Promise.resolve()\n\t\t\tretired = true\n\t\t\tif (current?.connection === connection) current = null\n\t\t\tclosing = connection.then((open) => open.close()).catch(() => {})\n\t\t\tif (!error) return closing\n\t\t\tconst lost = [...channels.values()].flatMap((state) => [...state.entries])\n\t\t\tchannels.clear()\n\t\t\tfor (const entry of lost) {\n\t\t\t\ttry {\n\t\t\t\t\tentry.onLost?.(error)\n\t\t\t\t} catch {\n\t\t\t\t\t/* One callback cannot interrupt retirement. */\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn closing\n\t\t}\n\t\tconst handlers: SubscriberHandlers = {\n\t\t\tonMessage(channel, message) {\n\t\t\t\tif (retired) return\n\t\t\t\tfor (const entry of [...(channels.get(channel)?.entries ?? [])]) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentry.listener(message)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// One listener must not starve the others.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tonReconnect() {\n\t\t\t\tif (retired) return\n\t\t\t\t// In-flight SUBSCRIBEs are excluded: they still land on their own.\n\t\t\t\tconst settled = [...channels]\n\t\t\t\t\t.filter(([, state]) => state.settled)\n\t\t\t\t\t.map(([channel]) => channel)\n\t\t\t\tvoid connection\n\t\t\t\t\t.then((open) =>\n\t\t\t\t\t\tPromise.all(settled.map((channel) => open.resubscribe?.(channel)))\n\t\t\t\t\t)\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\tif (retired) return\n\t\t\t\t\t\tfor (const channel of settled)\n\t\t\t\t\t\t\tfor (const entry of [...(channels.get(channel)?.entries ?? [])]) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tentry.onReconnect?.()\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t/* Isolate subscriber callbacks. */\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => retire(asError(error)))\n\t\t\t},\n\t\t\tonLost: (error) => retire(error)\n\t\t}\n\t\tconst connection = driver.connectSubscriber(handlers)\n\t\tconnection.catch(() => {\n\t\t\tif (current?.connection === connection) current = null\n\t\t})\n\t\tcurrent = { connection, retire: () => retire() }\n\t\treturn connection\n\t}\n\n\treturn {\n\t\tasync publish(messages, backlog) {\n\t\t\tconst [first] = messages\n\t\t\tif (!first) return\n\t\t\tif (!backlog && messages.length === 1) {\n\t\t\t\tawait driver.send('PUBLISH', [first.channel, first.payload])\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst keys: string[] = []\n\t\t\tconst slots = new Map<string, number>()\n\t\t\tconst args = [\n\t\t\t\tString(backlog?.size ?? 0),\n\t\t\t\tString(backlog?.ttlSeconds ?? 0)\n\t\t\t]\n\t\t\tfor (const { channel, payload } of messages) {\n\t\t\t\tlet slot = slots.get(channel)\n\t\t\t\tif (slot === undefined) {\n\t\t\t\t\tkeys.push(channel, backlogKey(channel))\n\t\t\t\t\tslot = keys.length / 2\n\t\t\t\t\tslots.set(channel, slot)\n\t\t\t\t}\n\t\t\t\targs.push(String(slot), payload)\n\t\t\t}\n\t\t\tawait publishScript(keys, args)\n\t\t},\n\t\tasync readBacklog(channel) {\n\t\t\tconst items = await driver.send('LRANGE', [\n\t\t\t\tbacklogKey(channel),\n\t\t\t\t'0',\n\t\t\t\t'-1'\n\t\t\t])\n\t\t\treturn Array.isArray(items) ? items.map(String) : []\n\t\t},\n\t\tasync subscribe(channel, listener, onLost, options) {\n\t\t\tconst connection = await (current?.connection ?? connect())\n\t\t\tconst entry: Entry = {\n\t\t\t\tlistener,\n\t\t\t\tonLost,\n\t\t\t\tonReconnect: options?.onReconnect\n\t\t\t}\n\t\t\tawait unsubscribing.get(channel)\n\t\t\tlet state = channels.get(channel)\n\t\t\tif (!state) {\n\t\t\t\tconst created: ChannelState = {\n\t\t\t\t\tentries: new Set(),\n\t\t\t\t\tready: connection.subscribe(channel),\n\t\t\t\t\tsettled: false\n\t\t\t\t}\n\t\t\t\tcreated.ready.then(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tcreated.settled = true\n\t\t\t\t\t},\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (channels.get(channel) === created) channels.delete(channel)\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t\tchannels.set(channel, created)\n\t\t\t\tstate = created\n\t\t\t}\n\t\t\tstate.entries.add(entry)\n\t\t\ttry {\n\t\t\t\tawait state.ready\n\t\t\t} catch (error) {\n\t\t\t\tstate.entries.delete(entry)\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tconst subscribed = state\n\t\t\treturn async () => {\n\t\t\t\tif (!subscribed.entries.delete(entry)) return\n\t\t\t\tif (subscribed.entries.size > 0 || channels.get(channel) !== subscribed)\n\t\t\t\t\treturn\n\t\t\t\tchannels.delete(channel)\n\t\t\t\tconst done = connection.unsubscribe(channel).finally(() => {\n\t\t\t\t\tif (unsubscribing.get(channel) === done) unsubscribing.delete(channel)\n\t\t\t\t})\n\t\t\t\tunsubscribing.set(channel, done)\n\t\t\t\tawait done\n\t\t\t}\n\t\t},\n\t\tasync close() {\n\t\t\tconst closing = current?.retire()\n\t\t\tchannels.clear()\n\t\t\tawait closing\n\t\t\tawait Promise.allSettled([...unsubscribing.values()])\n\t\t}\n\t}\n}\n\nconst asError = (error: unknown) =>\n\terror instanceof Error ? error : new Error(errorMessageOf(error))\n"]}
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/live/redis.ts"],"names":[],"mappings":"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","sourcesContent":["import { errorMessageOf } from '../otel.js'\nimport {\n\tbacklogKey,\n\ttype PubSubTransport,\n\ttype PubSubTransportStats\n} from './transport.js'\n\nexport type RedisSend = (command: string, args: string[]) => Promise<unknown>\n\nfunction isNoScriptError(error: unknown) {\n\treturn errorMessageOf(error).includes('NOSCRIPT')\n}\n\n/**\n * Ship a Lua script by SHA instead of resending its source on every call.\n * Redis forgets its script cache on restart and on `SCRIPT FLUSH`, so a\n * `NOSCRIPT` reply falls back to an inline `EVAL` (which repopulates the\n * cache) and drops the stale digest.\n */\nexport function createLuaScript(send: RedisSend, source: string) {\n\tlet sha: string | null = null\n\tlet loading: Promise<string> | null = null\n\n\tfunction load() {\n\t\tloading ??= send('SCRIPT', ['LOAD', source])\n\t\t\t.then(String)\n\t\t\t.finally(() => {\n\t\t\t\tloading = null\n\t\t\t})\n\t\treturn loading\n\t}\n\n\treturn async function evalScript(keys: string[], args: string[]) {\n\t\tconst tail = [String(keys.length), ...keys, ...args]\n\t\tsha ??= await load()\n\t\ttry {\n\t\t\treturn await send('EVALSHA', [sha, ...tail])\n\t\t} catch (error) {\n\t\t\tif (!isNoScriptError(error)) throw error\n\t\t\tsha = null\n\t\t\treturn await send('EVAL', [source, ...tail])\n\t\t}\n\t}\n}\n\n/**\n * Publish payloads and maintain each channel's capped, TTL'd backlog in one\n * atomic round-trip. Separate PUBLISH/RPUSH/LTRIM/EXPIRE calls can tear (a\n * message published but missing from the backlog, or a backlog without TTL).\n *\n * KEYS = channel1, backlog1, channel2, backlog2, ...\n * ARGV[1] = max backlog size (0 = no backlog), ARGV[2] = TTL seconds,\n * ARGV[3..] = pairs of (channel slot, payload)\n */\nconst PUBLISH_LUA = `\nlocal maxSize = tonumber(ARGV[1])\nlocal ttl = tonumber(ARGV[2])\nfor i = 3, #ARGV, 2 do\n local slot = tonumber(ARGV[i]) * 2\n redis.call('PUBLISH', KEYS[slot - 1], ARGV[i + 1])\n if maxSize > 0 then\n redis.call('RPUSH', KEYS[slot], ARGV[i + 1])\n end\nend\nif maxSize > 0 then\n for slot = 2, #KEYS, 2 do\n redis.call('LTRIM', KEYS[slot], -maxSize, -1)\n redis.call('EXPIRE', KEYS[slot], ttl)\n end\nend\nreturn (#ARGV - 2) / 2\n`\n\nexport type SubscriberConnection = {\n\tsubscribe(channel: string): Promise<unknown>\n\tunsubscribe(channel: string): Promise<unknown>\n\t/**\n\t * Restore one channel after the driver reconnected on its own and lost its\n\t * subscriptions. Omit when the client re-subscribes itself (ioredis).\n\t */\n\tresubscribe?(channel: string): Promise<unknown>\n\t/** Stop delivering and release the connection (if the driver created it). */\n\tclose(): void | Promise<void>\n\t/** The driver reports the connection open; omitted means open until lost. */\n\tconnected?(): boolean\n}\n\n/** Callbacks for one connection; they do nothing once it is replaced. */\nexport type SubscriberHandlers = {\n\tonMessage(channel: string, message: string): void\n\t/** The driver reconnected; its subscriptions must be restored. */\n\tonReconnect(): void\n\t/** The connection is gone for good; a fresh one replaces it. */\n\tonLost(error: Error): void\n}\n\ntype Entry = {\n\tlistener: (payload: string) => void\n\tonLost: ((error: Error) => void) | undefined\n\tonReconnect: (() => void) | undefined\n}\n\ntype ChannelState = {\n\tentries: Set<Entry>\n\tready: Promise<unknown>\n\tsettled: boolean\n}\n\n/**\n * Shared core of the Redis transports: one SUBSCRIBE per channel no matter\n * how many listeners, and one dedicated subscriber connection at a time.\n *\n * Each connection is a generation. Losing it (or failing to restore its\n * channels after a reconnect) retires it: the core closes it, ignores\n * anything it still emits, and tells every listener, whose resubscribe then\n * opens the next generation. Nothing from an old connection can reach the\n * channels of a new one.\n */\nexport function createRedisTransport(driver: {\n\tsend: RedisSend\n\tconnectSubscriber: (\n\t\thandlers: SubscriberHandlers\n\t) => Promise<SubscriberConnection>\n}): PubSubTransport & {\n\tclose(): Promise<void>\n\tstats(): PubSubTransportStats\n} {\n\tconst publishScript = createLuaScript(driver.send, PUBLISH_LUA)\n\tconst channels = new Map<string, ChannelState>()\n\t// An UNSUBSCRIBE still in flight would cancel a SUBSCRIBE sent after it.\n\tconst unsubscribing = new Map<string, Promise<unknown>>()\n\tlet current: {\n\t\tconnection: Promise<SubscriberConnection>\n\t\tretire: () => Promise<void>\n\t} | null = null\n\t// The open connection of the current generation, once it resolves.\n\tlet open: SubscriberConnection | null = null\n\n\tconst connect = () => {\n\t\tlet retired = false\n\t\tlet closing: Promise<void> | undefined\n\t\tconst retire = (error?: Error) => {\n\t\t\tif (retired) return closing ?? Promise.resolve()\n\t\t\tretired = true\n\t\t\tif (current?.connection === connection) {\n\t\t\t\tcurrent = null\n\t\t\t\topen = null\n\t\t\t}\n\t\t\tclosing = connection.then((open) => open.close()).catch(() => {})\n\t\t\tif (!error) return closing\n\t\t\tconst lost = [...channels.values()].flatMap((state) => [...state.entries])\n\t\t\tchannels.clear()\n\t\t\tfor (const entry of lost) {\n\t\t\t\ttry {\n\t\t\t\t\tentry.onLost?.(error)\n\t\t\t\t} catch {\n\t\t\t\t\t/* One callback cannot interrupt retirement. */\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn closing\n\t\t}\n\t\tconst handlers: SubscriberHandlers = {\n\t\t\tonMessage(channel, message) {\n\t\t\t\tif (retired) return\n\t\t\t\tfor (const entry of [...(channels.get(channel)?.entries ?? [])]) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tentry.listener(message)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// One listener must not starve the others.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tonReconnect() {\n\t\t\t\tif (retired) return\n\t\t\t\t// In-flight SUBSCRIBEs are excluded: they still land on their own.\n\t\t\t\tconst settled = [...channels]\n\t\t\t\t\t.filter(([, state]) => state.settled)\n\t\t\t\t\t.map(([channel]) => channel)\n\t\t\t\tvoid connection\n\t\t\t\t\t.then((open) =>\n\t\t\t\t\t\tPromise.all(settled.map((channel) => open.resubscribe?.(channel)))\n\t\t\t\t\t)\n\t\t\t\t\t.then(() => {\n\t\t\t\t\t\tif (retired) return\n\t\t\t\t\t\tfor (const channel of settled)\n\t\t\t\t\t\t\tfor (const entry of [...(channels.get(channel)?.entries ?? [])]) {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tentry.onReconnect?.()\n\t\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t\t/* Isolate subscriber callbacks. */\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t\t.catch((error) => retire(asError(error)))\n\t\t\t},\n\t\t\tonLost: (error) => retire(error)\n\t\t}\n\t\tconst connection = driver.connectSubscriber(handlers)\n\t\tconnection.then(\n\t\t\t(resolved) => {\n\t\t\t\tif (!retired) open = resolved\n\t\t\t},\n\t\t\t() => {\n\t\t\t\tif (current?.connection === connection) current = null\n\t\t\t}\n\t\t)\n\t\tcurrent = { connection, retire: () => retire() }\n\t\treturn connection\n\t}\n\n\treturn {\n\t\tasync publish(messages, backlog) {\n\t\t\tconst [first] = messages\n\t\t\tif (!first) return\n\t\t\tif (!backlog && messages.length === 1) {\n\t\t\t\tawait driver.send('PUBLISH', [first.channel, first.payload])\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst keys: string[] = []\n\t\t\tconst slots = new Map<string, number>()\n\t\t\tconst args = [\n\t\t\t\tString(backlog?.size ?? 0),\n\t\t\t\tString(backlog?.ttlSeconds ?? 0)\n\t\t\t]\n\t\t\tfor (const { channel, payload } of messages) {\n\t\t\t\tlet slot = slots.get(channel)\n\t\t\t\tif (slot === undefined) {\n\t\t\t\t\tkeys.push(channel, backlogKey(channel))\n\t\t\t\t\tslot = keys.length / 2\n\t\t\t\t\tslots.set(channel, slot)\n\t\t\t\t}\n\t\t\t\targs.push(String(slot), payload)\n\t\t\t}\n\t\t\tawait publishScript(keys, args)\n\t\t},\n\t\tasync readBacklog(channel) {\n\t\t\tconst items = await driver.send('LRANGE', [\n\t\t\t\tbacklogKey(channel),\n\t\t\t\t'0',\n\t\t\t\t'-1'\n\t\t\t])\n\t\t\treturn Array.isArray(items) ? items.map(String) : []\n\t\t},\n\t\tasync subscribe(channel, listener, onLost, options) {\n\t\t\tconst connection = await (current?.connection ?? connect())\n\t\t\tconst entry: Entry = {\n\t\t\t\tlistener,\n\t\t\t\tonLost,\n\t\t\t\tonReconnect: options?.onReconnect\n\t\t\t}\n\t\t\tawait unsubscribing.get(channel)\n\t\t\tlet state = channels.get(channel)\n\t\t\tif (!state) {\n\t\t\t\tconst created: ChannelState = {\n\t\t\t\t\tentries: new Set(),\n\t\t\t\t\tready: connection.subscribe(channel),\n\t\t\t\t\tsettled: false\n\t\t\t\t}\n\t\t\t\tcreated.ready.then(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tcreated.settled = true\n\t\t\t\t\t},\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (channels.get(channel) === created) channels.delete(channel)\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t\tchannels.set(channel, created)\n\t\t\t\tstate = created\n\t\t\t}\n\t\t\tstate.entries.add(entry)\n\t\t\ttry {\n\t\t\t\tawait state.ready\n\t\t\t} catch (error) {\n\t\t\t\tstate.entries.delete(entry)\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tconst subscribed = state\n\t\t\treturn async () => {\n\t\t\t\tif (!subscribed.entries.delete(entry)) return\n\t\t\t\tif (subscribed.entries.size > 0 || channels.get(channel) !== subscribed)\n\t\t\t\t\treturn\n\t\t\t\tchannels.delete(channel)\n\t\t\t\tconst done = connection.unsubscribe(channel).finally(() => {\n\t\t\t\t\tif (unsubscribing.get(channel) === done) unsubscribing.delete(channel)\n\t\t\t\t})\n\t\t\t\tunsubscribing.set(channel, done)\n\t\t\t\tawait done\n\t\t\t}\n\t\t},\n\t\tstats: () => ({\n\t\t\tchannels: channels.size,\n\t\t\tlisteners: [...channels.values()].reduce(\n\t\t\t\t(sum, state) => sum + state.entries.size,\n\t\t\t\t0\n\t\t\t),\n\t\t\tsubscriberConnected: open ? (open.connected?.() ?? true) : false\n\t\t}),\n\t\tasync close() {\n\t\t\tconst closing = current?.retire()\n\t\t\tchannels.clear()\n\t\t\tawait closing\n\t\t\tawait Promise.allSettled([...unsubscribing.values()])\n\t\t}\n\t}\n}\n\nconst asError = (error: unknown) =>\n\terror instanceof Error ? error : new Error(errorMessageOf(error))\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ZodType, z } from 'zod';
|
|
2
|
+
import type { AnyFnContext, FnDefinition, FnRouteOptions, GuardOptions, ProcedureKey, ProcedureOption } from '../types.js';
|
|
3
|
+
import type { CreatePubSub, ObjectSchema, PubSub, PubSubOptions } from './pub-sub.js';
|
|
4
|
+
/** Where a change bus partitions its channels, e.g. one per organization. */
|
|
5
|
+
export type ChangeBusScope<TContext, TEvent> = {
|
|
6
|
+
/** The subscriber's scope key, from its context. Throw to refuse the subscription. */
|
|
7
|
+
subscribe: (context: TContext) => string;
|
|
8
|
+
/** The event's scope key, carried in the event itself. */
|
|
9
|
+
publish: (event: TEvent) => string;
|
|
10
|
+
};
|
|
11
|
+
/** A bus's channel definition; pass it to `fnLive` to share the bus's events. */
|
|
12
|
+
export type ChangeBusChannel<TContext, TEvent> = {
|
|
13
|
+
subscribe: (params: {
|
|
14
|
+
context: TContext;
|
|
15
|
+
}) => string;
|
|
16
|
+
publish: (event: TEvent) => string;
|
|
17
|
+
};
|
|
18
|
+
export type ScopedChangeBus<TDef extends FnDefinition, TInputSchema extends ObjectSchema, TEventSchema extends ZodType, TKey extends ProcedureKey<TDef>, TScopeEvent> = PubSub<TDef, TInputSchema, TEventSchema, TKey> & {
|
|
19
|
+
channel: ChangeBusChannel<AnyFnContext<TDef>, TScopeEvent>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Binds the app's `createPubSub` to one scope rule, for buses that tell every
|
|
23
|
+
* subscriber in a scope "something changed". Each bus then names only its
|
|
24
|
+
* channel prefix: events go to `${prefix}:${scope}`. Publishing is safe by
|
|
25
|
+
* default (`safePublish: true`), because buses publish after committed writes.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const defineChangeBus = defineScopedChangeBus(createPubSub, {
|
|
29
|
+
* subscribe: (context) => activeOrganizationId(context),
|
|
30
|
+
* publish: (event: { organizationId: string }) => event.organizationId
|
|
31
|
+
* })
|
|
32
|
+
* export const dealChanges = defineChangeBus({
|
|
33
|
+
* name: 'deal.subscribe',
|
|
34
|
+
* prefix: 'deal:change',
|
|
35
|
+
* inputSchema: z.object({}),
|
|
36
|
+
* eventSchema: z.object({ organizationId: z.string(), dealId: z.string() })
|
|
37
|
+
* })
|
|
38
|
+
* // fnLive({ live: { channel: dealChanges.channel, ... } }) shares the channel.
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function defineScopedChangeBus<TDef extends FnDefinition, TScopeEvent>(createPubSub: CreatePubSub<TDef>, scope: ChangeBusScope<AnyFnContext<TDef>, TScopeEvent>): <TInputSchema extends ObjectSchema, TEventSchema extends ZodType<TScopeEvent>, TKey extends ProcedureKey<TDef> = TDef['default']>(options: Omit<PubSubOptions<TDef, TInputSchema, TEventSchema, NoInfer<TKey>>, 'channel'> & GuardOptions<TDef['guards']> & Pick<FnRouteOptions<TDef, TKey, z.output<TInputSchema>>, 'meta' | 'guardResolvers'> & ProcedureOption<TDef, TKey> & {
|
|
42
|
+
/** Channel prefix; the scope key is appended as `${prefix}:${scope}`. */
|
|
43
|
+
prefix: string;
|
|
44
|
+
}) => ScopedChangeBus<TDef, TInputSchema, TEventSchema, TKey, TScopeEvent>;
|
|
45
|
+
//# sourceMappingURL=scoped-change-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-change-bus.d.ts","sourceRoot":"","sources":["../../src/live/scoped-change-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACrC,OAAO,KAAK,EACX,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACX,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,aAAa,EACb,MAAM,cAAc,CAAA;AAErB,6EAA6E;AAC7E,MAAM,MAAM,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI;IAC9C,sFAAsF;IACtF,SAAS,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,CAAA;IACxC,0DAA0D;IAC1D,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,CAAC,QAAQ,EAAE,MAAM,IAAI;IAChD,SAAS,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,QAAQ,CAAA;KAAE,KAAK,MAAM,CAAA;IACpD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,eAAe,CAC1B,IAAI,SAAS,YAAY,EACzB,YAAY,SAAS,YAAY,EACjC,YAAY,SAAS,OAAO,EAC5B,IAAI,SAAS,YAAY,CAAC,IAAI,CAAC,EAC/B,WAAW,IACR,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG;IACpD,OAAO,EAAE,gBAAgB,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,CAAA;CAC1D,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,SAAS,YAAY,EAAE,WAAW,EAC3E,YAAY,EAAE,YAAY,CAAC,IAAI,CAAC,EAChC,KAAK,EAAE,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,IAGrD,YAAY,SAAS,YAAY,EACjC,YAAY,SAAS,OAAO,CAAC,WAAW,CAAC,EACzC,IAAI,SAAS,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,WAExC,IAAI,CACZ,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAC9D,SAAS,CACT,GACA,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAC5B,IAAI,CACH,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,EAClD,MAAM,GAAG,gBAAgB,CACzB,GACD,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;CACd,KACA,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,CAAC,CAevE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binds the app's `createPubSub` to one scope rule, for buses that tell every
|
|
3
|
+
* subscriber in a scope "something changed". Each bus then names only its
|
|
4
|
+
* channel prefix: events go to `${prefix}:${scope}`. Publishing is safe by
|
|
5
|
+
* default (`safePublish: true`), because buses publish after committed writes.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const defineChangeBus = defineScopedChangeBus(createPubSub, {
|
|
9
|
+
* subscribe: (context) => activeOrganizationId(context),
|
|
10
|
+
* publish: (event: { organizationId: string }) => event.organizationId
|
|
11
|
+
* })
|
|
12
|
+
* export const dealChanges = defineChangeBus({
|
|
13
|
+
* name: 'deal.subscribe',
|
|
14
|
+
* prefix: 'deal:change',
|
|
15
|
+
* inputSchema: z.object({}),
|
|
16
|
+
* eventSchema: z.object({ organizationId: z.string(), dealId: z.string() })
|
|
17
|
+
* })
|
|
18
|
+
* // fnLive({ live: { channel: dealChanges.channel, ... } }) shares the channel.
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function defineScopedChangeBus(createPubSub, scope) {
|
|
22
|
+
return (options) => {
|
|
23
|
+
const { prefix, ...pubsubOptions } = options;
|
|
24
|
+
const channel = {
|
|
25
|
+
subscribe: ({ context }) => `${prefix}:${scope.subscribe(context)}`,
|
|
26
|
+
publish: (event) => `${prefix}:${scope.publish(event)}`
|
|
27
|
+
};
|
|
28
|
+
// The bus's context is one member of AnyFnContext, which the generic
|
|
29
|
+
// signature of createPubSub cannot see.
|
|
30
|
+
const bus = createPubSub({
|
|
31
|
+
safePublish: true,
|
|
32
|
+
...pubsubOptions,
|
|
33
|
+
channel
|
|
34
|
+
});
|
|
35
|
+
return { ...bus, channel };
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=scoped-change-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-change-bus.js","sourceRoot":"","sources":["../../src/live/scoped-change-bus.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB,CACpC,YAAgC,EAChC,KAAsD;IAEtD,OAAO,CAKN,OAYE,EACqE,EAAE;QACzE,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,GAAG,OAAO,CAAA;QAC5C,MAAM,OAAO,GAAsD;YAClE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE;YACnE,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE;SACvD,CAAA;QACD,qEAAqE;QACrE,wCAAwC;QACxC,MAAM,GAAG,GAAI,YAA4C,CAAC;YACzD,WAAW,EAAE,IAAI;YACjB,GAAG,aAAa;YAChB,OAAO;SACP,CAAmD,CAAA;QACpD,OAAO,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,CAAA;IAC3B,CAAC,CAAA;AACF,CAAC","sourcesContent":["import type { ZodType, z } from 'zod'\nimport type {\n\tAnyFnContext,\n\tFnDefinition,\n\tFnRouteOptions,\n\tGuardOptions,\n\tProcedureKey,\n\tProcedureOption\n} from '../types.js'\nimport type {\n\tCreatePubSub,\n\tObjectSchema,\n\tPubSub,\n\tPubSubOptions\n} from './pub-sub.js'\n\n/** Where a change bus partitions its channels, e.g. one per organization. */\nexport type ChangeBusScope<TContext, TEvent> = {\n\t/** The subscriber's scope key, from its context. Throw to refuse the subscription. */\n\tsubscribe: (context: TContext) => string\n\t/** The event's scope key, carried in the event itself. */\n\tpublish: (event: TEvent) => string\n}\n\n/** A bus's channel definition; pass it to `fnLive` to share the bus's events. */\nexport type ChangeBusChannel<TContext, TEvent> = {\n\tsubscribe: (params: { context: TContext }) => string\n\tpublish: (event: TEvent) => string\n}\n\nexport type ScopedChangeBus<\n\tTDef extends FnDefinition,\n\tTInputSchema extends ObjectSchema,\n\tTEventSchema extends ZodType,\n\tTKey extends ProcedureKey<TDef>,\n\tTScopeEvent\n> = PubSub<TDef, TInputSchema, TEventSchema, TKey> & {\n\tchannel: ChangeBusChannel<AnyFnContext<TDef>, TScopeEvent>\n}\n\n/**\n * Binds the app's `createPubSub` to one scope rule, for buses that tell every\n * subscriber in a scope \"something changed\". Each bus then names only its\n * channel prefix: events go to `${prefix}:${scope}`. Publishing is safe by\n * default (`safePublish: true`), because buses publish after committed writes.\n *\n * ```ts\n * const defineChangeBus = defineScopedChangeBus(createPubSub, {\n * subscribe: (context) => activeOrganizationId(context),\n * publish: (event: { organizationId: string }) => event.organizationId\n * })\n * export const dealChanges = defineChangeBus({\n * name: 'deal.subscribe',\n * prefix: 'deal:change',\n * inputSchema: z.object({}),\n * eventSchema: z.object({ organizationId: z.string(), dealId: z.string() })\n * })\n * // fnLive({ live: { channel: dealChanges.channel, ... } }) shares the channel.\n * ```\n */\nexport function defineScopedChangeBus<TDef extends FnDefinition, TScopeEvent>(\n\tcreatePubSub: CreatePubSub<TDef>,\n\tscope: ChangeBusScope<AnyFnContext<TDef>, TScopeEvent>\n) {\n\treturn <\n\t\tTInputSchema extends ObjectSchema,\n\t\tTEventSchema extends ZodType<TScopeEvent>,\n\t\tTKey extends ProcedureKey<TDef> = TDef['default']\n\t>(\n\t\toptions: Omit<\n\t\t\tPubSubOptions<TDef, TInputSchema, TEventSchema, NoInfer<TKey>>,\n\t\t\t'channel'\n\t\t> &\n\t\t\tGuardOptions<TDef['guards']> &\n\t\t\tPick<\n\t\t\t\tFnRouteOptions<TDef, TKey, z.output<TInputSchema>>,\n\t\t\t\t'meta' | 'guardResolvers'\n\t\t\t> &\n\t\t\tProcedureOption<TDef, TKey> & {\n\t\t\t\t/** Channel prefix; the scope key is appended as `${prefix}:${scope}`. */\n\t\t\t\tprefix: string\n\t\t\t}\n\t): ScopedChangeBus<TDef, TInputSchema, TEventSchema, TKey, TScopeEvent> => {\n\t\tconst { prefix, ...pubsubOptions } = options\n\t\tconst channel: ChangeBusChannel<AnyFnContext<TDef>, TScopeEvent> = {\n\t\t\tsubscribe: ({ context }) => `${prefix}:${scope.subscribe(context)}`,\n\t\t\tpublish: (event) => `${prefix}:${scope.publish(event)}`\n\t\t}\n\t\t// The bus's context is one member of AnyFnContext, which the generic\n\t\t// signature of createPubSub cannot see.\n\t\tconst bus = (createPubSub as (options: object) => object)({\n\t\t\tsafePublish: true,\n\t\t\t...pubsubOptions,\n\t\t\tchannel\n\t\t}) as PubSub<TDef, TInputSchema, TEventSchema, TKey>\n\t\treturn { ...bus, channel }\n\t}\n}\n"]}
|
package/dist/live/transport.d.ts
CHANGED
|
@@ -3,6 +3,15 @@ export type PubSubMessage = {
|
|
|
3
3
|
channel: string;
|
|
4
4
|
payload: string;
|
|
5
5
|
};
|
|
6
|
+
/** What a transport holds open on this instance, for gauges and health checks. */
|
|
7
|
+
export type PubSubTransportStats = {
|
|
8
|
+
/** Broker channels subscribed (one per channel, however many listeners). */
|
|
9
|
+
channels: number;
|
|
10
|
+
/** Local listeners across those channels. */
|
|
11
|
+
listeners: number;
|
|
12
|
+
/** The subscriber connection is open; false before the first subscribe. */
|
|
13
|
+
subscriberConnected: boolean;
|
|
14
|
+
};
|
|
6
15
|
export type BacklogOptions = {
|
|
7
16
|
/** Items kept per channel (oldest dropped). */
|
|
8
17
|
size: number;
|
|
@@ -34,6 +43,8 @@ export interface PubSubTransport {
|
|
|
34
43
|
subscribe(channel: string, listener: (payload: string) => void, onLost?: (error: Error) => void, options?: {
|
|
35
44
|
onReconnect?: () => void;
|
|
36
45
|
}): Promise<() => Promise<void>>;
|
|
46
|
+
/** Current subscription counts and connection state. */
|
|
47
|
+
stats?(): PubSubTransportStats;
|
|
37
48
|
}
|
|
38
49
|
export declare const backlogKey: (channel: string) => string;
|
|
39
50
|
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/live/transport.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE,MAAM,MAAM,cAAc,GAAG;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;;OAIG;IACH,OAAO,CACN,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,2CAA2C;IAC3C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC/C;;;;OAIG;IACH,SAAS,CACR,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACnC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAC/B,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,GACpC,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/live/transport.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhE,kFAAkF;AAClF,MAAM,MAAM,oBAAoB,GAAG;IAClC,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,mBAAmB,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;;OAIG;IACH,OAAO,CACN,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,OAAO,CAAC,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAAA;IAChB,2CAA2C;IAC3C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC/C;;;;OAIG;IACH,SAAS,CACR,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EACnC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,EAC/B,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,GACpC,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,wDAAwD;IACxD,KAAK,CAAC,IAAI,oBAAoB,CAAA;CAC9B;AAED,eAAO,MAAM,UAAU,YAAa,MAAM,WAAyB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/live/transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/live/transport.ts"],"names":[],"mappings":"AAuDA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,GAAG,OAAO,UAAU,CAAA","sourcesContent":["/** One payload for one channel. */\nexport type PubSubMessage = { channel: string; payload: string }\n\n/** What a transport holds open on this instance, for gauges and health checks. */\nexport type PubSubTransportStats = {\n\t/** Broker channels subscribed (one per channel, however many listeners). */\n\tchannels: number\n\t/** Local listeners across those channels. */\n\tlisteners: number\n\t/** The subscriber connection is open; false before the first subscribe. */\n\tsubscriberConnected: boolean\n}\n\nexport type BacklogOptions = {\n\t/** Items kept per channel (oldest dropped). */\n\tsize: number\n\t/** Seconds the backlog lives after its last write. */\n\tttlSeconds: number\n}\n\n/**\n * The broker behind `createPubSub` / `fnLive`. Implementations:\n * `orpc-fn/live/redis-bun`, `orpc-fn/live/ioredis` and `orpc-fn/live/memory`.\n *\n * The backlog of a channel lives at `${channel}:backlog` (see {@link backlogKey}).\n */\nexport interface PubSubTransport {\n\t/** Optional disposal, used only with ownsTransport: true. */\n\tclose?: () => void | Promise<void>\n\t/**\n\t * Publish every message, in order. With `backlog`, also append each payload\n\t * to its channel's backlog, trim and refresh its TTL - atomically, so a\n\t * message is never published without its backlog entry or vice versa.\n\t */\n\tpublish(\n\t\tmessages: readonly PubSubMessage[],\n\t\tbacklog?: BacklogOptions\n\t): Promise<void>\n\t/** The channel's backlog, oldest first. */\n\treadBacklog(channel: string): Promise<string[]>\n\t/**\n\t * Deliver every payload published to `channel` until the returned function\n\t * is called. `onLost` fires when the subscription can no longer deliver\n\t * (connection closed for good); the caller resubscribes.\n\t */\n\tsubscribe(\n\t\tchannel: string,\n\t\tlistener: (payload: string) => void,\n\t\tonLost?: (error: Error) => void,\n\t\toptions?: { onReconnect?: () => void }\n\t): Promise<() => Promise<void>>\n\t/** Current subscription counts and connection state. */\n\tstats?(): PubSubTransportStats\n}\n\nexport const backlogKey = (channel: string) => `${channel}:backlog`\n"]}
|
package/dist/otel.d.ts
CHANGED
|
@@ -62,5 +62,10 @@ export type Tracing = {
|
|
|
62
62
|
};
|
|
63
63
|
export declare function createTracing(otel: OtelApiLike | undefined): Tracing;
|
|
64
64
|
export declare function errorMessageOf(error: unknown): string;
|
|
65
|
+
/** Error fields for a log line, named like the `fn.completed` ones. */
|
|
66
|
+
export declare function errorAttributes(error: unknown): {
|
|
67
|
+
error_type: string;
|
|
68
|
+
error_message: string;
|
|
69
|
+
};
|
|
65
70
|
export {};
|
|
66
71
|
//# sourceMappingURL=otel.d.ts.map
|
package/dist/otel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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"}
|
package/dist/otel.js
CHANGED
|
@@ -112,6 +112,13 @@ export function createTracing(otel) {
|
|
|
112
112
|
export function errorMessageOf(error) {
|
|
113
113
|
return error instanceof Error ? error.message : String(error);
|
|
114
114
|
}
|
|
115
|
+
/** Error fields for a log line, named like the `fn.completed` ones. */
|
|
116
|
+
export function errorAttributes(error) {
|
|
117
|
+
return {
|
|
118
|
+
error_type: error instanceof Error ? error.name : typeof error,
|
|
119
|
+
error_message: errorMessageOf(error)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
115
122
|
function protectSpan(span) {
|
|
116
123
|
return new Proxy(span, {
|
|
117
124
|
get(target, key) {
|
package/dist/otel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tisAsyncIteratorObject,\n\tonStreamEnd,\n\ttype StreamScope\n} from './stream.js'\n\n/** Attribute values accepted by OpenTelemetry spans. */\nexport type AttributeValue = string | number | boolean\n\n/** The part of an OpenTelemetry `Span` this package uses. */\nexport interface SpanLike {\n\tsetAttribute(key: string, value: AttributeValue): unknown\n\tsetStatus(status: { code: number; message?: string }): unknown\n\trecordException(exception: Error): unknown\n\tend(): unknown\n}\n\ntype SpanOptionsLike = {\n\tkind?: number\n\tattributes?: Record<string, AttributeValue>\n}\n\ninterface TracerLike<TSpan> {\n\tstartActiveSpan<F extends (span: TSpan) => unknown>(\n\t\tname: string,\n\t\toptions: SpanOptionsLike,\n\t\tfn: F\n\t): ReturnType<F>\n\tstartSpan(name: string, options?: SpanOptionsLike): TSpan\n}\n\n/**\n * The part of `@opentelemetry/api` this package uses. Pass the module itself:\n * `import * as otel from '@opentelemetry/api'` then `createFn({ otel })`.\n * Without it no spans are created and handlers receive `span: undefined`.\n */\nexport interface OtelApiLike<TSpan extends SpanLike = SpanLike> {\n\ttrace: { getTracer(name: string): TracerLike<TSpan> }\n\tSpanStatusCode: { OK: number; ERROR: number }\n\tSpanKind: {\n\t\tINTERNAL: number\n\t\tSERVER: number\n\t\tPRODUCER: number\n\t}\n\t/** Restores a span's context for each step of a stream it returned. */\n\tcontext: {\n\t\tactive(): unknown\n\t\twith<T>(context: never, fn: () => T): T\n\t}\n}\n\n/** The span type of an OpenTelemetry API, or `SpanLike` when absent. */\nexport type SpanOf<TOtel> =\n\tTOtel extends OtelApiLike<infer TSpan> ? TSpan : SpanLike\n\n/** Called once when a span's work is done, before the span ends. */\nexport type OnSettled = (\n\tspan: SpanLike | undefined,\n\t...error: [] | [unknown]\n) => void\n\nexport type Tracing = {\n\t/**\n\t * Run `fn` inside an active span: OK when it resolves, ERROR when it throws,\n\t * ended either way. When `fn` resolves to a stream, the span stays open -\n\t * and active for each step of the stream - until the stream ends.\n\t * `onSettled` runs once at that point, before the span ends. Without\n\t * OpenTelemetry `span` is undefined and `onSettled` still runs.\n\t */\n\tinSpan<T>(\n\t\tname: string,\n\t\tkind: 'INTERNAL' | 'SERVER' | 'PRODUCER',\n\t\tfn: (span: SpanLike | undefined) => Promise<T>,\n\t\tonSettled?: OnSettled\n\t): Promise<T>\n\t/** Start a span that the caller ends with `end` (long-lived streams). */\n\tstartSpan(\n\t\tname: string,\n\t\tkind: 'SERVER',\n\t\tattributes: Record<string, AttributeValue>\n\t): SpanLike | undefined\n\t/** Set OK, or ERROR with `error`, and end the span. */\n\tend(span: SpanLike | undefined, error?: unknown): void\n}\n\nexport function createTracing(otel: OtelApiLike | undefined): Tracing {\n\tlet tracer: TracerLike<SpanLike> | undefined\n\ttry {\n\t\ttracer = otel?.trace.getTracer('orpc-fn')\n\t} catch {\n\t\t/* Missing telemetry cannot prevent execution. */\n\t}\n\tconst end: Tracing['end'] = (span, ...error) => {\n\t\tif (!span || !otel) return\n\t\tspan = protectSpan(span)\n\t\tif (error.length === 0) {\n\t\t\tspan.setStatus({ code: otel.SpanStatusCode.OK })\n\t\t} else {\n\t\t\tspan.setStatus({\n\t\t\t\tcode: otel.SpanStatusCode.ERROR,\n\t\t\t\tmessage: errorMessageOf(error[0])\n\t\t\t})\n\t\t\tif (error[0] instanceof Error) span.recordException(error[0])\n\t\t}\n\t\tspan.end()\n\t}\n\treturn {\n\t\tinSpan(name, kind, fn, onSettled) {\n\t\t\tconst settle = (span: SpanLike | undefined, ...error: [] | [unknown]) => {\n\t\t\t\ttry {\n\t\t\t\t\tonSettled?.(span, ...error)\n\t\t\t\t} catch {\n\t\t\t\t\t/* Telemetry callbacks cannot replace the original result. */\n\t\t\t\t} finally {\n\t\t\t\t\tend(span, ...error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst run = async (\n\t\t\t\tspan: SpanLike | undefined,\n\t\t\t\tscope?: StreamScope\n\t\t\t): Promise<Awaited<ReturnType<typeof fn>>> => {\n\t\t\t\tspan = span ? protectSpan(span) : undefined\n\t\t\t\tlet result: Awaited<ReturnType<typeof fn>>\n\t\t\t\ttry {\n\t\t\t\t\tresult = await fn(span)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsettle(span, error)\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\tif (isAsyncIteratorObject(result)) {\n\t\t\t\t\treturn onStreamEnd(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\t(...error) => settle(span, ...error),\n\t\t\t\t\t\tscope\n\t\t\t\t\t) as typeof result\n\t\t\t\t}\n\t\t\t\tsettle(span)\n\t\t\t\treturn result\n\t\t\t}\n\t\t\tif (!tracer || !otel) return run(undefined)\n\t\t\tlet started: Promise<Awaited<ReturnType<typeof fn>>> | undefined\n\t\t\ttry {\n\t\t\t\treturn tracer.startActiveSpan(\n\t\t\t\t\tname,\n\t\t\t\t\t{ kind: otel.SpanKind[kind] },\n\t\t\t\t\t(span) => {\n\t\t\t\t\t\tlet active: unknown\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tactive = otel.context.active()\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t/* Fall back to the span without scope restoration. */\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstarted = run(span, (step) => {\n\t\t\t\t\t\t\tlet completed: { value: ReturnType<typeof step> } | undefined\n\t\t\t\t\t\t\tlet failed: { error: unknown } | undefined\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn otel.context.with(active as never, () => {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tconst value = step()\n\t\t\t\t\t\t\t\t\t\tcompleted = { value }\n\t\t\t\t\t\t\t\t\t\treturn value\n\t\t\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\t\t\tfailed = { error }\n\t\t\t\t\t\t\t\t\t\tthrow error\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tif (completed) return completed.value\n\t\t\t\t\t\t\t\tif (failed) throw failed.error\n\t\t\t\t\t\t\t\treturn step()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn started\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t} catch {\n\t\t\t\treturn started ?? run(undefined)\n\t\t\t}\n\t\t},\n\t\tstartSpan(name, kind, attributes) {\n\t\t\tif (!tracer || !otel) return undefined\n\t\t\ttry {\n\t\t\t\treturn protectSpan(\n\t\t\t\t\ttracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })\n\t\t\t\t)\n\t\t\t} catch {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tend\n\t}\n}\n\nexport function errorMessageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error)\n}\n\nfunction protectSpan<T extends SpanLike>(span: T): T {\n\treturn new Proxy(span, {\n\t\tget(target, key) {\n\t\t\tconst value = Reflect.get(target, key, target)\n\t\t\tif (typeof value !== 'function') return value\n\t\t\tif (\n\t\t\t\t['setAttribute', 'setStatus', 'recordException', 'end'].includes(\n\t\t\t\t\tString(key)\n\t\t\t\t)\n\t\t\t)\n\t\t\t\treturn (...args: unknown[]) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn value.apply(target, args)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/* Ignore exporter failures. */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\treturn value.bind(target)\n\t\t}\n\t})\n}\n"]}
|
|
1
|
+
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tisAsyncIteratorObject,\n\tonStreamEnd,\n\ttype StreamScope\n} from './stream.js'\n\n/** Attribute values accepted by OpenTelemetry spans. */\nexport type AttributeValue = string | number | boolean\n\n/** The part of an OpenTelemetry `Span` this package uses. */\nexport interface SpanLike {\n\tsetAttribute(key: string, value: AttributeValue): unknown\n\tsetStatus(status: { code: number; message?: string }): unknown\n\trecordException(exception: Error): unknown\n\tend(): unknown\n}\n\ntype SpanOptionsLike = {\n\tkind?: number\n\tattributes?: Record<string, AttributeValue>\n}\n\ninterface TracerLike<TSpan> {\n\tstartActiveSpan<F extends (span: TSpan) => unknown>(\n\t\tname: string,\n\t\toptions: SpanOptionsLike,\n\t\tfn: F\n\t): ReturnType<F>\n\tstartSpan(name: string, options?: SpanOptionsLike): TSpan\n}\n\n/**\n * The part of `@opentelemetry/api` this package uses. Pass the module itself:\n * `import * as otel from '@opentelemetry/api'` then `createFn({ otel })`.\n * Without it no spans are created and handlers receive `span: undefined`.\n */\nexport interface OtelApiLike<TSpan extends SpanLike = SpanLike> {\n\ttrace: { getTracer(name: string): TracerLike<TSpan> }\n\tSpanStatusCode: { OK: number; ERROR: number }\n\tSpanKind: {\n\t\tINTERNAL: number\n\t\tSERVER: number\n\t\tPRODUCER: number\n\t}\n\t/** Restores a span's context for each step of a stream it returned. */\n\tcontext: {\n\t\tactive(): unknown\n\t\twith<T>(context: never, fn: () => T): T\n\t}\n}\n\n/** The span type of an OpenTelemetry API, or `SpanLike` when absent. */\nexport type SpanOf<TOtel> =\n\tTOtel extends OtelApiLike<infer TSpan> ? TSpan : SpanLike\n\n/** Called once when a span's work is done, before the span ends. */\nexport type OnSettled = (\n\tspan: SpanLike | undefined,\n\t...error: [] | [unknown]\n) => void\n\nexport type Tracing = {\n\t/**\n\t * Run `fn` inside an active span: OK when it resolves, ERROR when it throws,\n\t * ended either way. When `fn` resolves to a stream, the span stays open -\n\t * and active for each step of the stream - until the stream ends.\n\t * `onSettled` runs once at that point, before the span ends. Without\n\t * OpenTelemetry `span` is undefined and `onSettled` still runs.\n\t */\n\tinSpan<T>(\n\t\tname: string,\n\t\tkind: 'INTERNAL' | 'SERVER' | 'PRODUCER',\n\t\tfn: (span: SpanLike | undefined) => Promise<T>,\n\t\tonSettled?: OnSettled\n\t): Promise<T>\n\t/** Start a span that the caller ends with `end` (long-lived streams). */\n\tstartSpan(\n\t\tname: string,\n\t\tkind: 'SERVER',\n\t\tattributes: Record<string, AttributeValue>\n\t): SpanLike | undefined\n\t/** Set OK, or ERROR with `error`, and end the span. */\n\tend(span: SpanLike | undefined, error?: unknown): void\n}\n\nexport function createTracing(otel: OtelApiLike | undefined): Tracing {\n\tlet tracer: TracerLike<SpanLike> | undefined\n\ttry {\n\t\ttracer = otel?.trace.getTracer('orpc-fn')\n\t} catch {\n\t\t/* Missing telemetry cannot prevent execution. */\n\t}\n\tconst end: Tracing['end'] = (span, ...error) => {\n\t\tif (!span || !otel) return\n\t\tspan = protectSpan(span)\n\t\tif (error.length === 0) {\n\t\t\tspan.setStatus({ code: otel.SpanStatusCode.OK })\n\t\t} else {\n\t\t\tspan.setStatus({\n\t\t\t\tcode: otel.SpanStatusCode.ERROR,\n\t\t\t\tmessage: errorMessageOf(error[0])\n\t\t\t})\n\t\t\tif (error[0] instanceof Error) span.recordException(error[0])\n\t\t}\n\t\tspan.end()\n\t}\n\treturn {\n\t\tinSpan(name, kind, fn, onSettled) {\n\t\t\tconst settle = (span: SpanLike | undefined, ...error: [] | [unknown]) => {\n\t\t\t\ttry {\n\t\t\t\t\tonSettled?.(span, ...error)\n\t\t\t\t} catch {\n\t\t\t\t\t/* Telemetry callbacks cannot replace the original result. */\n\t\t\t\t} finally {\n\t\t\t\t\tend(span, ...error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst run = async (\n\t\t\t\tspan: SpanLike | undefined,\n\t\t\t\tscope?: StreamScope\n\t\t\t): Promise<Awaited<ReturnType<typeof fn>>> => {\n\t\t\t\tspan = span ? protectSpan(span) : undefined\n\t\t\t\tlet result: Awaited<ReturnType<typeof fn>>\n\t\t\t\ttry {\n\t\t\t\t\tresult = await fn(span)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsettle(span, error)\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\tif (isAsyncIteratorObject(result)) {\n\t\t\t\t\treturn onStreamEnd(\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\t(...error) => settle(span, ...error),\n\t\t\t\t\t\tscope\n\t\t\t\t\t) as typeof result\n\t\t\t\t}\n\t\t\t\tsettle(span)\n\t\t\t\treturn result\n\t\t\t}\n\t\t\tif (!tracer || !otel) return run(undefined)\n\t\t\tlet started: Promise<Awaited<ReturnType<typeof fn>>> | undefined\n\t\t\ttry {\n\t\t\t\treturn tracer.startActiveSpan(\n\t\t\t\t\tname,\n\t\t\t\t\t{ kind: otel.SpanKind[kind] },\n\t\t\t\t\t(span) => {\n\t\t\t\t\t\tlet active: unknown\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tactive = otel.context.active()\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t/* Fall back to the span without scope restoration. */\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstarted = run(span, (step) => {\n\t\t\t\t\t\t\tlet completed: { value: ReturnType<typeof step> } | undefined\n\t\t\t\t\t\t\tlet failed: { error: unknown } | undefined\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn otel.context.with(active as never, () => {\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tconst value = step()\n\t\t\t\t\t\t\t\t\t\tcompleted = { value }\n\t\t\t\t\t\t\t\t\t\treturn value\n\t\t\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\t\t\tfailed = { error }\n\t\t\t\t\t\t\t\t\t\tthrow error\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tif (completed) return completed.value\n\t\t\t\t\t\t\t\tif (failed) throw failed.error\n\t\t\t\t\t\t\t\treturn step()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn started\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t} catch {\n\t\t\t\treturn started ?? run(undefined)\n\t\t\t}\n\t\t},\n\t\tstartSpan(name, kind, attributes) {\n\t\t\tif (!tracer || !otel) return undefined\n\t\t\ttry {\n\t\t\t\treturn protectSpan(\n\t\t\t\t\ttracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })\n\t\t\t\t)\n\t\t\t} catch {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tend\n\t}\n}\n\nexport function errorMessageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error)\n}\n\n/** Error fields for a log line, named like the `fn.completed` ones. */\nexport function errorAttributes(error: unknown) {\n\treturn {\n\t\terror_type: error instanceof Error ? error.name : typeof error,\n\t\terror_message: errorMessageOf(error)\n\t}\n}\n\nfunction protectSpan<T extends SpanLike>(span: T): T {\n\treturn new Proxy(span, {\n\t\tget(target, key) {\n\t\t\tconst value = Reflect.get(target, key, target)\n\t\t\tif (typeof value !== 'function') return value\n\t\t\tif (\n\t\t\t\t['setAttribute', 'setStatus', 'recordException', 'end'].includes(\n\t\t\t\t\tString(key)\n\t\t\t\t)\n\t\t\t)\n\t\t\t\treturn (...args: unknown[]) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn value.apply(target, args)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/* Ignore exporter failures. */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\treturn value.bind(target)\n\t\t}\n\t})\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CommandGroup } from 'hanzio/cli';
|
|
2
|
+
import { type FnMeta } from './meta.js';
|
|
3
|
+
export type ProcedureCommandsOptions<C> = {
|
|
4
|
+
/**
|
|
5
|
+
* Which procedures become commands. Default: those declared with
|
|
6
|
+
* `meta: { cli: true }` (add `cli?: boolean` to the app's meta type).
|
|
7
|
+
*/
|
|
8
|
+
filter?: (meta: FnMeta) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The oRPC context a command calls its procedure with, from the CLI's
|
|
11
|
+
* context (`defineCli({ context })`). Default: that context as-is. It gets
|
|
12
|
+
* the procedure's meta, so it can refuse early with a `CliError`, e.g. a
|
|
13
|
+
* protected procedure run without a user.
|
|
14
|
+
*/
|
|
15
|
+
context?: (cliContext: C, meta: FnMeta) => unknown;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The opted-in `fn()` procedures of a router as hanzio/cli commands, nested
|
|
19
|
+
* like the router: `supportLearning.syncRun` is `support-learning sync-run`.
|
|
20
|
+
* Flags come from the procedure's input schema, help from its summary. A
|
|
21
|
+
* command calls the procedure through oRPC, so middleware, guards, tracing
|
|
22
|
+
* and errors run as they do over HTTP.
|
|
23
|
+
*/
|
|
24
|
+
export declare function procedureCommands<C = unknown>(router: object, options?: ProcedureCommandsOptions<C>): CommandGroup<C>;
|
|
25
|
+
//# sourceMappingURL=procedure-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"procedure-cli.d.ts","sourceRoot":"","sources":["../src/procedure-cli.ts"],"names":[],"mappings":"AACA,OAAO,EAGN,KAAK,YAAY,EAEjB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,KAAK,MAAM,EAAc,MAAM,WAAW,CAAA;AAEnD,MAAM,MAAM,wBAAwB,CAAC,CAAC,IAAI;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAClC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAClD,CAAA;AAOD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAC5C,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,wBAAwB,CAAC,CAAC,CAAM,GACvC,YAAY,CAAC,CAAC,CAAC,CAejB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { call, isProcedure, ORPCError } from '@orpc/server';
|
|
2
|
+
import { CliError, defineCommand } from 'hanzio/cli';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { readFnMeta } from './meta.js';
|
|
5
|
+
const optedIn = (meta) => meta.meta.cli === true;
|
|
6
|
+
const kebab = (key) => key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
7
|
+
/**
|
|
8
|
+
* The opted-in `fn()` procedures of a router as hanzio/cli commands, nested
|
|
9
|
+
* like the router: `supportLearning.syncRun` is `support-learning sync-run`.
|
|
10
|
+
* Flags come from the procedure's input schema, help from its summary. A
|
|
11
|
+
* command calls the procedure through oRPC, so middleware, guards, tracing
|
|
12
|
+
* and errors run as they do over HTTP.
|
|
13
|
+
*/
|
|
14
|
+
export function procedureCommands(router, options = {}) {
|
|
15
|
+
const filter = options.filter ?? optedIn;
|
|
16
|
+
const commands = {};
|
|
17
|
+
for (const [key, value] of Object.entries(router)) {
|
|
18
|
+
if (isProcedure(value)) {
|
|
19
|
+
const meta = readFnMeta(value);
|
|
20
|
+
if (filter(meta))
|
|
21
|
+
commands[kebab(key)] = toCommand(value, meta, options);
|
|
22
|
+
}
|
|
23
|
+
else if (value && typeof value === 'object') {
|
|
24
|
+
const group = procedureCommands(value, options);
|
|
25
|
+
if (Object.keys(group).length > 0)
|
|
26
|
+
commands[kebab(key)] = group;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return commands;
|
|
30
|
+
}
|
|
31
|
+
function toCommand(procedure, meta, options) {
|
|
32
|
+
const name = meta.name ?? 'procedure';
|
|
33
|
+
if (meta.stream) {
|
|
34
|
+
throw new TypeError(`orpc-fn: ${name} streams and cannot be a CLI command`);
|
|
35
|
+
}
|
|
36
|
+
const input = meta.inputSchema;
|
|
37
|
+
if (input !== undefined && !(input instanceof z.ZodObject)) {
|
|
38
|
+
throw new TypeError(`orpc-fn: ${name} must take a z.object() input to be a CLI command`);
|
|
39
|
+
}
|
|
40
|
+
return defineCommand({
|
|
41
|
+
description: meta.description,
|
|
42
|
+
input: input ?? z.object({}),
|
|
43
|
+
run: async (parsed, ctx) => {
|
|
44
|
+
const context = options.context
|
|
45
|
+
? await options.context(ctx.context, meta)
|
|
46
|
+
: ctx.context;
|
|
47
|
+
try {
|
|
48
|
+
// The flags were validated against this schema; the procedure parses
|
|
49
|
+
// them again, like any nested call with a parsed value.
|
|
50
|
+
return await call(procedure, input ? parsed : undefined, {
|
|
51
|
+
context: context
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// A refusal the caller drives (auth, permission, not found) reads as
|
|
56
|
+
// a message, not a crash with a stack trace.
|
|
57
|
+
if (error instanceof ORPCError && error.status < 500) {
|
|
58
|
+
throw new CliError(`${error.code}: ${error.message}`, {
|
|
59
|
+
cause: error
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=procedure-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"procedure-cli.js","sourceRoot":"","sources":["../src/procedure-cli.ts"],"names":[],"mappings":"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","sourcesContent":["import { type AnyProcedure, call, isProcedure, ORPCError } from '@orpc/server'\nimport {\n\tCliError,\n\ttype Command,\n\ttype CommandGroup,\n\tdefineCommand\n} from 'hanzio/cli'\nimport { z } from 'zod'\nimport { type FnMeta, readFnMeta } from './meta.js'\n\nexport type ProcedureCommandsOptions<C> = {\n\t/**\n\t * Which procedures become commands. Default: those declared with\n\t * `meta: { cli: true }` (add `cli?: boolean` to the app's meta type).\n\t */\n\tfilter?: (meta: FnMeta) => boolean\n\t/**\n\t * The oRPC context a command calls its procedure with, from the CLI's\n\t * context (`defineCli({ context })`). Default: that context as-is. It gets\n\t * the procedure's meta, so it can refuse early with a `CliError`, e.g. a\n\t * protected procedure run without a user.\n\t */\n\tcontext?: (cliContext: C, meta: FnMeta) => unknown\n}\n\nconst optedIn = (meta: FnMeta) => (meta.meta as { cli?: unknown }).cli === true\n\nconst kebab = (key: string) =>\n\tkey.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)\n\n/**\n * The opted-in `fn()` procedures of a router as hanzio/cli commands, nested\n * like the router: `supportLearning.syncRun` is `support-learning sync-run`.\n * Flags come from the procedure's input schema, help from its summary. A\n * command calls the procedure through oRPC, so middleware, guards, tracing\n * and errors run as they do over HTTP.\n */\nexport function procedureCommands<C = unknown>(\n\trouter: object,\n\toptions: ProcedureCommandsOptions<C> = {}\n): CommandGroup<C> {\n\tconst filter = options.filter ?? optedIn\n\tconst commands: Record<string, Command<C> | CommandGroup<C>> = {}\n\n\tfor (const [key, value] of Object.entries(router)) {\n\t\tif (isProcedure(value)) {\n\t\t\tconst meta = readFnMeta(value)\n\t\t\tif (filter(meta)) commands[kebab(key)] = toCommand(value, meta, options)\n\t\t} else if (value && typeof value === 'object') {\n\t\t\tconst group = procedureCommands(value, options)\n\t\t\tif (Object.keys(group).length > 0) commands[kebab(key)] = group\n\t\t}\n\t}\n\n\treturn commands\n}\n\nfunction toCommand<C>(\n\tprocedure: AnyProcedure,\n\tmeta: FnMeta,\n\toptions: ProcedureCommandsOptions<C>\n): Command<C> {\n\tconst name = meta.name ?? 'procedure'\n\tif (meta.stream) {\n\t\tthrow new TypeError(`orpc-fn: ${name} streams and cannot be a CLI command`)\n\t}\n\tconst input = meta.inputSchema\n\tif (input !== undefined && !(input instanceof z.ZodObject)) {\n\t\tthrow new TypeError(\n\t\t\t`orpc-fn: ${name} must take a z.object() input to be a CLI command`\n\t\t)\n\t}\n\n\treturn defineCommand<z.ZodObject, unknown, C>({\n\t\tdescription: meta.description,\n\t\tinput: input ?? z.object({}),\n\t\trun: async (parsed, ctx) => {\n\t\t\tconst context = options.context\n\t\t\t\t? await options.context(ctx.context, meta)\n\t\t\t\t: ctx.context\n\t\t\ttry {\n\t\t\t\t// The flags were validated against this schema; the procedure parses\n\t\t\t\t// them again, like any nested call with a parsed value.\n\t\t\t\treturn await call(procedure, input ? parsed : undefined, {\n\t\t\t\t\tcontext: context as Record<never, never>\n\t\t\t\t})\n\t\t\t} catch (error) {\n\t\t\t\t// A refusal the caller drives (auth, permission, not found) reads as\n\t\t\t\t// a message, not a crash with a stack trace.\n\t\t\t\tif (error instanceof ORPCError && error.status < 500) {\n\t\t\t\t\tthrow new CliError(`${error.code}: ${error.message}`, {\n\t\t\t\t\t\tcause: error\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t}\n\t})\n}\n"]}
|