ras-stack 0.13.0 → 0.15.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 +27 -0
- package/dist/auth/client.d.ts +8 -0
- package/dist/auth/client.js +17 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/react.d.ts +18 -0
- package/dist/auth/react.js +34 -0
- package/dist/auth/react.js.map +1 -0
- package/dist/realtime/react.d.ts +13 -0
- package/dist/realtime/react.js +50 -0
- package/dist/realtime/react.js.map +1 -0
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -100,6 +100,19 @@ Applications still own their auth clients, authorization, file-route declaration
|
|
|
100
100
|
|
|
101
101
|
Only enable `trustForwardedHeaders` behind a proxy that replaces incoming forwarded headers. Otherwise a client could choose the origin used by the check.
|
|
102
102
|
|
|
103
|
+
Browser auth flows can share failure classification and pending/error state without sharing forms or navigation:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { classifySignInFailure } from 'ras-stack/auth/client'
|
|
107
|
+
import { useAuthAction } from 'ras-stack/auth/react'
|
|
108
|
+
|
|
109
|
+
const signIn = useAuthAction({ failureMessage: (failure) => messageFor(classifySignInFailure(failure)) })
|
|
110
|
+
const result = await signIn.run(() => authClient.signIn.email({ email, password }))
|
|
111
|
+
if (!result.error) await navigateAfterSignIn()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Applications retain field models, validation, password-reset disclosure policy, two-factor transitions, telemetry, copy, and success navigation.
|
|
115
|
+
|
|
103
116
|
## Database lifecycle
|
|
104
117
|
|
|
105
118
|
The SQLite entrypoint owns the native client lifecycle, standard safety PRAGMAs, and optional Drizzle migrations while returning the upstream typed database:
|
|
@@ -182,6 +195,20 @@ live.close()
|
|
|
182
195
|
disconnect()
|
|
183
196
|
```
|
|
184
197
|
|
|
198
|
+
React applications can keep transport ownership equally small while retaining the native client and subscription:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useCallback } from 'react'
|
|
202
|
+
import { useConnectedRealtimeClient, useRealtimePresence, useRealtimeSubscription } from 'ras-stack/realtime/react'
|
|
203
|
+
|
|
204
|
+
const createClient = useCallback(() => createSameOriginRealtimeClient({ getToken }), [workspaceId])
|
|
205
|
+
const client = useConnectedRealtimeClient(createClient)
|
|
206
|
+
const subscription = useRealtimeSubscription({ client, channel, options, configure })
|
|
207
|
+
const clients = useRealtimePresence(subscription)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The factory, subscription options, and configure callback should have stable identities and change only when their corresponding lifecycle should restart.
|
|
211
|
+
|
|
185
212
|
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
213
|
|
|
187
214
|
## Email and uploads
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type AuthFailure = {
|
|
2
|
+
status?: number;
|
|
3
|
+
code?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
} | null | undefined;
|
|
6
|
+
export type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error';
|
|
7
|
+
export declare function classifySignInFailure(failure: unknown): SignInFailureReason;
|
|
8
|
+
export declare function authFailureMessage(failure: unknown, fallback: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function classifySignInFailure(failure) {
|
|
2
|
+
if (!failure || typeof failure !== 'object')
|
|
3
|
+
return 'error';
|
|
4
|
+
const status = 'status' in failure ? failure.status : undefined;
|
|
5
|
+
const code = 'code' in failure ? failure.code : undefined;
|
|
6
|
+
if (status === 429)
|
|
7
|
+
return 'rate_limited';
|
|
8
|
+
if (status === 401 || code === 'INVALID_EMAIL_OR_PASSWORD')
|
|
9
|
+
return 'invalid_credentials';
|
|
10
|
+
return 'error';
|
|
11
|
+
}
|
|
12
|
+
export function authFailureMessage(failure, fallback) {
|
|
13
|
+
if (!failure || typeof failure !== 'object' || !('message' in failure))
|
|
14
|
+
return fallback;
|
|
15
|
+
return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/auth/client.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,qBAAqB,CAAC,OAAgB;IACpD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAA;IAC3D,MAAM,MAAM,GAAG,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;IAC/D,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAA;IACzC,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,2BAA2B;QAAE,OAAO,qBAAqB,CAAA;IACxF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,QAAgB;IACnE,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAA;IACvF,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;AACnG,CAAC","sourcesContent":["export type AuthFailure = { status?: number; code?: string; message?: string } | null | undefined\nexport type SignInFailureReason = 'invalid_credentials' | 'rate_limited' | 'error'\n\nexport function classifySignInFailure(failure: unknown): SignInFailureReason {\n if (!failure || typeof failure !== 'object') return 'error'\n const status = 'status' in failure ? failure.status : undefined\n const code = 'code' in failure ? failure.code : undefined\n if (status === 429) return 'rate_limited'\n if (status === 401 || code === 'INVALID_EMAIL_OR_PASSWORD') return 'invalid_credentials'\n return 'error'\n}\n\nexport function authFailureMessage(failure: unknown, fallback: string) {\n if (!failure || typeof failure !== 'object' || !('message' in failure)) return fallback\n return typeof failure.message === 'string' && failure.message.trim() ? failure.message : fallback\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type AuthActionResult<T, TFailure = unknown> = {
|
|
2
|
+
data?: T;
|
|
3
|
+
error?: TFailure | null;
|
|
4
|
+
};
|
|
5
|
+
export type AuthActionState = {
|
|
6
|
+
busy: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function useAuthAction(options?: {
|
|
10
|
+
failureMessage?: (failure: unknown) => string;
|
|
11
|
+
}): {
|
|
12
|
+
busy: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
clearError: () => void;
|
|
15
|
+
run: <T, TFailure>(work: () => Promise<AuthActionResult<T, TFailure>>) => Promise<AuthActionResult<T, TFailure> | {
|
|
16
|
+
error: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { authFailureMessage } from './client.js';
|
|
3
|
+
const defaultFailureMessage = (failure) => authFailureMessage(failure, 'That did not work. Try again.');
|
|
4
|
+
export function useAuthAction(options = {}) {
|
|
5
|
+
const failureMessage = options.failureMessage ?? defaultFailureMessage;
|
|
6
|
+
const [state, setState] = useState({ busy: false });
|
|
7
|
+
const active = useRef(true);
|
|
8
|
+
const invocation = useRef(0);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
active.current = true;
|
|
11
|
+
return () => {
|
|
12
|
+
active.current = false;
|
|
13
|
+
};
|
|
14
|
+
}, []);
|
|
15
|
+
const clearError = useCallback(() => setState((current) => ({ busy: current.busy })), []);
|
|
16
|
+
const run = useCallback(async (work) => {
|
|
17
|
+
const current = ++invocation.current;
|
|
18
|
+
setState({ busy: true });
|
|
19
|
+
try {
|
|
20
|
+
const result = await work();
|
|
21
|
+
if (active.current && current === invocation.current) {
|
|
22
|
+
setState(result.error ? { busy: false, error: failureMessage(result.error) } : { busy: false });
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (active.current && current === invocation.current)
|
|
28
|
+
setState({ busy: false, error: failureMessage(error) });
|
|
29
|
+
return { error };
|
|
30
|
+
}
|
|
31
|
+
}, [failureMessage]);
|
|
32
|
+
return { ...state, clearError, run };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/auth/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAKhD,MAAM,qBAAqB,GAAG,CAAC,OAAgB,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,+BAA+B,CAAC,CAAA;AAEhH,MAAM,UAAU,aAAa,CAAC,OAAO,GAAsD,EAAE;IAC3F,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,qBAAqB,CAAA;IACtE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAkB,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;QACrB,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,OAAO,GAAG,KAAK,CAAA;QACxB,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACzF,MAAM,GAAG,GAAG,WAAW,CACrB,KAAK,EAAe,IAAkD,EAAE,EAAE;QACxE,MAAM,OAAO,GAAG,EAAE,UAAU,CAAC,OAAO,CAAA;QACpC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAA;YAC3B,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,KAAK,UAAU,CAAC,OAAO,EAAE,CAAC;gBACrD,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YACjG,CAAC;YACD,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,OAAO,IAAI,OAAO,KAAK,UAAU,CAAC,OAAO;gBAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;YAC7G,OAAO,EAAE,KAAK,EAAoC,CAAA;QACpD,CAAC;IACH,CAAC,EACD,CAAC,cAAc,CAAC,CACjB,CAAA;IACD,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,CAAA;AACtC,CAAC","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { authFailureMessage } from './client.js'\n\nexport type AuthActionResult<T, TFailure = unknown> = { data?: T; error?: TFailure | null }\nexport type AuthActionState = { busy: boolean; error?: string }\n\nconst defaultFailureMessage = (failure: unknown) => authFailureMessage(failure, 'That did not work. Try again.')\n\nexport function useAuthAction(options: { failureMessage?: (failure: unknown) => string } = {}) {\n const failureMessage = options.failureMessage ?? defaultFailureMessage\n const [state, setState] = useState<AuthActionState>({ busy: false })\n const active = useRef(true)\n const invocation = useRef(0)\n useEffect(() => {\n active.current = true\n return () => {\n active.current = false\n }\n }, [])\n const clearError = useCallback(() => setState((current) => ({ busy: current.busy })), [])\n const run = useCallback(\n async <T, TFailure>(work: () => Promise<AuthActionResult<T, TFailure>>) => {\n const current = ++invocation.current\n setState({ busy: true })\n try {\n const result = await work()\n if (active.current && current === invocation.current) {\n setState(result.error ? { busy: false, error: failureMessage(result.error) } : { busy: false })\n }\n return result\n } catch (error) {\n if (active.current && current === invocation.current) setState({ busy: false, error: failureMessage(error) })\n return { error } satisfies AuthActionResult<never>\n }\n },\n [failureMessage],\n )\n return { ...state, clearError, run }\n}\n"]}
|
|
@@ -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.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Composable full-stack primitives shared across Richard Solomou's applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"types": "./dist/auth/index.d.ts",
|
|
31
31
|
"default": "./dist/auth/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./auth/client": {
|
|
34
|
+
"types": "./dist/auth/client.d.ts",
|
|
35
|
+
"default": "./dist/auth/client.js"
|
|
36
|
+
},
|
|
37
|
+
"./auth/react": {
|
|
38
|
+
"types": "./dist/auth/react.d.ts",
|
|
39
|
+
"default": "./dist/auth/react.js"
|
|
40
|
+
},
|
|
33
41
|
"./email": {
|
|
34
42
|
"types": "./dist/email/index.d.ts",
|
|
35
43
|
"default": "./dist/email/index.js"
|
|
@@ -58,6 +66,10 @@
|
|
|
58
66
|
"types": "./dist/realtime/client.d.ts",
|
|
59
67
|
"default": "./dist/realtime/client.js"
|
|
60
68
|
},
|
|
69
|
+
"./realtime/react": {
|
|
70
|
+
"types": "./dist/realtime/react.d.ts",
|
|
71
|
+
"default": "./dist/realtime/react.js"
|
|
72
|
+
},
|
|
61
73
|
"./server": {
|
|
62
74
|
"types": "./dist/server/index.d.ts",
|
|
63
75
|
"default": "./dist/server/index.js"
|
|
@@ -109,6 +121,8 @@
|
|
|
109
121
|
"@types/better-sqlite3": "9.6.0",
|
|
110
122
|
"@types/node": "^26.0.0",
|
|
111
123
|
"@types/nodemailer": "^8.0.1",
|
|
124
|
+
"@types/react": "19.2.8",
|
|
125
|
+
"@types/react-test-renderer": "19.1.0",
|
|
112
126
|
"better-sqlite3": "12.11.1",
|
|
113
127
|
"centrifuge": "5.7.0",
|
|
114
128
|
"drizzle-orm": "0.45.2",
|
|
@@ -117,6 +131,8 @@
|
|
|
117
131
|
"oxlint": "^1.74.0",
|
|
118
132
|
"oxlint-tsgolint": "^7.0.2001",
|
|
119
133
|
"postgres": "3.4.9",
|
|
134
|
+
"react": "19.2.8",
|
|
135
|
+
"react-test-renderer": "19.2.8",
|
|
120
136
|
"tus-js-client": "^4.3.1",
|
|
121
137
|
"typescript": "^7.0.2",
|
|
122
138
|
"vite": "8.2.1",
|
|
@@ -130,6 +146,7 @@
|
|
|
130
146
|
"drizzle-orm": ">=0.45 <1",
|
|
131
147
|
"nodemailer": ">=9 <10",
|
|
132
148
|
"postgres": ">=3.4 <4",
|
|
149
|
+
"react": ">=19 <20",
|
|
133
150
|
"tus-js-client": ">=4 <5"
|
|
134
151
|
},
|
|
135
152
|
"peerDependenciesMeta": {
|
|
@@ -154,6 +171,9 @@
|
|
|
154
171
|
"postgres": {
|
|
155
172
|
"optional": true
|
|
156
173
|
},
|
|
174
|
+
"react": {
|
|
175
|
+
"optional": true
|
|
176
|
+
},
|
|
157
177
|
"tus-js-client": {
|
|
158
178
|
"optional": true
|
|
159
179
|
}
|