@luckystack/core 0.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/CHANGELOG.md +14 -0
- package/CLAUDE.md +210 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/apiTypeStubs.d.ts +7 -0
- package/dist/apiTypeStubs.js +1 -0
- package/dist/apiTypeStubs.js.map +1 -0
- package/dist/client.d.ts +236 -0
- package/dist/client.js +1344 -0
- package/dist/client.js.map +1 -0
- package/dist/eslint/index.d.ts +10 -0
- package/dist/eslint/index.js +436 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/index.d.ts +1128 -0
- package/dist/index.js +2619 -0
- package/dist/index.js.map +1 -0
- package/dist/pageRouteValidation-CxZJVwZh.d.ts +665 -0
- package/docs/app-bootstrap.md +419 -0
- package/docs/config-registry.md +350 -0
- package/docs/csrf-config.md +88 -0
- package/docs/error-tracker-registry.md +174 -0
- package/docs/hooks.md +298 -0
- package/docs/rate-limit-strategy.md +170 -0
- package/docs/redis-adapter.md +111 -0
- package/docs/session-types.md +180 -0
- package/docs/socket-bootstrap.md +366 -0
- package/package.json +91 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
// src/projectConfig.ts
|
|
2
|
+
var DEFAULT_PROJECT_CONFIG = {
|
|
3
|
+
app: {
|
|
4
|
+
publicUrl: ""
|
|
5
|
+
},
|
|
6
|
+
logging: {
|
|
7
|
+
devLogs: false,
|
|
8
|
+
devNotifications: false,
|
|
9
|
+
socketStatus: false,
|
|
10
|
+
socketStartup: false,
|
|
11
|
+
stream: false
|
|
12
|
+
},
|
|
13
|
+
rateLimiting: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
store: "memory",
|
|
16
|
+
redisKeyPrefix: "rate-limit",
|
|
17
|
+
defaultApiLimit: 60,
|
|
18
|
+
defaultIpLimit: 100,
|
|
19
|
+
windowMs: 6e4,
|
|
20
|
+
cleanupIntervalMs: 6e4
|
|
21
|
+
},
|
|
22
|
+
session: {
|
|
23
|
+
basedToken: false,
|
|
24
|
+
expiryDays: 7,
|
|
25
|
+
allowMultiple: false,
|
|
26
|
+
perBrowser: "single",
|
|
27
|
+
perUser: "single",
|
|
28
|
+
maxConcurrentPerUser: null,
|
|
29
|
+
onConflict: "revokeOld",
|
|
30
|
+
notifyOldDeviceOnRevoke: true,
|
|
31
|
+
//? Empty default — `getProjectName()` reads `process.env.PROJECT_NAME` at
|
|
32
|
+
//? call time and falls back to `'luckystack'`. Avoids capturing env at
|
|
33
|
+
//? module-load before dotenv runs.
|
|
34
|
+
projectName: ""
|
|
35
|
+
},
|
|
36
|
+
http: {
|
|
37
|
+
sessionCookieName: "token",
|
|
38
|
+
sessionCookieSameSite: "Strict",
|
|
39
|
+
sessionCookiePath: "/",
|
|
40
|
+
requestBodyMaxBytes: 1024 * 1024,
|
|
41
|
+
healthEndpoint: "/_health",
|
|
42
|
+
liveEndpoint: "/livez",
|
|
43
|
+
readyEndpoint: "/readyz",
|
|
44
|
+
testResetEndpoint: "/_test/reset",
|
|
45
|
+
stream: {
|
|
46
|
+
queryParam: "stream",
|
|
47
|
+
enabledValue: "true",
|
|
48
|
+
connectedComment: ": connected"
|
|
49
|
+
},
|
|
50
|
+
securityHeaders: {
|
|
51
|
+
frameOptions: "SAMEORIGIN",
|
|
52
|
+
referrerPolicy: "no-referrer",
|
|
53
|
+
xssProtection: "1; mode=block",
|
|
54
|
+
contentTypeOptions: "nosniff"
|
|
55
|
+
},
|
|
56
|
+
cors: {
|
|
57
|
+
allowedMethods: "GET, POST, PUT, DELETE, OPTIONS",
|
|
58
|
+
allowedHeaders: "Content-Type, Authorization, X-Session-Based-Token, X-CSRF-Token, X-Request-Id",
|
|
59
|
+
exposedHeaders: "X-Session-Token, X-Request-Id",
|
|
60
|
+
credentials: true,
|
|
61
|
+
allowedOrigins: [],
|
|
62
|
+
allowLocalhost: false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
auth: {
|
|
66
|
+
oauthStateTtlSeconds: 60 * 10,
|
|
67
|
+
passwordMinLength: 8,
|
|
68
|
+
passwordMaxLength: 191,
|
|
69
|
+
passwordPolicy: {
|
|
70
|
+
minLength: 8,
|
|
71
|
+
maxLength: 191,
|
|
72
|
+
requireUppercase: false,
|
|
73
|
+
requireLowercase: false,
|
|
74
|
+
requireNumber: false,
|
|
75
|
+
requireSpecial: false,
|
|
76
|
+
forbidCommon: true
|
|
77
|
+
},
|
|
78
|
+
emailMaxLength: 191,
|
|
79
|
+
nameMaxLength: 191,
|
|
80
|
+
bcryptRounds: 10,
|
|
81
|
+
providerAccountStrategy: "per-provider",
|
|
82
|
+
forgotPassword: "disabled",
|
|
83
|
+
passwordResetTtlSeconds: 60 * 60,
|
|
84
|
+
emailChangeTtlSeconds: 60 * 60
|
|
85
|
+
},
|
|
86
|
+
socket: {
|
|
87
|
+
maxHttpBufferSize: 5 * 1024 * 1024,
|
|
88
|
+
pingTimeout: 2e4,
|
|
89
|
+
pingInterval: 25e3
|
|
90
|
+
},
|
|
91
|
+
sync: {
|
|
92
|
+
streamThrottle: {
|
|
93
|
+
flushAtChars: 32,
|
|
94
|
+
flushEveryMs: 50,
|
|
95
|
+
field: "chunk"
|
|
96
|
+
},
|
|
97
|
+
fanoutYieldEvery: 100,
|
|
98
|
+
fanoutYieldMs: 1
|
|
99
|
+
},
|
|
100
|
+
offlineQueue: {
|
|
101
|
+
maxSize: 200,
|
|
102
|
+
maxAgeMs: 60 * 60 * 1e3,
|
|
103
|
+
dropPolicy: "drop-oldest"
|
|
104
|
+
},
|
|
105
|
+
dev: {
|
|
106
|
+
hotReloadDebounceMs: 120,
|
|
107
|
+
watcherStabilityThresholdMs: 120,
|
|
108
|
+
watcherPollIntervalMs: 20
|
|
109
|
+
},
|
|
110
|
+
paths: {
|
|
111
|
+
srcDir: "src",
|
|
112
|
+
serverDir: "server",
|
|
113
|
+
sharedDir: "shared",
|
|
114
|
+
uploadsDir: "uploads",
|
|
115
|
+
publicDir: "public",
|
|
116
|
+
serverFunctionsDir: "server/functions",
|
|
117
|
+
serverFunctionDirs: ["functions", "shared"],
|
|
118
|
+
generatedSocketTypes: "src/_sockets/apiTypes.generated.ts",
|
|
119
|
+
generatedApiSchemas: "src/_sockets/apiInputSchemas.generated.ts",
|
|
120
|
+
generatedApiDocs: "src/docs/apiDocs.generated.json"
|
|
121
|
+
},
|
|
122
|
+
defaultLanguage: "en",
|
|
123
|
+
defaultTheme: "light",
|
|
124
|
+
socketActivityBroadcaster: false,
|
|
125
|
+
socketStatusIndicator: false,
|
|
126
|
+
locationProviderEnabled: false,
|
|
127
|
+
loginRedirectUrl: "/"
|
|
128
|
+
};
|
|
129
|
+
var isPlainObject = (value) => {
|
|
130
|
+
if (value === null || typeof value !== "object") return false;
|
|
131
|
+
const proto = Object.getPrototypeOf(value);
|
|
132
|
+
return proto === Object.prototype || proto === null;
|
|
133
|
+
};
|
|
134
|
+
var deepMerge = (base, override) => {
|
|
135
|
+
if (override === void 0) return base;
|
|
136
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
137
|
+
return override ?? base;
|
|
138
|
+
}
|
|
139
|
+
const result = { ...base };
|
|
140
|
+
for (const [key, overrideValue] of Object.entries(override)) {
|
|
141
|
+
if (overrideValue === void 0) continue;
|
|
142
|
+
const baseValue = base[key];
|
|
143
|
+
result[key] = isPlainObject(baseValue) && isPlainObject(overrideValue) ? deepMerge(baseValue, overrideValue) : overrideValue;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
};
|
|
147
|
+
var activeConfig = DEFAULT_PROJECT_CONFIG;
|
|
148
|
+
var isRegistered = false;
|
|
149
|
+
var registerProjectConfig = (config) => {
|
|
150
|
+
activeConfig = deepMerge(DEFAULT_PROJECT_CONFIG, config);
|
|
151
|
+
isRegistered = true;
|
|
152
|
+
};
|
|
153
|
+
var getProjectConfig = () => activeConfig;
|
|
154
|
+
var isProjectConfigRegistered = () => isRegistered;
|
|
155
|
+
|
|
156
|
+
// src/loggerRegistry.ts
|
|
157
|
+
var defaultLogger = {
|
|
158
|
+
debug: (message, context) => {
|
|
159
|
+
if (context === void 0) {
|
|
160
|
+
console.debug(message);
|
|
161
|
+
} else {
|
|
162
|
+
console.debug(message, context);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
info: (message, context) => {
|
|
166
|
+
if (context === void 0) {
|
|
167
|
+
console.info(message);
|
|
168
|
+
} else {
|
|
169
|
+
console.info(message, context);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
warn: (message, context) => {
|
|
173
|
+
if (context === void 0) {
|
|
174
|
+
console.warn(message);
|
|
175
|
+
} else {
|
|
176
|
+
console.warn(message, context);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
error: (message, error, context) => {
|
|
180
|
+
if (error !== void 0 && context !== void 0) console.error(message, error, context);
|
|
181
|
+
else if (error !== void 0) console.error(message, error);
|
|
182
|
+
else if (context === void 0) {
|
|
183
|
+
console.error(message);
|
|
184
|
+
} else {
|
|
185
|
+
console.error(message, context);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var activeLogger = defaultLogger;
|
|
190
|
+
var isRegistered2 = false;
|
|
191
|
+
var registerLogger = (logger) => {
|
|
192
|
+
activeLogger = logger;
|
|
193
|
+
isRegistered2 = true;
|
|
194
|
+
};
|
|
195
|
+
var getLogger = () => activeLogger;
|
|
196
|
+
|
|
197
|
+
// src/apiMethodMapRegistry.ts
|
|
198
|
+
var activeMap = null;
|
|
199
|
+
var registerApiMethodMap = (map) => {
|
|
200
|
+
activeMap = map;
|
|
201
|
+
};
|
|
202
|
+
var getRegisteredApiMethod = (pagePath, apiName, version) => {
|
|
203
|
+
if (!activeMap) return void 0;
|
|
204
|
+
return activeMap[pagePath]?.[apiName]?.[version];
|
|
205
|
+
};
|
|
206
|
+
var isApiMethodMapRegistered = () => activeMap !== null;
|
|
207
|
+
|
|
208
|
+
// src/socketState.ts
|
|
209
|
+
var socket = null;
|
|
210
|
+
var setSocket = (next) => {
|
|
211
|
+
socket = next;
|
|
212
|
+
};
|
|
213
|
+
var responseIndex = 0;
|
|
214
|
+
var WAIT_FOR_SOCKET_INTERVAL_MS = 10;
|
|
215
|
+
var WAIT_FOR_SOCKET_MAX_ITERATIONS = 500;
|
|
216
|
+
var incrementResponseIndex = () => {
|
|
217
|
+
responseIndex = responseIndex + 1;
|
|
218
|
+
return responseIndex;
|
|
219
|
+
};
|
|
220
|
+
var waitForSocket = async () => {
|
|
221
|
+
let i = 0;
|
|
222
|
+
while (!socket) {
|
|
223
|
+
await new Promise((resolve2) => setTimeout(resolve2, WAIT_FOR_SOCKET_INTERVAL_MS));
|
|
224
|
+
i++;
|
|
225
|
+
if (i > WAIT_FOR_SOCKET_MAX_ITERATIONS) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return socket;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/notifier.ts
|
|
233
|
+
var noopNotifier = {
|
|
234
|
+
success: () => {
|
|
235
|
+
},
|
|
236
|
+
error: () => {
|
|
237
|
+
},
|
|
238
|
+
info: () => {
|
|
239
|
+
},
|
|
240
|
+
warning: () => {
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var activeNotifier = noopNotifier;
|
|
244
|
+
var registerNotifier = (notifier) => {
|
|
245
|
+
activeNotifier = notifier;
|
|
246
|
+
};
|
|
247
|
+
var getNotifier = () => activeNotifier;
|
|
248
|
+
var notify = {
|
|
249
|
+
success: (input) => {
|
|
250
|
+
activeNotifier.success(input);
|
|
251
|
+
},
|
|
252
|
+
error: (input) => {
|
|
253
|
+
activeNotifier.error(input);
|
|
254
|
+
},
|
|
255
|
+
info: (input) => {
|
|
256
|
+
activeNotifier.info(input);
|
|
257
|
+
},
|
|
258
|
+
warning: (input) => {
|
|
259
|
+
activeNotifier.warning(input);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/offlineQueue.ts
|
|
264
|
+
var apiQueue = [];
|
|
265
|
+
var syncQueue = [];
|
|
266
|
+
var isFlushingApi = false;
|
|
267
|
+
var isFlushingSync = false;
|
|
268
|
+
var isOnline = () => {
|
|
269
|
+
if (typeof navigator === "undefined") return true;
|
|
270
|
+
return navigator.onLine;
|
|
271
|
+
};
|
|
272
|
+
var evictExpired = (queue) => {
|
|
273
|
+
const maxAgeMs = getProjectConfig().offlineQueue.maxAgeMs;
|
|
274
|
+
if (maxAgeMs <= 0) return;
|
|
275
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
276
|
+
for (let i = queue.length - 1; i >= 0; i -= 1) {
|
|
277
|
+
const item = queue[i];
|
|
278
|
+
if (item && item.createdAt < cutoff) {
|
|
279
|
+
queue.splice(i, 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
var enqueueWithPolicy = (queue, item, label) => {
|
|
284
|
+
evictExpired(queue);
|
|
285
|
+
const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;
|
|
286
|
+
const effectivePolicy = item.dropPolicy ?? globalPolicy;
|
|
287
|
+
if (queue.length < maxSize) {
|
|
288
|
+
queue.push(item);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (effectivePolicy === "drop-oldest") {
|
|
292
|
+
queue.shift();
|
|
293
|
+
queue.push(item);
|
|
294
|
+
getLogger().debug(`offlineQueue:${label} full \u2014 dropped oldest item (policy=${effectivePolicy})`);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (effectivePolicy === "drop-newest") {
|
|
298
|
+
getLogger().debug(`offlineQueue:${label} full \u2014 dropped newest item (policy=${effectivePolicy})`);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
getLogger().warn(`offlineQueue:${label} full \u2014 rejecting enqueue (policy=${effectivePolicy})`);
|
|
302
|
+
return false;
|
|
303
|
+
};
|
|
304
|
+
var enqueueApiRequest = (item) => enqueueWithPolicy(apiQueue, item, "api");
|
|
305
|
+
var enqueueSyncRequest = (item) => enqueueWithPolicy(syncQueue, item, "sync");
|
|
306
|
+
var removeApiQueueItem = (id) => {
|
|
307
|
+
const index = apiQueue.findIndex((item) => item.id === id);
|
|
308
|
+
if (index !== -1) {
|
|
309
|
+
apiQueue.splice(index, 1);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var removeSyncQueueItem = (id) => {
|
|
313
|
+
const index = syncQueue.findIndex((item) => item.id === id);
|
|
314
|
+
if (index !== -1) {
|
|
315
|
+
syncQueue.splice(index, 1);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var removeApiQueueItemsByKey = (key) => {
|
|
319
|
+
for (let i = apiQueue.length - 1; i >= 0; i -= 1) {
|
|
320
|
+
if (apiQueue[i]?.key === key) {
|
|
321
|
+
apiQueue.splice(i, 1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var flushApiQueue = (canRun, socketInstance) => {
|
|
326
|
+
if (isFlushingApi) return;
|
|
327
|
+
evictExpired(apiQueue);
|
|
328
|
+
if (apiQueue.length === 0) return;
|
|
329
|
+
if (!canRun()) return;
|
|
330
|
+
isFlushingApi = true;
|
|
331
|
+
while (apiQueue.length > 0) {
|
|
332
|
+
if (!canRun()) break;
|
|
333
|
+
const item = apiQueue.shift();
|
|
334
|
+
item?.run(socketInstance);
|
|
335
|
+
}
|
|
336
|
+
isFlushingApi = false;
|
|
337
|
+
};
|
|
338
|
+
var flushSyncQueue = (canRun, socketInstance) => {
|
|
339
|
+
if (isFlushingSync) return;
|
|
340
|
+
evictExpired(syncQueue);
|
|
341
|
+
if (syncQueue.length === 0) return;
|
|
342
|
+
if (!canRun()) return;
|
|
343
|
+
isFlushingSync = true;
|
|
344
|
+
while (syncQueue.length > 0) {
|
|
345
|
+
if (!canRun()) break;
|
|
346
|
+
const item = syncQueue.shift();
|
|
347
|
+
item?.run(socketInstance);
|
|
348
|
+
}
|
|
349
|
+
isFlushingSync = false;
|
|
350
|
+
};
|
|
351
|
+
var getApiQueueSize = () => apiQueue.length;
|
|
352
|
+
var getSyncQueueSize = () => syncQueue.length;
|
|
353
|
+
|
|
354
|
+
// src/responseNormalizer.ts
|
|
355
|
+
var INVALID_ERROR_RESPONSE_CODE = "error.invalidResponse";
|
|
356
|
+
var isErrorParamArray = (value) => {
|
|
357
|
+
return Array.isArray(value) && value.every((item) => {
|
|
358
|
+
return !!item && typeof item === "object" && typeof item.key === "string" && ["string", "number", "boolean"].includes(typeof item.value);
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
var normalizeErrorResponseCore = ({
|
|
362
|
+
response,
|
|
363
|
+
fallbackHttpStatus,
|
|
364
|
+
fallbackErrorCode,
|
|
365
|
+
resolveMessage
|
|
366
|
+
}) => {
|
|
367
|
+
const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : void 0;
|
|
368
|
+
const errorCodeValue = typeof response.errorCode === "string" ? response.errorCode.trim() : "";
|
|
369
|
+
const hasErrorCode = errorCodeValue.length > 0;
|
|
370
|
+
const finalErrorCode = hasErrorCode ? errorCodeValue : fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE;
|
|
371
|
+
const providedHttpStatus = typeof response.httpStatus === "number" ? response.httpStatus : void 0;
|
|
372
|
+
const message = resolveMessage ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams }) : finalErrorCode;
|
|
373
|
+
return {
|
|
374
|
+
status: "error",
|
|
375
|
+
message,
|
|
376
|
+
errorCode: finalErrorCode,
|
|
377
|
+
errorParams: normalizedParams,
|
|
378
|
+
httpStatus: providedHttpStatus ?? fallbackHttpStatus
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// src/serviceRoute.ts
|
|
383
|
+
var normalizeRawRoute = (value) => {
|
|
384
|
+
return value.trim().replaceAll("\\", "/").replaceAll(/^\/+/g, "").replaceAll(/\/+$/g, "");
|
|
385
|
+
};
|
|
386
|
+
var hasEmptySegments = (value) => {
|
|
387
|
+
return value.split("/").some((segment) => segment.length === 0);
|
|
388
|
+
};
|
|
389
|
+
var parseServiceRouteName = (value) => {
|
|
390
|
+
const normalized = normalizeRawRoute(value);
|
|
391
|
+
if (!normalized) {
|
|
392
|
+
return { status: "error", reason: "Route name cannot be empty." };
|
|
393
|
+
}
|
|
394
|
+
if (hasEmptySegments(normalized)) {
|
|
395
|
+
return { status: "error", reason: "Route name cannot contain empty segments." };
|
|
396
|
+
}
|
|
397
|
+
const segments = normalized.split("/");
|
|
398
|
+
if (segments.length < 2) {
|
|
399
|
+
return { status: "error", reason: "Route name must include service and route segments." };
|
|
400
|
+
}
|
|
401
|
+
const [service, ...routeSegments] = segments;
|
|
402
|
+
const routeName = routeSegments.join("/");
|
|
403
|
+
if (!service || !routeName) {
|
|
404
|
+
return { status: "error", reason: "Route name must include service and route segments." };
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
status: "success",
|
|
408
|
+
normalizedRouteName: `${service}/${routeName}`,
|
|
409
|
+
service,
|
|
410
|
+
routeName
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/socketEvents.ts
|
|
415
|
+
var withIndex = (prefix, responseIndex2) => {
|
|
416
|
+
return `${prefix}${String(responseIndex2)}`;
|
|
417
|
+
};
|
|
418
|
+
var socketEventNames = {
|
|
419
|
+
apiRequest: "apiRequest",
|
|
420
|
+
apiCancel: "apiCancel",
|
|
421
|
+
sync: "sync",
|
|
422
|
+
syncCancel: "syncCancel",
|
|
423
|
+
joinRoom: "joinRoom",
|
|
424
|
+
leaveRoom: "leaveRoom",
|
|
425
|
+
getJoinedRooms: "getJoinedRooms",
|
|
426
|
+
updateLocation: "updateLocation",
|
|
427
|
+
updateSession: "updateSession",
|
|
428
|
+
sessionReplaced: "sessionReplaced",
|
|
429
|
+
logout: "logout",
|
|
430
|
+
intentionalDisconnect: "intentionalDisconnect",
|
|
431
|
+
intentionalReconnect: "intentionalReconnect",
|
|
432
|
+
userAfk: "userAfk",
|
|
433
|
+
userBack: "userBack",
|
|
434
|
+
connect: "connect",
|
|
435
|
+
disconnect: "disconnect",
|
|
436
|
+
reconnectAttempt: "reconnect_attempt",
|
|
437
|
+
connectError: "connect_error",
|
|
438
|
+
apiResponsePrefix: "apiResponse-",
|
|
439
|
+
apiStreamPrefix: "apiStream-",
|
|
440
|
+
syncResponsePrefix: "sync-",
|
|
441
|
+
syncProgressPrefix: "sync-progress-",
|
|
442
|
+
joinRoomResponsePrefix: "joinRoom-",
|
|
443
|
+
leaveRoomResponsePrefix: "leaveRoom-",
|
|
444
|
+
getJoinedRoomsResponsePrefix: "getJoinedRooms-"
|
|
445
|
+
};
|
|
446
|
+
var buildApiResponseEventName = (responseIndex2) => {
|
|
447
|
+
return withIndex(socketEventNames.apiResponsePrefix, responseIndex2);
|
|
448
|
+
};
|
|
449
|
+
var buildApiStreamEventName = (responseIndex2) => {
|
|
450
|
+
return withIndex(socketEventNames.apiStreamPrefix, responseIndex2);
|
|
451
|
+
};
|
|
452
|
+
var buildSyncResponseEventName = (responseIndex2) => {
|
|
453
|
+
return withIndex(socketEventNames.syncResponsePrefix, responseIndex2);
|
|
454
|
+
};
|
|
455
|
+
var buildSyncProgressEventName = (responseIndex2) => {
|
|
456
|
+
return withIndex(socketEventNames.syncProgressPrefix, responseIndex2);
|
|
457
|
+
};
|
|
458
|
+
var buildJoinRoomResponseEventName = (responseIndex2) => {
|
|
459
|
+
return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex2);
|
|
460
|
+
};
|
|
461
|
+
var buildLeaveRoomResponseEventName = (responseIndex2) => {
|
|
462
|
+
return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex2);
|
|
463
|
+
};
|
|
464
|
+
var buildGetJoinedRoomsResponseEventName = (responseIndex2) => {
|
|
465
|
+
return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex2);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// src/apiRequest.ts
|
|
469
|
+
var abortControllers = /* @__PURE__ */ new Map();
|
|
470
|
+
var isGetMethodByPrefix = (apiName) => {
|
|
471
|
+
const lower = apiName.toLowerCase();
|
|
472
|
+
return lower.startsWith("get") || lower.startsWith("fetch") || lower.startsWith("list");
|
|
473
|
+
};
|
|
474
|
+
var isGetMethod = (apiName, version) => {
|
|
475
|
+
const lastSlash = apiName.lastIndexOf("/");
|
|
476
|
+
if (lastSlash > 0) {
|
|
477
|
+
const pagePath = apiName.slice(0, lastSlash);
|
|
478
|
+
const leaf = apiName.slice(lastSlash + 1);
|
|
479
|
+
const method = getRegisteredApiMethod(pagePath, leaf, version);
|
|
480
|
+
if (method) return method === "GET";
|
|
481
|
+
}
|
|
482
|
+
return isGetMethodByPrefix(apiName);
|
|
483
|
+
};
|
|
484
|
+
var canSendNow = (socketInstance) => {
|
|
485
|
+
if (!socketInstance.connected) return false;
|
|
486
|
+
return isOnline();
|
|
487
|
+
};
|
|
488
|
+
var createQueueId = () => {
|
|
489
|
+
return `${String(Date.now())}-${String(Math.random())}`;
|
|
490
|
+
};
|
|
491
|
+
var getLogging = () => getProjectConfig().logging;
|
|
492
|
+
var shouldLogDev = () => getLogging().devLogs;
|
|
493
|
+
var shouldNotifyDev = () => getLogging().devNotifications;
|
|
494
|
+
var shouldLogStream = () => getLogging().stream;
|
|
495
|
+
var shouldUseAbortController = ({
|
|
496
|
+
abortable,
|
|
497
|
+
isGet
|
|
498
|
+
}) => {
|
|
499
|
+
if (abortable === true) return true;
|
|
500
|
+
if (abortable === false) return false;
|
|
501
|
+
return isGet;
|
|
502
|
+
};
|
|
503
|
+
var normalizeApiError = ({
|
|
504
|
+
response,
|
|
505
|
+
fallbackErrorCode,
|
|
506
|
+
fallbackHttpStatus = 500
|
|
507
|
+
}) => {
|
|
508
|
+
const normalized = normalizeErrorResponseCore({
|
|
509
|
+
response,
|
|
510
|
+
fallbackErrorCode,
|
|
511
|
+
fallbackHttpStatus,
|
|
512
|
+
resolveMessage: ({ errorCode }) => {
|
|
513
|
+
if (typeof response.message === "string" && response.message.trim().length > 0) {
|
|
514
|
+
return response.message;
|
|
515
|
+
}
|
|
516
|
+
return errorCode;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
status: "error",
|
|
521
|
+
message: normalized.message,
|
|
522
|
+
errorCode: normalized.errorCode,
|
|
523
|
+
errorParams: normalized.errorParams,
|
|
524
|
+
httpStatus: normalized.httpStatus ?? fallbackHttpStatus
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
function apiRequest(params) {
|
|
528
|
+
const runtimeParams = params;
|
|
529
|
+
const { name, version, disableErrorMessage = false, onStream, signal: externalSignal } = runtimeParams;
|
|
530
|
+
const payloadData = runtimeParams.data;
|
|
531
|
+
return new Promise((resolve2, reject) => {
|
|
532
|
+
void (async () => {
|
|
533
|
+
if (externalSignal?.aborted) {
|
|
534
|
+
resolve2(normalizeApiError({
|
|
535
|
+
response: { status: "error", errorCode: "request.aborted" },
|
|
536
|
+
fallbackErrorCode: "request.aborted"
|
|
537
|
+
}));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!name || typeof name !== "string") {
|
|
541
|
+
if (shouldLogDev()) {
|
|
542
|
+
getLogger().error("apiRequest: Invalid name");
|
|
543
|
+
}
|
|
544
|
+
if (shouldNotifyDev()) {
|
|
545
|
+
notify.error({ key: "api.invalidName" });
|
|
546
|
+
}
|
|
547
|
+
resolve2(normalizeApiError({
|
|
548
|
+
response: { status: "error", errorCode: "api.invalidName" },
|
|
549
|
+
fallbackErrorCode: "api.invalidName"
|
|
550
|
+
}));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (!version || typeof version !== "string") {
|
|
554
|
+
if (shouldLogDev()) {
|
|
555
|
+
getLogger().error("apiRequest: Invalid version");
|
|
556
|
+
}
|
|
557
|
+
if (shouldNotifyDev()) {
|
|
558
|
+
notify.error({ key: "api.invalidVersion" });
|
|
559
|
+
}
|
|
560
|
+
resolve2(normalizeApiError({
|
|
561
|
+
response: { status: "error", errorCode: "api.invalidVersion" },
|
|
562
|
+
fallbackErrorCode: "api.invalidVersion"
|
|
563
|
+
}));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const parsedRoute = parseServiceRouteName(name);
|
|
567
|
+
if (parsedRoute.status === "error") {
|
|
568
|
+
if (shouldLogDev()) {
|
|
569
|
+
getLogger().error(`[apiRequest] Invalid service route name`, void 0, { name, reason: parsedRoute.reason });
|
|
570
|
+
}
|
|
571
|
+
if (shouldNotifyDev()) {
|
|
572
|
+
notify.error({ key: "routing.invalidServiceRouteName" });
|
|
573
|
+
}
|
|
574
|
+
resolve2(normalizeErrorResponseCore({
|
|
575
|
+
response: {
|
|
576
|
+
status: "error",
|
|
577
|
+
errorCode: "routing.invalidServiceRouteName",
|
|
578
|
+
errorParams: [{ key: "name", value: name }]
|
|
579
|
+
},
|
|
580
|
+
fallbackErrorCode: "routing.invalidServiceRouteName"
|
|
581
|
+
}));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const sanitizedName = parsedRoute.normalizedRouteName;
|
|
585
|
+
const data = payloadData && typeof payloadData === "object" ? payloadData : {};
|
|
586
|
+
if (!await waitForSocket()) {
|
|
587
|
+
resolve2(normalizeApiError({
|
|
588
|
+
response: { status: "error", errorCode: "api.ioUnavailable" },
|
|
589
|
+
fallbackErrorCode: "api.ioUnavailable"
|
|
590
|
+
}));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (!socket) {
|
|
594
|
+
resolve2(normalizeApiError({
|
|
595
|
+
response: { status: "error", errorCode: "api.ioUnavailable" },
|
|
596
|
+
fallbackErrorCode: "api.ioUnavailable"
|
|
597
|
+
}));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const isGet = isGetMethod(sanitizedName, version);
|
|
601
|
+
const useAbortController = shouldUseAbortController({
|
|
602
|
+
abortable: runtimeParams.abortable,
|
|
603
|
+
isGet
|
|
604
|
+
});
|
|
605
|
+
const fullName = `api/${sanitizedName}/${version}`;
|
|
606
|
+
let signal = null;
|
|
607
|
+
let abortHandler = null;
|
|
608
|
+
let queueId = null;
|
|
609
|
+
let cleanupStreamListener = null;
|
|
610
|
+
const cleanupAbortController = () => {
|
|
611
|
+
if (signal && abortHandler) {
|
|
612
|
+
signal.removeEventListener("abort", abortHandler);
|
|
613
|
+
}
|
|
614
|
+
abortControllers.delete(fullName);
|
|
615
|
+
};
|
|
616
|
+
if (useAbortController) {
|
|
617
|
+
if (abortControllers.has(fullName)) {
|
|
618
|
+
const prevAbortController = abortControllers.get(fullName);
|
|
619
|
+
prevAbortController?.abort();
|
|
620
|
+
}
|
|
621
|
+
const abortController = new AbortController();
|
|
622
|
+
abortControllers.set(fullName, abortController);
|
|
623
|
+
signal = abortController.signal;
|
|
624
|
+
abortHandler = () => {
|
|
625
|
+
cleanupStreamListener?.();
|
|
626
|
+
cleanupStreamListener = null;
|
|
627
|
+
cleanupAbortController();
|
|
628
|
+
if (queueId) {
|
|
629
|
+
removeApiQueueItem(queueId);
|
|
630
|
+
}
|
|
631
|
+
reject(new Error(`Request ${fullName} aborted`));
|
|
632
|
+
};
|
|
633
|
+
signal.addEventListener("abort", abortHandler);
|
|
634
|
+
}
|
|
635
|
+
const runRequest = (socketInstance) => {
|
|
636
|
+
if (!canSendNow(socketInstance)) {
|
|
637
|
+
queueId ??= createQueueId();
|
|
638
|
+
const enqueued = enqueueApiRequest({
|
|
639
|
+
id: queueId,
|
|
640
|
+
key: fullName,
|
|
641
|
+
run: (nextSocket) => {
|
|
642
|
+
runRequest(nextSocket);
|
|
643
|
+
},
|
|
644
|
+
createdAt: Date.now()
|
|
645
|
+
});
|
|
646
|
+
if (!enqueued) {
|
|
647
|
+
resolve2(normalizeApiError({
|
|
648
|
+
response: { status: "error", errorCode: "offline.queueFull", httpStatus: 503 },
|
|
649
|
+
fallbackErrorCode: "offline.queueFull",
|
|
650
|
+
fallbackHttpStatus: 503
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (signal?.aborted) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const tempIndex = incrementResponseIndex();
|
|
659
|
+
socketInstance.emit(socketEventNames.apiRequest, { name: fullName, data, responseIndex: tempIndex });
|
|
660
|
+
let cleanupExternalAbort = null;
|
|
661
|
+
if (externalSignal) {
|
|
662
|
+
const externalAbortHandler = () => {
|
|
663
|
+
socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });
|
|
664
|
+
cleanupStreamListener?.();
|
|
665
|
+
cleanupStreamListener = null;
|
|
666
|
+
cleanupAbortController();
|
|
667
|
+
cleanupExternalAbort?.();
|
|
668
|
+
cleanupExternalAbort = null;
|
|
669
|
+
resolve2(normalizeApiError({
|
|
670
|
+
response: { status: "error", errorCode: "request.aborted" },
|
|
671
|
+
fallbackErrorCode: "request.aborted"
|
|
672
|
+
}));
|
|
673
|
+
};
|
|
674
|
+
externalSignal.addEventListener("abort", externalAbortHandler);
|
|
675
|
+
cleanupExternalAbort = () => {
|
|
676
|
+
externalSignal.removeEventListener("abort", externalAbortHandler);
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (typeof onStream === "function") {
|
|
680
|
+
const streamEventName = buildApiStreamEventName(tempIndex);
|
|
681
|
+
const streamListener = (streamPayload) => {
|
|
682
|
+
if (signal?.aborted) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (shouldLogStream()) {
|
|
686
|
+
getLogger().debug(`Server API Stream(${String(tempIndex)})`, { APINAME: sanitizedName, streamPayload });
|
|
687
|
+
}
|
|
688
|
+
onStream(streamPayload);
|
|
689
|
+
};
|
|
690
|
+
socketInstance.on(streamEventName, streamListener);
|
|
691
|
+
cleanupStreamListener = () => {
|
|
692
|
+
socketInstance.off(streamEventName, streamListener);
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (shouldLogDev()) {
|
|
696
|
+
getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });
|
|
697
|
+
}
|
|
698
|
+
socketInstance.once(buildApiResponseEventName(tempIndex), (response) => {
|
|
699
|
+
if (signal?.aborted) {
|
|
700
|
+
cleanupExternalAbort?.();
|
|
701
|
+
cleanupExternalAbort = null;
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
cleanupStreamListener?.();
|
|
705
|
+
cleanupStreamListener = null;
|
|
706
|
+
cleanupExternalAbort?.();
|
|
707
|
+
cleanupExternalAbort = null;
|
|
708
|
+
const status = response.status;
|
|
709
|
+
if (shouldLogDev()) {
|
|
710
|
+
getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });
|
|
711
|
+
}
|
|
712
|
+
if (status === "error") {
|
|
713
|
+
const normalizedError = normalizeErrorResponseCore({ response });
|
|
714
|
+
if (!disableErrorMessage) {
|
|
715
|
+
if (normalizedError.errorCode) {
|
|
716
|
+
notify.error({ key: normalizedError.errorCode, params: normalizedError.errorParams });
|
|
717
|
+
} else {
|
|
718
|
+
notify.error({ key: normalizedError.message });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
Object.assign(response, normalizedError);
|
|
722
|
+
cleanupAbortController();
|
|
723
|
+
resolve2(response);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
cleanupAbortController();
|
|
727
|
+
resolve2(response);
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
runRequest(socket);
|
|
731
|
+
})();
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/sleep.ts
|
|
736
|
+
function sleep(ms) {
|
|
737
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/errorTrackerRegistry.ts
|
|
741
|
+
var activeTrackers = [];
|
|
742
|
+
var captureExceptionAcrossTrackers = (error, context) => {
|
|
743
|
+
for (const tracker of activeTrackers) {
|
|
744
|
+
try {
|
|
745
|
+
tracker.captureException(error, context);
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// src/sentrySetup.ts
|
|
752
|
+
var sentry;
|
|
753
|
+
var captureException = (error, context) => {
|
|
754
|
+
if (sentry) {
|
|
755
|
+
if (context) {
|
|
756
|
+
sentry.captureException(error, { extra: context });
|
|
757
|
+
} else {
|
|
758
|
+
sentry.captureException(error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
captureExceptionAcrossTrackers(error, context);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// src/tryCatch.ts
|
|
765
|
+
async function tryCatch(func, params, context) {
|
|
766
|
+
try {
|
|
767
|
+
const response = await func(params);
|
|
768
|
+
return [null, response];
|
|
769
|
+
} catch (error) {
|
|
770
|
+
captureException(error, context);
|
|
771
|
+
return [error, null];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/csrfConfig.ts
|
|
776
|
+
var DEFAULT_CSRF_CONFIG = {
|
|
777
|
+
cookieName: "csrf-token",
|
|
778
|
+
headerName: "x-csrf-token",
|
|
779
|
+
tokenLength: 32,
|
|
780
|
+
cookieOptions: {
|
|
781
|
+
sameSite: "lax",
|
|
782
|
+
secure: true,
|
|
783
|
+
httpOnly: false,
|
|
784
|
+
path: "/",
|
|
785
|
+
maxAgeMs: 24 * 60 * 60 * 1e3
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
var active = DEFAULT_CSRF_CONFIG;
|
|
789
|
+
var getCsrfConfig = () => active;
|
|
790
|
+
|
|
791
|
+
// src/csrf.ts
|
|
792
|
+
var cachedToken = null;
|
|
793
|
+
var inflightFetch = null;
|
|
794
|
+
var resolveBackendUrl = () => {
|
|
795
|
+
if (globalThis.window !== void 0 && globalThis.location?.origin) {
|
|
796
|
+
return globalThis.location.origin;
|
|
797
|
+
}
|
|
798
|
+
return "";
|
|
799
|
+
};
|
|
800
|
+
var fetchCsrfToken = async () => {
|
|
801
|
+
const base = resolveBackendUrl();
|
|
802
|
+
const [error, token] = await tryCatch(async () => {
|
|
803
|
+
const response = await fetch(`${base}/auth/csrf`, {
|
|
804
|
+
method: "GET",
|
|
805
|
+
credentials: "include"
|
|
806
|
+
});
|
|
807
|
+
if (!response.ok) return null;
|
|
808
|
+
const body = await response.json();
|
|
809
|
+
return body.csrfToken ?? null;
|
|
810
|
+
});
|
|
811
|
+
if (error) return null;
|
|
812
|
+
return token ?? null;
|
|
813
|
+
};
|
|
814
|
+
var getCsrfToken = async () => {
|
|
815
|
+
if (getProjectConfig().session.basedToken) {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
if (cachedToken) return cachedToken;
|
|
819
|
+
if (inflightFetch) return inflightFetch;
|
|
820
|
+
inflightFetch = fetchCsrfToken().then((token) => {
|
|
821
|
+
cachedToken = token;
|
|
822
|
+
inflightFetch = null;
|
|
823
|
+
return token;
|
|
824
|
+
});
|
|
825
|
+
return inflightFetch;
|
|
826
|
+
};
|
|
827
|
+
var clearCsrfToken = () => {
|
|
828
|
+
cachedToken = null;
|
|
829
|
+
inflightFetch = null;
|
|
830
|
+
};
|
|
831
|
+
var STATE_CHANGING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
832
|
+
var isCsrfMismatchResponse = async (response) => {
|
|
833
|
+
if (response.status !== 403) return false;
|
|
834
|
+
const [error, isMismatch] = await tryCatch(async () => {
|
|
835
|
+
const cloned = response.clone();
|
|
836
|
+
const body = await cloned.json();
|
|
837
|
+
return body.errorCode === "auth.csrfMismatch";
|
|
838
|
+
});
|
|
839
|
+
if (error) return false;
|
|
840
|
+
return isMismatch ?? false;
|
|
841
|
+
};
|
|
842
|
+
var httpFetch = async (input, init = {}) => {
|
|
843
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
844
|
+
const headers = new Headers(init.headers);
|
|
845
|
+
if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
|
|
846
|
+
headers.set("Content-Type", "application/json");
|
|
847
|
+
}
|
|
848
|
+
const csrfHeaderName = getCsrfConfig().headerName;
|
|
849
|
+
const send = async (csrfToken) => {
|
|
850
|
+
const finalHeaders = new Headers(headers);
|
|
851
|
+
if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {
|
|
852
|
+
finalHeaders.set(csrfHeaderName, csrfToken);
|
|
853
|
+
}
|
|
854
|
+
return fetch(input, {
|
|
855
|
+
...init,
|
|
856
|
+
method,
|
|
857
|
+
headers: finalHeaders,
|
|
858
|
+
credentials: init.credentials ?? "include"
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
if (!STATE_CHANGING.has(method) || getProjectConfig().session.basedToken) {
|
|
862
|
+
return send(null);
|
|
863
|
+
}
|
|
864
|
+
const token = await getCsrfToken();
|
|
865
|
+
let response = await send(token);
|
|
866
|
+
if (await isCsrfMismatchResponse(response)) {
|
|
867
|
+
clearCsrfToken();
|
|
868
|
+
const refreshed = await getCsrfToken();
|
|
869
|
+
if (refreshed) {
|
|
870
|
+
response = await send(refreshed);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return response;
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// src/localesRegistry.ts
|
|
877
|
+
var activeLocales = {};
|
|
878
|
+
var activeLanguageSource = null;
|
|
879
|
+
var registerLocales = (locales) => {
|
|
880
|
+
activeLocales = locales;
|
|
881
|
+
};
|
|
882
|
+
var getRegisteredLocales = () => activeLocales;
|
|
883
|
+
var getDefaultLocale = () => getProjectConfig().defaultLanguage || "en";
|
|
884
|
+
var registerLanguageSource = (fn) => {
|
|
885
|
+
activeLanguageSource = fn;
|
|
886
|
+
};
|
|
887
|
+
var getActiveLanguage = () => {
|
|
888
|
+
if (activeLanguageSource) {
|
|
889
|
+
const lang = activeLanguageSource();
|
|
890
|
+
if (typeof lang === "string" && lang.length > 0) return lang;
|
|
891
|
+
}
|
|
892
|
+
return getDefaultLocale();
|
|
893
|
+
};
|
|
894
|
+
var getLocaleByCode = (code) => {
|
|
895
|
+
if (code in activeLocales) return activeLocales[code];
|
|
896
|
+
const fallback = getDefaultLocale();
|
|
897
|
+
if (fallback in activeLocales) return activeLocales[fallback];
|
|
898
|
+
return {};
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// src/middlewareRegistry.ts
|
|
902
|
+
var DEFAULT_HANDLER = () => ({ success: true });
|
|
903
|
+
var activeHandler = DEFAULT_HANDLER;
|
|
904
|
+
var registerMiddlewareHandler = (handler) => {
|
|
905
|
+
activeHandler = handler;
|
|
906
|
+
};
|
|
907
|
+
var getMiddlewareHandler = () => activeHandler;
|
|
908
|
+
var pageMiddlewares = /* @__PURE__ */ new Map();
|
|
909
|
+
var registerPageMiddleware = (path, middleware) => {
|
|
910
|
+
pageMiddlewares.set(path, middleware);
|
|
911
|
+
};
|
|
912
|
+
var getPageMiddleware = (path) => pageMiddlewares.get(path);
|
|
913
|
+
var hasPageMiddleware = (path) => pageMiddlewares.has(path);
|
|
914
|
+
|
|
915
|
+
// src/clientHookBus.ts
|
|
916
|
+
var handlerSets = {};
|
|
917
|
+
function registerClientHook(name, handler) {
|
|
918
|
+
const existing = handlerSets[name];
|
|
919
|
+
const set = existing ?? /* @__PURE__ */ new Set();
|
|
920
|
+
if (!existing) handlerSets[name] = set;
|
|
921
|
+
const wrapped = handler;
|
|
922
|
+
set.add(wrapped);
|
|
923
|
+
return () => {
|
|
924
|
+
set.delete(wrapped);
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function dispatchClientHook(name, payload) {
|
|
928
|
+
const set = handlerSets[name];
|
|
929
|
+
if (!set || set.size === 0) return;
|
|
930
|
+
for (const handler of set) {
|
|
931
|
+
try {
|
|
932
|
+
const result = handler(payload);
|
|
933
|
+
if (result && typeof result.catch === "function") {
|
|
934
|
+
result.catch((error) => {
|
|
935
|
+
console.error(`[clientHook:${name}] async handler rejected:`, error);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
} catch (error) {
|
|
939
|
+
console.error(`[clientHook:${name}] handler threw:`, error);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function dispatchVetoableClientHook(name, payload) {
|
|
944
|
+
const set = handlerSets[name];
|
|
945
|
+
if (!set || set.size === 0) return { stopped: false };
|
|
946
|
+
for (const handler of set) {
|
|
947
|
+
let result;
|
|
948
|
+
try {
|
|
949
|
+
result = await handler(payload);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
console.error(`[clientHook:${name}] vetoable handler threw:`, error);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (result?.stop) {
|
|
955
|
+
return { stopped: true, signal: result };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return { stopped: false };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/pageRouteValidation.ts
|
|
962
|
+
var DEFAULT_PAGE_ROUTE_RULES = {
|
|
963
|
+
privateFolderPrefix: "_",
|
|
964
|
+
scaffoldIgnoredFolders: [
|
|
965
|
+
"_api",
|
|
966
|
+
"_sync",
|
|
967
|
+
"_function",
|
|
968
|
+
"_functions",
|
|
969
|
+
"_component",
|
|
970
|
+
"_components",
|
|
971
|
+
"_provider",
|
|
972
|
+
"_providers",
|
|
973
|
+
"_locale",
|
|
974
|
+
"_locales",
|
|
975
|
+
"_socket",
|
|
976
|
+
"_sockets",
|
|
977
|
+
"_shared",
|
|
978
|
+
"_server"
|
|
979
|
+
]
|
|
980
|
+
};
|
|
981
|
+
var validatePagePath = (srcRelativePath, rules = DEFAULT_PAGE_ROUTE_RULES) => {
|
|
982
|
+
const normalized = srcRelativePath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
983
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
984
|
+
if (segments.length === 0) {
|
|
985
|
+
return { valid: false, reason: "empty path" };
|
|
986
|
+
}
|
|
987
|
+
const filename = segments.at(-1);
|
|
988
|
+
if (filename !== "page.tsx" && filename !== "page.jsx") {
|
|
989
|
+
return { valid: false, reason: "not a page file (expected page.tsx)" };
|
|
990
|
+
}
|
|
991
|
+
const folderSegments = segments.slice(0, -1);
|
|
992
|
+
const prefix = rules.privateFolderPrefix;
|
|
993
|
+
const reserved = rules.scaffoldIgnoredFolders;
|
|
994
|
+
for (const reservedName of reserved) {
|
|
995
|
+
if (folderSegments.includes(reservedName)) {
|
|
996
|
+
return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));
|
|
1000
|
+
if (folderSegments.length > 0 && visibleSegments.length === 0) {
|
|
1001
|
+
return { valid: false, reason: "no URL segment left after stripping underscore folders" };
|
|
1002
|
+
}
|
|
1003
|
+
const route = visibleSegments.length === 0 ? "/" : `/${visibleSegments.join("/")}`;
|
|
1004
|
+
return { valid: true, route };
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/react/sessionContext.ts
|
|
1008
|
+
import { createContext, use } from "react";
|
|
1009
|
+
var DEFAULT_VALUE = {
|
|
1010
|
+
session: null,
|
|
1011
|
+
sessionLoaded: false
|
|
1012
|
+
};
|
|
1013
|
+
var SessionContext = createContext(DEFAULT_VALUE);
|
|
1014
|
+
function useSession() {
|
|
1015
|
+
return use(SessionContext);
|
|
1016
|
+
}
|
|
1017
|
+
var latestSession = null;
|
|
1018
|
+
var setLatestSession = (session) => {
|
|
1019
|
+
const previous = latestSession;
|
|
1020
|
+
latestSession = session;
|
|
1021
|
+
if (previous === null && session !== null) {
|
|
1022
|
+
dispatchClientHook("postLogin", { session });
|
|
1023
|
+
} else if (previous !== null && session === null) {
|
|
1024
|
+
dispatchClientHook("postLogout", { previousSession: previous });
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
var getCurrentSession = () => latestSession;
|
|
1028
|
+
var proposeLogin = async (session) => {
|
|
1029
|
+
const veto = await dispatchVetoableClientHook("preLogin", { candidateSession: session });
|
|
1030
|
+
if (veto.stopped) return { stopped: true, signal: veto.signal, committed: false };
|
|
1031
|
+
setLatestSession(session);
|
|
1032
|
+
return { stopped: false, committed: true };
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// src/react/AvatarProvider.tsx
|
|
1036
|
+
import { createContext as createContext2, use as use2, useState, useMemo, useCallback } from "react";
|
|
1037
|
+
import { jsx } from "react/jsx-runtime";
|
|
1038
|
+
var AvatarContext = createContext2(null);
|
|
1039
|
+
var AvatarProvider = ({ children }) => {
|
|
1040
|
+
const [avatarStatuses, setAvatarStatuses] = useState({});
|
|
1041
|
+
const setAvatarStatus = useCallback((key, status) => {
|
|
1042
|
+
setAvatarStatuses((prev) => ({ ...prev, [key]: status }));
|
|
1043
|
+
}, []);
|
|
1044
|
+
const contextValue = useMemo(() => ({
|
|
1045
|
+
avatarStatuses,
|
|
1046
|
+
setAvatarStatus
|
|
1047
|
+
}), [avatarStatuses, setAvatarStatus]);
|
|
1048
|
+
return /* @__PURE__ */ jsx(AvatarContext, { value: contextValue, children });
|
|
1049
|
+
};
|
|
1050
|
+
function useAvatarContext() {
|
|
1051
|
+
const ctx = use2(AvatarContext);
|
|
1052
|
+
if (!ctx) throw new Error("useAvatarContext must be used within AvatarProvider");
|
|
1053
|
+
return ctx;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/react/useTheme.ts
|
|
1057
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
1058
|
+
var applyDomClass = (theme) => {
|
|
1059
|
+
if (typeof document === "undefined") return;
|
|
1060
|
+
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
1061
|
+
};
|
|
1062
|
+
var useTheme = () => {
|
|
1063
|
+
const initialTheme = getProjectConfig().defaultTheme ?? "light";
|
|
1064
|
+
const [theme, setThemeState] = useState2(initialTheme);
|
|
1065
|
+
const setTheme = useCallback2((next) => {
|
|
1066
|
+
setThemeState(next);
|
|
1067
|
+
applyDomClass(next);
|
|
1068
|
+
}, []);
|
|
1069
|
+
return { theme, setTheme };
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// src/react/TranslationProvider.tsx
|
|
1073
|
+
import { createContext as createContext3, use as use3, useEffect, useMemo as useMemo2, useState as useState3 } from "react";
|
|
1074
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1075
|
+
var TranslationContext = createContext3(null);
|
|
1076
|
+
var resolveTranslations = (code) => {
|
|
1077
|
+
const value = getLocaleByCode(code);
|
|
1078
|
+
if (value && typeof value === "object") return value;
|
|
1079
|
+
return {};
|
|
1080
|
+
};
|
|
1081
|
+
function TranslationProvider({ children }) {
|
|
1082
|
+
const [language, setLanguage] = useState3(() => getActiveLanguage());
|
|
1083
|
+
const translations = useMemo2(() => resolveTranslations(language), [language]);
|
|
1084
|
+
useEffect(() => {
|
|
1085
|
+
const handle = window.setInterval(() => {
|
|
1086
|
+
const next = getActiveLanguage();
|
|
1087
|
+
if (next !== language) setLanguage(next);
|
|
1088
|
+
}, 250);
|
|
1089
|
+
return () => {
|
|
1090
|
+
window.clearInterval(handle);
|
|
1091
|
+
};
|
|
1092
|
+
}, [language]);
|
|
1093
|
+
const contextValue = useMemo2(() => ({ translations, setLanguage }), [translations]);
|
|
1094
|
+
return /* @__PURE__ */ jsx2(TranslationContext, { value: contextValue, children });
|
|
1095
|
+
}
|
|
1096
|
+
function useTranslation() {
|
|
1097
|
+
const context = use3(TranslationContext);
|
|
1098
|
+
if (!context) throw new Error("useTranslation must be used within a TranslationProvider");
|
|
1099
|
+
return context.translations;
|
|
1100
|
+
}
|
|
1101
|
+
function useUpdateLanguage() {
|
|
1102
|
+
const context = use3(TranslationContext);
|
|
1103
|
+
if (!context) throw new Error("useUpdateLanguage must be used within a TranslationProvider");
|
|
1104
|
+
return context.setLanguage;
|
|
1105
|
+
}
|
|
1106
|
+
function translate({ translationList, key, params }) {
|
|
1107
|
+
if (typeof key !== "string" || key.length === 0) return "";
|
|
1108
|
+
const parts = key.split(".");
|
|
1109
|
+
let result = translationList;
|
|
1110
|
+
for (const part of parts) {
|
|
1111
|
+
if (result && typeof result === "object" && part in result) {
|
|
1112
|
+
result = result[part];
|
|
1113
|
+
} else {
|
|
1114
|
+
return key;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof result !== "string") return key;
|
|
1118
|
+
if (!Array.isArray(params) || params.length === 0) return result;
|
|
1119
|
+
let finalResult = result;
|
|
1120
|
+
for (const param of params) {
|
|
1121
|
+
if (!param.key) continue;
|
|
1122
|
+
const regex = new RegExp(`\\{\\{${param.key}\\}\\}`, "g");
|
|
1123
|
+
finalResult = finalResult.replace(regex, String(param.value));
|
|
1124
|
+
}
|
|
1125
|
+
return finalResult;
|
|
1126
|
+
}
|
|
1127
|
+
var useTranslator = () => {
|
|
1128
|
+
const translations = useTranslation();
|
|
1129
|
+
return ({ key, params }) => translate({ translationList: translations, key, params });
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
// src/react/notify.ts
|
|
1133
|
+
import { toast } from "sonner";
|
|
1134
|
+
var resolve = (input) => {
|
|
1135
|
+
const language = getActiveLanguage();
|
|
1136
|
+
const list = getLocaleByCode(language);
|
|
1137
|
+
return translate({
|
|
1138
|
+
translationList: list && typeof list === "object" ? list : {},
|
|
1139
|
+
key: input.key,
|
|
1140
|
+
params: input.params
|
|
1141
|
+
});
|
|
1142
|
+
};
|
|
1143
|
+
var notify2 = {
|
|
1144
|
+
success: (input) => {
|
|
1145
|
+
toast.success(resolve(input));
|
|
1146
|
+
},
|
|
1147
|
+
error: (input) => {
|
|
1148
|
+
toast.error(resolve(input));
|
|
1149
|
+
},
|
|
1150
|
+
info: (input) => {
|
|
1151
|
+
toast.info(resolve(input));
|
|
1152
|
+
},
|
|
1153
|
+
warning: (input) => {
|
|
1154
|
+
toast.warning(resolve(input));
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
registerNotifier(notify2);
|
|
1158
|
+
var notify_default = notify2;
|
|
1159
|
+
|
|
1160
|
+
// src/react/Middleware.tsx
|
|
1161
|
+
import { useEffect as useEffect2, useRef, useState as useState4 } from "react";
|
|
1162
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
1163
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1164
|
+
var LOADER_DELAY_MS = 200;
|
|
1165
|
+
function MiddlewareLoader() {
|
|
1166
|
+
const [show, setShow] = useState4(false);
|
|
1167
|
+
useEffect2(() => {
|
|
1168
|
+
const id = globalThis.setTimeout(() => {
|
|
1169
|
+
setShow(true);
|
|
1170
|
+
}, LOADER_DELAY_MS);
|
|
1171
|
+
return () => {
|
|
1172
|
+
globalThis.clearTimeout(id);
|
|
1173
|
+
};
|
|
1174
|
+
}, []);
|
|
1175
|
+
if (!show) return null;
|
|
1176
|
+
return /* @__PURE__ */ jsx3("div", { className: "w-full h-full flex items-center justify-center", "aria-busy": "true", "aria-live": "polite", children: /* @__PURE__ */ jsx3(
|
|
1177
|
+
"div",
|
|
1178
|
+
{
|
|
1179
|
+
className: "w-8 h-8 rounded-full border-2 border-container2-border border-t-primary animate-spin",
|
|
1180
|
+
role: "status",
|
|
1181
|
+
"aria-label": "Loading"
|
|
1182
|
+
}
|
|
1183
|
+
) });
|
|
1184
|
+
}
|
|
1185
|
+
function Middleware({ children }) {
|
|
1186
|
+
const [allowed, setAllowed] = useState4(false);
|
|
1187
|
+
const [checking, setChecking] = useState4(true);
|
|
1188
|
+
const location = useLocation();
|
|
1189
|
+
const navigate = useNavigate();
|
|
1190
|
+
const { session, sessionLoaded } = useSession();
|
|
1191
|
+
const routeKey = `${location.pathname}\0${location.search}`;
|
|
1192
|
+
const guardedRouteRef = useRef(null);
|
|
1193
|
+
useEffect2(() => {
|
|
1194
|
+
let isMounted = true;
|
|
1195
|
+
const isRouteChange = guardedRouteRef.current !== routeKey;
|
|
1196
|
+
guardedRouteRef.current = routeKey;
|
|
1197
|
+
if (isRouteChange) {
|
|
1198
|
+
setAllowed(false);
|
|
1199
|
+
setChecking(true);
|
|
1200
|
+
}
|
|
1201
|
+
void (async () => {
|
|
1202
|
+
const params = new URLSearchParams(location.search);
|
|
1203
|
+
const queryObject = {};
|
|
1204
|
+
for (const [key, value] of params.entries()) {
|
|
1205
|
+
queryObject[key] = value;
|
|
1206
|
+
}
|
|
1207
|
+
let count = 0;
|
|
1208
|
+
while (!sessionLoaded) {
|
|
1209
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
1210
|
+
count++;
|
|
1211
|
+
if (count > 500) break;
|
|
1212
|
+
}
|
|
1213
|
+
const pageMw = getPageMiddleware(location.pathname);
|
|
1214
|
+
const handler = pageMw ?? getMiddlewareHandler();
|
|
1215
|
+
const result = await handler({
|
|
1216
|
+
location: location.pathname,
|
|
1217
|
+
searchParams: queryObject,
|
|
1218
|
+
session
|
|
1219
|
+
});
|
|
1220
|
+
if (!isMounted) return;
|
|
1221
|
+
if (result?.success) {
|
|
1222
|
+
setAllowed(true);
|
|
1223
|
+
} else if (result?.redirect) {
|
|
1224
|
+
void navigate(result.redirect);
|
|
1225
|
+
} else {
|
|
1226
|
+
void navigate(-1);
|
|
1227
|
+
}
|
|
1228
|
+
setChecking(false);
|
|
1229
|
+
})();
|
|
1230
|
+
return () => {
|
|
1231
|
+
isMounted = false;
|
|
1232
|
+
};
|
|
1233
|
+
}, [routeKey, location.pathname, location.search, navigate, session, sessionLoaded]);
|
|
1234
|
+
if (checking) return /* @__PURE__ */ jsx3(MiddlewareLoader, {});
|
|
1235
|
+
if (!allowed) return null;
|
|
1236
|
+
return /* @__PURE__ */ jsx3("div", { className: "w-full h-full", children });
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/react/Router.tsx
|
|
1240
|
+
import { useLocation as useLocation2, useNavigate as useNavigate2 } from "react-router-dom";
|
|
1241
|
+
var getParams = (locationSearch) => {
|
|
1242
|
+
const params = new URLSearchParams(locationSearch);
|
|
1243
|
+
const queryObject = {};
|
|
1244
|
+
for (const [key, value] of params.entries()) {
|
|
1245
|
+
queryObject[key] = value;
|
|
1246
|
+
}
|
|
1247
|
+
return queryObject;
|
|
1248
|
+
};
|
|
1249
|
+
function useRouter() {
|
|
1250
|
+
const navigateHandler = useNavigate2();
|
|
1251
|
+
const location = useLocation2();
|
|
1252
|
+
const { session } = useSession();
|
|
1253
|
+
const navigate = async (path) => {
|
|
1254
|
+
const queryObject = getParams(location.search);
|
|
1255
|
+
const pageMw = getPageMiddleware(path);
|
|
1256
|
+
const handler = pageMw ?? getMiddlewareHandler();
|
|
1257
|
+
const result = await handler({ location: path, searchParams: queryObject, session });
|
|
1258
|
+
if (result?.success) {
|
|
1259
|
+
return navigateHandler(path);
|
|
1260
|
+
}
|
|
1261
|
+
if (result && !result.success && result.redirect) {
|
|
1262
|
+
return navigateHandler(result.redirect);
|
|
1263
|
+
}
|
|
1264
|
+
return;
|
|
1265
|
+
};
|
|
1266
|
+
return navigate;
|
|
1267
|
+
}
|
|
1268
|
+
export {
|
|
1269
|
+
AvatarProvider,
|
|
1270
|
+
DEFAULT_PAGE_ROUTE_RULES,
|
|
1271
|
+
Middleware,
|
|
1272
|
+
SessionContext,
|
|
1273
|
+
TranslationProvider,
|
|
1274
|
+
apiRequest,
|
|
1275
|
+
buildApiResponseEventName,
|
|
1276
|
+
buildApiStreamEventName,
|
|
1277
|
+
buildGetJoinedRoomsResponseEventName,
|
|
1278
|
+
buildJoinRoomResponseEventName,
|
|
1279
|
+
buildLeaveRoomResponseEventName,
|
|
1280
|
+
buildSyncProgressEventName,
|
|
1281
|
+
buildSyncResponseEventName,
|
|
1282
|
+
clearCsrfToken,
|
|
1283
|
+
dispatchClientHook,
|
|
1284
|
+
dispatchVetoableClientHook,
|
|
1285
|
+
enqueueApiRequest,
|
|
1286
|
+
enqueueSyncRequest,
|
|
1287
|
+
flushApiQueue,
|
|
1288
|
+
flushSyncQueue,
|
|
1289
|
+
getActiveLanguage,
|
|
1290
|
+
getApiQueueSize,
|
|
1291
|
+
getCsrfToken,
|
|
1292
|
+
getCurrentSession,
|
|
1293
|
+
getDefaultLocale,
|
|
1294
|
+
getLocaleByCode,
|
|
1295
|
+
getLogger,
|
|
1296
|
+
getMiddlewareHandler,
|
|
1297
|
+
getNotifier,
|
|
1298
|
+
getPageMiddleware,
|
|
1299
|
+
getProjectConfig,
|
|
1300
|
+
getRegisteredApiMethod,
|
|
1301
|
+
getRegisteredLocales,
|
|
1302
|
+
getSyncQueueSize,
|
|
1303
|
+
hasPageMiddleware,
|
|
1304
|
+
httpFetch,
|
|
1305
|
+
notify_default as i18nNotify,
|
|
1306
|
+
incrementResponseIndex,
|
|
1307
|
+
isApiMethodMapRegistered,
|
|
1308
|
+
isOnline,
|
|
1309
|
+
isProjectConfigRegistered,
|
|
1310
|
+
normalizeErrorResponseCore,
|
|
1311
|
+
notify,
|
|
1312
|
+
parseServiceRouteName,
|
|
1313
|
+
proposeLogin,
|
|
1314
|
+
registerApiMethodMap,
|
|
1315
|
+
registerClientHook,
|
|
1316
|
+
registerLanguageSource,
|
|
1317
|
+
registerLocales,
|
|
1318
|
+
registerLogger,
|
|
1319
|
+
registerMiddlewareHandler,
|
|
1320
|
+
registerNotifier,
|
|
1321
|
+
registerPageMiddleware,
|
|
1322
|
+
registerProjectConfig,
|
|
1323
|
+
removeApiQueueItem,
|
|
1324
|
+
removeApiQueueItemsByKey,
|
|
1325
|
+
removeSyncQueueItem,
|
|
1326
|
+
setLatestSession,
|
|
1327
|
+
setSocket,
|
|
1328
|
+
sleep,
|
|
1329
|
+
socket,
|
|
1330
|
+
socketEventNames,
|
|
1331
|
+
translate,
|
|
1332
|
+
tryCatch,
|
|
1333
|
+
useAvatarContext,
|
|
1334
|
+
useRouter,
|
|
1335
|
+
useSession,
|
|
1336
|
+
useTheme,
|
|
1337
|
+
useTranslation,
|
|
1338
|
+
useTranslator,
|
|
1339
|
+
useUpdateLanguage,
|
|
1340
|
+
validatePagePath,
|
|
1341
|
+
waitForSocket
|
|
1342
|
+
};
|
|
1343
|
+
//! isMounted check is load-bearing — without it, repeated middleware
|
|
1344
|
+
//# sourceMappingURL=client.js.map
|