@lowdefy/client 6.1.0 → 7.1.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/dist/Client.js +54 -17
- package/dist/Context.js +30 -12
- package/dist/HtmlOverlay.js +127 -0
- package/dist/IconProvider.js +24 -0
- package/dist/MountEvents.js +3 -3
- package/dist/adapters/createRouter.js +9 -3
- package/dist/adapters/url.js +1 -7
- package/dist/auth/createAuthMethods.js +1034 -41
- package/dist/block/Block.js +6 -3
- package/dist/block/CategorySwitch.js +6 -0
- package/dist/block/Container.js +3 -0
- package/dist/block/InputContainer.js +8 -2
- package/dist/block/List.js +3 -0
- package/dist/block/LoadingBlock.js +6 -12
- package/dist/block/LoadingContainer.js +6 -1
- package/dist/block/LoadingList.js +6 -1
- package/dist/block/loadingBlockMethods.js +26 -0
- package/dist/claimCallerReload.js +38 -0
- package/dist/createAntdIconConfig.js +91 -0
- package/dist/createCallAPI.js +6 -2
- package/dist/createCallRequest.js +6 -2
- package/dist/createHandleError.js +6 -4
- package/dist/createIcon.js +138 -36
- package/dist/createPageLifecycleManager.js +172 -0
- package/dist/formatIconTitle.js +26 -0
- package/dist/getActiveLocale.js +27 -0
- package/dist/getContextKey.js +29 -0
- package/dist/getMessageIcon.js +43 -0
- package/dist/hasOtherRoles.js +30 -0
- package/dist/index.js +1 -0
- package/dist/initLowdefyContext.js +103 -4
- package/dist/isOtherCaller.js +29 -0
- package/dist/recordCallerStamp.js +31 -0
- package/dist/renderIconNodes.js +46 -0
- package/dist/request.js +72 -5
- package/dist/setLowdefyUser.js +32 -0
- package/dist/setupLink.js +19 -13
- package/dist/shouldReloadForAuthError.js +39 -0
- package/dist/shouldReloadForBuild.js +44 -0
- package/dist/shouldReloadForCaller.js +50 -0
- package/dist/storage/clearUserStorage.js +36 -0
- package/dist/storage/clearUserStorageOnOwnerChange.js +41 -0
- package/dist/storage/createStorage.js +48 -0
- package/dist/storage/listUserStorageKeys.js +36 -0
- package/dist/storage/readStorageItem.js +31 -0
- package/dist/storage/userStorageKey.js +24 -0
- package/dist/storage/userStoragePrefix.js +25 -0
- package/dist/useDarkMode.js +17 -4
- package/dist/useLocale.js +18 -5
- package/dist/websocket/createWebSocketClient.js +234 -98
- package/package.json +11 -10
package/dist/useLocale.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
16
16
|
import dayjs from 'dayjs';
|
|
17
|
+
import antdEnUS from 'antd/locale/en_US.js';
|
|
18
|
+
import readStorageItem from './storage/readStorageItem.js';
|
|
19
|
+
// antd shows its en_US strings until an app locale loads, and with none configured.
|
|
20
|
+
const defaultAntdLocale = antdEnUS.default ?? antdEnUS;
|
|
17
21
|
function pickBest(candidates, supported) {
|
|
18
22
|
for (const candidate of candidates ?? []){
|
|
19
23
|
if (supported.includes(candidate)) return candidate;
|
|
@@ -40,12 +44,17 @@ async function loadAntdXLocale({ active, antdXLocaleLoaders }) {
|
|
|
40
44
|
const mod = await antdXLocaleLoaders[active]();
|
|
41
45
|
return mod?.default ?? mod;
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
// storageKey is the app key the preference is kept under (see @lowdefy/helpers appStorageKey).
|
|
48
|
+
function useLocale({ i18n, antdLocaleLoaders, antdXLocaleLoaders, dayjsLocaleMap, storageKey }) {
|
|
44
49
|
const supportedCodes = useMemo(()=>(i18n?.locales ?? []).map((l)=>l.code), [
|
|
45
50
|
i18n?.locales
|
|
46
51
|
]);
|
|
47
52
|
const [userPreference, setUserPreference] = useState(()=>{
|
|
48
|
-
return
|
|
53
|
+
return readStorageItem({
|
|
54
|
+
key: storageKey,
|
|
55
|
+
legacyKey: 'lowdefy_locale',
|
|
56
|
+
storage: window.localStorage
|
|
57
|
+
}) ?? 'auto';
|
|
49
58
|
});
|
|
50
59
|
const browserLocale = useMemo(()=>{
|
|
51
60
|
if (supportedCodes.length === 0) return undefined;
|
|
@@ -58,12 +67,14 @@ function useLocale({ i18n, antdLocaleLoaders, antdXLocaleLoaders, dayjsLocaleMap
|
|
|
58
67
|
]);
|
|
59
68
|
const setPreference = useCallback((code)=>{
|
|
60
69
|
if (code === 'auto') {
|
|
61
|
-
window.localStorage?.removeItem(
|
|
70
|
+
window.localStorage?.removeItem(storageKey);
|
|
62
71
|
} else {
|
|
63
|
-
window.localStorage?.setItem(
|
|
72
|
+
window.localStorage?.setItem(storageKey, code);
|
|
64
73
|
}
|
|
65
74
|
setUserPreference(code ?? 'auto');
|
|
66
|
-
}, [
|
|
75
|
+
}, [
|
|
76
|
+
storageKey
|
|
77
|
+
]);
|
|
67
78
|
window.__lowdefy_setLocale = setPreference;
|
|
68
79
|
const active = resolveActive({
|
|
69
80
|
userPreference,
|
|
@@ -75,6 +86,8 @@ function useLocale({ i18n, antdLocaleLoaders, antdXLocaleLoaders, dayjsLocaleMap
|
|
|
75
86
|
window.__lowdefy_supported_locales = i18n?.locales ?? [];
|
|
76
87
|
const [antdLocale, setAntdLocale] = useState(null);
|
|
77
88
|
const [antdXLocale, setAntdXLocale] = useState(null);
|
|
89
|
+
// The engine trace reads the antd strings in use as config text; it has no antd dependency.
|
|
90
|
+
window.__lowdefy_antd_locale = antdLocale ?? defaultAntdLocale;
|
|
78
91
|
useEffect(()=>{
|
|
79
92
|
let cancelled = false;
|
|
80
93
|
loadAntdLocale({
|
|
@@ -13,31 +13,52 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { decodeServerError } from '@lowdefy/engine';
|
|
16
|
+
import { ServiceError } from '@lowdefy/errors';
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
16
18
|
const ACK_TIMEOUT_MS = 10 * 1000;
|
|
17
19
|
const IDLE_CLOSE_GRACE_MS = 5 * 1000;
|
|
18
20
|
const RECONNECT_BASE_MS = 500;
|
|
19
21
|
const RECONNECT_CAP_MS = 15 * 1000;
|
|
22
|
+
// A connection that stays open this long resets the reconnect backoff. A
|
|
23
|
+
// server that accepts connections and drops them straight away keeps backing
|
|
24
|
+
// off instead of being retried every half second.
|
|
25
|
+
const STABLE_CONNECTION_MS = 10 * 1000;
|
|
26
|
+
// The server closes with this code when the socket's caller signed out or
|
|
27
|
+
// changed (user, organization or roles). The client reconnects straight away,
|
|
28
|
+
// which authenticates afresh, and resubscribes as the new caller.
|
|
29
|
+
const CALLER_CHANGED_CODE = 4401;
|
|
30
|
+
// A connection closed for a caller change again within this long of opening
|
|
31
|
+
// reconnects with the normal backoff, so a server that keeps closing for that
|
|
32
|
+
// reason does not get a tight reconnect loop.
|
|
33
|
+
const CALLER_CHANGE_REPEAT_MS = 30 * 1000;
|
|
20
34
|
// One websocket per browser tab, shared by all channels, created lazily on
|
|
21
35
|
// the first subscribe or publish. Connections are ephemeral in production
|
|
22
36
|
// (serverless platforms close them at function max duration), so reconnect
|
|
23
37
|
// with backoff and resubscribe is the steady state, not an edge case.
|
|
24
38
|
function createWebSocketClient(lowdefy) {
|
|
25
39
|
const { window } = lowdefy._internal.globals;
|
|
26
|
-
// websocketId → { payload, handlers }
|
|
40
|
+
// websocketId → { websocketId, payload, handlers, requestId, ackTimer, pending }
|
|
41
|
+
// requestId names the last subscribe frame sent, which the server echoes in
|
|
42
|
+
// its reply. ackTimer runs while that frame waits for its ack. pending holds
|
|
43
|
+
// the subscribe promise's { resolve, reject } until the first ack, then is
|
|
44
|
+
// null.
|
|
27
45
|
const subscriptions = new Map();
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
//
|
|
46
|
+
// requestId → { frame, sent, resolve, reject, timer } for publishes waiting
|
|
47
|
+
// for their ack. A publish made while disconnected is sent when the
|
|
48
|
+
// connection opens, unless it timed out first.
|
|
31
49
|
const pendingPublishes = new Map();
|
|
32
|
-
// Frames queued while the socket is (re)connecting.
|
|
33
|
-
const sendQueue = [];
|
|
34
50
|
let socket = null;
|
|
35
51
|
let openPromise = null;
|
|
36
52
|
let reconnectAttempt = 0;
|
|
37
53
|
let reconnectTimer = null;
|
|
38
54
|
let idleTimer = null;
|
|
39
|
-
let
|
|
55
|
+
let stableTimer = null;
|
|
56
|
+
// When the current connection opened, and whether it was opened by the
|
|
57
|
+
// reconnect that follows a caller change.
|
|
58
|
+
let openedAt = null;
|
|
59
|
+
let reconnectingAfterCallerChange = false;
|
|
40
60
|
let publishCounter = 0;
|
|
61
|
+
let subscribeCounter = 0;
|
|
41
62
|
function url() {
|
|
42
63
|
const { location } = window;
|
|
43
64
|
const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
@@ -49,43 +70,119 @@ function createWebSocketClient(lowdefy) {
|
|
|
49
70
|
idleTimer = null;
|
|
50
71
|
}
|
|
51
72
|
}
|
|
73
|
+
function isIdle() {
|
|
74
|
+
return subscriptions.size === 0 && pendingPublishes.size === 0;
|
|
75
|
+
}
|
|
52
76
|
function scheduleIdleClose() {
|
|
53
77
|
clearIdleTimer();
|
|
54
|
-
if (
|
|
78
|
+
if (!isIdle()) {
|
|
55
79
|
return;
|
|
56
80
|
}
|
|
57
81
|
// Grace period so rapid page navigation doesn't thrash connections.
|
|
58
82
|
idleTimer = setTimeout(()=>{
|
|
59
|
-
if (
|
|
60
|
-
|
|
83
|
+
if (isIdle() && socket) {
|
|
84
|
+
// The close event arrives after the closing handshake, when a new
|
|
85
|
+
// socket may already be connecting or open. This socket is finished
|
|
86
|
+
// with, so its events must not reach the client state that now
|
|
87
|
+
// belongs to the new socket.
|
|
88
|
+
socket.onclose = null;
|
|
89
|
+
socket.onmessage = null;
|
|
90
|
+
clearTimeout(stableTimer);
|
|
91
|
+
reconnectingAfterCallerChange = false;
|
|
61
92
|
socket.close();
|
|
62
93
|
socket = null;
|
|
63
94
|
openPromise = null;
|
|
64
95
|
}
|
|
65
96
|
}, IDLE_CLOSE_GRACE_MS);
|
|
66
97
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
function isSocketOpen() {
|
|
99
|
+
return socket !== null && socket.readyState === window.WebSocket.OPEN;
|
|
100
|
+
}
|
|
101
|
+
function sendPublish(pending) {
|
|
102
|
+
pending.sent = true;
|
|
103
|
+
socket.send(JSON.stringify(pending.frame));
|
|
104
|
+
}
|
|
105
|
+
function hasUnsentPublishes() {
|
|
106
|
+
return [
|
|
107
|
+
...pendingPublishes.values()
|
|
108
|
+
].some((pending)=>!pending.sent);
|
|
109
|
+
}
|
|
110
|
+
function sendUnsentPublishes() {
|
|
111
|
+
pendingPublishes.forEach((pending)=>{
|
|
112
|
+
if (!pending.sent) {
|
|
113
|
+
sendPublish(pending);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function clearAckTimer(subscription) {
|
|
118
|
+
clearTimeout(subscription.ackTimer);
|
|
119
|
+
subscription.ackTimer = null;
|
|
120
|
+
}
|
|
121
|
+
function handleAckTimeout(subscription) {
|
|
122
|
+
subscription.ackTimer = null;
|
|
123
|
+
const error = new ServiceError(`Subscribe to "${subscription.websocketId}" timed out.`, {
|
|
124
|
+
service: 'WebSocket'
|
|
125
|
+
});
|
|
126
|
+
if (subscription.pending) {
|
|
127
|
+
subscriptions.delete(subscription.websocketId);
|
|
128
|
+
// The server handles the frame late, or not yet, and would open the feed
|
|
129
|
+
// for a caller that has given up on it.
|
|
130
|
+
if (isSocketOpen()) {
|
|
131
|
+
socket.send(JSON.stringify({
|
|
132
|
+
type: 'unsubscribe',
|
|
133
|
+
websocketId: subscription.websocketId
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
scheduleIdleClose();
|
|
137
|
+
subscription.pending.reject(error);
|
|
71
138
|
return;
|
|
72
139
|
}
|
|
73
|
-
|
|
74
|
-
|
|
140
|
+
subscription.handlers.onError(error.message);
|
|
141
|
+
}
|
|
142
|
+
// Every path that removes a subscription from the map or answers its
|
|
143
|
+
// subscribe clears this timer, so a timer that fires always belongs to the
|
|
144
|
+
// subscription currently in the map.
|
|
145
|
+
function startAckTimer(subscription) {
|
|
146
|
+
clearAckTimer(subscription);
|
|
147
|
+
subscription.ackTimer = setTimeout(()=>handleAckTimeout(subscription), ACK_TIMEOUT_MS);
|
|
148
|
+
}
|
|
149
|
+
function sendSubscribe(subscription) {
|
|
150
|
+
const { payload, websocketId } = subscription;
|
|
151
|
+
subscribeCounter += 1;
|
|
152
|
+
subscription.requestId = `s${subscribeCounter}`;
|
|
153
|
+
startAckTimer(subscription);
|
|
154
|
+
socket.send(JSON.stringify({
|
|
155
|
+
type: 'subscribe',
|
|
156
|
+
websocketId,
|
|
157
|
+
payload,
|
|
158
|
+
requestId: subscription.requestId
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
// A reply to an earlier subscribe frame for the same feed, one that a newer
|
|
162
|
+
// subscribe or a resubscribe replaced, does not answer this subscription. A
|
|
163
|
+
// server that does not echo requestId answers by websocketId alone.
|
|
164
|
+
function answersSubscription(subscription, requestId) {
|
|
165
|
+
return type.isNone(requestId) || requestId === subscription.requestId;
|
|
166
|
+
}
|
|
167
|
+
function settlePublish(requestId) {
|
|
168
|
+
const pending = pendingPublishes.get(requestId);
|
|
169
|
+
clearTimeout(pending.timer);
|
|
170
|
+
pendingPublishes.delete(requestId);
|
|
171
|
+
scheduleIdleClose();
|
|
172
|
+
return pending;
|
|
75
173
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
174
|
+
// A caller that unsubscribed, or whose subscribe was replaced by a newer one
|
|
175
|
+
// to the same feed, no longer wants this subscription, so its subscribe
|
|
176
|
+
// promise settles without an error.
|
|
177
|
+
function releaseSubscription(subscription) {
|
|
178
|
+
clearAckTimer(subscription);
|
|
179
|
+
if (subscription.pending) {
|
|
180
|
+
subscription.pending.resolve();
|
|
181
|
+
subscription.pending = null;
|
|
79
182
|
}
|
|
80
183
|
}
|
|
81
184
|
function resubscribeAll() {
|
|
82
|
-
subscriptions.forEach(
|
|
83
|
-
socket.send(JSON.stringify({
|
|
84
|
-
type: 'subscribe',
|
|
85
|
-
websocketId,
|
|
86
|
-
payload
|
|
87
|
-
}));
|
|
88
|
-
});
|
|
185
|
+
subscriptions.forEach(sendSubscribe);
|
|
89
186
|
}
|
|
90
187
|
function handleFrame(frame) {
|
|
91
188
|
const { error: errorPayload, message, payload, requestId, websocketId } = frame;
|
|
@@ -95,70 +192,85 @@ function createWebSocketClient(lowdefy) {
|
|
|
95
192
|
subscription?.handlers.onMessage(payload);
|
|
96
193
|
return;
|
|
97
194
|
case 'subscribed':
|
|
98
|
-
{
|
|
99
|
-
const pending = pendingSubscribes.get(websocketId);
|
|
100
|
-
if (pending) {
|
|
101
|
-
clearTimeout(pending.timer);
|
|
102
|
-
pendingSubscribes.delete(websocketId);
|
|
103
|
-
pending.resolve();
|
|
104
|
-
}
|
|
105
|
-
subscription?.handlers.onConnected();
|
|
195
|
+
if (!subscription || !answersSubscription(subscription, requestId)) {
|
|
106
196
|
return;
|
|
107
197
|
}
|
|
198
|
+
clearAckTimer(subscription);
|
|
199
|
+
if (subscription.pending) {
|
|
200
|
+
subscription.pending.resolve();
|
|
201
|
+
subscription.pending = null;
|
|
202
|
+
}
|
|
203
|
+
subscription.handlers.onConnected();
|
|
204
|
+
return;
|
|
108
205
|
case 'unsubscribed':
|
|
109
206
|
return;
|
|
110
207
|
case 'published':
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
if (pending) {
|
|
114
|
-
clearTimeout(pending.timer);
|
|
115
|
-
pendingPublishes.delete(requestId);
|
|
116
|
-
pending.resolve();
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
208
|
+
if (pendingPublishes.has(requestId)) {
|
|
209
|
+
settlePublish(requestId).resolve();
|
|
119
210
|
}
|
|
211
|
+
return;
|
|
120
212
|
case 'error':
|
|
121
213
|
{
|
|
122
214
|
// Reply frames carry the error payload; broadcasts and frame-format
|
|
123
215
|
// errors carry only a message string.
|
|
124
|
-
const error = errorPayload ? decodeServerError(errorPayload) : new
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
pending.reject(error);
|
|
216
|
+
const error = errorPayload ? decodeServerError(errorPayload) : new ServiceError(message ?? 'WebSocket error.', {
|
|
217
|
+
service: 'WebSocket'
|
|
218
|
+
});
|
|
219
|
+
if (!type.isNone(requestId) && pendingPublishes.has(requestId)) {
|
|
220
|
+
settlePublish(requestId).reject(error);
|
|
130
221
|
return;
|
|
131
222
|
}
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
clearTimeout(pending.timer);
|
|
135
|
-
pendingSubscribes.delete(websocketId);
|
|
136
|
-
subscriptions.delete(websocketId);
|
|
137
|
-
pending.reject(error);
|
|
223
|
+
if (!subscription) {
|
|
224
|
+
lowdefy._internal.logger.warn(error);
|
|
138
225
|
return;
|
|
139
226
|
}
|
|
140
|
-
|
|
227
|
+
// A broadcast from the feed's source carries no requestId.
|
|
228
|
+
if (type.isNone(requestId)) {
|
|
141
229
|
subscription.handlers.onError(error.message);
|
|
142
230
|
return;
|
|
143
231
|
}
|
|
144
|
-
|
|
232
|
+
if (requestId !== subscription.requestId) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
clearAckTimer(subscription);
|
|
236
|
+
if (subscription.pending) {
|
|
237
|
+
subscriptions.delete(websocketId);
|
|
238
|
+
scheduleIdleClose();
|
|
239
|
+
subscription.pending.reject(error);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
subscription.handlers.onError(error.message);
|
|
145
243
|
return;
|
|
146
244
|
}
|
|
147
245
|
default:
|
|
148
246
|
}
|
|
149
247
|
}
|
|
150
|
-
function handleClose() {
|
|
248
|
+
function handleClose(event) {
|
|
151
249
|
socket = null;
|
|
152
250
|
openPromise = null;
|
|
153
|
-
|
|
154
|
-
|
|
251
|
+
clearTimeout(stableTimer);
|
|
252
|
+
subscriptions.forEach((subscription)=>{
|
|
253
|
+
// A resubscribe lost with the connection is sent again, with a new ack
|
|
254
|
+
// timer, when the connection reopens. A subscribe whose caller still
|
|
255
|
+
// waits keeps its timer, so it fails if no connection opens in time.
|
|
256
|
+
if (!subscription.pending) {
|
|
257
|
+
clearAckTimer(subscription);
|
|
258
|
+
}
|
|
259
|
+
subscription.handlers.onDisconnected();
|
|
155
260
|
});
|
|
156
|
-
if (
|
|
157
|
-
closedByClient = false;
|
|
261
|
+
if (subscriptions.size === 0 && !hasUnsentPublishes()) {
|
|
158
262
|
return;
|
|
159
263
|
}
|
|
160
|
-
|
|
161
|
-
|
|
264
|
+
// A caller change is not a failing server, so the first reconnect after
|
|
265
|
+
// one does not back off.
|
|
266
|
+
if (event.code === CALLER_CHANGED_CODE) {
|
|
267
|
+
const repeated = reconnectingAfterCallerChange && Date.now() - openedAt < CALLER_CHANGE_REPEAT_MS;
|
|
268
|
+
if (!repeated) {
|
|
269
|
+
reconnectAttempt = 0;
|
|
270
|
+
}
|
|
271
|
+
reconnectingAfterCallerChange = true;
|
|
272
|
+
} else {
|
|
273
|
+
reconnectingAfterCallerChange = false;
|
|
162
274
|
}
|
|
163
275
|
// Capped exponential backoff with full jitter.
|
|
164
276
|
const base = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempt, RECONNECT_CAP_MS);
|
|
@@ -178,9 +290,15 @@ function createWebSocketClient(lowdefy) {
|
|
|
178
290
|
const ws = new window.WebSocket(url());
|
|
179
291
|
ws.onopen = ()=>{
|
|
180
292
|
socket = ws;
|
|
181
|
-
|
|
293
|
+
openedAt = Date.now();
|
|
294
|
+
stableTimer = setTimeout(()=>{
|
|
295
|
+
reconnectAttempt = 0;
|
|
296
|
+
}, STABLE_CONNECTION_MS);
|
|
182
297
|
resubscribeAll();
|
|
183
|
-
|
|
298
|
+
sendUnsentPublishes();
|
|
299
|
+
// Everything that asked for this connection may have finished while
|
|
300
|
+
// it was opening.
|
|
301
|
+
scheduleIdleClose();
|
|
184
302
|
resolve();
|
|
185
303
|
};
|
|
186
304
|
ws.onmessage = (event)=>{
|
|
@@ -192,13 +310,15 @@ function createWebSocketClient(lowdefy) {
|
|
|
192
310
|
}
|
|
193
311
|
handleFrame(frame);
|
|
194
312
|
};
|
|
195
|
-
ws.onclose = ()=>{
|
|
313
|
+
ws.onclose = (event)=>{
|
|
196
314
|
if (socket !== ws) {
|
|
197
315
|
// Never opened — treat as a failed connect and retry.
|
|
198
316
|
openPromise = null;
|
|
199
|
-
reject(new
|
|
317
|
+
reject(new ServiceError('Connection failed.', {
|
|
318
|
+
service: 'WebSocket'
|
|
319
|
+
}));
|
|
200
320
|
}
|
|
201
|
-
handleClose();
|
|
321
|
+
handleClose(event);
|
|
202
322
|
};
|
|
203
323
|
ws.onerror = ()=>{
|
|
204
324
|
// onclose always follows onerror — reconnect is handled there.
|
|
@@ -210,34 +330,41 @@ function createWebSocketClient(lowdefy) {
|
|
|
210
330
|
}
|
|
211
331
|
function subscribe({ handlers, payload, websocketId }) {
|
|
212
332
|
clearIdleTimer();
|
|
213
|
-
subscriptions.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
333
|
+
const previous = subscriptions.get(websocketId);
|
|
334
|
+
if (previous) {
|
|
335
|
+
releaseSubscription(previous);
|
|
336
|
+
}
|
|
217
337
|
return new Promise((resolve, reject)=>{
|
|
218
|
-
const
|
|
219
|
-
pendingSubscribes.delete(websocketId);
|
|
220
|
-
subscriptions.delete(websocketId);
|
|
221
|
-
reject(new Error(`Subscribe to "${websocketId}" timed out.`));
|
|
222
|
-
}, ACK_TIMEOUT_MS);
|
|
223
|
-
pendingSubscribes.set(websocketId, {
|
|
224
|
-
resolve,
|
|
225
|
-
reject,
|
|
226
|
-
timer
|
|
227
|
-
});
|
|
228
|
-
send({
|
|
229
|
-
type: 'subscribe',
|
|
338
|
+
const subscription = {
|
|
230
339
|
websocketId,
|
|
231
|
-
payload
|
|
232
|
-
|
|
340
|
+
payload,
|
|
341
|
+
handlers,
|
|
342
|
+
ackTimer: null,
|
|
343
|
+
pending: {
|
|
344
|
+
resolve,
|
|
345
|
+
reject
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
subscriptions.set(websocketId, subscription);
|
|
349
|
+
if (isSocketOpen()) {
|
|
350
|
+
sendSubscribe(subscription);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// The frame is sent by resubscribeAll when the connection opens, which
|
|
354
|
+
// restarts the ack timer. Until then the timer bounds the wait for a
|
|
355
|
+
// connection.
|
|
356
|
+
startAckTimer(subscription);
|
|
357
|
+
connect();
|
|
233
358
|
});
|
|
234
359
|
}
|
|
235
360
|
function unsubscribe({ websocketId }) {
|
|
236
|
-
|
|
361
|
+
const subscription = subscriptions.get(websocketId);
|
|
362
|
+
if (!subscription) {
|
|
237
363
|
return;
|
|
238
364
|
}
|
|
365
|
+
releaseSubscription(subscription);
|
|
239
366
|
subscriptions.delete(websocketId);
|
|
240
|
-
if (
|
|
367
|
+
if (isSocketOpen()) {
|
|
241
368
|
socket.send(JSON.stringify({
|
|
242
369
|
type: 'unsubscribe',
|
|
243
370
|
websocketId
|
|
@@ -246,24 +373,33 @@ function createWebSocketClient(lowdefy) {
|
|
|
246
373
|
scheduleIdleClose();
|
|
247
374
|
}
|
|
248
375
|
function publish({ payload, websocketId }) {
|
|
376
|
+
clearIdleTimer();
|
|
249
377
|
publishCounter += 1;
|
|
250
378
|
const requestId = `p${publishCounter}`;
|
|
251
379
|
return new Promise((resolve, reject)=>{
|
|
252
380
|
const timer = setTimeout(()=>{
|
|
253
|
-
|
|
254
|
-
|
|
381
|
+
settlePublish(requestId).reject(new ServiceError(`Publish to "${websocketId}" timed out.`, {
|
|
382
|
+
service: 'WebSocket'
|
|
383
|
+
}));
|
|
255
384
|
}, ACK_TIMEOUT_MS);
|
|
256
|
-
|
|
385
|
+
const pending = {
|
|
386
|
+
frame: {
|
|
387
|
+
type: 'publish',
|
|
388
|
+
websocketId,
|
|
389
|
+
requestId,
|
|
390
|
+
payload
|
|
391
|
+
},
|
|
392
|
+
sent: false,
|
|
257
393
|
resolve,
|
|
258
394
|
reject,
|
|
259
395
|
timer
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
396
|
+
};
|
|
397
|
+
pendingPublishes.set(requestId, pending);
|
|
398
|
+
if (isSocketOpen()) {
|
|
399
|
+
sendPublish(pending);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
connect();
|
|
267
403
|
});
|
|
268
404
|
}
|
|
269
405
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy Client",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -33,22 +33,23 @@
|
|
|
33
33
|
"dist/*"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ant-design/icons": "6.
|
|
37
|
-
"
|
|
38
|
-
"@lowdefy/
|
|
39
|
-
"@lowdefy/
|
|
40
|
-
"@lowdefy/
|
|
41
|
-
"@lowdefy/
|
|
42
|
-
"@lowdefy/
|
|
43
|
-
"
|
|
36
|
+
"@ant-design/icons": "6.3.4",
|
|
37
|
+
"@lowdefy/block-utils": "7.1.0",
|
|
38
|
+
"@lowdefy/engine": "7.1.0",
|
|
39
|
+
"@lowdefy/errors": "7.1.0",
|
|
40
|
+
"@lowdefy/helpers": "7.1.0",
|
|
41
|
+
"@lowdefy/layout": "7.1.0",
|
|
42
|
+
"@lowdefy/logger": "7.1.0",
|
|
43
|
+
"antd": "6.6.5",
|
|
44
44
|
"dayjs": "1.11.20",
|
|
45
|
+
"lucide-react": "1.48.0",
|
|
45
46
|
"react": "18.2.0",
|
|
46
47
|
"react-dom": "18.2.0",
|
|
47
48
|
"tinykeys": "^3.0.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@jest/globals": "28.1.3",
|
|
51
|
-
"@lowdefy/jest-yaml-transform": "
|
|
52
|
+
"@lowdefy/jest-yaml-transform": "7.1.0",
|
|
52
53
|
"@swc/cli": "0.8.1",
|
|
53
54
|
"@swc/core": "1.15.32",
|
|
54
55
|
"@swc/jest": "0.2.39",
|