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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/bound-call.d.ts +23 -0
  4. package/dist/bound-call.d.ts.map +1 -0
  5. package/dist/bound-call.js +23 -0
  6. package/dist/bound-call.js.map +1 -0
  7. package/dist/client.d.ts +44 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +49 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/create-fn.d.ts +97 -0
  12. package/dist/create-fn.d.ts.map +1 -0
  13. package/dist/create-fn.js +224 -0
  14. package/dist/create-fn.js.map +1 -0
  15. package/dist/expected-client-error.d.ts +11 -0
  16. package/dist/expected-client-error.d.ts.map +1 -0
  17. package/dist/expected-client-error.js +21 -0
  18. package/dist/expected-client-error.js.map +1 -0
  19. package/dist/expo.d.ts +66 -0
  20. package/dist/expo.d.ts.map +1 -0
  21. package/dist/expo.js +74 -0
  22. package/dist/expo.js.map +1 -0
  23. package/dist/hono.d.ts +81 -0
  24. package/dist/hono.d.ts.map +1 -0
  25. package/dist/hono.js +138 -0
  26. package/dist/hono.js.map +1 -0
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/json-schema.d.ts +47 -0
  32. package/dist/json-schema.d.ts.map +1 -0
  33. package/dist/json-schema.js +44 -0
  34. package/dist/json-schema.js.map +1 -0
  35. package/dist/live/codec.d.ts +9 -0
  36. package/dist/live/codec.d.ts.map +1 -0
  37. package/dist/live/codec.js +27 -0
  38. package/dist/live/codec.js.map +1 -0
  39. package/dist/live/fn-live.d.ts +155 -0
  40. package/dist/live/fn-live.d.ts.map +1 -0
  41. package/dist/live/fn-live.js +210 -0
  42. package/dist/live/fn-live.js.map +1 -0
  43. package/dist/live/index.d.ts +4 -0
  44. package/dist/live/index.d.ts.map +1 -0
  45. package/dist/live/index.js +5 -0
  46. package/dist/live/index.js.map +1 -0
  47. package/dist/live/ioredis.d.ts +29 -0
  48. package/dist/live/ioredis.d.ts.map +1 -0
  49. package/dist/live/ioredis.js +49 -0
  50. package/dist/live/ioredis.js.map +1 -0
  51. package/dist/live/memory.d.ts +10 -0
  52. package/dist/live/memory.d.ts.map +1 -0
  53. package/dist/live/memory.js +45 -0
  54. package/dist/live/memory.js.map +1 -0
  55. package/dist/live/pub-sub.d.ts +211 -0
  56. package/dist/live/pub-sub.d.ts.map +1 -0
  57. package/dist/live/pub-sub.js +529 -0
  58. package/dist/live/pub-sub.js.map +1 -0
  59. package/dist/live/redis-bun.d.ts +26 -0
  60. package/dist/live/redis-bun.d.ts.map +1 -0
  61. package/dist/live/redis-bun.js +59 -0
  62. package/dist/live/redis-bun.js.map +1 -0
  63. package/dist/live/redis.d.ts +45 -0
  64. package/dist/live/redis.d.ts.map +1 -0
  65. package/dist/live/redis.js +214 -0
  66. package/dist/live/redis.js.map +1 -0
  67. package/dist/live/transport.d.ts +35 -0
  68. package/dist/live/transport.d.ts.map +1 -0
  69. package/dist/live/transport.js +2 -0
  70. package/dist/live/transport.js.map +1 -0
  71. package/dist/logger.d.ts +15 -0
  72. package/dist/logger.d.ts.map +1 -0
  73. package/dist/logger.js +15 -0
  74. package/dist/logger.js.map +1 -0
  75. package/dist/mastra.d.ts +44 -0
  76. package/dist/mastra.d.ts.map +1 -0
  77. package/dist/mastra.js +78 -0
  78. package/dist/mastra.js.map +1 -0
  79. package/dist/mcp.d.ts +39 -0
  80. package/dist/mcp.d.ts.map +1 -0
  81. package/dist/mcp.js +38 -0
  82. package/dist/mcp.js.map +1 -0
  83. package/dist/meta.d.ts +35 -0
  84. package/dist/meta.d.ts.map +1 -0
  85. package/dist/meta.js +30 -0
  86. package/dist/meta.js.map +1 -0
  87. package/dist/otel.d.ts +66 -0
  88. package/dist/otel.d.ts.map +1 -0
  89. package/dist/otel.js +63 -0
  90. package/dist/otel.js.map +1 -0
  91. package/dist/router.d.ts +13 -0
  92. package/dist/router.d.ts.map +1 -0
  93. package/dist/router.js +14 -0
  94. package/dist/router.js.map +1 -0
  95. package/dist/stream.d.ts +13 -0
  96. package/dist/stream.d.ts.map +1 -0
  97. package/dist/stream.js +44 -0
  98. package/dist/stream.js.map +1 -0
  99. package/dist/types.d.ts +196 -0
  100. package/dist/types.d.ts.map +1 -0
  101. package/dist/types.js +29 -0
  102. package/dist/types.js.map +1 -0
  103. package/package.json +157 -0
  104. package/src/bound-call.ts +50 -0
  105. package/src/client.ts +82 -0
  106. package/src/create-fn.ts +461 -0
  107. package/src/expected-client-error.ts +23 -0
  108. package/src/expo.ts +127 -0
  109. package/src/hono.ts +247 -0
  110. package/src/index.ts +45 -0
  111. package/src/json-schema.ts +82 -0
  112. package/src/live/codec.ts +31 -0
  113. package/src/live/fn-live.ts +470 -0
  114. package/src/live/index.ts +31 -0
  115. package/src/live/ioredis.ts +74 -0
  116. package/src/live/memory.ts +50 -0
  117. package/src/live/pub-sub.ts +828 -0
  118. package/src/live/redis-bun.ts +76 -0
  119. package/src/live/redis.ts +255 -0
  120. package/src/live/transport.ts +41 -0
  121. package/src/logger.ts +28 -0
  122. package/src/mastra.ts +140 -0
  123. package/src/mcp.ts +73 -0
  124. package/src/meta.ts +72 -0
  125. package/src/otel.ts +151 -0
  126. package/src/router.ts +15 -0
  127. package/src/stream.ts +54 -0
  128. package/src/types.ts +389 -0
@@ -0,0 +1,59 @@
1
+ import { createRedisTransport } from './redis.js';
2
+ /**
3
+ * `PubSubTransport` for Bun's built-in `RedisClient`. Subscriptions use a
4
+ * dedicated connection (`client.duplicate()` unless `subscriber` is given)
5
+ * that re-subscribes its channels after every reconnect; configure retries on
6
+ * the client (`maxRetries`) since the duplicate copies its options.
7
+ *
8
+ * Call `close()` on shutdown to close the subscriber connection it created.
9
+ */
10
+ export function bunRedisTransport(client, options = {}) {
11
+ return createRedisTransport({
12
+ send: (command, args) => client.send(command, args),
13
+ async connectSubscriber(handlers) {
14
+ const owned = !options.subscriber;
15
+ const subscriber = options.subscriber ?? (await client.duplicate());
16
+ const dispatch = (message, channel) => handlers.onMessage(channel, message);
17
+ const ours = new Set();
18
+ // Bun reconnects on its own but drops every subscription. Also fires on
19
+ // the first connect, when there is nothing to restore yet.
20
+ subscriber.onconnect = () => handlers.onReconnect();
21
+ // Fires only when Bun gives up reconnecting (and on close(), which
22
+ // detaches it first).
23
+ subscriber.onclose = (error) => handlers.onLost(error ?? new Error('Redis subscriber closed'));
24
+ return {
25
+ subscribe: (channel) => {
26
+ ours.add(channel);
27
+ return subscriber.subscribe(channel, dispatch);
28
+ },
29
+ // Bun keeps listeners across a reconnect and does not dedupe them, so
30
+ // clear the channel before adding its listener again. unsubscribe
31
+ // throws synchronously outside subscriber mode.
32
+ resubscribe: async (channel) => {
33
+ await Promise.resolve()
34
+ .then(() => subscriber.unsubscribe(channel))
35
+ .catch(() => { });
36
+ return subscriber.subscribe(channel, dispatch);
37
+ },
38
+ unsubscribe: (channel) => {
39
+ ours.delete(channel);
40
+ return subscriber.unsubscribe(channel, dispatch);
41
+ },
42
+ close() {
43
+ // No-ops, not null: Bun calls `onclose` without checking it.
44
+ subscriber.onconnect = () => { };
45
+ subscriber.onclose = () => { };
46
+ if (owned)
47
+ return subscriber.close();
48
+ // A given subscriber stays open: remove only this transport's listeners.
49
+ for (const channel of ours) {
50
+ void Promise.resolve()
51
+ .then(() => subscriber.unsubscribe(channel, dispatch))
52
+ .catch(() => { });
53
+ }
54
+ }
55
+ };
56
+ }
57
+ });
58
+ }
59
+ //# sourceMappingURL=redis-bun.js.map
@@ -0,0 +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;AAejD;;;;;;;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,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,KAAK,MAAM,OAAO,IAAI,IAAI,EAAE,CAAC;wBAC5B,KAAK,OAAO,CAAC,OAAO,EAAE;6BACpB,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;6BACrD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;oBAClB,CAAC;gBACF,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\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\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\tfor (const channel of ours) {\n\t\t\t\t\t\tvoid Promise.resolve()\n\t\t\t\t\t\t\t.then(() => subscriber.unsubscribe(channel, dispatch))\n\t\t\t\t\t\t\t.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,45 @@
1
+ import { type PubSubTransport } from './transport.js';
2
+ export type RedisSend = (command: string, args: string[]) => Promise<unknown>;
3
+ /**
4
+ * Ship a Lua script by SHA instead of resending its source on every call.
5
+ * Redis forgets its script cache on restart and on `SCRIPT FLUSH`, so a
6
+ * `NOSCRIPT` reply falls back to an inline `EVAL` (which repopulates the
7
+ * cache) and drops the stale digest.
8
+ */
9
+ export declare function createLuaScript(send: RedisSend, source: string): (keys: string[], args: string[]) => Promise<unknown>;
10
+ export type SubscriberConnection = {
11
+ subscribe(channel: string): Promise<unknown>;
12
+ unsubscribe(channel: string): Promise<unknown>;
13
+ /**
14
+ * Restore one channel after the driver reconnected on its own and lost its
15
+ * subscriptions. Omit when the client re-subscribes itself (ioredis).
16
+ */
17
+ resubscribe?(channel: string): Promise<unknown>;
18
+ /** Stop delivering and release the connection (if the driver created it). */
19
+ close(): void;
20
+ };
21
+ /** Callbacks for one connection; they do nothing once it is replaced. */
22
+ export type SubscriberHandlers = {
23
+ onMessage(channel: string, message: string): void;
24
+ /** The driver reconnected; its subscriptions must be restored. */
25
+ onReconnect(): void;
26
+ /** The connection is gone for good; a fresh one replaces it. */
27
+ onLost(error: Error): void;
28
+ };
29
+ /**
30
+ * Shared core of the Redis transports: one SUBSCRIBE per channel no matter
31
+ * how many listeners, and one dedicated subscriber connection at a time.
32
+ *
33
+ * Each connection is a generation. Losing it (or failing to restore its
34
+ * channels after a reconnect) retires it: the core closes it, ignores
35
+ * anything it still emits, and tells every listener, whose resubscribe then
36
+ * opens the next generation. Nothing from an old connection can reach the
37
+ * channels of a new one.
38
+ */
39
+ export declare function createRedisTransport(driver: {
40
+ send: RedisSend;
41
+ connectSubscriber: (handlers: SubscriberHandlers) => Promise<SubscriberConnection>;
42
+ }): PubSubTransport & {
43
+ close(): void;
44
+ };
45
+ //# sourceMappingURL=redis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/live/redis.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEjE,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,CAAA;CACb,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;AAaD;;;;;;;;;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;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CAuItC"}
@@ -0,0 +1,214 @@
1
+ import { errorMessageOf } from '../otel.js';
2
+ import { backlogKey } from './transport.js';
3
+ function isNoScriptError(error) {
4
+ return errorMessageOf(error).includes('NOSCRIPT');
5
+ }
6
+ /**
7
+ * Ship a Lua script by SHA instead of resending its source on every call.
8
+ * Redis forgets its script cache on restart and on `SCRIPT FLUSH`, so a
9
+ * `NOSCRIPT` reply falls back to an inline `EVAL` (which repopulates the
10
+ * cache) and drops the stale digest.
11
+ */
12
+ export function createLuaScript(send, source) {
13
+ let sha = null;
14
+ let loading = null;
15
+ function load() {
16
+ loading ??= send('SCRIPT', ['LOAD', source])
17
+ .then(String)
18
+ .finally(() => {
19
+ loading = null;
20
+ });
21
+ return loading;
22
+ }
23
+ return async function evalScript(keys, args) {
24
+ const tail = [String(keys.length), ...keys, ...args];
25
+ sha ??= await load();
26
+ try {
27
+ return await send('EVALSHA', [sha, ...tail]);
28
+ }
29
+ catch (error) {
30
+ if (!isNoScriptError(error))
31
+ throw error;
32
+ sha = null;
33
+ return await send('EVAL', [source, ...tail]);
34
+ }
35
+ };
36
+ }
37
+ /**
38
+ * Publish payloads and maintain each channel's capped, TTL'd backlog in one
39
+ * atomic round-trip. Separate PUBLISH/RPUSH/LTRIM/EXPIRE calls can tear (a
40
+ * message published but missing from the backlog, or a backlog without TTL).
41
+ *
42
+ * KEYS = channel1, backlog1, channel2, backlog2, ...
43
+ * ARGV[1] = max backlog size (0 = no backlog), ARGV[2] = TTL seconds,
44
+ * ARGV[3..] = pairs of (channel slot, payload)
45
+ */
46
+ const PUBLISH_LUA = `
47
+ local maxSize = tonumber(ARGV[1])
48
+ local ttl = tonumber(ARGV[2])
49
+ for i = 3, #ARGV, 2 do
50
+ local slot = tonumber(ARGV[i]) * 2
51
+ redis.call('PUBLISH', KEYS[slot - 1], ARGV[i + 1])
52
+ if maxSize > 0 then
53
+ redis.call('RPUSH', KEYS[slot], ARGV[i + 1])
54
+ end
55
+ end
56
+ if maxSize > 0 then
57
+ for slot = 2, #KEYS, 2 do
58
+ redis.call('LTRIM', KEYS[slot], -maxSize, -1)
59
+ redis.call('EXPIRE', KEYS[slot], ttl)
60
+ end
61
+ end
62
+ return (#ARGV - 2) / 2
63
+ `;
64
+ /**
65
+ * Shared core of the Redis transports: one SUBSCRIBE per channel no matter
66
+ * how many listeners, and one dedicated subscriber connection at a time.
67
+ *
68
+ * Each connection is a generation. Losing it (or failing to restore its
69
+ * channels after a reconnect) retires it: the core closes it, ignores
70
+ * anything it still emits, and tells every listener, whose resubscribe then
71
+ * opens the next generation. Nothing from an old connection can reach the
72
+ * channels of a new one.
73
+ */
74
+ export function createRedisTransport(driver) {
75
+ const publishScript = createLuaScript(driver.send, PUBLISH_LUA);
76
+ const channels = new Map();
77
+ // An UNSUBSCRIBE still in flight would cancel a SUBSCRIBE sent after it.
78
+ const unsubscribing = new Map();
79
+ let current = null;
80
+ const connect = () => {
81
+ let retired = false;
82
+ const retire = (error) => {
83
+ if (retired)
84
+ return;
85
+ retired = true;
86
+ if (current?.connection === connection)
87
+ current = null;
88
+ void connection.then((open) => open.close()).catch(() => { });
89
+ if (!error)
90
+ return;
91
+ const lost = [...channels.values()].flatMap((state) => [...state.entries]);
92
+ channels.clear();
93
+ for (const entry of lost)
94
+ entry.onLost?.(error);
95
+ };
96
+ const handlers = {
97
+ onMessage(channel, message) {
98
+ if (retired)
99
+ return;
100
+ for (const entry of [...(channels.get(channel)?.entries ?? [])]) {
101
+ try {
102
+ entry.listener(message);
103
+ }
104
+ catch {
105
+ // One listener must not starve the others.
106
+ }
107
+ }
108
+ },
109
+ onReconnect() {
110
+ if (retired)
111
+ return;
112
+ // In-flight SUBSCRIBEs are excluded: they still land on their own.
113
+ const settled = [...channels]
114
+ .filter(([, state]) => state.settled)
115
+ .map(([channel]) => channel);
116
+ void connection
117
+ .then((open) => Promise.all(settled.map((channel) => open.resubscribe?.(channel))))
118
+ .catch((error) => retire(asError(error)));
119
+ },
120
+ onLost: (error) => retire(error)
121
+ };
122
+ const connection = driver.connectSubscriber(handlers);
123
+ connection.catch(() => {
124
+ if (current?.connection === connection)
125
+ current = null;
126
+ });
127
+ current = { connection, retire: () => retire() };
128
+ return connection;
129
+ };
130
+ return {
131
+ async publish(messages, backlog) {
132
+ const [first] = messages;
133
+ if (!first)
134
+ return;
135
+ if (!backlog && messages.length === 1) {
136
+ await driver.send('PUBLISH', [first.channel, first.payload]);
137
+ return;
138
+ }
139
+ const keys = [];
140
+ const slots = new Map();
141
+ const args = [
142
+ String(backlog?.size ?? 0),
143
+ String(backlog?.ttlSeconds ?? 0)
144
+ ];
145
+ for (const { channel, payload } of messages) {
146
+ let slot = slots.get(channel);
147
+ if (slot === undefined) {
148
+ keys.push(channel, backlogKey(channel));
149
+ slot = keys.length / 2;
150
+ slots.set(channel, slot);
151
+ }
152
+ args.push(String(slot), payload);
153
+ }
154
+ await publishScript(keys, args);
155
+ },
156
+ async readBacklog(channel) {
157
+ const items = await driver.send('LRANGE', [
158
+ backlogKey(channel),
159
+ '0',
160
+ '-1'
161
+ ]);
162
+ return Array.isArray(items) ? items.map(String) : [];
163
+ },
164
+ async subscribe(channel, listener, onLost) {
165
+ const connection = await (current?.connection ?? connect());
166
+ const entry = { listener, onLost };
167
+ await unsubscribing.get(channel);
168
+ let state = channels.get(channel);
169
+ if (!state) {
170
+ const created = {
171
+ entries: new Set(),
172
+ ready: connection.subscribe(channel),
173
+ settled: false
174
+ };
175
+ created.ready.then(() => {
176
+ created.settled = true;
177
+ }, () => {
178
+ if (channels.get(channel) === created)
179
+ channels.delete(channel);
180
+ });
181
+ channels.set(channel, created);
182
+ state = created;
183
+ }
184
+ state.entries.add(entry);
185
+ try {
186
+ await state.ready;
187
+ }
188
+ catch (error) {
189
+ state.entries.delete(entry);
190
+ throw error;
191
+ }
192
+ const subscribed = state;
193
+ return async () => {
194
+ if (!subscribed.entries.delete(entry))
195
+ return;
196
+ if (subscribed.entries.size > 0 || channels.get(channel) !== subscribed)
197
+ return;
198
+ channels.delete(channel);
199
+ const done = connection.unsubscribe(channel).finally(() => {
200
+ if (unsubscribing.get(channel) === done)
201
+ unsubscribing.delete(channel);
202
+ });
203
+ unsubscribing.set(channel, done);
204
+ await done;
205
+ };
206
+ },
207
+ close() {
208
+ current?.retire();
209
+ channels.clear();
210
+ }
211
+ };
212
+ }
213
+ const asError = (error) => error instanceof Error ? error : new Error(errorMessageOf(error));
214
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
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 { 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\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}\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(): 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: () => void\n\t} | null = null\n\n\tconst connect = () => {\n\t\tlet retired = false\n\t\tconst retire = (error?: Error) => {\n\t\t\tif (retired) return\n\t\t\tretired = true\n\t\t\tif (current?.connection === connection) current = null\n\t\t\tvoid connection.then((open) => open.close()).catch(() => {})\n\t\t\tif (!error) return\n\t\t\tconst lost = [...channels.values()].flatMap((state) => [...state.entries])\n\t\t\tchannels.clear()\n\t\t\tfor (const entry of lost) entry.onLost?.(error)\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.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) {\n\t\t\tconst connection = await (current?.connection ?? connect())\n\t\t\tconst entry: Entry = { listener, onLost }\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\tclose() {\n\t\t\tcurrent?.retire()\n\t\t\tchannels.clear()\n\t\t}\n\t}\n}\n\nconst asError = (error: unknown) =>\n\terror instanceof Error ? error : new Error(errorMessageOf(error))\n"]}
@@ -0,0 +1,35 @@
1
+ /** One payload for one channel. */
2
+ export type PubSubMessage = {
3
+ channel: string;
4
+ payload: string;
5
+ };
6
+ export type BacklogOptions = {
7
+ /** Items kept per channel (oldest dropped). */
8
+ size: number;
9
+ /** Seconds the backlog lives after its last write. */
10
+ ttlSeconds: number;
11
+ };
12
+ /**
13
+ * The broker behind `createPubSub` / `fnLive`. Implementations:
14
+ * `orpc-fn/live/redis-bun`, `orpc-fn/live/ioredis` and `orpc-fn/live/memory`.
15
+ *
16
+ * The backlog of a channel lives at `${channel}:backlog` (see {@link backlogKey}).
17
+ */
18
+ export interface PubSubTransport {
19
+ /**
20
+ * Publish every message, in order. With `backlog`, also append each payload
21
+ * to its channel's backlog, trim and refresh its TTL - atomically, so a
22
+ * message is never published without its backlog entry or vice versa.
23
+ */
24
+ publish(messages: readonly PubSubMessage[], backlog?: BacklogOptions): Promise<void>;
25
+ /** The channel's backlog, oldest first. */
26
+ readBacklog(channel: string): Promise<string[]>;
27
+ /**
28
+ * Deliver every payload published to `channel` until the returned function
29
+ * is called. `onLost` fires when the subscription can no longer deliver
30
+ * (connection closed for good); the caller resubscribes.
31
+ */
32
+ subscribe(channel: string, listener: (payload: string) => void, onLost?: (error: Error) => void): Promise<() => Promise<void>>;
33
+ }
34
+ export declare const backlogKey: (channel: string) => string;
35
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +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;;;;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,GAC7B,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;CAC/B;AAED,eAAO,MAAM,UAAU,YAAa,MAAM,WAAyB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export const backlogKey = (channel) => `${channel}:backlog`;
2
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/live/transport.ts"],"names":[],"mappings":"AAwCA,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\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/**\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): Promise<() => Promise<void>>\n}\n\nexport const backlogKey = (channel: string) => `${channel}:backlog`\n"]}
@@ -0,0 +1,15 @@
1
+ export type LogAttributes = Record<string, unknown>;
2
+ /** The logger handlers receive. Any app logger with these four methods fits. */
3
+ export interface FnLogger {
4
+ debug(message: string, attributes?: LogAttributes): void;
5
+ info(message: string, attributes?: LogAttributes): void;
6
+ warn(message: string, attributes?: LogAttributes): void;
7
+ error(message: string, attributes?: LogAttributes): void;
8
+ }
9
+ /**
10
+ * Default logger: silent for `debug`/`info`, console for `warn`/`error`, so a
11
+ * library default never floods stdout with one line per call. Inject your own
12
+ * with `createFn({ logger })`.
13
+ */
14
+ export declare function createDefaultLogger(scope: string): FnLogger;
15
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEnD,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACxB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IACxD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IACvD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IACvD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;CACxD;AAKD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAS3D"}
package/dist/logger.js ADDED
@@ -0,0 +1,15 @@
1
+ const format = (scope, message, attributes) => `[${scope}] ${message}${attributes ? ` ${JSON.stringify(attributes)}` : ''}`;
2
+ /**
3
+ * Default logger: silent for `debug`/`info`, console for `warn`/`error`, so a
4
+ * library default never floods stdout with one line per call. Inject your own
5
+ * with `createFn({ logger })`.
6
+ */
7
+ export function createDefaultLogger(scope) {
8
+ return {
9
+ debug() { },
10
+ info() { },
11
+ warn: (message, attributes) => console.warn(format(scope, message, attributes)),
12
+ error: (message, attributes) => console.error(format(scope, message, attributes))
13
+ };
14
+ }
15
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,OAAe,EAAE,UAA0B,EAAE,EAAE,CAC7E,IAAI,KAAK,KAAK,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;AAE7E;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAChD,OAAO;QACN,KAAK,KAAI,CAAC;QACV,IAAI,KAAI,CAAC;QACT,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,CAC7B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;QACjD,KAAK,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,CAC9B,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;KAClD,CAAA;AACF,CAAC","sourcesContent":["export type LogAttributes = Record<string, unknown>\n\n/** The logger handlers receive. Any app logger with these four methods fits. */\nexport interface FnLogger {\n\tdebug(message: string, attributes?: LogAttributes): void\n\tinfo(message: string, attributes?: LogAttributes): void\n\twarn(message: string, attributes?: LogAttributes): void\n\terror(message: string, attributes?: LogAttributes): void\n}\n\nconst format = (scope: string, message: string, attributes?: LogAttributes) =>\n\t`[${scope}] ${message}${attributes ? ` ${JSON.stringify(attributes)}` : ''}`\n\n/**\n * Default logger: silent for `debug`/`info`, console for `warn`/`error`, so a\n * library default never floods stdout with one line per call. Inject your own\n * with `createFn({ logger })`.\n */\nexport function createDefaultLogger(scope: string): FnLogger {\n\treturn {\n\t\tdebug() {},\n\t\tinfo() {},\n\t\twarn: (message, attributes) =>\n\t\t\tconsole.warn(format(scope, message, attributes)),\n\t\terror: (message, attributes) =>\n\t\t\tconsole.error(format(scope, message, attributes))\n\t}\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { type Tool } from '@mastra/core/tools';
2
+ import { type AnyProcedure } from '@orpc/server';
3
+ import { toToolName } from './meta.js';
4
+ import type { ProcedureInput, ProcedureOutput } from './types.js';
5
+ export { toToolName };
6
+ export type CreateMastraToolOptions = {
7
+ /** Override the tool id (defaults to the sanitized `fn` name). */
8
+ id?: string;
9
+ /** Override the tool description (defaults to the route summary). */
10
+ description?: string;
11
+ /** Gate execution behind the AI SDK tool-approval flow. */
12
+ requireApproval?: boolean;
13
+ /**
14
+ * Accept a procedure without an input schema (the tool then takes an empty
15
+ * object). For no-input queries like `teams.list`.
16
+ */
17
+ allowMissingInputSchema?: boolean;
18
+ onExecuteFinish?: (event: {
19
+ toolId: string;
20
+ durationMs: number;
21
+ outcome: 'success' | 'error';
22
+ }) => void;
23
+ /** Key holding the oRPC context in Mastra's `RequestContext`. Default 'orpcContext'. */
24
+ contextKey?: string;
25
+ };
26
+ /** What the tool takes: the procedure's raw input, or `{}` without a schema. */
27
+ export type MastraToolInput<TProc> = unknown extends ProcedureInput<TProc> ? Record<string, never> : ProcedureInput<TProc>;
28
+ /** A Mastra tool typed by the procedure's raw input and parsed output. */
29
+ export type MastraTool<TProc extends AnyProcedure> = Tool<MastraToolInput<TProc>, ProcedureOutput<TProc>>;
30
+ /** LLM tool parameters must be an object; other inputs are a type error. */
31
+ type ObjectInputCheck<TProc> = MastraToolInput<TProc> extends Record<string, unknown> ? unknown : {
32
+ 'orpc-fn: an agent tool needs an object input': ProcedureInput<TProc>;
33
+ };
34
+ /**
35
+ * Creates a Mastra tool from a procedure made with `fn()`. Execution calls
36
+ * the procedure with the oRPC context stored in the Mastra request context
37
+ * (`requestContext.set('orpcContext', context)`), so auth and guards apply.
38
+ *
39
+ * Mastra checks the input against the procedure's schema (with dates coerced
40
+ * from strings) but passes the raw input on for the procedure to parse;
41
+ * the output is the procedure's parsed result, passed through unchanged.
42
+ */
43
+ export declare function createMastraTool<TProc extends AnyProcedure>(procedure: TProc & ObjectInputCheck<TProc>, options?: CreateMastraToolOptions): MastraTool<TProc>;
44
+ //# sourceMappingURL=mastra.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mastra.d.ts","sourceRoot":"","sources":["../src/mastra.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,KAAK,YAAY,EAAQ,MAAM,cAAc,CAAA;AAMtD,OAAO,EAAc,UAAU,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjE,OAAO,EAAE,UAAU,EAAE,CAAA;AAErB,MAAM,MAAM,uBAAuB,GAAG;IACrC,kEAAkE;IAClE,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2DAA2D;IAC3D,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE;QACzB,MAAM,EAAE,MAAM,CAAA;QACd,UAAU,EAAE,MAAM,CAAA;QAClB,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;KAC5B,KAAK,IAAI,CAAA;IACV,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,gFAAgF;AAChF,MAAM,MAAM,eAAe,CAAC,KAAK,IAChC,OAAO,SAAS,cAAc,CAAC,KAAK,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GACrB,cAAc,CAAC,KAAK,CAAC,CAAA;AAEzB,0EAA0E;AAC1E,MAAM,MAAM,UAAU,CAAC,KAAK,SAAS,YAAY,IAAI,IAAI,CACxD,eAAe,CAAC,KAAK,CAAC,EACtB,eAAe,CAAC,KAAK,CAAC,CACtB,CAAA;AAED,4EAA4E;AAC5E,KAAK,gBAAgB,CAAC,KAAK,IAC1B,eAAe,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnD,OAAO,GACP;IAAE,8CAA8C,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,CAAA;AAE7E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,YAAY,EAC1D,SAAS,EAAE,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAC1C,OAAO,GAAE,uBAA4B,GACnC,UAAU,CAAC,KAAK,CAAC,CA2EnB"}
package/dist/mastra.js ADDED
@@ -0,0 +1,78 @@
1
+ import { createTool } from '@mastra/core/tools';
2
+ import { call } from '@orpc/server';
3
+ import { passThroughOutputSchema, rawInputSchema } from './json-schema.js';
4
+ import { readFnMeta, toToolName } from './meta.js';
5
+ export { toToolName };
6
+ /**
7
+ * Creates a Mastra tool from a procedure made with `fn()`. Execution calls
8
+ * the procedure with the oRPC context stored in the Mastra request context
9
+ * (`requestContext.set('orpcContext', context)`), so auth and guards apply.
10
+ *
11
+ * Mastra checks the input against the procedure's schema (with dates coerced
12
+ * from strings) but passes the raw input on for the procedure to parse;
13
+ * the output is the procedure's parsed result, passed through unchanged.
14
+ */
15
+ export function createMastraTool(procedure, options = {}) {
16
+ const meta = readFnMeta(procedure);
17
+ const name = options.id ?? meta.name;
18
+ if (!name) {
19
+ throw new Error('Procedure must be created with fn() to use createMastraTool');
20
+ }
21
+ let inputSchema;
22
+ if (meta.inputSchema) {
23
+ inputSchema = rawInputSchema(meta.inputSchema);
24
+ if (inputSchema['~standard'].jsonSchema.input().type !== 'object') {
25
+ throw new Error(`${name} must take an object input to be an agent tool`);
26
+ }
27
+ }
28
+ else if (!options.allowMissingInputSchema) {
29
+ throw new Error(`${name} must declare an input schema to be exposed as an agent tool`);
30
+ }
31
+ const id = options.id ?? toToolName(name);
32
+ const contextKey = options.contextKey ?? 'orpcContext';
33
+ const tool = createTool({
34
+ id,
35
+ description: options.description ?? meta.description,
36
+ requireApproval: options.requireApproval === true,
37
+ inputSchema: inputSchema ??
38
+ {
39
+ '~standard': {
40
+ version: 1,
41
+ vendor: 'orpc-fn',
42
+ validate: () => ({ value: {} }),
43
+ jsonSchema: {
44
+ input: () => ({ type: 'object', properties: {} }),
45
+ output: () => ({ type: 'object', properties: {} })
46
+ }
47
+ }
48
+ },
49
+ ...(meta.outputSchema
50
+ ? { outputSchema: passThroughOutputSchema(meta.outputSchema) }
51
+ : {}),
52
+ execute: async (input, ctx) => {
53
+ const requestContext = ctx?.requestContext;
54
+ const context = requestContext?.get(contextKey);
55
+ if (!context) {
56
+ throw new Error(`createMastraTool execution requires ${contextKey} in the Mastra request context`);
57
+ }
58
+ const startedAt = performance.now();
59
+ const finish = (outcome) => options.onExecuteFinish?.({
60
+ toolId: id,
61
+ durationMs: performance.now() - startedAt,
62
+ outcome
63
+ });
64
+ try {
65
+ const result = await call(procedure, meta.inputSchema ? input : undefined, ctx?.abortSignal ? { context, signal: ctx.abortSignal } : { context });
66
+ finish('success');
67
+ return result;
68
+ }
69
+ catch (error) {
70
+ finish('error');
71
+ throw error;
72
+ }
73
+ }
74
+ });
75
+ // The runtime schemas are untyped `ToolSchema`s; `MastraTool` is the typed surface.
76
+ return tool;
77
+ }
78
+ //# sourceMappingURL=mastra.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mastra.js","sourceRoot":"","sources":["../src/mastra.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RequestContext } from '@mastra/core/request-context'\nimport { createTool, type Tool } from '@mastra/core/tools'\nimport { type AnyProcedure, call } from '@orpc/server'\nimport {\n\tpassThroughOutputSchema,\n\trawInputSchema,\n\ttype ToolSchema\n} from './json-schema.js'\nimport { readFnMeta, toToolName } from './meta.js'\nimport type { ProcedureInput, ProcedureOutput } from './types.js'\n\nexport { toToolName }\n\nexport type CreateMastraToolOptions = {\n\t/** Override the tool id (defaults to the sanitized `fn` name). */\n\tid?: string\n\t/** Override the tool description (defaults to the route summary). */\n\tdescription?: string\n\t/** Gate execution behind the AI SDK tool-approval flow. */\n\trequireApproval?: boolean\n\t/**\n\t * Accept a procedure without an input schema (the tool then takes an empty\n\t * object). For no-input queries like `teams.list`.\n\t */\n\tallowMissingInputSchema?: boolean\n\tonExecuteFinish?: (event: {\n\t\ttoolId: string\n\t\tdurationMs: number\n\t\toutcome: 'success' | 'error'\n\t}) => void\n\t/** Key holding the oRPC context in Mastra's `RequestContext`. Default 'orpcContext'. */\n\tcontextKey?: string\n}\n\n/** What the tool takes: the procedure's raw input, or `{}` without a schema. */\nexport type MastraToolInput<TProc> =\n\tunknown extends ProcedureInput<TProc>\n\t\t? Record<string, never>\n\t\t: ProcedureInput<TProc>\n\n/** A Mastra tool typed by the procedure's raw input and parsed output. */\nexport type MastraTool<TProc extends AnyProcedure> = Tool<\n\tMastraToolInput<TProc>,\n\tProcedureOutput<TProc>\n>\n\n/** LLM tool parameters must be an object; other inputs are a type error. */\ntype ObjectInputCheck<TProc> =\n\tMastraToolInput<TProc> extends Record<string, unknown>\n\t\t? unknown\n\t\t: { 'orpc-fn: an agent tool needs an object input': ProcedureInput<TProc> }\n\n/**\n * Creates a Mastra tool from a procedure made with `fn()`. Execution calls\n * the procedure with the oRPC context stored in the Mastra request context\n * (`requestContext.set('orpcContext', context)`), so auth and guards apply.\n *\n * Mastra checks the input against the procedure's schema (with dates coerced\n * from strings) but passes the raw input on for the procedure to parse;\n * the output is the procedure's parsed result, passed through unchanged.\n */\nexport function createMastraTool<TProc extends AnyProcedure>(\n\tprocedure: TProc & ObjectInputCheck<TProc>,\n\toptions: CreateMastraToolOptions = {}\n): MastraTool<TProc> {\n\tconst meta = readFnMeta(procedure)\n\tconst name = options.id ?? meta.name\n\tif (!name) {\n\t\tthrow new Error(\n\t\t\t'Procedure must be created with fn() to use createMastraTool'\n\t\t)\n\t}\n\tlet inputSchema: ToolSchema | undefined\n\tif (meta.inputSchema) {\n\t\tinputSchema = rawInputSchema(meta.inputSchema)\n\t\tif (inputSchema['~standard'].jsonSchema.input().type !== 'object') {\n\t\t\tthrow new Error(`${name} must take an object input to be an agent tool`)\n\t\t}\n\t} else if (!options.allowMissingInputSchema) {\n\t\tthrow new Error(\n\t\t\t`${name} must declare an input schema to be exposed as an agent tool`\n\t\t)\n\t}\n\n\tconst id = options.id ?? toToolName(name)\n\tconst contextKey = options.contextKey ?? 'orpcContext'\n\n\tconst tool = createTool({\n\t\tid,\n\t\tdescription: options.description ?? meta.description,\n\t\trequireApproval: options.requireApproval === true,\n\t\tinputSchema:\n\t\t\tinputSchema ??\n\t\t\t({\n\t\t\t\t'~standard': {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tvendor: 'orpc-fn',\n\t\t\t\t\tvalidate: () => ({ value: {} }),\n\t\t\t\t\tjsonSchema: {\n\t\t\t\t\t\tinput: () => ({ type: 'object', properties: {} }),\n\t\t\t\t\t\toutput: () => ({ type: 'object', properties: {} })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} satisfies ToolSchema),\n\t\t...(meta.outputSchema\n\t\t\t? { outputSchema: passThroughOutputSchema(meta.outputSchema) }\n\t\t\t: {}),\n\t\texecute: async (input, ctx) => {\n\t\t\tconst requestContext = ctx?.requestContext as RequestContext | undefined\n\t\t\tconst context = requestContext?.get(contextKey)\n\t\t\tif (!context) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`createMastraTool execution requires ${contextKey} in the Mastra request context`\n\t\t\t\t)\n\t\t\t}\n\t\t\tconst startedAt = performance.now()\n\t\t\tconst finish = (outcome: 'success' | 'error') =>\n\t\t\t\toptions.onExecuteFinish?.({\n\t\t\t\t\ttoolId: id,\n\t\t\t\t\tdurationMs: performance.now() - startedAt,\n\t\t\t\t\toutcome\n\t\t\t\t})\n\t\t\ttry {\n\t\t\t\tconst result = await call(\n\t\t\t\t\tprocedure,\n\t\t\t\t\tmeta.inputSchema ? input : undefined,\n\t\t\t\t\tctx?.abortSignal ? { context, signal: ctx.abortSignal } : { context }\n\t\t\t\t)\n\t\t\t\tfinish('success')\n\t\t\t\treturn result\n\t\t\t} catch (error) {\n\t\t\t\tfinish('error')\n\t\t\t\tthrow error\n\t\t\t}\n\t\t}\n\t})\n\n\t// The runtime schemas are untyped `ToolSchema`s; `MastraTool` is the typed surface.\n\treturn tool as unknown as MastraTool<TProc>\n}\n"]}