ras-stack 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -182,6 +182,20 @@ live.close()
182
182
  disconnect()
183
183
  ```
184
184
 
185
+ React applications can keep transport ownership equally small while retaining the native client and subscription:
186
+
187
+ ```tsx
188
+ import { useCallback } from 'react'
189
+ import { useConnectedRealtimeClient, useRealtimePresence, useRealtimeSubscription } from 'ras-stack/realtime/react'
190
+
191
+ const createClient = useCallback(() => createSameOriginRealtimeClient({ getToken }), [workspaceId])
192
+ const client = useConnectedRealtimeClient(createClient)
193
+ const subscription = useRealtimeSubscription({ client, channel, options, configure })
194
+ const clients = useRealtimePresence(subscription)
195
+ ```
196
+
197
+ The factory, subscription options, and configure callback should have stable identities and change only when their corresponding lifecycle should restart.
198
+
185
199
  The client helpers return the underlying Centrifuge client and subscription. React ownership, channel conventions, ticket validation, event parsing, presence models, and query invalidation remain application code. `publish()` returns `false` when the publisher is closed, disabled, or at capacity. `close()` rejects new work and waits for accepted publications and their bounded retries to finish.
186
200
 
187
201
  ## Email and uploads
@@ -0,0 +1,13 @@
1
+ import type { Centrifuge, ClientInfo, Subscription, SubscriptionOptions } from 'centrifuge';
2
+ export declare function useConnectedRealtimeClient(create: () => Centrifuge, enabled?: boolean): Centrifuge | undefined;
3
+ export type RealtimeSubscriptionHookOptions = {
4
+ client?: Centrifuge;
5
+ channel?: string;
6
+ enabled?: boolean;
7
+ options?: SubscriptionOptions;
8
+ configure?: (subscription: Subscription) => void | (() => void);
9
+ };
10
+ export declare function useRealtimeSubscription(options: RealtimeSubscriptionHookOptions): Subscription | undefined;
11
+ export declare function useRealtimePresence(subscription: Subscription | undefined, options?: {
12
+ onError?: (error: unknown) => void;
13
+ }): Record<string, ClientInfo>;
@@ -0,0 +1,50 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { connectRealtimeClient, openRealtimeSubscription, watchSubscriptionPresence } from './client.js';
3
+ export function useConnectedRealtimeClient(create, enabled = true) {
4
+ const [client, setClient] = useState();
5
+ useEffect(() => {
6
+ if (!enabled) {
7
+ setClient(undefined);
8
+ return undefined;
9
+ }
10
+ const next = create();
11
+ setClient(next);
12
+ return connectRealtimeClient(next);
13
+ }, [create, enabled]);
14
+ return client;
15
+ }
16
+ export function useRealtimeSubscription(options) {
17
+ const [subscription, setSubscription] = useState();
18
+ const { channel, client, configure, enabled = true, options: subscriptionOptions } = options;
19
+ useEffect(() => {
20
+ if (!enabled || !client || !channel) {
21
+ setSubscription(undefined);
22
+ return undefined;
23
+ }
24
+ const opened = openRealtimeSubscription(client, channel, subscriptionOptions, configure);
25
+ setSubscription(opened.subscription);
26
+ return opened.close;
27
+ }, [channel, client, configure, enabled, subscriptionOptions]);
28
+ return subscription;
29
+ }
30
+ export function useRealtimePresence(subscription, options = {}) {
31
+ const [clients, setClients] = useState({});
32
+ const onError = options.onError;
33
+ useEffect(() => {
34
+ if (!subscription) {
35
+ setClients({});
36
+ return undefined;
37
+ }
38
+ let active = true;
39
+ const stop = watchSubscriptionPresence(subscription, (next) => {
40
+ if (active)
41
+ setClients(next);
42
+ }, onError ? { onError } : {});
43
+ return () => {
44
+ active = false;
45
+ stop();
46
+ };
47
+ }, [onError, subscription]);
48
+ return clients;
49
+ }
50
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/realtime/react.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAExG,MAAM,UAAU,0BAA0B,CAAC,MAAwB,EAAE,OAAO,GAAG,IAAI;IACjF,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,EAAc,CAAA;IAClD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,SAAS,CAAC,SAAS,CAAC,CAAA;YACpB,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAA;QACrB,SAAS,CAAC,IAAI,CAAC,CAAA;QACf,OAAO,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACpC,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACrB,OAAO,MAAM,CAAA;AACf,CAAC;AAUD,MAAM,UAAU,uBAAuB,CAAC,OAAwC;IAC9E,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,EAAgB,CAAA;IAChE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,EAAE,OAAO,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAA;IAC5F,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpC,eAAe,CAAC,SAAS,CAAC,CAAA;YAC1B,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,MAAM,MAAM,GAAG,wBAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,mBAAmB,EAAE,SAAS,CAAC,CAAA;QACxF,eAAe,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QACpC,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAA;IAC9D,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,YAAsC,EAAE,OAAO,GAA2C,EAAE;IAC9H,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAA6B,EAAE,CAAC,CAAA;IACtE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;IAC/B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,UAAU,CAAC,EAAE,CAAC,CAAA;YACd,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,IAAI,MAAM,GAAG,IAAI,CAAA;QACjB,MAAM,IAAI,GAAG,yBAAyB,CACpC,YAAY,EACZ,CAAC,IAAI,EAAE,EAAE;YACP,IAAI,MAAM;gBAAE,UAAU,CAAC,IAAI,CAAC,CAAA;QAC9B,CAAC,EACD,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAC3B,CAAA;QACD,OAAO,GAAG,EAAE;YACV,MAAM,GAAG,KAAK,CAAA;YACd,IAAI,EAAE,CAAA;QACR,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAA;IAC3B,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["import type { Centrifuge, ClientInfo, Subscription, SubscriptionOptions } from 'centrifuge'\nimport { useEffect, useState } from 'react'\nimport { connectRealtimeClient, openRealtimeSubscription, watchSubscriptionPresence } from './client.js'\n\nexport function useConnectedRealtimeClient(create: () => Centrifuge, enabled = true) {\n const [client, setClient] = useState<Centrifuge>()\n useEffect(() => {\n if (!enabled) {\n setClient(undefined)\n return undefined\n }\n const next = create()\n setClient(next)\n return connectRealtimeClient(next)\n }, [create, enabled])\n return client\n}\n\nexport type RealtimeSubscriptionHookOptions = {\n client?: Centrifuge\n channel?: string\n enabled?: boolean\n options?: SubscriptionOptions\n configure?: (subscription: Subscription) => void | (() => void)\n}\n\nexport function useRealtimeSubscription(options: RealtimeSubscriptionHookOptions) {\n const [subscription, setSubscription] = useState<Subscription>()\n const { channel, client, configure, enabled = true, options: subscriptionOptions } = options\n useEffect(() => {\n if (!enabled || !client || !channel) {\n setSubscription(undefined)\n return undefined\n }\n const opened = openRealtimeSubscription(client, channel, subscriptionOptions, configure)\n setSubscription(opened.subscription)\n return opened.close\n }, [channel, client, configure, enabled, subscriptionOptions])\n return subscription\n}\n\nexport function useRealtimePresence(subscription: Subscription | undefined, options: { onError?: (error: unknown) => void } = {}) {\n const [clients, setClients] = useState<Record<string, ClientInfo>>({})\n const onError = options.onError\n useEffect(() => {\n if (!subscription) {\n setClients({})\n return undefined\n }\n let active = true\n const stop = watchSubscriptionPresence(\n subscription,\n (next) => {\n if (active) setClients(next)\n },\n onError ? { onError } : {},\n )\n return () => {\n active = false\n stop()\n }\n }, [onError, subscription])\n return clients\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/realtime/client.d.ts",
59
59
  "default": "./dist/realtime/client.js"
60
60
  },
61
+ "./realtime/react": {
62
+ "types": "./dist/realtime/react.d.ts",
63
+ "default": "./dist/realtime/react.js"
64
+ },
61
65
  "./server": {
62
66
  "types": "./dist/server/index.d.ts",
63
67
  "default": "./dist/server/index.js"
@@ -109,6 +113,8 @@
109
113
  "@types/better-sqlite3": "9.6.0",
110
114
  "@types/node": "^26.0.0",
111
115
  "@types/nodemailer": "^8.0.1",
116
+ "@types/react": "19.2.8",
117
+ "@types/react-test-renderer": "19.1.0",
112
118
  "better-sqlite3": "12.11.1",
113
119
  "centrifuge": "5.7.0",
114
120
  "drizzle-orm": "0.45.2",
@@ -117,6 +123,8 @@
117
123
  "oxlint": "^1.74.0",
118
124
  "oxlint-tsgolint": "^7.0.2001",
119
125
  "postgres": "3.4.9",
126
+ "react": "19.2.8",
127
+ "react-test-renderer": "19.2.8",
120
128
  "tus-js-client": "^4.3.1",
121
129
  "typescript": "^7.0.2",
122
130
  "vite": "8.2.1",
@@ -130,6 +138,7 @@
130
138
  "drizzle-orm": ">=0.45 <1",
131
139
  "nodemailer": ">=9 <10",
132
140
  "postgres": ">=3.4 <4",
141
+ "react": ">=19 <20",
133
142
  "tus-js-client": ">=4 <5"
134
143
  },
135
144
  "peerDependenciesMeta": {
@@ -154,6 +163,9 @@
154
163
  "postgres": {
155
164
  "optional": true
156
165
  },
166
+ "react": {
167
+ "optional": true
168
+ },
157
169
  "tus-js-client": {
158
170
  "optional": true
159
171
  }