@arkstack/realtime 0.16.7
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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/RealtimeClient-D6-kesaz.d.ts +101 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +150 -0
- package/dist/react/index.d.ts +24 -0
- package/dist/react/index.js +43 -0
- package/dist/vue/index.d.ts +26 -0
- package/dist/vue/index.js +41 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Toneflix Technologies Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @arkstack/realtime
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@arkstack/realtime)
|
|
4
|
+
|
|
5
|
+
The client for consuming [Arkstack](https://arkstack.toneflix.net) realtime notifications. A framework-agnostic core plus React and Vue bindings, backed by [Pusher](https://pusher.com/channels) or [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging).
|
|
6
|
+
|
|
7
|
+
Pairs with the `realtime` notification driver in [`@arkstack/notifications`](https://www.npmjs.com/package/@arkstack/notifications), which broadcasts a per-user channel (`user.<id>`) that this client subscribes to.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @arkstack/realtime
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then install the client SDK for your transport (both are optional peer dependencies — install only the one you use):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pnpm add pusher-js # Pusher transport
|
|
19
|
+
pnpm add firebase # Firebase transport
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Create a client and subscribe to a user's channel. `subscribe`/`forUser` resolve to an unsubscribe function.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { createRealtime } from '@arkstack/realtime';
|
|
28
|
+
|
|
29
|
+
const realtime = createRealtime({
|
|
30
|
+
transport: 'pusher',
|
|
31
|
+
pusher: {
|
|
32
|
+
key: import.meta.env.VITE_PUSHER_KEY,
|
|
33
|
+
cluster: 'mt1',
|
|
34
|
+
authEndpoint: '/broadcasting/auth',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const unsubscribe = await realtime.forUser(user.id, (notification) => {
|
|
39
|
+
console.log(notification.title, notification.description);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// later
|
|
43
|
+
unsubscribe();
|
|
44
|
+
await realtime.disconnect();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each `notification` matches the payload broadcast by the server:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
interface RealtimeNotification {
|
|
51
|
+
id: string;
|
|
52
|
+
type: string | null;
|
|
53
|
+
title: string;
|
|
54
|
+
description: string;
|
|
55
|
+
actionText?: string | null;
|
|
56
|
+
actionLink?: string | null;
|
|
57
|
+
meta?: Record<string, unknown> | null;
|
|
58
|
+
read_at: string | null;
|
|
59
|
+
created_at: string;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## React
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useNotifications } from '@arkstack/realtime/react';
|
|
67
|
+
|
|
68
|
+
function Bell({ realtime, userId }) {
|
|
69
|
+
const { notifications, latest, clear } = useNotifications(
|
|
70
|
+
realtime,
|
|
71
|
+
realtime.channelFor(userId),
|
|
72
|
+
{ limit: 20 },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<span onClick={clear}>
|
|
77
|
+
{notifications.length} · {latest?.title}
|
|
78
|
+
</span>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The hook accumulates notifications (newest first), caps them at `limit`, and unsubscribes automatically on unmount or when `client`/`channel` change.
|
|
84
|
+
|
|
85
|
+
## Vue
|
|
86
|
+
|
|
87
|
+
```vue
|
|
88
|
+
<script setup>
|
|
89
|
+
import { useNotifications } from '@arkstack/realtime/vue';
|
|
90
|
+
|
|
91
|
+
const props = defineProps(['realtime', 'userId']);
|
|
92
|
+
const { notifications, latest, clear } = useNotifications(
|
|
93
|
+
props.realtime,
|
|
94
|
+
props.realtime.channelFor(props.userId),
|
|
95
|
+
{ limit: 20 },
|
|
96
|
+
);
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<span @click="clear">{{ notifications.length }} · {{ latest?.title }}</span>
|
|
101
|
+
</template>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The composable unsubscribes automatically when the component's scope is disposed.
|
|
105
|
+
|
|
106
|
+
## Firebase
|
|
107
|
+
|
|
108
|
+
Firebase Cloud Messaging delivers to the device (via `onMessage`), so notifications are matched by event name rather than channel:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const realtime = createRealtime({
|
|
112
|
+
transport: 'firebase',
|
|
113
|
+
firebase: {
|
|
114
|
+
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
|
115
|
+
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
|
116
|
+
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
|
117
|
+
messagingSenderId: import.meta.env.VITE_FIREBASE_SENDER_ID,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Custom transport
|
|
123
|
+
|
|
124
|
+
Provide `transportFactory` to bridge any backend (a raw WebSocket, SSE, a test double, …):
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { createRealtime, type RealtimeTransport } from '@arkstack/realtime';
|
|
128
|
+
|
|
129
|
+
const transport: RealtimeTransport = {
|
|
130
|
+
subscribe(channel, event, handler) {
|
|
131
|
+
const socket = new WebSocket(`wss://example.test/${channel}`);
|
|
132
|
+
socket.addEventListener('message', (e) => {
|
|
133
|
+
const { event: name, payload } = JSON.parse(e.data);
|
|
134
|
+
if (name === event) handler(payload);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return { channel, unsubscribe: () => socket.close() };
|
|
138
|
+
},
|
|
139
|
+
disconnect() {},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const realtime = createRealtime({ transportFactory: () => transport });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## API
|
|
146
|
+
|
|
147
|
+
- `createRealtime(config)` — create a `RealtimeClient`. Config: `transport` (`'pusher'` | `'firebase'`), `event` (default `notification`), `channelPrefix` (default `user.`), `pusher`/`firebase` credentials, or a custom `transportFactory`.
|
|
148
|
+
- `client.subscribe(channel, handler)` / `client.forUser(userId, handler)` — subscribe; returns an unsubscribe function.
|
|
149
|
+
- `client.channelFor(userId)` — the per-user channel name.
|
|
150
|
+
- `client.disconnect()` — tear down the transport connection.
|
|
151
|
+
- `@arkstack/realtime/react` — `useNotifications(client, channel, { limit? })` → `{ notifications, latest, clear }`.
|
|
152
|
+
- `@arkstack/realtime/vue` — `useNotifications(client, channel, { limit? })` → `{ notifications, latest, clear, stop }`.
|
|
153
|
+
|
|
154
|
+
See the [notifications guide](https://arkstack.toneflix.net/guide/notifications) for the server side.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The notification payload delivered to realtime clients. Mirrors the server
|
|
4
|
+
* `RealtimeNotificationPayload` in `@arkstack/notifications` (kept local so the
|
|
5
|
+
* client has no server dependency).
|
|
6
|
+
*/
|
|
7
|
+
interface RealtimeNotification {
|
|
8
|
+
id: string;
|
|
9
|
+
type: string | null;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
actionText?: string | null;
|
|
13
|
+
actionLink?: string | null;
|
|
14
|
+
meta?: Record<string, unknown> | null;
|
|
15
|
+
read_at: string | null;
|
|
16
|
+
created_at: string;
|
|
17
|
+
}
|
|
18
|
+
type RealtimeTransportName = 'pusher' | 'firebase';
|
|
19
|
+
type NotificationHandler = (notification: RealtimeNotification) => void;
|
|
20
|
+
/** A live subscription to one channel; call `unsubscribe()` to stop listening. */
|
|
21
|
+
interface RealtimeSubscription {
|
|
22
|
+
channel: string;
|
|
23
|
+
unsubscribe(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A transport binds a `(channel, event)` to a handler and tears the binding down
|
|
27
|
+
* on `unsubscribe`. Implemented by the built-in Pusher/Firebase transports, or
|
|
28
|
+
* supplied via {@link RealtimeConfig.transportFactory} for custom backends/tests.
|
|
29
|
+
*/
|
|
30
|
+
interface RealtimeTransport {
|
|
31
|
+
subscribe(channel: string, event: string, handler: NotificationHandler): RealtimeSubscription | Promise<RealtimeSubscription>;
|
|
32
|
+
disconnect(): void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
interface PusherClientConfig {
|
|
35
|
+
key: string;
|
|
36
|
+
cluster?: string;
|
|
37
|
+
/** Endpoint that authorizes private/presence channels. */
|
|
38
|
+
authEndpoint?: string;
|
|
39
|
+
auth?: {
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
params?: Record<string, string>;
|
|
42
|
+
};
|
|
43
|
+
forceTLS?: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface FirebaseClientConfig {
|
|
46
|
+
apiKey: string;
|
|
47
|
+
projectId: string;
|
|
48
|
+
appId: string;
|
|
49
|
+
messagingSenderId: string;
|
|
50
|
+
/** Web push VAPID key used when requesting a messaging token. */
|
|
51
|
+
vapidKey?: string;
|
|
52
|
+
}
|
|
53
|
+
interface RealtimeConfig {
|
|
54
|
+
transport?: RealtimeTransportName;
|
|
55
|
+
/** Event name broadcasts are published under (default `notification`). */
|
|
56
|
+
event?: string;
|
|
57
|
+
/** Prefix for the per-user channel (default `user.`), used by `forUser()`. */
|
|
58
|
+
channelPrefix?: string;
|
|
59
|
+
pusher?: PusherClientConfig;
|
|
60
|
+
firebase?: FirebaseClientConfig;
|
|
61
|
+
/** Inject a transport directly — bypasses the built-ins (tests, custom backends). */
|
|
62
|
+
transportFactory?: () => RealtimeTransport | Promise<RealtimeTransport>;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/RealtimeClient.d.ts
|
|
66
|
+
/**
|
|
67
|
+
* Consumes Arkstack realtime notifications. Resolves a transport (Pusher,
|
|
68
|
+
* Firebase, or an injected one) lazily on first subscribe, and exposes a small
|
|
69
|
+
* channel-oriented API used directly or by the React/Vue bindings.
|
|
70
|
+
*/
|
|
71
|
+
declare class RealtimeClient {
|
|
72
|
+
private config;
|
|
73
|
+
private transportPromise?;
|
|
74
|
+
private readonly event;
|
|
75
|
+
private readonly channelPrefix;
|
|
76
|
+
constructor(config?: RealtimeConfig);
|
|
77
|
+
/** The channel name a given user's notifications are broadcast on. */
|
|
78
|
+
channelFor(userId: string | number): string;
|
|
79
|
+
private transport;
|
|
80
|
+
private resolveTransport;
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to a channel. Returns a function that unsubscribes.
|
|
83
|
+
*
|
|
84
|
+
* @param channel The channel name (e.g. `user.7`).
|
|
85
|
+
* @param handler Called with each incoming notification.
|
|
86
|
+
*/
|
|
87
|
+
subscribe(channel: string, handler: NotificationHandler): Promise<() => void>;
|
|
88
|
+
/**
|
|
89
|
+
* Subscribe to a user's channel (`{channelPrefix}{userId}`).
|
|
90
|
+
*
|
|
91
|
+
* @param userId The user id.
|
|
92
|
+
* @param handler Called with each incoming notification.
|
|
93
|
+
*/
|
|
94
|
+
forUser(userId: string | number, handler: NotificationHandler): Promise<() => void>;
|
|
95
|
+
/** Tear down the underlying transport connection. */
|
|
96
|
+
disconnect(): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
/** Create a {@link RealtimeClient}. */
|
|
99
|
+
declare const createRealtime: (config?: RealtimeConfig) => RealtimeClient;
|
|
100
|
+
//#endregion
|
|
101
|
+
export { PusherClientConfig as a, RealtimeSubscription as c, NotificationHandler as i, RealtimeTransport as l, createRealtime as n, RealtimeConfig as o, FirebaseClientConfig as r, RealtimeNotification as s, RealtimeClient as t, RealtimeTransportName as u };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { a as PusherClientConfig, c as RealtimeSubscription, i as NotificationHandler, l as RealtimeTransport, n as createRealtime, o as RealtimeConfig, r as FirebaseClientConfig, s as RealtimeNotification, t as RealtimeClient, u as RealtimeTransportName } from "./RealtimeClient-D6-kesaz.js";
|
|
2
|
+
|
|
3
|
+
//#region src/transports/pusher.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Realtime transport backed by [pusher-js](https://github.com/pusher/pusher-js).
|
|
6
|
+
* The SDK is an optional peer dependency imported lazily, so consumers only pull
|
|
7
|
+
* it in when they use the Pusher transport.
|
|
8
|
+
*/
|
|
9
|
+
declare const createPusherTransport: (config: PusherClientConfig) => Promise<RealtimeTransport>;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/transports/firebase.d.ts
|
|
12
|
+
/**
|
|
13
|
+
* Realtime transport backed by [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/js/receive)
|
|
14
|
+
* foreground messages. `firebase` is an optional peer dependency imported lazily.
|
|
15
|
+
*
|
|
16
|
+
* FCM delivers to the device (not per-channel), so the channel is informational;
|
|
17
|
+
* messages are matched by `event` and the JSON-encoded payload is parsed back.
|
|
18
|
+
*/
|
|
19
|
+
declare const createFirebaseTransport: (config: FirebaseClientConfig) => Promise<RealtimeTransport>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type FirebaseClientConfig, type NotificationHandler, type PusherClientConfig, RealtimeClient, type RealtimeConfig, type RealtimeNotification, type RealtimeSubscription, type RealtimeTransport, type RealtimeTransportName, createFirebaseTransport, createPusherTransport, createRealtime };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/RealtimeClient.ts
|
|
14
|
+
/**
|
|
15
|
+
* Consumes Arkstack realtime notifications. Resolves a transport (Pusher,
|
|
16
|
+
* Firebase, or an injected one) lazily on first subscribe, and exposes a small
|
|
17
|
+
* channel-oriented API used directly or by the React/Vue bindings.
|
|
18
|
+
*/
|
|
19
|
+
var RealtimeClient = class {
|
|
20
|
+
config;
|
|
21
|
+
transportPromise;
|
|
22
|
+
event;
|
|
23
|
+
channelPrefix;
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.event = config.event ?? "notification";
|
|
27
|
+
this.channelPrefix = config.channelPrefix ?? "user.";
|
|
28
|
+
}
|
|
29
|
+
/** The channel name a given user's notifications are broadcast on. */
|
|
30
|
+
channelFor(userId) {
|
|
31
|
+
return `${this.channelPrefix}${userId}`;
|
|
32
|
+
}
|
|
33
|
+
transport() {
|
|
34
|
+
this.transportPromise ??= this.resolveTransport();
|
|
35
|
+
return this.transportPromise;
|
|
36
|
+
}
|
|
37
|
+
async resolveTransport() {
|
|
38
|
+
if (this.config.transportFactory) return await this.config.transportFactory();
|
|
39
|
+
if (this.config.transport === "firebase") {
|
|
40
|
+
if (!this.config.firebase) throw new Error("Realtime: `firebase` config is required for the Firebase transport");
|
|
41
|
+
const { createFirebaseTransport } = await Promise.resolve().then(() => firebase_exports);
|
|
42
|
+
return await createFirebaseTransport(this.config.firebase);
|
|
43
|
+
}
|
|
44
|
+
if (!this.config.pusher) throw new Error("Realtime: `pusher` config is required for the Pusher transport");
|
|
45
|
+
const { createPusherTransport } = await Promise.resolve().then(() => pusher_exports);
|
|
46
|
+
return await createPusherTransport(this.config.pusher);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to a channel. Returns a function that unsubscribes.
|
|
50
|
+
*
|
|
51
|
+
* @param channel The channel name (e.g. `user.7`).
|
|
52
|
+
* @param handler Called with each incoming notification.
|
|
53
|
+
*/
|
|
54
|
+
async subscribe(channel, handler) {
|
|
55
|
+
const subscription = await (await this.transport()).subscribe(channel, this.event, handler);
|
|
56
|
+
return () => subscription.unsubscribe();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to a user's channel (`{channelPrefix}{userId}`).
|
|
60
|
+
*
|
|
61
|
+
* @param userId The user id.
|
|
62
|
+
* @param handler Called with each incoming notification.
|
|
63
|
+
*/
|
|
64
|
+
async forUser(userId, handler) {
|
|
65
|
+
return await this.subscribe(this.channelFor(userId), handler);
|
|
66
|
+
}
|
|
67
|
+
/** Tear down the underlying transport connection. */
|
|
68
|
+
async disconnect() {
|
|
69
|
+
if (!this.transportPromise) return;
|
|
70
|
+
await (await this.transportPromise).disconnect();
|
|
71
|
+
this.transportPromise = void 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/** Create a {@link RealtimeClient}. */
|
|
75
|
+
const createRealtime = (config = {}) => new RealtimeClient(config);
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/transports/pusher.ts
|
|
78
|
+
var pusher_exports = /* @__PURE__ */ __exportAll({ createPusherTransport: () => createPusherTransport });
|
|
79
|
+
/**
|
|
80
|
+
* Realtime transport backed by [pusher-js](https://github.com/pusher/pusher-js).
|
|
81
|
+
* The SDK is an optional peer dependency imported lazily, so consumers only pull
|
|
82
|
+
* it in when they use the Pusher transport.
|
|
83
|
+
*/
|
|
84
|
+
const createPusherTransport = async (config) => {
|
|
85
|
+
const mod = await import("pusher-js").catch(() => {
|
|
86
|
+
throw new Error("The \"pusher-js\" package is required for the Pusher transport. Install it with `npm i pusher-js`.");
|
|
87
|
+
});
|
|
88
|
+
const client = new (mod.default ?? mod)(config.key, {
|
|
89
|
+
cluster: config.cluster ?? "mt1",
|
|
90
|
+
forceTLS: config.forceTLS ?? true,
|
|
91
|
+
authEndpoint: config.authEndpoint,
|
|
92
|
+
auth: config.auth
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
subscribe(channel, event, handler) {
|
|
96
|
+
const subscription = client.subscribe(channel);
|
|
97
|
+
const listener = (data) => handler(data);
|
|
98
|
+
subscription.bind(event, listener);
|
|
99
|
+
return {
|
|
100
|
+
channel,
|
|
101
|
+
unsubscribe() {
|
|
102
|
+
subscription.unbind(event, listener);
|
|
103
|
+
client.unsubscribe(channel);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
disconnect() {
|
|
108
|
+
client.disconnect();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/transports/firebase.ts
|
|
114
|
+
var firebase_exports = /* @__PURE__ */ __exportAll({ createFirebaseTransport: () => createFirebaseTransport });
|
|
115
|
+
/**
|
|
116
|
+
* Realtime transport backed by [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/js/receive)
|
|
117
|
+
* foreground messages. `firebase` is an optional peer dependency imported lazily.
|
|
118
|
+
*
|
|
119
|
+
* FCM delivers to the device (not per-channel), so the channel is informational;
|
|
120
|
+
* messages are matched by `event` and the JSON-encoded payload is parsed back.
|
|
121
|
+
*/
|
|
122
|
+
const createFirebaseTransport = async (config) => {
|
|
123
|
+
const [appMod, messagingMod] = await Promise.all([import("firebase/app"), import("firebase/messaging")]).catch(() => {
|
|
124
|
+
throw new Error("The \"firebase\" package is required for the Firebase transport. Install it with `npm i firebase`.");
|
|
125
|
+
});
|
|
126
|
+
const app = appMod.initializeApp({
|
|
127
|
+
apiKey: config.apiKey,
|
|
128
|
+
projectId: config.projectId,
|
|
129
|
+
appId: config.appId,
|
|
130
|
+
messagingSenderId: config.messagingSenderId
|
|
131
|
+
});
|
|
132
|
+
const messaging = messagingMod.getMessaging(app);
|
|
133
|
+
const onMessage = messagingMod.onMessage;
|
|
134
|
+
return {
|
|
135
|
+
subscribe(channel, event, handler) {
|
|
136
|
+
return {
|
|
137
|
+
channel,
|
|
138
|
+
unsubscribe: onMessage(messaging, (payload) => {
|
|
139
|
+
if (payload.data?.event !== event || !payload.data.payload) return;
|
|
140
|
+
try {
|
|
141
|
+
handler(JSON.parse(payload.data.payload));
|
|
142
|
+
} catch {}
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
disconnect() {}
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
export { RealtimeClient, createFirebaseTransport, createPusherTransport, createRealtime };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { s as RealtimeNotification, t as RealtimeClient } from "../RealtimeClient-D6-kesaz.js";
|
|
2
|
+
|
|
3
|
+
//#region src/react/index.d.ts
|
|
4
|
+
interface UseNotificationsOptions {
|
|
5
|
+
/** Cap the number of retained notifications (newest kept). Default: unbounded. */
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
interface UseNotificationsResult {
|
|
9
|
+
notifications: RealtimeNotification[];
|
|
10
|
+
latest: RealtimeNotification | null;
|
|
11
|
+
clear: () => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe a React component to a realtime channel, accumulating incoming
|
|
15
|
+
* notifications (newest first) into state. Automatically unsubscribes on unmount
|
|
16
|
+
* or when `client`/`channel` change.
|
|
17
|
+
*
|
|
18
|
+
* @param client A {@link RealtimeClient} (from `createRealtime`).
|
|
19
|
+
* @param channel The channel to subscribe to, e.g. `client.channelFor(user.id)`.
|
|
20
|
+
* @param options `limit` caps how many notifications are retained.
|
|
21
|
+
*/
|
|
22
|
+
declare function useNotifications(client: RealtimeClient, channel: string, options?: UseNotificationsOptions): UseNotificationsResult;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { UseNotificationsOptions, UseNotificationsResult, useNotifications };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
//#region src/react/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe a React component to a realtime channel, accumulating incoming
|
|
5
|
+
* notifications (newest first) into state. Automatically unsubscribes on unmount
|
|
6
|
+
* or when `client`/`channel` change.
|
|
7
|
+
*
|
|
8
|
+
* @param client A {@link RealtimeClient} (from `createRealtime`).
|
|
9
|
+
* @param channel The channel to subscribe to, e.g. `client.channelFor(user.id)`.
|
|
10
|
+
* @param options `limit` caps how many notifications are retained.
|
|
11
|
+
*/
|
|
12
|
+
function useNotifications(client, channel, options = {}) {
|
|
13
|
+
const [notifications, setNotifications] = useState([]);
|
|
14
|
+
const { limit } = options;
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let unsubscribe;
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
const push = (notification) => setNotifications((prev) => {
|
|
19
|
+
const next = [notification, ...prev];
|
|
20
|
+
return limit ? next.slice(0, limit) : next;
|
|
21
|
+
});
|
|
22
|
+
client.subscribe(channel, push).then((off) => {
|
|
23
|
+
if (cancelled) off();
|
|
24
|
+
else unsubscribe = off;
|
|
25
|
+
}).catch(() => { /** surfaced by the caller's own error handling */});
|
|
26
|
+
return () => {
|
|
27
|
+
cancelled = true;
|
|
28
|
+
unsubscribe?.();
|
|
29
|
+
};
|
|
30
|
+
}, [
|
|
31
|
+
client,
|
|
32
|
+
channel,
|
|
33
|
+
limit
|
|
34
|
+
]);
|
|
35
|
+
const clear = useCallback(() => setNotifications([]), []);
|
|
36
|
+
return {
|
|
37
|
+
notifications,
|
|
38
|
+
latest: notifications[0] ?? null,
|
|
39
|
+
clear
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { useNotifications };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { s as RealtimeNotification, t as RealtimeClient } from "../RealtimeClient-D6-kesaz.js";
|
|
2
|
+
import { ComputedRef, Ref } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/vue/index.d.ts
|
|
5
|
+
interface UseNotificationsOptions {
|
|
6
|
+
/** Cap the number of retained notifications (newest kept). Default: unbounded. */
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
interface UseNotificationsResult {
|
|
10
|
+
notifications: Ref<RealtimeNotification[]>;
|
|
11
|
+
latest: ComputedRef<RealtimeNotification | null>;
|
|
12
|
+
clear: () => void;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Subscribe the current Vue scope to a realtime channel, accumulating incoming
|
|
17
|
+
* notifications (newest first) into a ref. Automatically unsubscribes when the
|
|
18
|
+
* scope is disposed (component unmount).
|
|
19
|
+
*
|
|
20
|
+
* @param client A {@link RealtimeClient} (from `createRealtime`).
|
|
21
|
+
* @param channel The channel to subscribe to, e.g. `client.channelFor(user.id)`.
|
|
22
|
+
* @param options `limit` caps how many notifications are retained.
|
|
23
|
+
*/
|
|
24
|
+
declare function useNotifications(client: RealtimeClient, channel: string, options?: UseNotificationsOptions): UseNotificationsResult;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { UseNotificationsOptions, UseNotificationsResult, useNotifications };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { computed, onScopeDispose, ref } from "vue";
|
|
2
|
+
//#region src/vue/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe the current Vue scope to a realtime channel, accumulating incoming
|
|
5
|
+
* notifications (newest first) into a ref. Automatically unsubscribes when the
|
|
6
|
+
* scope is disposed (component unmount).
|
|
7
|
+
*
|
|
8
|
+
* @param client A {@link RealtimeClient} (from `createRealtime`).
|
|
9
|
+
* @param channel The channel to subscribe to, e.g. `client.channelFor(user.id)`.
|
|
10
|
+
* @param options `limit` caps how many notifications are retained.
|
|
11
|
+
*/
|
|
12
|
+
function useNotifications(client, channel, options = {}) {
|
|
13
|
+
const notifications = ref([]);
|
|
14
|
+
const { limit } = options;
|
|
15
|
+
let unsubscribe;
|
|
16
|
+
let cancelled = false;
|
|
17
|
+
const push = (notification) => {
|
|
18
|
+
const next = [notification, ...notifications.value];
|
|
19
|
+
notifications.value = limit ? next.slice(0, limit) : next;
|
|
20
|
+
};
|
|
21
|
+
client.subscribe(channel, push).then((off) => {
|
|
22
|
+
if (cancelled) off();
|
|
23
|
+
else unsubscribe = off;
|
|
24
|
+
}).catch(() => { /** surfaced by the caller's own error handling */});
|
|
25
|
+
const stop = () => {
|
|
26
|
+
cancelled = true;
|
|
27
|
+
unsubscribe?.();
|
|
28
|
+
unsubscribe = void 0;
|
|
29
|
+
};
|
|
30
|
+
onScopeDispose(stop);
|
|
31
|
+
return {
|
|
32
|
+
notifications,
|
|
33
|
+
latest: computed(() => notifications.value[0] ?? null),
|
|
34
|
+
clear: () => {
|
|
35
|
+
notifications.value = [];
|
|
36
|
+
},
|
|
37
|
+
stop
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { useNotifications };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arkstack/realtime",
|
|
3
|
+
"version": "0.16.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Client for consuming Arkstack realtime notifications (Pusher/Firebase), with framework-agnostic core plus React and Vue bindings.",
|
|
6
|
+
"homepage": "https://arkstack.toneflix.net/guide/notifications",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/arkstack-hq/arkstack.git",
|
|
10
|
+
"directory": "packages/realtime"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"realtime",
|
|
14
|
+
"notifications",
|
|
15
|
+
"pusher",
|
|
16
|
+
"firebase",
|
|
17
|
+
"websocket",
|
|
18
|
+
"react",
|
|
19
|
+
"vue",
|
|
20
|
+
"arkstack"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./dist/index.js",
|
|
30
|
+
"./react": "./dist/react/index.js",
|
|
31
|
+
"./vue": "./dist/vue/index.js",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"pusher-js": "^8.4.0",
|
|
36
|
+
"firebase": "^11.0.0",
|
|
37
|
+
"react": ">=18",
|
|
38
|
+
"vue": "^3.4.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"pusher-js": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"firebase": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"react": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"vue": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/react": "^19.2.3",
|
|
56
|
+
"react": "^19.2.3",
|
|
57
|
+
"vue": "^3.5.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"test": "vitest",
|
|
62
|
+
"version:patch": "pnpm version patch"
|
|
63
|
+
}
|
|
64
|
+
}
|