@godxjp/sdk 0.3.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 +45 -2
- package/dist/laravel/index.d.ts +32 -4
- package/dist/laravel/index.js +21 -8
- package/dist/realtime/index.d.ts +26 -28
- package/dist/realtime/index.js +165 -11
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -99,11 +99,20 @@ export const apiClient = createClient({
|
|
|
99
99
|
// On a 401, before `signInUrl`: resolve `true` and the request is sent once
|
|
100
100
|
// more; resolve `false` and the 401 takes its usual course. Never more than
|
|
101
101
|
// one retry per request, and 401s that arrive together share one call.
|
|
102
|
-
|
|
102
|
+
// `ctx` is the request that was answered 401 — method, URL and the headers
|
|
103
|
+
// it was sent with — so a login that failed is not mistaken for an expired
|
|
104
|
+
// token.
|
|
105
|
+
onUnauthorized: ({ headers }) =>
|
|
106
|
+
headers.Authorization === undefined
|
|
107
|
+
? Promise.resolve(false)
|
|
108
|
+
: tokens.refresh(),
|
|
109
|
+
// A request that never reached the server: `ApiError` with status 0 by
|
|
110
|
+
// default ("wrap"); "throw" rethrows fetch's own `TypeError`.
|
|
111
|
+
networkErrors: "throw",
|
|
103
112
|
});
|
|
104
113
|
```
|
|
105
114
|
|
|
106
|
-
A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError
|
|
115
|
+
A request body is always JSON text or `FormData`, never a stream, so it can be sent a second time. A request cancelled while the token was refreshed stays an `AbortError`, whatever `networkErrors` says.
|
|
107
116
|
|
|
108
117
|
### Downloading a file
|
|
109
118
|
|
|
@@ -129,10 +138,23 @@ createClient({
|
|
|
129
138
|
});
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
Everything else the server sent is on `ApiError.body` — the parsed JSON, or `undefined` when there was none, it was not JSON, or the server was never reached. Read an `error` code, a `message` that is not a string, or every message per field from there:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
if (error instanceof ApiError) {
|
|
145
|
+
const { error: code, errors } = (error.body ?? {}) as {
|
|
146
|
+
error?: string;
|
|
147
|
+
errors?: Record<string, string[]>;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
132
152
|
An unreachable server (`status: 0`) keeps its built-in text, `Could not reach the server. Check your connection and try again.`, unless `messages(0)` answers one.
|
|
133
153
|
|
|
134
154
|
**Changed in 0.3.0.** Before, a 419 always carried `Your session has expired. Reload the page and try again.` and a response without a message fell back to `statusText`. Both are now the server's message or empty. To keep the old 419 text, pass `messages: (status) => status === 419 ? "Your session has expired. Reload the page and try again." : undefined`.
|
|
135
155
|
|
|
156
|
+
**Changed in 0.4.0.** Additions only; a client configured for 0.3.0 sends and throws exactly what it did. `ApiError` gains `body` (also on a `download()` failure). `onUnauthorized` now receives the request that was answered 401 (`{ method, url, headers }`); a function that takes no argument still works. `networkErrors: "throw"` rethrows `fetch`'s own error instead of `ApiError(0)` — including a response body that breaks off mid-way.
|
|
157
|
+
|
|
136
158
|
## `./realtime`
|
|
137
159
|
|
|
138
160
|
```ts
|
|
@@ -149,6 +171,27 @@ import {
|
|
|
149
171
|
|
|
150
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.
|
|
151
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
|
+
|
|
152
195
|
### Three words, not six
|
|
153
196
|
|
|
154
197
|
```ts
|
package/dist/laravel/index.d.ts
CHANGED
|
@@ -13,7 +13,14 @@ declare class ApiError extends Error {
|
|
|
13
13
|
readonly status: number;
|
|
14
14
|
/** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
|
|
15
15
|
readonly errors: Record<string, string>;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The error response's body, parsed as JSON — everything `message` and
|
|
18
|
+
* `errors` leave out: an `error` code, a `message` that is not a string,
|
|
19
|
+
* every message per field. `undefined` when there was no body, it was not
|
|
20
|
+
* JSON, or the server was never reached.
|
|
21
|
+
*/
|
|
22
|
+
readonly body: unknown;
|
|
23
|
+
constructor(status: number, message: string, errors?: Record<string, string>, body?: unknown);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -57,6 +64,14 @@ type DownloadResult = {
|
|
|
57
64
|
headers: Headers;
|
|
58
65
|
status: number;
|
|
59
66
|
};
|
|
67
|
+
/** The request a 401 answered, as `onUnauthorized` receives it. */
|
|
68
|
+
type UnauthorizedContext = {
|
|
69
|
+
method: string;
|
|
70
|
+
/** The full URL, `baseUrl` and query string included. */
|
|
71
|
+
url: string;
|
|
72
|
+
/** Exactly the headers the request was sent with. */
|
|
73
|
+
headers: Record<string, string>;
|
|
74
|
+
};
|
|
60
75
|
type ClientConfig = {
|
|
61
76
|
/** Prefixed to every path, e.g. `/api/app/v1`. */
|
|
62
77
|
baseUrl: string;
|
|
@@ -81,9 +96,22 @@ type ClientConfig = {
|
|
|
81
96
|
* Called on a 401 before `signInUrl`. Resolve `true` (a token was refreshed,
|
|
82
97
|
* say) and the request is sent once more, headers read afresh; resolve
|
|
83
98
|
* `false` and the 401 takes its usual course. A request is retried at most
|
|
84
|
-
* once, and 401s that arrive together share one call
|
|
99
|
+
* once, and 401s that arrive together share one call — made with the
|
|
100
|
+
* context of the request that started it.
|
|
101
|
+
*
|
|
102
|
+
* `ctx` describes the request that was answered 401, with the headers it
|
|
103
|
+
* was actually sent with — so a host can refresh only when that request
|
|
104
|
+
* carried a token.
|
|
105
|
+
*/
|
|
106
|
+
onUnauthorized?: (ctx: UnauthorizedContext) => Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* What a request that never reached the server throws. `wrap` (the
|
|
109
|
+
* default) turns it into `ApiError` with status `0`; `throw` rethrows what
|
|
110
|
+
* `fetch` threw (a `TypeError`), for a host that tells a network failure
|
|
111
|
+
* from an HTTP one by exactly that. A cancelled request is an `AbortError`
|
|
112
|
+
* either way.
|
|
85
113
|
*/
|
|
86
|
-
|
|
114
|
+
networkErrors?: "wrap" | "throw";
|
|
87
115
|
/**
|
|
88
116
|
* The message for an `ApiError` whose response carried none — the client
|
|
89
117
|
* has no language of its own. Unset, or answering `undefined`, leaves
|
|
@@ -115,4 +143,4 @@ declare function queryString(query: ApiQuery | undefined): string;
|
|
|
115
143
|
declare function flattenErrors(errors: unknown): Record<string, string>;
|
|
116
144
|
declare function createClient(config: ClientConfig): ApiClient;
|
|
117
145
|
|
|
118
|
-
export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type WriteOptions, createClient, flattenErrors, queryString };
|
|
146
|
+
export { type ApiClient, ApiError, type ApiQuery, type ClientConfig, type DownloadOptions, type DownloadResult, type GetOptions, type UnauthorizedContext, type WriteOptions, createClient, flattenErrors, queryString };
|
package/dist/laravel/index.js
CHANGED
|
@@ -3,11 +3,19 @@ var ApiError = class extends Error {
|
|
|
3
3
|
status;
|
|
4
4
|
/** Laravel's `{errors: {field: string[]}}`, first message per key; `{}` when absent. */
|
|
5
5
|
errors;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The error response's body, parsed as JSON — everything `message` and
|
|
8
|
+
* `errors` leave out: an `error` code, a `message` that is not a string,
|
|
9
|
+
* every message per field. `undefined` when there was no body, it was not
|
|
10
|
+
* JSON, or the server was never reached.
|
|
11
|
+
*/
|
|
12
|
+
body;
|
|
13
|
+
constructor(status, message, errors = {}, body) {
|
|
7
14
|
super(message);
|
|
8
15
|
this.name = "ApiError";
|
|
9
16
|
this.status = status;
|
|
10
17
|
this.errors = errors;
|
|
18
|
+
this.body = body;
|
|
11
19
|
}
|
|
12
20
|
};
|
|
13
21
|
|
|
@@ -112,7 +120,10 @@ function send(url, init, onUploadProgress) {
|
|
|
112
120
|
xhr.send(init.body ?? null);
|
|
113
121
|
});
|
|
114
122
|
}
|
|
115
|
-
function unreachable(config) {
|
|
123
|
+
function unreachable(config, error) {
|
|
124
|
+
if (config.networkErrors === "throw") {
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
116
127
|
return new ApiError(0, config.messages?.(0) ?? UNREACHABLE);
|
|
117
128
|
}
|
|
118
129
|
async function read(response, as, config, signal) {
|
|
@@ -122,7 +133,7 @@ async function read(response, as, config, signal) {
|
|
|
122
133
|
if (signal?.aborted) {
|
|
123
134
|
throw error;
|
|
124
135
|
}
|
|
125
|
-
throw unreachable(config);
|
|
136
|
+
throw unreachable(config, error);
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
async function exchange({ config, unauthorized }, method, path, body, { signal, query, onUploadProgress }, retried = false) {
|
|
@@ -142,10 +153,11 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
142
153
|
payload = JSON.stringify(body);
|
|
143
154
|
}
|
|
144
155
|
Object.assign(headers, await config.headers?.());
|
|
156
|
+
const url = `${config.baseUrl}${path}${queryString(query)}`;
|
|
145
157
|
let response;
|
|
146
158
|
try {
|
|
147
159
|
response = await send(
|
|
148
|
-
|
|
160
|
+
url,
|
|
149
161
|
{
|
|
150
162
|
method,
|
|
151
163
|
headers,
|
|
@@ -159,12 +171,12 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
159
171
|
if (signal?.aborted) {
|
|
160
172
|
throw error;
|
|
161
173
|
}
|
|
162
|
-
throw unreachable(config);
|
|
174
|
+
throw unreachable(config, error);
|
|
163
175
|
}
|
|
164
176
|
if (response.status >= 200 && response.status < 300) {
|
|
165
177
|
return response;
|
|
166
178
|
}
|
|
167
|
-
if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized()) {
|
|
179
|
+
if (response.status === 401 && !retried && config.onUnauthorized !== void 0 && await unauthorized({ method, url, headers: { ...headers } })) {
|
|
168
180
|
return exchange(
|
|
169
181
|
{ config, unauthorized },
|
|
170
182
|
method,
|
|
@@ -187,7 +199,8 @@ async function exchange({ config, unauthorized }, method, path, body, { signal,
|
|
|
187
199
|
throw new ApiError(
|
|
188
200
|
response.status,
|
|
189
201
|
typeof bag.message === "string" && bag.message !== "" ? bag.message : config.messages?.(response.status) ?? "",
|
|
190
|
-
flattenErrors(bag.errors)
|
|
202
|
+
flattenErrors(bag.errors),
|
|
203
|
+
json
|
|
191
204
|
);
|
|
192
205
|
}
|
|
193
206
|
async function request(client, method, path, body, options) {
|
|
@@ -206,7 +219,7 @@ function createClient(config) {
|
|
|
206
219
|
let refreshing;
|
|
207
220
|
const client = {
|
|
208
221
|
config,
|
|
209
|
-
unauthorized: () => refreshing ??= (config.onUnauthorized?.() ?? Promise.resolve(false)).finally(() => {
|
|
222
|
+
unauthorized: (ctx) => refreshing ??= (config.onUnauthorized?.(ctx) ?? Promise.resolve(false)).finally(() => {
|
|
210
223
|
refreshing = void 0;
|
|
211
224
|
})
|
|
212
225
|
};
|
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"
|