@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 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
@@ -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
- /** What the host must hand over to reach its own server. */
26
- type RealtimeConfig = {
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
- * an API call) and passes them in. The SDK does not guess: `import.meta.env`
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 six. */
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 };
@@ -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/echo-adapter.ts
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 configureRealtime(config) {
139
+ function configureEchoRealtime(config2) {
18
140
  configureEcho({
19
141
  broadcaster: "reverb",
20
- key: config.key,
21
- wsHost: config.host,
22
- wsPort: config.port,
23
- wssPort: config.port,
24
- forceTLS: config.scheme === "https",
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 isRealtimeConfigured() {
150
+ function isEchoRealtimeConfigured() {
29
151
  return echoIsConfigured();
30
152
  }
31
- function useRealtimeStatus() {
153
+ function useEchoRealtimeStatus() {
32
154
  return toRealtimeStatus(useConnectionStatus());
33
155
  }
34
- function useChannelEvent(channel, event, handler, deps = []) {
156
+ function useEchoChannelEvent(channel, event, handler, deps = []) {
35
157
  useEcho(channel, event, handler, deps);
36
158
  }
37
- function useUserNotification(channel, handler) {
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.4.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"