@godxjp/sdk 0.4.0 → 0.5.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 +21 -0
- package/dist/realtime/index.d.ts +26 -28
- package/dist/realtime/index.js +165 -11
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -171,6 +171,27 @@ import {
|
|
|
171
171
|
|
|
172
172
|
So nothing from the transport appears in this API — no Echo type, no Pusher connection state, no channel object. A consumer able to name the underlying client has already defeated the point.
|
|
173
173
|
|
|
174
|
+
### Two transports
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// The product's own Reverb, as before (a config without `driver`):
|
|
178
|
+
configureRealtime({ key, host, port, scheme });
|
|
179
|
+
|
|
180
|
+
// The platform's Centrifugo, on the socket the GoDX Dock already holds (godx-jp/id#2019):
|
|
181
|
+
configureRealtime({
|
|
182
|
+
driver: "godx-dock",
|
|
183
|
+
channelPrefix: `private-svc.${clientId}.`,
|
|
184
|
+
authEndpoint: "/broadcasting/auth",
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
With `godx-dock` the server publishes through php-sdk's `godx` broadcast driver, and this package
|
|
189
|
+
listens through `window.GodxDock.realtime()`. The Dock lends only a narrow `subscribe` for
|
|
190
|
+
`private-svc.*` channels, never its client. Channel and event names stay the product's own
|
|
191
|
+
(`issue.7`, `.issue.changed`). The subscription token comes from `authEndpoint`, so your
|
|
192
|
+
`routes/channels.php` still decides. A page with no Dock, no session or no Centrifugo reports
|
|
193
|
+
`unavailable`, and your polling fallback takes over.
|
|
194
|
+
|
|
174
195
|
### Three words, not six
|
|
175
196
|
|
|
176
197
|
```ts
|
package/dist/realtime/index.d.ts
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* because a screen has the same job in all three.
|
|
23
23
|
*/
|
|
24
24
|
type RealtimeStatus = "connecting" | "connected" | "unavailable";
|
|
25
|
-
/**
|
|
26
|
-
type
|
|
25
|
+
/** A product's own Reverb server — what every config meant before `driver` existed. */
|
|
26
|
+
type ReverbRealtimeConfig = {
|
|
27
|
+
driver?: "reverb";
|
|
27
28
|
/** The app key the server issued. */
|
|
28
29
|
key: string;
|
|
29
30
|
host: string;
|
|
@@ -31,6 +32,19 @@ type RealtimeConfig = {
|
|
|
31
32
|
/** `https` turns on TLS and the `wss` transport. */
|
|
32
33
|
scheme: "http" | "https";
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The platform's Centrifugo, over the socket the GoDX Dock already holds (godx-jp/id#2019). The
|
|
37
|
+
* server side is php-sdk's `godx` broadcast driver.
|
|
38
|
+
*/
|
|
39
|
+
type DockRealtimeConfig = {
|
|
40
|
+
driver: "godx-dock";
|
|
41
|
+
/** `private-svc.{client_id}.` — where the platform relay puts every channel of this service. */
|
|
42
|
+
channelPrefix: string;
|
|
43
|
+
/** The product's own channel authorization endpoint, usually `/broadcasting/auth`. */
|
|
44
|
+
authEndpoint: string;
|
|
45
|
+
};
|
|
46
|
+
/** What the host must hand over to reach its realtime server. */
|
|
47
|
+
type RealtimeConfig = ReverbRealtimeConfig | DockRealtimeConfig;
|
|
34
48
|
/**
|
|
35
49
|
* Map a transport's own vocabulary onto ours.
|
|
36
50
|
*
|
|
@@ -39,44 +53,28 @@ type RealtimeConfig = {
|
|
|
39
53
|
*/
|
|
40
54
|
declare function toRealtimeStatus(raw: string | null | undefined): RealtimeStatus;
|
|
41
55
|
|
|
42
|
-
/**
|
|
43
|
-
* The Echo/Pusher adapter — today's transport, and the only file in this
|
|
44
|
-
* package that may import it.
|
|
45
|
-
*/
|
|
46
56
|
/**
|
|
47
57
|
* Wire the connection once, for the life of the tab.
|
|
48
58
|
*
|
|
49
|
-
* The host reads its own server's details wherever it keeps them (a meta tag,
|
|
50
|
-
*
|
|
51
|
-
* belongs to whoever runs the bundler, and four products do not share one.
|
|
59
|
+
* The host reads its own server's details wherever it keeps them (a meta tag, an API call) and
|
|
60
|
+
* passes them in. A config without `driver` is the product's own Reverb, as before.
|
|
52
61
|
*/
|
|
53
62
|
declare function configureRealtime(config: RealtimeConfig): void;
|
|
54
63
|
/**
|
|
55
|
-
* Whether `configureRealtime` ran.
|
|
56
|
-
*
|
|
57
|
-
* CONFIGURED IS NOT CONNECTED, and conflating them is a real bug this project
|
|
58
|
-
* has already shipped once: a screen that reads "configured" as "live" keeps
|
|
59
|
-
* its polling fallback switched off while the server is down, and freezes on
|
|
60
|
-
* whatever it loaded with. Ask `useRealtimeStatus()` for liveness.
|
|
64
|
+
* Whether `configureRealtime` ran. CONFIGURED IS NOT CONNECTED: ask `useRealtimeStatus()` for
|
|
65
|
+
* liveness.
|
|
61
66
|
*/
|
|
62
67
|
declare function isRealtimeConfigured(): boolean;
|
|
63
|
-
/** Liveness, in this package's three words rather than the transport's
|
|
68
|
+
/** Liveness, in this package's three words rather than the transport's own. */
|
|
64
69
|
declare function useRealtimeStatus(): RealtimeStatus;
|
|
65
70
|
/**
|
|
66
|
-
* Listen to one named event on one channel.
|
|
67
|
-
*
|
|
68
|
-
* `event` is the name the server broadcasts, leading dot and all — the
|
|
69
|
-
* transport's convention for "this is our own event, not a framework one".
|
|
71
|
+
* Listen to one named event on one private channel. `event` is the name the server broadcasts,
|
|
72
|
+
* leading dot and all.
|
|
70
73
|
*/
|
|
71
74
|
declare function useChannelEvent<T = unknown>(channel: string, event: string, handler: (payload: T) => void, deps?: unknown[]): void;
|
|
72
75
|
/**
|
|
73
|
-
* Listen to a user's notification channel.
|
|
74
|
-
*
|
|
75
|
-
* SEPARATE FROM `useChannelEvent` ON PURPOSE. Laravel broadcasts every
|
|
76
|
-
* notification under one framework event name, so subscribing by a custom name
|
|
77
|
-
* succeeds and then never fires — a failure that presents as "connected but
|
|
78
|
-
* nothing happens". Naming the two cases apart is what stops the next person
|
|
79
|
-
* reaching for the wrong one.
|
|
76
|
+
* Listen to a user's notification channel. SEPARATE FROM `useChannelEvent` ON PURPOSE: Laravel
|
|
77
|
+
* broadcasts every notification under one framework event name.
|
|
80
78
|
*/
|
|
81
79
|
declare function useUserNotification<T = unknown>(channel: string, handler: (payload: T) => void): void;
|
|
82
80
|
|
|
@@ -113,4 +111,4 @@ declare function platformChannel(resource: string, resourceId: string): ChannelN
|
|
|
113
111
|
declare function parseChannelName(name: string): ParsedChannelName;
|
|
114
112
|
declare function tryParseChannelName(name: string): ParsedChannelName | null;
|
|
115
113
|
|
|
116
|
-
export { type ChannelName, ChannelNameError, type ParsedChannelName, type RealtimeConfig, type RealtimeStatus, configureRealtime, isRealtimeConfigured, organizationChannel, parseChannelName, platformChannel, toRealtimeStatus, tryParseChannelName, useChannelEvent, useRealtimeStatus, useUserNotification };
|
|
114
|
+
export { type ChannelName, ChannelNameError, type DockRealtimeConfig, type ParsedChannelName, type RealtimeConfig, type RealtimeStatus, type ReverbRealtimeConfig, configureRealtime, isRealtimeConfigured, organizationChannel, parseChannelName, platformChannel, toRealtimeStatus, tryParseChannelName, useChannelEvent, useRealtimeStatus, useUserNotification };
|
package/dist/realtime/index.js
CHANGED
|
@@ -1,6 +1,128 @@
|
|
|
1
|
+
import { useSyncExternalStore, useRef, useEffect } from 'react';
|
|
1
2
|
import { configureEcho, echoIsConfigured, useConnectionStatus, useEcho, useEchoNotification } from '@laravel/echo-react';
|
|
2
3
|
|
|
3
|
-
// src/realtime/
|
|
4
|
+
// src/realtime/dock-adapter.ts
|
|
5
|
+
var NOTIFICATION_EVENT = "Illuminate\\Notifications\\Events\\BroadcastNotificationCreated";
|
|
6
|
+
var config = null;
|
|
7
|
+
var dock = null;
|
|
8
|
+
function configureDockRealtime(next) {
|
|
9
|
+
config = next;
|
|
10
|
+
dock = null;
|
|
11
|
+
}
|
|
12
|
+
function isDockRealtimeConfigured() {
|
|
13
|
+
return config !== null;
|
|
14
|
+
}
|
|
15
|
+
function dockRealtime() {
|
|
16
|
+
dock ??= new Promise((resolve) => {
|
|
17
|
+
const found = () => window.GodxDock ?? null;
|
|
18
|
+
if (found() !== null || document.readyState === "complete") {
|
|
19
|
+
resolve(found());
|
|
20
|
+
} else {
|
|
21
|
+
window.addEventListener("load", () => resolve(found()), { once: true });
|
|
22
|
+
}
|
|
23
|
+
}).then((global) => global?.realtime === void 0 ? null : global.realtime());
|
|
24
|
+
return dock;
|
|
25
|
+
}
|
|
26
|
+
function readCookie(name) {
|
|
27
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
28
|
+
return match?.[1] === void 0 ? null : decodeURIComponent(match[1]);
|
|
29
|
+
}
|
|
30
|
+
async function subscriptionToken(channel) {
|
|
31
|
+
const active = config;
|
|
32
|
+
if (active === null) {
|
|
33
|
+
throw new Error("realtime is not configured");
|
|
34
|
+
}
|
|
35
|
+
const xsrf = readCookie("XSRF-TOKEN");
|
|
36
|
+
const response = await fetch(active.authEndpoint, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
credentials: "same-origin",
|
|
39
|
+
headers: {
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
...xsrf === null ? {} : { "X-XSRF-TOKEN": xsrf }
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({ channel_name: `private-${channel}` })
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`channel authorization refused (${response.status})`);
|
|
48
|
+
}
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
if (typeof data.token !== "string") {
|
|
51
|
+
throw new Error("channel authorization answered without a token");
|
|
52
|
+
}
|
|
53
|
+
return data.token;
|
|
54
|
+
}
|
|
55
|
+
function literal(event) {
|
|
56
|
+
return event.startsWith(".") ? event.slice(1) : event;
|
|
57
|
+
}
|
|
58
|
+
function useDockSubscription(channel, event, handler, deps) {
|
|
59
|
+
const latest = useRef(handler);
|
|
60
|
+
latest.current = handler;
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const active = config;
|
|
63
|
+
if (active === null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let stop = null;
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
const wanted = literal(event);
|
|
69
|
+
void dockRealtime().then((realtime) => {
|
|
70
|
+
if (realtime === null || cancelled) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
stop = realtime.subscribe(`${active.channelPrefix}${channel}`, {
|
|
74
|
+
getToken: () => subscriptionToken(channel),
|
|
75
|
+
onPublication: (name, data) => {
|
|
76
|
+
if (name === wanted) {
|
|
77
|
+
latest.current(data);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
cancelled = true;
|
|
84
|
+
stop?.();
|
|
85
|
+
};
|
|
86
|
+
}, [channel, event, ...deps]);
|
|
87
|
+
}
|
|
88
|
+
function useDockChannelEvent(channel, event, handler, deps) {
|
|
89
|
+
useDockSubscription(channel, event, handler, deps);
|
|
90
|
+
}
|
|
91
|
+
function useDockUserNotification(channel, handler) {
|
|
92
|
+
useDockSubscription(channel, NOTIFICATION_EVENT, handler, []);
|
|
93
|
+
}
|
|
94
|
+
var status = "connecting";
|
|
95
|
+
var statusListeners = /* @__PURE__ */ new Set();
|
|
96
|
+
var watching = false;
|
|
97
|
+
function watchStatus() {
|
|
98
|
+
if (watching || config === null) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
watching = true;
|
|
102
|
+
void dockRealtime().then((realtime) => {
|
|
103
|
+
const set = (next) => {
|
|
104
|
+
status = next;
|
|
105
|
+
statusListeners.forEach((listener) => listener());
|
|
106
|
+
};
|
|
107
|
+
if (realtime === null) {
|
|
108
|
+
set("unavailable");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
set(realtime.status());
|
|
112
|
+
realtime.onStatus(set);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function useDockRealtimeStatus() {
|
|
116
|
+
return useSyncExternalStore(
|
|
117
|
+
(listener) => {
|
|
118
|
+
statusListeners.add(listener);
|
|
119
|
+
watchStatus();
|
|
120
|
+
return () => statusListeners.delete(listener);
|
|
121
|
+
},
|
|
122
|
+
() => status,
|
|
123
|
+
() => "connecting"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
4
126
|
|
|
5
127
|
// src/realtime/types.ts
|
|
6
128
|
function toRealtimeStatus(raw) {
|
|
@@ -14,30 +136,62 @@ function toRealtimeStatus(raw) {
|
|
|
14
136
|
}
|
|
15
137
|
|
|
16
138
|
// src/realtime/echo-adapter.ts
|
|
17
|
-
function
|
|
139
|
+
function configureEchoRealtime(config2) {
|
|
18
140
|
configureEcho({
|
|
19
141
|
broadcaster: "reverb",
|
|
20
|
-
key:
|
|
21
|
-
wsHost:
|
|
22
|
-
wsPort:
|
|
23
|
-
wssPort:
|
|
24
|
-
forceTLS:
|
|
142
|
+
key: config2.key,
|
|
143
|
+
wsHost: config2.host,
|
|
144
|
+
wsPort: config2.port,
|
|
145
|
+
wssPort: config2.port,
|
|
146
|
+
forceTLS: config2.scheme === "https",
|
|
25
147
|
enabledTransports: ["ws", "wss"]
|
|
26
148
|
});
|
|
27
149
|
}
|
|
28
|
-
function
|
|
150
|
+
function isEchoRealtimeConfigured() {
|
|
29
151
|
return echoIsConfigured();
|
|
30
152
|
}
|
|
31
|
-
function
|
|
153
|
+
function useEchoRealtimeStatus() {
|
|
32
154
|
return toRealtimeStatus(useConnectionStatus());
|
|
33
155
|
}
|
|
34
|
-
function
|
|
156
|
+
function useEchoChannelEvent(channel, event, handler, deps = []) {
|
|
35
157
|
useEcho(channel, event, handler, deps);
|
|
36
158
|
}
|
|
37
|
-
function
|
|
159
|
+
function useEchoUserNotification(channel, handler) {
|
|
38
160
|
useEchoNotification(channel, handler);
|
|
39
161
|
}
|
|
40
162
|
|
|
163
|
+
// src/realtime/select.ts
|
|
164
|
+
var driver = "reverb";
|
|
165
|
+
function configureRealtime(config2) {
|
|
166
|
+
if (config2.driver === "godx-dock") {
|
|
167
|
+
driver = "godx-dock";
|
|
168
|
+
configureDockRealtime(config2);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
driver = "reverb";
|
|
172
|
+
configureEchoRealtime(config2);
|
|
173
|
+
}
|
|
174
|
+
function isRealtimeConfigured() {
|
|
175
|
+
return driver === "godx-dock" ? isDockRealtimeConfigured() : isEchoRealtimeConfigured();
|
|
176
|
+
}
|
|
177
|
+
function useRealtimeStatus() {
|
|
178
|
+
return driver === "godx-dock" ? useDockRealtimeStatus() : useEchoRealtimeStatus();
|
|
179
|
+
}
|
|
180
|
+
function useChannelEvent(channel, event, handler, deps = []) {
|
|
181
|
+
if (driver === "godx-dock") {
|
|
182
|
+
useDockChannelEvent(channel, event, handler, deps);
|
|
183
|
+
} else {
|
|
184
|
+
useEchoChannelEvent(channel, event, handler, deps);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function useUserNotification(channel, handler) {
|
|
188
|
+
if (driver === "godx-dock") {
|
|
189
|
+
useDockUserNotification(channel, handler);
|
|
190
|
+
} else {
|
|
191
|
+
useEchoUserNotification(channel, handler);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
41
195
|
// src/realtime/channel-name.ts
|
|
42
196
|
var ChannelNameError = class extends Error {
|
|
43
197
|
constructor(message) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "GoDX JavaScript SDK \u2014 shared frontend infrastructure for GoDX products.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/godx-jp/web-sdk.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/godx-jp/web-sdk#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/godx-jp/web-sdk/issues"
|
|
13
|
+
},
|
|
6
14
|
"type": "module",
|
|
7
15
|
"sideEffects": false,
|
|
8
16
|
"files": [
|
|
@@ -36,10 +44,12 @@
|
|
|
36
44
|
"@laravel/echo-react": "^2.5.0",
|
|
37
45
|
"@types/node": "^22.10.2",
|
|
38
46
|
"@types/react": "^19.3.0",
|
|
47
|
+
"@types/react-dom": "^19.3.0",
|
|
39
48
|
"jsdom": "^25.0.1",
|
|
40
49
|
"laravel-echo": "^2.5.0",
|
|
41
50
|
"pusher-js": "^8.6.0",
|
|
42
51
|
"react": "^19.3.0",
|
|
52
|
+
"react-dom": "^19.3.0",
|
|
43
53
|
"tsup": "^8.5.1",
|
|
44
54
|
"typescript": "^5.7.2",
|
|
45
55
|
"vitest": "^2.1.8"
|