@jskit-ai/realtime 0.1.4
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/package.descriptor.mjs +142 -0
- package/package.json +26 -0
- package/src/client/RealtimeClientProvider.js +302 -0
- package/src/client/components/RealtimeConnectionIndicator.js +122 -0
- package/src/client/composables/useRealtimeEvent.js +147 -0
- package/src/client/listeners.js +69 -0
- package/src/client/runtime.js +37 -0
- package/src/client/tokens.js +11 -0
- package/src/server/RealtimeServiceProvider.js +743 -0
- package/src/server/runtime.js +134 -0
- package/src/server/tokens.js +7 -0
- package/test/clientListeners.test.js +66 -0
- package/test/clientRuntime.test.js +81 -0
- package/test/entrypoints.boundary.test.js +45 -0
- package/test/providerRuntime.test.js +582 -0
- package/test/serverRuntime.test.js +149 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/realtime",
|
|
4
|
+
version: "0.1.4",
|
|
5
|
+
description: "Thin, generic realtime runtime wrappers for socket.io server and client.",
|
|
6
|
+
options: {
|
|
7
|
+
"realtime-redis-url": {
|
|
8
|
+
required: true,
|
|
9
|
+
allowEmpty: true,
|
|
10
|
+
values: [],
|
|
11
|
+
defaultValue: "",
|
|
12
|
+
promptLabel: "Realtime Redis URL",
|
|
13
|
+
promptHint: "Leave empty to use in-memory socket adapter."
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
dependsOn: [
|
|
17
|
+
"@jskit-ai/kernel"
|
|
18
|
+
],
|
|
19
|
+
capabilities: {
|
|
20
|
+
provides: [
|
|
21
|
+
"runtime.realtime",
|
|
22
|
+
"runtime.realtime.client"
|
|
23
|
+
],
|
|
24
|
+
requires: []
|
|
25
|
+
},
|
|
26
|
+
runtime: {
|
|
27
|
+
server: {
|
|
28
|
+
providerEntrypoint: "src/server/RealtimeServiceProvider.js",
|
|
29
|
+
providers: [
|
|
30
|
+
{
|
|
31
|
+
entrypoint: "src/server/RealtimeServiceProvider.js",
|
|
32
|
+
export: "RealtimeServiceProvider"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
client: {
|
|
37
|
+
providers: [
|
|
38
|
+
{
|
|
39
|
+
entrypoint: "src/client/RealtimeClientProvider.js",
|
|
40
|
+
export: "RealtimeClientProvider"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
metadata: {
|
|
46
|
+
apiSummary: {
|
|
47
|
+
surfaces: [
|
|
48
|
+
{
|
|
49
|
+
subpath: "./server",
|
|
50
|
+
summary: "Exports RealtimeServiceProvider only."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
subpath: "./client",
|
|
54
|
+
summary: "Exports RealtimeClientProvider only."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
subpath: "./client/listeners",
|
|
58
|
+
summary: "Exports client listener registration helpers for provider-level realtime subscriptions."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
subpath: "./client/composables/*",
|
|
62
|
+
summary: "Exports component-level realtime socket composables."
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
containerTokens: {
|
|
66
|
+
server: [
|
|
67
|
+
"runtime.realtime"
|
|
68
|
+
],
|
|
69
|
+
client: [
|
|
70
|
+
"runtime.realtime.client",
|
|
71
|
+
"runtime.realtime.client.socket",
|
|
72
|
+
"realtime.web.connection.indicator"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
ui: {
|
|
77
|
+
placements: {
|
|
78
|
+
outlets: [],
|
|
79
|
+
contributions: [
|
|
80
|
+
{
|
|
81
|
+
id: "realtime.connection.indicator",
|
|
82
|
+
host: "shell-layout",
|
|
83
|
+
position: "top-right",
|
|
84
|
+
surfaces: ["*"],
|
|
85
|
+
order: 950,
|
|
86
|
+
componentToken: "realtime.web.connection.indicator",
|
|
87
|
+
source: "mutations.text#realtime-placement-indicator"
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
mutations: {
|
|
94
|
+
dependencies: {
|
|
95
|
+
runtime: {
|
|
96
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
97
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
98
|
+
"redis": "^5.8.2",
|
|
99
|
+
"socket.io": "^4.8.3",
|
|
100
|
+
"socket.io-client": "^4.8.3"
|
|
101
|
+
},
|
|
102
|
+
dev: {}
|
|
103
|
+
},
|
|
104
|
+
packageJson: {
|
|
105
|
+
scripts: {}
|
|
106
|
+
},
|
|
107
|
+
procfile: {},
|
|
108
|
+
files: [],
|
|
109
|
+
vite: {
|
|
110
|
+
proxy: [
|
|
111
|
+
{
|
|
112
|
+
id: "realtime-socket-io",
|
|
113
|
+
path: "/socket.io",
|
|
114
|
+
changeOrigin: true,
|
|
115
|
+
ws: true
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
text: [
|
|
120
|
+
{
|
|
121
|
+
file: ".env",
|
|
122
|
+
op: "upsert-env",
|
|
123
|
+
key: "REALTIME_REDIS_URL",
|
|
124
|
+
value: "${option:realtime-redis-url}",
|
|
125
|
+
reason: "Configure optional Redis backplane URL for realtime socket adapter.",
|
|
126
|
+
category: "runtime-config",
|
|
127
|
+
id: "realtime-redis-url"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
op: "append-text",
|
|
131
|
+
file: "src/placement.js",
|
|
132
|
+
position: "bottom",
|
|
133
|
+
skipIfContains: "id: \"realtime.connection.indicator\"",
|
|
134
|
+
value:
|
|
135
|
+
"\naddPlacement({\n id: \"realtime.connection.indicator\",\n host: \"shell-layout\",\n position: \"top-right\",\n surfaces: [\"*\"],\n order: 950,\n componentToken: \"realtime.web.connection.indicator\"\n});\n",
|
|
136
|
+
reason: "Append realtime connection indicator placement into app-owned placement registry.",
|
|
137
|
+
category: "realtime-web",
|
|
138
|
+
id: "realtime-placement-indicator"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/realtime",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./server/RealtimeServiceProvider": "./src/server/RealtimeServiceProvider.js",
|
|
10
|
+
"./server/runtime": "./src/server/runtime.js",
|
|
11
|
+
"./server/tokens": "./src/server/tokens.js",
|
|
12
|
+
"./client": "./src/client/RealtimeClientProvider.js",
|
|
13
|
+
"./client/RealtimeClientProvider": "./src/client/RealtimeClientProvider.js",
|
|
14
|
+
"./client/listeners": "./src/client/listeners.js",
|
|
15
|
+
"./client/composables/*": "./src/client/composables/*.js",
|
|
16
|
+
"./client/runtime": "./src/client/runtime.js",
|
|
17
|
+
"./client/tokens": "./src/client/tokens.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
21
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
22
|
+
"redis": "^5.8.2",
|
|
23
|
+
"socket.io": "^4.8.3",
|
|
24
|
+
"socket.io-client": "^4.8.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createSocketIoClient, disconnectSocketIoClient } from "./runtime.js";
|
|
2
|
+
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import {
|
|
4
|
+
CLIENT_MODULE_ENV_TOKEN,
|
|
5
|
+
CLIENT_MODULE_VUE_APP_TOKEN
|
|
6
|
+
} from "@jskit-ai/kernel/client/moduleBootstrap";
|
|
7
|
+
import { resolveClientBootstrapDebugEnabled } from "@jskit-ai/kernel/client";
|
|
8
|
+
import RealtimeConnectionIndicator from "./components/RealtimeConnectionIndicator.js";
|
|
9
|
+
import {
|
|
10
|
+
REALTIME_RUNTIME_CLIENT_TOKEN,
|
|
11
|
+
REALTIME_SOCKET_CLIENT_TOKEN,
|
|
12
|
+
REALTIME_SOCKET_CLIENT_INJECTION_KEY
|
|
13
|
+
} from "./tokens.js";
|
|
14
|
+
import { resolveRealtimeClientListeners } from "./listeners.js";
|
|
15
|
+
|
|
16
|
+
const REALTIME_CONNECTION_INDICATOR_COMPONENT_TOKEN = "realtime.web.connection.indicator";
|
|
17
|
+
|
|
18
|
+
const REALTIME_RUNTIME_CLIENT_API = Object.freeze({
|
|
19
|
+
createSocketIoClient,
|
|
20
|
+
disconnectSocketIoClient
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function createProviderLogger(app, { debugEnabled = false } = {}) {
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
debug: (...args) => {
|
|
26
|
+
if (debugEnabled !== true) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (app && typeof app.info === "function") {
|
|
30
|
+
app.info(...args);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.info(...args);
|
|
34
|
+
},
|
|
35
|
+
info: (...args) => {
|
|
36
|
+
if (app && typeof app.info === "function") {
|
|
37
|
+
app.info(...args);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.info(...args);
|
|
41
|
+
},
|
|
42
|
+
warn: (...args) => {
|
|
43
|
+
if (app && typeof app.warn === "function") {
|
|
44
|
+
app.warn(...args);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.warn(...args);
|
|
48
|
+
},
|
|
49
|
+
error: (...args) => {
|
|
50
|
+
if (app && typeof app.error === "function") {
|
|
51
|
+
app.error(...args);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.error(...args);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveRealtimeClientConfig(app) {
|
|
60
|
+
const appConfig = app && typeof app.has === "function" && app.has("appConfig") ? normalizeObject(app.make("appConfig")) : {};
|
|
61
|
+
const env = app && typeof app.has === "function" && app.has(CLIENT_MODULE_ENV_TOKEN) ? normalizeObject(app.make(CLIENT_MODULE_ENV_TOKEN)) : {};
|
|
62
|
+
const realtime = normalizeObject(appConfig.realtime);
|
|
63
|
+
const realtimeClient = normalizeObject(appConfig.realtimeClient);
|
|
64
|
+
const url = normalizeText(realtimeClient.url);
|
|
65
|
+
const options = normalizeObject(realtimeClient.options);
|
|
66
|
+
const explicitDebugEnabled =
|
|
67
|
+
typeof realtimeClient.debug === "boolean"
|
|
68
|
+
? realtimeClient.debug
|
|
69
|
+
: typeof realtime.debug === "boolean"
|
|
70
|
+
? realtime.debug
|
|
71
|
+
: undefined;
|
|
72
|
+
const hasRealtimeDebugEnvOverride = Object.hasOwn(env, "VITE_REALTIME_DEBUG");
|
|
73
|
+
const debugEnabled = hasRealtimeDebugEnvOverride
|
|
74
|
+
? resolveClientBootstrapDebugEnabled({
|
|
75
|
+
env,
|
|
76
|
+
debugEnabled: undefined,
|
|
77
|
+
debugEnvKey: "VITE_REALTIME_DEBUG"
|
|
78
|
+
})
|
|
79
|
+
: resolveClientBootstrapDebugEnabled({
|
|
80
|
+
env,
|
|
81
|
+
debugEnabled: explicitDebugEnabled,
|
|
82
|
+
debugEnvKey: "VITE_REALTIME_DEBUG"
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
url,
|
|
87
|
+
options,
|
|
88
|
+
debugEnabled
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class RealtimeClientProvider {
|
|
93
|
+
static id = REALTIME_RUNTIME_CLIENT_TOKEN;
|
|
94
|
+
|
|
95
|
+
register(app) {
|
|
96
|
+
if (!app || typeof app.singleton !== "function") {
|
|
97
|
+
throw new Error("RealtimeClientProvider requires application singleton().");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
app.singleton(REALTIME_RUNTIME_CLIENT_TOKEN, () => REALTIME_RUNTIME_CLIENT_API);
|
|
101
|
+
app.singleton(REALTIME_CONNECTION_INDICATOR_COMPONENT_TOKEN, () => RealtimeConnectionIndicator);
|
|
102
|
+
app.singleton(REALTIME_SOCKET_CLIENT_TOKEN, (scope) => {
|
|
103
|
+
const realtimeRuntime = scope.make(REALTIME_RUNTIME_CLIENT_TOKEN);
|
|
104
|
+
const realtimeClientConfig = resolveRealtimeClientConfig(scope);
|
|
105
|
+
return realtimeRuntime.createSocketIoClient({
|
|
106
|
+
url: realtimeClientConfig.url,
|
|
107
|
+
options: realtimeClientConfig.options
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
boot(app) {
|
|
113
|
+
if (!app || typeof app.make !== "function") {
|
|
114
|
+
throw new Error("RealtimeClientProvider requires application make().");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const realtimeClientConfig = resolveRealtimeClientConfig(app);
|
|
118
|
+
const logger = createProviderLogger(app, {
|
|
119
|
+
debugEnabled: realtimeClientConfig.debugEnabled
|
|
120
|
+
});
|
|
121
|
+
const socket = app.make(REALTIME_SOCKET_CLIENT_TOKEN);
|
|
122
|
+
const listeners = resolveRealtimeClientListeners(app);
|
|
123
|
+
const detach = [];
|
|
124
|
+
|
|
125
|
+
logger.debug(
|
|
126
|
+
{
|
|
127
|
+
providerId: RealtimeClientProvider.id,
|
|
128
|
+
listenerCount: listeners.length,
|
|
129
|
+
listeners: listeners.map((listener) => ({
|
|
130
|
+
listenerId: listener.listenerId,
|
|
131
|
+
event: listener.event
|
|
132
|
+
}))
|
|
133
|
+
},
|
|
134
|
+
"Realtime client booted listeners."
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (typeof socket.on === "function") {
|
|
138
|
+
const onConnect = () => {
|
|
139
|
+
logger.debug(
|
|
140
|
+
{
|
|
141
|
+
providerId: RealtimeClientProvider.id,
|
|
142
|
+
socketConnected: true
|
|
143
|
+
},
|
|
144
|
+
"Realtime client socket connected."
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
const onDisconnect = (reason) => {
|
|
148
|
+
logger.debug(
|
|
149
|
+
{
|
|
150
|
+
providerId: RealtimeClientProvider.id,
|
|
151
|
+
socketConnected: false,
|
|
152
|
+
reason: String(reason || "")
|
|
153
|
+
},
|
|
154
|
+
"Realtime client socket disconnected."
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
const onConnectError = (error) => {
|
|
158
|
+
logger.warn(
|
|
159
|
+
{
|
|
160
|
+
providerId: RealtimeClientProvider.id,
|
|
161
|
+
error: String(error?.message || error || "unknown error")
|
|
162
|
+
},
|
|
163
|
+
"Realtime client socket connect error."
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
socket.on("connect", onConnect);
|
|
168
|
+
socket.on("disconnect", onDisconnect);
|
|
169
|
+
socket.on("connect_error", onConnectError);
|
|
170
|
+
detach.push(() => {
|
|
171
|
+
if (typeof socket.off === "function") {
|
|
172
|
+
socket.off("connect", onConnect);
|
|
173
|
+
socket.off("disconnect", onDisconnect);
|
|
174
|
+
socket.off("connect_error", onConnectError);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (realtimeClientConfig.debugEnabled === true && typeof socket.onAny === "function") {
|
|
179
|
+
const onAnyDebug = (eventName, payload) => {
|
|
180
|
+
logger.debug(
|
|
181
|
+
{
|
|
182
|
+
providerId: RealtimeClientProvider.id,
|
|
183
|
+
event: String(eventName || ""),
|
|
184
|
+
payloadScope: payload?.scope || null,
|
|
185
|
+
payloadEntityId: payload?.entityId || null
|
|
186
|
+
},
|
|
187
|
+
"Realtime client received socket event."
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
socket.onAny(onAnyDebug);
|
|
191
|
+
detach.push(() => {
|
|
192
|
+
if (typeof socket.offAny === "function") {
|
|
193
|
+
socket.offAny(onAnyDebug);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const listener of listeners) {
|
|
200
|
+
const invoke = (eventName, payload) => {
|
|
201
|
+
const context = Object.freeze({
|
|
202
|
+
event: eventName,
|
|
203
|
+
payload,
|
|
204
|
+
socket,
|
|
205
|
+
app
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (listener.matches && listener.matches(context) !== true) {
|
|
209
|
+
logger.debug(
|
|
210
|
+
{
|
|
211
|
+
listenerId: listener.listenerId,
|
|
212
|
+
event: eventName
|
|
213
|
+
},
|
|
214
|
+
"Realtime client listener skipped event by matches()."
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
logger.debug(
|
|
220
|
+
{
|
|
221
|
+
listenerId: listener.listenerId,
|
|
222
|
+
event: eventName,
|
|
223
|
+
payloadScope: payload?.scope || null,
|
|
224
|
+
payloadEntityId: payload?.entityId || null
|
|
225
|
+
},
|
|
226
|
+
"Realtime client listener handling event."
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
Promise.resolve(listener.handle(context)).catch((error) => {
|
|
230
|
+
logger.error(
|
|
231
|
+
{
|
|
232
|
+
listenerId: listener.listenerId,
|
|
233
|
+
event: eventName,
|
|
234
|
+
error: String(error?.message || error || "unknown error")
|
|
235
|
+
},
|
|
236
|
+
"Realtime client listener failed."
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (listener.event === "*") {
|
|
242
|
+
if (typeof socket.onAny === "function") {
|
|
243
|
+
const onAny = (eventName, payload) => invoke(eventName, payload);
|
|
244
|
+
socket.onAny(onAny);
|
|
245
|
+
detach.push(() => {
|
|
246
|
+
if (typeof socket.offAny === "function") {
|
|
247
|
+
socket.offAny(onAny);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof socket.on === "function") {
|
|
255
|
+
const onEvent = (payload) => invoke(listener.event, payload);
|
|
256
|
+
socket.on(listener.event, onEvent);
|
|
257
|
+
detach.push(() => {
|
|
258
|
+
if (typeof socket.off === "function") {
|
|
259
|
+
socket.off(listener.event, onEvent);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.socket = socket;
|
|
266
|
+
this.detach = detach;
|
|
267
|
+
|
|
268
|
+
if (!app.has(CLIENT_MODULE_VUE_APP_TOKEN)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const vueApp = app.make(CLIENT_MODULE_VUE_APP_TOKEN);
|
|
273
|
+
if (!vueApp || typeof vueApp.provide !== "function") {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
vueApp.provide(REALTIME_SOCKET_CLIENT_INJECTION_KEY, socket);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
shutdown(app) {
|
|
280
|
+
if (Array.isArray(this.detach)) {
|
|
281
|
+
for (const release of this.detach) {
|
|
282
|
+
if (typeof release === "function") {
|
|
283
|
+
try {
|
|
284
|
+
release();
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.detach = [];
|
|
290
|
+
|
|
291
|
+
if (!this.socket) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const runtimeApi =
|
|
296
|
+
app && typeof app.make === "function" ? app.make(REALTIME_RUNTIME_CLIENT_TOKEN) : REALTIME_RUNTIME_CLIENT_API;
|
|
297
|
+
runtimeApi.disconnectSocketIoClient(this.socket);
|
|
298
|
+
this.socket = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { RealtimeClientProvider };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref } from "vue";
|
|
2
|
+
import { EMPTY_REALTIME_SOCKET, useRealtimeSocket } from "../composables/useRealtimeEvent.js";
|
|
3
|
+
|
|
4
|
+
const ROOT_STYLE = Object.freeze({
|
|
5
|
+
alignItems: "center",
|
|
6
|
+
display: "inline-flex",
|
|
7
|
+
height: "32px",
|
|
8
|
+
justifyContent: "center",
|
|
9
|
+
width: "32px"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const DOT_STYLE = Object.freeze({
|
|
13
|
+
borderRadius: "9999px",
|
|
14
|
+
boxShadow: "0 0 0 2px rgba(255, 255, 255, 0.82)",
|
|
15
|
+
display: "block",
|
|
16
|
+
height: "10px",
|
|
17
|
+
width: "10px"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const SR_ONLY_STYLE = Object.freeze({
|
|
21
|
+
border: "0",
|
|
22
|
+
clip: "rect(0, 0, 0, 0)",
|
|
23
|
+
clipPath: "inset(50%)",
|
|
24
|
+
height: "1px",
|
|
25
|
+
margin: "-1px",
|
|
26
|
+
overflow: "hidden",
|
|
27
|
+
padding: "0",
|
|
28
|
+
position: "absolute",
|
|
29
|
+
whiteSpace: "nowrap",
|
|
30
|
+
width: "1px"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function resolveTooltipText({ realtimeAvailable, connected }) {
|
|
34
|
+
if (!realtimeAvailable) {
|
|
35
|
+
return "Realtime is unavailable. Live updates are disabled for this page.";
|
|
36
|
+
}
|
|
37
|
+
if (connected) {
|
|
38
|
+
return "Realtime is connected. Live updates are active.";
|
|
39
|
+
}
|
|
40
|
+
return "Realtime is disconnected. The client will keep trying to reconnect.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RealtimeConnectionIndicator = defineComponent({
|
|
44
|
+
name: "RealtimeConnectionIndicator",
|
|
45
|
+
setup() {
|
|
46
|
+
const socket = useRealtimeSocket({ required: false });
|
|
47
|
+
const connected = ref(Boolean(socket?.connected));
|
|
48
|
+
const realtimeAvailable = socket !== EMPTY_REALTIME_SOCKET;
|
|
49
|
+
let detach = null;
|
|
50
|
+
|
|
51
|
+
const tooltipText = computed(() => {
|
|
52
|
+
return resolveTooltipText({
|
|
53
|
+
realtimeAvailable,
|
|
54
|
+
connected: connected.value
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const statusLabel = computed(() => (connected.value ? "Realtime connected" : "Realtime disconnected"));
|
|
59
|
+
|
|
60
|
+
const dotStyle = computed(() => ({
|
|
61
|
+
...DOT_STYLE,
|
|
62
|
+
backgroundColor: connected.value ? "#22c55e" : "#ef4444"
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
function syncConnected(nextState) {
|
|
66
|
+
connected.value = Boolean(nextState);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function bindSocketStatusListeners() {
|
|
70
|
+
if (!realtimeAvailable || typeof socket.on !== "function") {
|
|
71
|
+
syncConnected(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
syncConnected(socket.connected);
|
|
76
|
+
|
|
77
|
+
const onConnect = () => syncConnected(true);
|
|
78
|
+
const onDisconnect = () => syncConnected(false);
|
|
79
|
+
const onConnectError = () => syncConnected(false);
|
|
80
|
+
|
|
81
|
+
socket.on("connect", onConnect);
|
|
82
|
+
socket.on("disconnect", onDisconnect);
|
|
83
|
+
socket.on("connect_error", onConnectError);
|
|
84
|
+
|
|
85
|
+
detach = () => {
|
|
86
|
+
if (typeof socket.off === "function") {
|
|
87
|
+
socket.off("connect", onConnect);
|
|
88
|
+
socket.off("disconnect", onDisconnect);
|
|
89
|
+
socket.off("connect_error", onConnectError);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
bindSocketStatusListeners();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
onBeforeUnmount(() => {
|
|
99
|
+
if (typeof detach === "function") {
|
|
100
|
+
detach();
|
|
101
|
+
}
|
|
102
|
+
detach = null;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return () =>
|
|
106
|
+
h(
|
|
107
|
+
"span",
|
|
108
|
+
{
|
|
109
|
+
style: ROOT_STYLE,
|
|
110
|
+
title: tooltipText.value,
|
|
111
|
+
"aria-label": statusLabel.value
|
|
112
|
+
},
|
|
113
|
+
[
|
|
114
|
+
h("span", { style: dotStyle.value, "aria-hidden": "true" }),
|
|
115
|
+
h("span", { style: SR_ONLY_STYLE }, statusLabel.value)
|
|
116
|
+
]
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export { RealtimeConnectionIndicator };
|
|
122
|
+
export default RealtimeConnectionIndicator;
|