orpc-fn 0.4.0 → 0.5.1

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 (93) hide show
  1. package/README.md +146 -4
  2. package/dist/cli.js +37 -48
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client.d.ts +29 -0
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/client.js +49 -0
  7. package/dist/client.js.map +1 -1
  8. package/dist/create-fn.d.ts.map +1 -1
  9. package/dist/create-fn.js +0 -1
  10. package/dist/create-fn.js.map +1 -1
  11. package/dist/hono.d.ts +12 -1
  12. package/dist/hono.d.ts.map +1 -1
  13. package/dist/hono.js +15 -1
  14. package/dist/hono.js.map +1 -1
  15. package/dist/live/fn-live.d.ts +1 -6
  16. package/dist/live/fn-live.d.ts.map +1 -1
  17. package/dist/live/fn-live.js +2 -18
  18. package/dist/live/fn-live.js.map +1 -1
  19. package/dist/live/index.d.ts +3 -1
  20. package/dist/live/index.d.ts.map +1 -1
  21. package/dist/live/index.js +2 -0
  22. package/dist/live/index.js.map +1 -1
  23. package/dist/live/invalidation-stream.d.ts +52 -0
  24. package/dist/live/invalidation-stream.d.ts.map +1 -0
  25. package/dist/live/invalidation-stream.js +98 -0
  26. package/dist/live/invalidation-stream.js.map +1 -0
  27. package/dist/live/ioredis.d.ts +1 -0
  28. package/dist/live/ioredis.d.ts.map +1 -1
  29. package/dist/live/ioredis.js +1 -0
  30. package/dist/live/ioredis.js.map +1 -1
  31. package/dist/live/memory.d.ts +2 -1
  32. package/dist/live/memory.d.ts.map +1 -1
  33. package/dist/live/memory.js +6 -1
  34. package/dist/live/memory.js.map +1 -1
  35. package/dist/live/pub-sub.d.ts +5 -0
  36. package/dist/live/pub-sub.d.ts.map +1 -1
  37. package/dist/live/pub-sub.js +16 -3
  38. package/dist/live/pub-sub.js.map +1 -1
  39. package/dist/live/redis-bun.d.ts +3 -0
  40. package/dist/live/redis-bun.d.ts.map +1 -1
  41. package/dist/live/redis-bun.js +1 -0
  42. package/dist/live/redis-bun.js.map +1 -1
  43. package/dist/live/redis.d.ts +4 -1
  44. package/dist/live/redis.d.ts.map +1 -1
  45. package/dist/live/redis.js +14 -2
  46. package/dist/live/redis.js.map +1 -1
  47. package/dist/live/scoped-change-bus.d.ts +45 -0
  48. package/dist/live/scoped-change-bus.d.ts.map +1 -0
  49. package/dist/live/scoped-change-bus.js +38 -0
  50. package/dist/live/scoped-change-bus.js.map +1 -0
  51. package/dist/live/transport.d.ts +11 -0
  52. package/dist/live/transport.d.ts.map +1 -1
  53. package/dist/live/transport.js.map +1 -1
  54. package/dist/procedure-cli.d.ts +25 -0
  55. package/dist/procedure-cli.d.ts.map +1 -0
  56. package/dist/procedure-cli.js +67 -0
  57. package/dist/procedure-cli.js.map +1 -0
  58. package/dist/react.d.ts +116 -0
  59. package/dist/react.d.ts.map +1 -0
  60. package/dist/react.js +156 -0
  61. package/dist/react.js.map +1 -0
  62. package/dist/realtime.d.ts +114 -0
  63. package/dist/realtime.d.ts.map +1 -0
  64. package/dist/realtime.js +316 -0
  65. package/dist/realtime.js.map +1 -0
  66. package/dist/vue/testing.d.ts +67 -0
  67. package/dist/vue/testing.d.ts.map +1 -0
  68. package/dist/vue/testing.js +225 -0
  69. package/dist/vue/testing.js.map +1 -0
  70. package/dist/vue.d.ts +211 -0
  71. package/dist/vue.d.ts.map +1 -0
  72. package/dist/vue.js +312 -0
  73. package/dist/vue.js.map +1 -0
  74. package/package.json +49 -3
  75. package/src/cli.ts +47 -57
  76. package/src/client.ts +57 -0
  77. package/src/create-fn.ts +0 -1
  78. package/src/hono.ts +22 -2
  79. package/src/live/fn-live.ts +3 -22
  80. package/src/live/index.ts +12 -1
  81. package/src/live/invalidation-stream.ts +142 -0
  82. package/src/live/ioredis.ts +2 -0
  83. package/src/live/memory.ts +11 -2
  84. package/src/live/pub-sub.ts +23 -2
  85. package/src/live/redis-bun.ts +3 -0
  86. package/src/live/redis.ts +33 -6
  87. package/src/live/scoped-change-bus.ts +98 -0
  88. package/src/live/transport.ts +12 -0
  89. package/src/procedure-cli.ts +99 -0
  90. package/src/react.ts +355 -0
  91. package/src/realtime.ts +416 -0
  92. package/src/vue/testing.ts +296 -0
  93. package/src/vue.ts +783 -0
@@ -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"]}
@@ -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;CAC/B;AAED,eAAO,MAAM,UAAU,YAAa,MAAM,WAAyB,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":"AA2CA,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/** 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}\n\nexport const backlogKey = (channel: string) => `${channel}:backlog`\n"]}
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"]}
@@ -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"]}
@@ -0,0 +1,116 @@
1
+ import { type QueryKey, type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
2
+ import { type InvalidationStreamOptions, type LiveConnectionStatus } from './realtime.js';
3
+ export type { InvalidationDomain, LiveConnectionStatus } from './realtime.js';
4
+ /** An oRPC client procedure fits: `client.order.watch`. */
5
+ export type SubscribeFn<TInput, TEvent> = (input: TInput, options: {
6
+ signal: AbortSignal;
7
+ }) => Promise<AsyncIterable<TEvent>>;
8
+ export type UseOrpcSubscriptionOptions<TInput, TEvent> = {
9
+ /** Run the stream (default: true). */
10
+ enabled?: boolean | undefined;
11
+ onEvent?: (event: TEvent, meta: {
12
+ connectionId: number;
13
+ input: TInput;
14
+ }) => void;
15
+ /** Every successful connect; `connectionId > 1` is a reconnect. */
16
+ onConnect?: (connectionId: number) => void;
17
+ onError?: ((error: Error) => void) | undefined;
18
+ onClose?: () => void;
19
+ /**
20
+ * Close the stream while the app is not focused (default: true). Focus is
21
+ * TanStack's `focusManager`: on React Native, wire it to `AppState` once.
22
+ */
23
+ pauseWhenUnfocused?: boolean | undefined;
24
+ };
25
+ /**
26
+ * An oRPC event iterator with backoff reconnects that closes while the app is
27
+ * backgrounded. A changed input (compared as JSON) reconnects with a fresh
28
+ * retry budget.
29
+ */
30
+ export declare function useOrpcSubscription<TInput, TEvent>(input: TInput, subscribe: SubscribeFn<TInput, TEvent>, options?: UseOrpcSubscriptionOptions<TInput, TEvent>): {
31
+ status: LiveConnectionStatus;
32
+ };
33
+ type ApplyEvent<TStreamEvent, TData> = (event: TStreamEvent, previous: TData | undefined) => TData | undefined;
34
+ type LiveQueryOptions<TInput, TRouteData, TStreamEvent, TData, TSubscriptionInput> = {
35
+ /** Run the query and its stream (default: true). */
36
+ enabled?: boolean | undefined;
37
+ /** Input for the stream when it differs from the query's. */
38
+ subscriptionInput?: TSubscriptionInput | undefined;
39
+ /**
40
+ * Convert the route output once at fetch time into a differently typed
41
+ * cache; `applyEvent` then reduces stream events into that cache.
42
+ */
43
+ select?: (data: TRouteData) => TData;
44
+ /** The raw route output of a seed, before `select` drops fields. */
45
+ onSeed?: (data: TRouteData, input: TInput) => void;
46
+ /**
47
+ * Fold a stream event into the cached data. Required when the stream sends
48
+ * something other than the cached type (an `fnLive` patch route, or a
49
+ * `select`ed cache). Return `undefined` to skip the write.
50
+ */
51
+ applyEvent?: ApplyEvent<TStreamEvent, NoInfer<TData>>;
52
+ onEvent?: (event: TStreamEvent, meta: {
53
+ connectionId: number;
54
+ }) => void;
55
+ onError?: ((error: Error) => void) | undefined;
56
+ /**
57
+ * Reconcile a fetched seed with what the stream already wrote. The HTTP
58
+ * request can resolve last, so a plain overwrite would drop events that
59
+ * arrived while it was in flight. Default: take the fetched value.
60
+ */
61
+ mergeSeed?: (fetched: NoInfer<TData>, cached: NoInfer<TData> | undefined) => NoInfer<TData>;
62
+ /** Show the previous key's data while a new key loads (default: true). */
63
+ keepPreviousData?: boolean;
64
+ /** Close the stream while the app is not focused (default: true). */
65
+ pauseWhenUnfocused?: boolean | undefined;
66
+ };
67
+ type LiveQueryOptionsFn<TInput, TData, TError> = (input: TInput) => UseQueryOptions<TData, TError, TData, QueryKey>;
68
+ type LiveQueryResult<TData, TError> = UseQueryResult<TData, TError> & {
69
+ liveStatus: LiveConnectionStatus;
70
+ };
71
+ /**
72
+ * The client half of `fnLive`: the HTTP query seeds the cache, and the live
73
+ * route's stream writes each snapshot into the same cache entry. Marked
74
+ * `meta.live`, so invalidation streams leave it alone.
75
+ *
76
+ * ```tsx
77
+ * const order = useLiveQuery({ id }, (input) => orpc.order.get.queryOptions({ input }), client.order.watch)
78
+ * ```
79
+ *
80
+ * One signature, not overloads: TypeScript fixes an untyped callback's
81
+ * parameters at the first overload it tries, which made valid calls fail.
82
+ */
83
+ export declare function useLiveQuery<TInput, TRouteData, TStreamEvent, TData = TRouteData, TError = Error, TSubscriptionInput = TInput>(input: TInput, queryOptionsFn: LiveQueryOptionsFn<TInput, TRouteData, TError>, subscribe: SubscribeFn<NoInfer<TSubscriptionInput>, TStreamEvent>, ...options: [TStreamEvent] extends [TData] ? [
84
+ options?: LiveQueryOptions<TInput, TRouteData, TStreamEvent, TData, TSubscriptionInput>
85
+ ] : [
86
+ options: LiveQueryOptions<TInput, TRouteData, TStreamEvent, TData, TSubscriptionInput> & {
87
+ applyEvent: ApplyEvent<TStreamEvent, NoInfer<TData>>;
88
+ }
89
+ ]): LiveQueryResult<TData, TError>;
90
+ /**
91
+ * One app-wide stream of `{ domain }` change events that keeps list and count
92
+ * queries fresh: each event invalidates its domain's query keys (debounced),
93
+ * and a reconnect that proves healthy invalidates every domain to cover
94
+ * events lost while it was down. Mount once, in the signed-in shell.
95
+ *
96
+ * ```ts
97
+ * useInvalidationStream({}, client.realtime.subscribeChanges, {
98
+ * enabled: signedIn,
99
+ * domains: { order: { debounceMs: 100, queryKeys: [orpc.order.key()] } }
100
+ * })
101
+ * ```
102
+ */
103
+ export declare function useInvalidationStream<TInput, TEvent extends {
104
+ domain: string;
105
+ }>(input: TInput, subscribe: SubscribeFn<TInput, TEvent>, options: InvalidationStreamOptions<TEvent> & Pick<UseOrpcSubscriptionOptions<TInput, TEvent>, 'enabled' | 'pauseWhenUnfocused'>): {
106
+ status: LiveConnectionStatus;
107
+ };
108
+ /**
109
+ * Clear the query cache when the signed-in owner of the cached data changes
110
+ * (another user, another organization), so the next one never sees it.
111
+ * `null` means signed out: signing in starts from the cache sign-out emptied.
112
+ * Pass the same owner as the persister's `buster` so a saved cache from
113
+ * someone else is discarded on the next launch too.
114
+ */
115
+ export declare function useQueryCacheOwner(owner: string | null): void;
116
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"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"}
package/dist/react.js ADDED
@@ -0,0 +1,156 @@
1
+ import { focusManager, hashKey, keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { createInvalidationCoordinator, createStreamScope, createSubscriptionRunner, seedQueryFn } from './realtime.js';
4
+ const subscribeFocus = (listener) => focusManager.subscribe(listener);
5
+ const getFocus = () => focusManager.isFocused();
6
+ /** Whether the app is focused, from TanStack's `focusManager`. */
7
+ function useIsFocused() {
8
+ return useSyncExternalStore(subscribeFocus, getFocus, getFocus);
9
+ }
10
+ /**
11
+ * An oRPC event iterator with backoff reconnects that closes while the app is
12
+ * backgrounded. A changed input (compared as JSON) reconnects with a fresh
13
+ * retry budget.
14
+ */
15
+ export function useOrpcSubscription(input, subscribe, options = {}) {
16
+ const { enabled = true, pauseWhenUnfocused = true } = options;
17
+ const focused = useIsFocused();
18
+ const shouldRun = enabled && (!pauseWhenUnfocused || focused);
19
+ const [status, setStatus] = useState('idle');
20
+ const latest = useRef({ input, subscribe, options, shouldRun });
21
+ latest.current = { input, subscribe, options, shouldRun };
22
+ const runnerRef = useRef(null);
23
+ if (!runnerRef.current) {
24
+ let openedInput = input;
25
+ runnerRef.current = createSubscriptionRunner({
26
+ open: (signal) => {
27
+ openedInput = latest.current.input;
28
+ return latest.current.subscribe(openedInput, { signal });
29
+ },
30
+ canConnect: () => latest.current.shouldRun,
31
+ shouldReconnect: () => latest.current.shouldRun,
32
+ onStatus: setStatus,
33
+ onEvent: (event, connectionId) => latest.current.options.onEvent?.(event, {
34
+ connectionId,
35
+ input: openedInput
36
+ }),
37
+ onConnect: (id) => latest.current.options.onConnect?.(id),
38
+ onError: (error) => latest.current.options.onError?.(error),
39
+ onClose: () => latest.current.options.onClose?.()
40
+ });
41
+ }
42
+ const runner = runnerRef.current;
43
+ const serializedInput = JSON.stringify(input);
44
+ // biome-ignore lint/correctness/useExhaustiveDependencies: a changed input resubscribes
45
+ useEffect(() => {
46
+ if (!shouldRun)
47
+ return;
48
+ void runner.start();
49
+ return () => void runner.disconnect();
50
+ }, [runner, shouldRun, serializedInput]);
51
+ return { status };
52
+ }
53
+ export function useLiveQuery(input, queryOptionsFn, subscribe, options = {}) {
54
+ const queryClient = useQueryClient();
55
+ const focused = useIsFocused();
56
+ const [scope] = useState(createStreamScope);
57
+ // oRPC query options are plain objects; `queryFn` is read through one shape.
58
+ const raw = queryOptionsFn(input);
59
+ const keyHash = hashKey(raw.queryKey);
60
+ const latest = useRef({ raw, keyHash, options });
61
+ latest.current = { raw, keyHash, options };
62
+ const { status } = useOrpcSubscription((options.subscriptionInput === undefined
63
+ ? input
64
+ : options.subscriptionInput), subscribe, {
65
+ enabled: options.enabled,
66
+ pauseWhenUnfocused: options.pauseWhenUnfocused,
67
+ onError: options.onError,
68
+ onConnect: (connectionId) => scope.open(connectionId, latest.current.keyHash),
69
+ onEvent: (event, { connectionId }) => {
70
+ const current = latest.current;
71
+ // The consumer callback must not see an event for a scope no longer shown.
72
+ if (!scope.owns(connectionId, current.keyHash))
73
+ return;
74
+ // Without `applyEvent` the stream event is the cache type (the first overload).
75
+ const applyEvent = current.options.applyEvent ??
76
+ ((streamEvent) => streamEvent);
77
+ const key = current.raw.queryKey;
78
+ const next = applyEvent(event, queryClient.getQueryData(key));
79
+ if (next !== undefined)
80
+ queryClient.setQueryData(key, next);
81
+ current.options.onEvent?.(event, { connectionId });
82
+ }
83
+ });
84
+ const wrapsSeed = options.select || options.mergeSeed;
85
+ const queryFn = raw.queryFn && wrapsSeed
86
+ ? seedQueryFn(raw.queryFn, {
87
+ input,
88
+ readCache: () => queryClient.getQueryData(raw.queryKey),
89
+ select: options.select,
90
+ onSeed: options.onSeed,
91
+ mergeSeed: options.mergeSeed
92
+ })
93
+ : raw.queryFn;
94
+ const runs = options.enabled !== false && focused;
95
+ const query = useQuery({
96
+ ...raw,
97
+ ...(queryFn ? { queryFn } : {}),
98
+ enabled: runs ? (raw.enabled ?? true) : false,
99
+ ...(options.keepPreviousData === false
100
+ ? {}
101
+ : { placeholderData: keepPreviousData }),
102
+ staleTime: Number.POSITIVE_INFINITY,
103
+ meta: { ...raw.meta, live: true }
104
+ });
105
+ return Object.assign(query, { liveStatus: status });
106
+ }
107
+ /**
108
+ * One app-wide stream of `{ domain }` change events that keeps list and count
109
+ * queries fresh: each event invalidates its domain's query keys (debounced),
110
+ * and a reconnect that proves healthy invalidates every domain to cover
111
+ * events lost while it was down. Mount once, in the signed-in shell.
112
+ *
113
+ * ```ts
114
+ * useInvalidationStream({}, client.realtime.subscribeChanges, {
115
+ * enabled: signedIn,
116
+ * domains: { order: { debounceMs: 100, queryKeys: [orpc.order.key()] } }
117
+ * })
118
+ * ```
119
+ */
120
+ export function useInvalidationStream(input, subscribe, options) {
121
+ const queryClient = useQueryClient();
122
+ const latestOptions = useRef(options);
123
+ latestOptions.current = options;
124
+ // `domains`, `refetchType` and `logger` are read on mount; `onEvent` stays current.
125
+ const [coordinator] = useState(() => createInvalidationCoordinator(queryClient, {
126
+ ...options,
127
+ onEvent: (event) => latestOptions.current.onEvent?.(event)
128
+ }));
129
+ useEffect(() => coordinator.dispose, [coordinator]);
130
+ return useOrpcSubscription(input, subscribe, {
131
+ enabled: options.enabled,
132
+ pauseWhenUnfocused: options.pauseWhenUnfocused,
133
+ onEvent: coordinator.event,
134
+ onConnect: coordinator.connect,
135
+ onError: coordinator.lost,
136
+ onClose: coordinator.lost
137
+ });
138
+ }
139
+ /**
140
+ * Clear the query cache when the signed-in owner of the cached data changes
141
+ * (another user, another organization), so the next one never sees it.
142
+ * `null` means signed out: signing in starts from the cache sign-out emptied.
143
+ * Pass the same owner as the persister's `buster` so a saved cache from
144
+ * someone else is discarded on the next launch too.
145
+ */
146
+ export function useQueryCacheOwner(owner) {
147
+ const queryClient = useQueryClient();
148
+ const previous = useRef(owner);
149
+ useEffect(() => {
150
+ const before = previous.current;
151
+ previous.current = owner;
152
+ if (owner !== before && before !== null)
153
+ queryClient.clear();
154
+ }, [owner, queryClient]);
155
+ }
156
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tfocusManager,\n\thashKey,\n\tkeepPreviousData,\n\ttype QueryKey,\n\ttype UseQueryOptions,\n\ttype UseQueryResult,\n\tuseQuery,\n\tuseQueryClient\n} from '@tanstack/react-query'\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react'\nimport {\n\tcreateInvalidationCoordinator,\n\tcreateStreamScope,\n\tcreateSubscriptionRunner,\n\ttype InvalidationStreamOptions,\n\ttype LiveConnectionStatus,\n\ttype SubscriptionRunner,\n\tseedQueryFn\n} from './realtime.js'\n\nexport type { InvalidationDomain, LiveConnectionStatus } from './realtime.js'\n\n/** An oRPC client procedure fits: `client.order.watch`. */\nexport type SubscribeFn<TInput, TEvent> = (\n\tinput: TInput,\n\toptions: { signal: AbortSignal }\n) => Promise<AsyncIterable<TEvent>>\n\nexport type UseOrpcSubscriptionOptions<TInput, TEvent> = {\n\t/** Run the stream (default: true). */\n\tenabled?: boolean | undefined\n\tonEvent?: (\n\t\tevent: TEvent,\n\t\tmeta: { connectionId: number; input: TInput }\n\t) => void\n\t/** Every successful connect; `connectionId > 1` is a reconnect. */\n\tonConnect?: (connectionId: number) => void\n\tonError?: ((error: Error) => void) | undefined\n\tonClose?: () => void\n\t/**\n\t * Close the stream while the app is not focused (default: true). Focus is\n\t * TanStack's `focusManager`: on React Native, wire it to `AppState` once.\n\t */\n\tpauseWhenUnfocused?: boolean | undefined\n}\n\nconst subscribeFocus = (listener: () => void) =>\n\tfocusManager.subscribe(listener)\nconst getFocus = () => focusManager.isFocused()\n\n/** Whether the app is focused, from TanStack's `focusManager`. */\nfunction useIsFocused(): boolean {\n\treturn useSyncExternalStore(subscribeFocus, getFocus, getFocus)\n}\n\n/**\n * An oRPC event iterator with backoff reconnects that closes while the app is\n * backgrounded. A changed input (compared as JSON) reconnects with a fresh\n * retry budget.\n */\nexport function useOrpcSubscription<TInput, TEvent>(\n\tinput: TInput,\n\tsubscribe: SubscribeFn<TInput, TEvent>,\n\toptions: UseOrpcSubscriptionOptions<TInput, TEvent> = {}\n): { status: LiveConnectionStatus } {\n\tconst { enabled = true, pauseWhenUnfocused = true } = options\n\tconst focused = useIsFocused()\n\tconst shouldRun = enabled && (!pauseWhenUnfocused || focused)\n\tconst [status, setStatus] = useState<LiveConnectionStatus>('idle')\n\n\tconst latest = useRef({ input, subscribe, options, shouldRun })\n\tlatest.current = { input, subscribe, options, shouldRun }\n\n\tconst runnerRef = useRef<SubscriptionRunner | null>(null)\n\tif (!runnerRef.current) {\n\t\tlet openedInput = input\n\t\trunnerRef.current = createSubscriptionRunner<TEvent>({\n\t\t\topen: (signal) => {\n\t\t\t\topenedInput = latest.current.input\n\t\t\t\treturn latest.current.subscribe(openedInput, { signal })\n\t\t\t},\n\t\t\tcanConnect: () => latest.current.shouldRun,\n\t\t\tshouldReconnect: () => latest.current.shouldRun,\n\t\t\tonStatus: setStatus,\n\t\t\tonEvent: (event, connectionId) =>\n\t\t\t\tlatest.current.options.onEvent?.(event, {\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tinput: openedInput\n\t\t\t\t}),\n\t\t\tonConnect: (id) => latest.current.options.onConnect?.(id),\n\t\t\tonError: (error) => latest.current.options.onError?.(error),\n\t\t\tonClose: () => latest.current.options.onClose?.()\n\t\t})\n\t}\n\tconst runner = runnerRef.current\n\tconst serializedInput = JSON.stringify(input)\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: a changed input resubscribes\n\tuseEffect(() => {\n\t\tif (!shouldRun) return\n\t\tvoid runner.start()\n\t\treturn () => void runner.disconnect()\n\t}, [runner, shouldRun, serializedInput])\n\n\treturn { status }\n}\n\ntype ApplyEvent<TStreamEvent, TData> = (\n\tevent: TStreamEvent,\n\tprevious: TData | undefined\n) => TData | undefined\n\ntype LiveQueryOptions<\n\tTInput,\n\tTRouteData,\n\tTStreamEvent,\n\tTData,\n\tTSubscriptionInput\n> = {\n\t/** Run the query and its stream (default: true). */\n\tenabled?: boolean | undefined\n\t/** Input for the stream when it differs from the query's. */\n\tsubscriptionInput?: TSubscriptionInput | undefined\n\t/**\n\t * Convert the route output once at fetch time into a differently typed\n\t * cache; `applyEvent` then reduces stream events into that cache.\n\t */\n\tselect?: (data: TRouteData) => TData\n\t/** The raw route output of a seed, before `select` drops fields. */\n\tonSeed?: (data: TRouteData, input: TInput) => void\n\t/**\n\t * Fold a stream event into the cached data. Required when the stream sends\n\t * something other than the cached type (an `fnLive` patch route, or a\n\t * `select`ed cache). Return `undefined` to skip the write.\n\t */\n\tapplyEvent?: ApplyEvent<TStreamEvent, NoInfer<TData>>\n\tonEvent?: (event: TStreamEvent, meta: { connectionId: number }) => void\n\tonError?: ((error: Error) => void) | undefined\n\t/**\n\t * Reconcile a fetched seed with what the stream already wrote. The HTTP\n\t * request can resolve last, so a plain overwrite would drop events that\n\t * arrived while it was in flight. Default: take the fetched value.\n\t */\n\tmergeSeed?: (\n\t\tfetched: NoInfer<TData>,\n\t\tcached: NoInfer<TData> | undefined\n\t) => NoInfer<TData>\n\t/** Show the previous key's data while a new key loads (default: true). */\n\tkeepPreviousData?: boolean\n\t/** Close the stream while the app is not focused (default: true). */\n\tpauseWhenUnfocused?: boolean | undefined\n}\n\ntype LiveQueryOptionsFn<TInput, TData, TError> = (\n\tinput: TInput\n) => UseQueryOptions<TData, TError, TData, QueryKey>\n\ntype LiveQueryResult<TData, TError> = UseQueryResult<TData, TError> & {\n\tliveStatus: LiveConnectionStatus\n}\n\n/**\n * The client half of `fnLive`: the HTTP query seeds the cache, and the live\n * route's stream writes each snapshot into the same cache entry. Marked\n * `meta.live`, so invalidation streams leave it alone.\n *\n * ```tsx\n * const order = useLiveQuery({ id }, (input) => orpc.order.get.queryOptions({ input }), client.order.watch)\n * ```\n *\n * One signature, not overloads: TypeScript fixes an untyped callback's\n * parameters at the first overload it tries, which made valid calls fail.\n */\nexport function useLiveQuery<\n\tTInput,\n\tTRouteData,\n\tTStreamEvent,\n\tTData = TRouteData,\n\tTError = Error,\n\tTSubscriptionInput = TInput\n>(\n\tinput: TInput,\n\tqueryOptionsFn: LiveQueryOptionsFn<TInput, TRouteData, TError>,\n\tsubscribe: SubscribeFn<NoInfer<TSubscriptionInput>, TStreamEvent>,\n\t...options: [TStreamEvent] extends [TData]\n\t\t? [\n\t\t\t\toptions?: LiveQueryOptions<\n\t\t\t\t\tTInput,\n\t\t\t\t\tTRouteData,\n\t\t\t\t\tTStreamEvent,\n\t\t\t\t\tTData,\n\t\t\t\t\tTSubscriptionInput\n\t\t\t\t>\n\t\t\t]\n\t\t: [\n\t\t\t\toptions: LiveQueryOptions<\n\t\t\t\t\tTInput,\n\t\t\t\t\tTRouteData,\n\t\t\t\t\tTStreamEvent,\n\t\t\t\t\tTData,\n\t\t\t\t\tTSubscriptionInput\n\t\t\t\t> & { applyEvent: ApplyEvent<TStreamEvent, NoInfer<TData>> }\n\t\t\t]\n): LiveQueryResult<TData, TError>\nexport function useLiveQuery<\n\tTInput,\n\tTRouteData,\n\tTStreamEvent,\n\tTData,\n\tTError,\n\tTSubscriptionInput\n>(\n\tinput: TInput,\n\tqueryOptionsFn: LiveQueryOptionsFn<TInput, TRouteData, TError>,\n\tsubscribe: SubscribeFn<TSubscriptionInput, TStreamEvent>,\n\toptions: LiveQueryOptions<\n\t\tTInput,\n\t\tTRouteData,\n\t\tTStreamEvent,\n\t\tTData,\n\t\tTSubscriptionInput\n\t> = {}\n): LiveQueryResult<TData, TError> {\n\tconst queryClient = useQueryClient()\n\tconst focused = useIsFocused()\n\tconst [scope] = useState(createStreamScope)\n\n\t// oRPC query options are plain objects; `queryFn` is read through one shape.\n\tconst raw = queryOptionsFn(input) as unknown as {\n\t\tqueryKey: QueryKey\n\t\tqueryFn?: (context: unknown) => Promise<TRouteData>\n\t\tenabled?: UseQueryOptions['enabled']\n\t\tmeta?: Record<string, unknown>\n\t} & Record<string, unknown>\n\tconst keyHash = hashKey(raw.queryKey)\n\tconst latest = useRef({ raw, keyHash, options })\n\tlatest.current = { raw, keyHash, options }\n\n\tconst { status } = useOrpcSubscription(\n\t\t(options.subscriptionInput === undefined\n\t\t\t? input\n\t\t\t: options.subscriptionInput) as TSubscriptionInput,\n\t\tsubscribe,\n\t\t{\n\t\t\tenabled: options.enabled,\n\t\t\tpauseWhenUnfocused: options.pauseWhenUnfocused,\n\t\t\tonError: options.onError,\n\t\t\tonConnect: (connectionId) =>\n\t\t\t\tscope.open(connectionId, latest.current.keyHash),\n\t\t\tonEvent: (event, { connectionId }) => {\n\t\t\t\tconst current = latest.current\n\t\t\t\t// The consumer callback must not see an event for a scope no longer shown.\n\t\t\t\tif (!scope.owns(connectionId, current.keyHash)) return\n\t\t\t\t// Without `applyEvent` the stream event is the cache type (the first overload).\n\t\t\t\tconst applyEvent =\n\t\t\t\t\tcurrent.options.applyEvent ??\n\t\t\t\t\t((streamEvent: TStreamEvent) => streamEvent as unknown as TData)\n\t\t\t\tconst key = current.raw.queryKey\n\t\t\t\tconst next = applyEvent(event, queryClient.getQueryData<TData>(key))\n\t\t\t\tif (next !== undefined) queryClient.setQueryData(key, next)\n\t\t\t\tcurrent.options.onEvent?.(event, { connectionId })\n\t\t\t}\n\t\t}\n\t)\n\n\tconst wrapsSeed = options.select || options.mergeSeed\n\tconst queryFn =\n\t\traw.queryFn && wrapsSeed\n\t\t\t? seedQueryFn(raw.queryFn, {\n\t\t\t\t\tinput,\n\t\t\t\t\treadCache: () => queryClient.getQueryData<TData>(raw.queryKey),\n\t\t\t\t\tselect: options.select,\n\t\t\t\t\tonSeed: options.onSeed,\n\t\t\t\t\tmergeSeed: options.mergeSeed\n\t\t\t\t})\n\t\t\t: raw.queryFn\n\tconst runs = options.enabled !== false && focused\n\tconst query = useQuery({\n\t\t...raw,\n\t\t...(queryFn ? { queryFn } : {}),\n\t\tenabled: runs ? (raw.enabled ?? true) : false,\n\t\t...(options.keepPreviousData === false\n\t\t\t? {}\n\t\t\t: { placeholderData: keepPreviousData }),\n\t\tstaleTime: Number.POSITIVE_INFINITY,\n\t\tmeta: { ...raw.meta, live: true }\n\t} as UseQueryOptions<TData, TError, TData, QueryKey>)\n\n\treturn Object.assign(query, { liveStatus: status })\n}\n\n/**\n * One app-wide stream of `{ domain }` change events that keeps list and count\n * queries fresh: each event invalidates its domain's query keys (debounced),\n * and a reconnect that proves healthy invalidates every domain to cover\n * events lost while it was down. Mount once, in the signed-in shell.\n *\n * ```ts\n * useInvalidationStream({}, client.realtime.subscribeChanges, {\n * enabled: signedIn,\n * domains: { order: { debounceMs: 100, queryKeys: [orpc.order.key()] } }\n * })\n * ```\n */\nexport function useInvalidationStream<\n\tTInput,\n\tTEvent extends { domain: string }\n>(\n\tinput: TInput,\n\tsubscribe: SubscribeFn<TInput, TEvent>,\n\toptions: InvalidationStreamOptions<TEvent> &\n\t\tPick<\n\t\t\tUseOrpcSubscriptionOptions<TInput, TEvent>,\n\t\t\t'enabled' | 'pauseWhenUnfocused'\n\t\t>\n): { status: LiveConnectionStatus } {\n\tconst queryClient = useQueryClient()\n\tconst latestOptions = useRef(options)\n\tlatestOptions.current = options\n\t// `domains`, `refetchType` and `logger` are read on mount; `onEvent` stays current.\n\tconst [coordinator] = useState(() =>\n\t\tcreateInvalidationCoordinator<TEvent>(queryClient, {\n\t\t\t...options,\n\t\t\tonEvent: (event) => latestOptions.current.onEvent?.(event)\n\t\t})\n\t)\n\tuseEffect(() => coordinator.dispose, [coordinator])\n\n\treturn useOrpcSubscription<TInput, TEvent>(input, subscribe, {\n\t\tenabled: options.enabled,\n\t\tpauseWhenUnfocused: options.pauseWhenUnfocused,\n\t\tonEvent: coordinator.event,\n\t\tonConnect: coordinator.connect,\n\t\tonError: coordinator.lost,\n\t\tonClose: coordinator.lost\n\t})\n}\n\n/**\n * Clear the query cache when the signed-in owner of the cached data changes\n * (another user, another organization), so the next one never sees it.\n * `null` means signed out: signing in starts from the cache sign-out emptied.\n * Pass the same owner as the persister's `buster` so a saved cache from\n * someone else is discarded on the next launch too.\n */\nexport function useQueryCacheOwner(owner: string | null): void {\n\tconst queryClient = useQueryClient()\n\tconst previous = useRef(owner)\n\tuseEffect(() => {\n\t\tconst before = previous.current\n\t\tprevious.current = owner\n\t\tif (owner !== before && before !== null) queryClient.clear()\n\t}, [owner, queryClient])\n}\n"]}