@luckystack/core 0.1.8 → 0.2.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 CHANGED
@@ -1,485 +1,133 @@
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;
1
+ import {
2
+ DEFAULT_PAGE_ROUTE_RULES,
3
+ buildApiResponseEventName,
4
+ buildApiStreamEventName,
5
+ buildGetJoinedRoomsResponseEventName,
6
+ buildJoinRoomResponseEventName,
7
+ buildLeaveRoomResponseEventName,
8
+ buildSyncProgressEventName,
9
+ buildSyncResponseEventName,
10
+ clearCsrfToken,
11
+ dispatchClientHook,
12
+ dispatchVetoableClientHook,
13
+ enqueueApiRequest,
14
+ enqueueSyncRequest,
15
+ flushApiQueue,
16
+ flushSyncQueue,
17
+ getApiQueueSize,
18
+ getCsrfToken,
19
+ getNotifier,
20
+ getRegisteredApiMethod,
21
+ getSyncQueueSize,
22
+ httpFetch,
23
+ incrementResponseIndex,
24
+ isApiMethodMapRegistered,
25
+ isGetMethodName,
26
+ isOnline,
27
+ normalizeErrorResponseCore,
28
+ notify,
29
+ parseServiceRouteName,
30
+ registerApiMethodMap,
31
+ registerClientHook,
32
+ registerNotifier,
33
+ removeApiQueueItem,
34
+ removeApiQueueItemsByKey,
35
+ removeSyncQueueItem,
36
+ setSocket,
37
+ sleep,
38
+ socket,
39
+ socketEventNames,
40
+ tryCatchSync,
41
+ validatePagePath,
42
+ waitForSocket
43
+ } from "./chunk-OZTB2RL2.js";
44
+ import {
45
+ getProjectConfig,
46
+ isProjectConfigRegistered,
47
+ registerProjectConfig
48
+ } from "./chunk-W62GGT2P.js";
49
+ import {
50
+ getLogger,
51
+ registerLogger
52
+ } from "./chunk-V2WKGTZH.js";
53
+ import "./chunk-USQA6QTX.js";
155
54
 
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);
55
+ // src/apiInterceptors.ts
56
+ var requestInterceptors = /* @__PURE__ */ new Set();
57
+ var responseInterceptors = /* @__PURE__ */ new Set();
58
+ function registerApiRequestInterceptor(interceptor) {
59
+ requestInterceptors.add(interceptor);
60
+ return () => {
61
+ requestInterceptors.delete(interceptor);
62
+ };
63
+ }
64
+ function registerApiResponseInterceptor(interceptor) {
65
+ responseInterceptors.add(interceptor);
66
+ return () => {
67
+ responseInterceptors.delete(interceptor);
68
+ };
69
+ }
70
+ async function dispatchApiRequestInterceptors(ctx) {
71
+ if (requestInterceptors.size === 0) return;
72
+ const interceptors = [...requestInterceptors];
73
+ for (const interceptor of interceptors) {
74
+ try {
75
+ await interceptor(ctx);
76
+ } catch (error) {
77
+ getLogger().error("[apiRequestInterceptor] handler threw", error);
186
78
  }
187
79
  }
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;
80
+ }
81
+ function dispatchApiResponseInterceptors(ctx) {
82
+ if (responseInterceptors.size === 0) return;
83
+ const interceptors = [...responseInterceptors];
84
+ for (const interceptor of interceptors) {
85
+ try {
86
+ const result = interceptor(ctx);
87
+ if (result && typeof result.catch === "function") {
88
+ result.catch((error) => {
89
+ getLogger().error("[apiResponseInterceptor] async handler rejected", error);
90
+ });
91
+ }
92
+ } catch (error) {
93
+ getLogger().error("[apiResponseInterceptor] handler threw", error);
227
94
  }
228
95
  }
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
- };
96
+ }
262
97
 
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);
98
+ // src/apiRequest.ts
99
+ var abortControllers = /* @__PURE__ */ new Map();
100
+ var stableStringify = (value) => {
101
+ const seen = /* @__PURE__ */ new WeakSet();
102
+ const walk = (input) => {
103
+ if (input === null || typeof input !== "object") return input;
104
+ if (seen.has(input)) return "[circular]";
105
+ seen.add(input);
106
+ if (Array.isArray(input)) return input.map((entry) => walk(entry));
107
+ const record = input;
108
+ const out = {};
109
+ for (const key of Object.keys(record).toSorted()) {
110
+ out[key] = walk(record[key]);
322
111
  }
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
112
+ return out;
379
113
  };
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." };
114
+ const [error, json] = tryCatchSync(() => JSON.stringify(walk(value)));
115
+ if (error || typeof json !== "string") {
116
+ return `__unserializable__:${String(Date.now())}-${String(Math.random())}`;
393
117
  }
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-"
118
+ return json;
445
119
  };
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) => {
120
+ var buildAbortKey = (fullName, data) => `${fullName}::${stableStringify(data)}`;
121
+ var isGetMethodByPrefix = (apiName) => isGetMethodName(apiName.toLowerCase());
122
+ var resolveGetMethod = (apiName, version) => {
475
123
  const lastSlash = apiName.lastIndexOf("/");
476
124
  if (lastSlash > 0) {
477
125
  const pagePath = apiName.slice(0, lastSlash);
478
126
  const leaf = apiName.slice(lastSlash + 1);
479
127
  const method = getRegisteredApiMethod(pagePath, leaf, version);
480
- if (method) return method === "GET";
128
+ if (method) return { isGet: method === "GET", usingHeuristic: false };
481
129
  }
482
- return isGetMethodByPrefix(apiName);
130
+ return { isGet: isGetMethodByPrefix(apiName), usingHeuristic: true };
483
131
  };
484
132
  var canSendNow = (socketInstance) => {
485
133
  if (!socketInstance.connected) return false;
@@ -494,10 +142,12 @@ var shouldNotifyDev = () => getLogging().devNotifications;
494
142
  var shouldLogStream = () => getLogging().stream;
495
143
  var shouldUseAbortController = ({
496
144
  abortable,
497
- isGet
145
+ isGet,
146
+ usingHeuristic
498
147
  }) => {
499
148
  if (abortable === true) return true;
500
149
  if (abortable === false) return false;
150
+ if (usingHeuristic) return false;
501
151
  return isGet;
502
152
  };
503
153
  var normalizeApiError = ({
@@ -526,9 +176,9 @@ var normalizeApiError = ({
526
176
  };
527
177
  function apiRequest(params) {
528
178
  const runtimeParams = params;
529
- const { name, version, disableErrorMessage = false, onStream, signal: externalSignal } = runtimeParams;
179
+ const { name, version, disableErrorMessage = false, onStream, signal: externalSignal, timeoutMs } = runtimeParams;
530
180
  const payloadData = runtimeParams.data;
531
- return new Promise((resolve2, reject) => {
181
+ return new Promise((resolve2) => {
532
182
  void (async () => {
533
183
  if (externalSignal?.aborted) {
534
184
  resolve2(normalizeApiError({
@@ -597,58 +247,102 @@ function apiRequest(params) {
597
247
  }));
598
248
  return;
599
249
  }
600
- const isGet = isGetMethod(sanitizedName, version);
250
+ const { isGet, usingHeuristic } = resolveGetMethod(sanitizedName, version);
601
251
  const useAbortController = shouldUseAbortController({
602
252
  abortable: runtimeParams.abortable,
603
- isGet
253
+ isGet,
254
+ usingHeuristic
604
255
  });
605
256
  const fullName = `api/${sanitizedName}/${version}`;
257
+ const abortKey = buildAbortKey(fullName, data);
606
258
  let signal = null;
607
259
  let abortHandler = null;
608
260
  let queueId = null;
609
261
  let cleanupStreamListener = null;
262
+ let cleanupResponseListener = null;
263
+ let responseTimeout = null;
264
+ const clearResponseTimeout = () => {
265
+ if (responseTimeout) {
266
+ clearTimeout(responseTimeout);
267
+ responseTimeout = null;
268
+ }
269
+ };
610
270
  const cleanupAbortController = () => {
611
271
  if (signal && abortHandler) {
612
272
  signal.removeEventListener("abort", abortHandler);
613
273
  }
614
- abortControllers.delete(fullName);
274
+ abortControllers.delete(abortKey);
615
275
  };
616
276
  if (useAbortController) {
617
- if (abortControllers.has(fullName)) {
618
- const prevAbortController = abortControllers.get(fullName);
277
+ if (abortControllers.has(abortKey)) {
278
+ const prevAbortController = abortControllers.get(abortKey);
619
279
  prevAbortController?.abort();
620
280
  }
621
281
  const abortController = new AbortController();
622
- abortControllers.set(fullName, abortController);
282
+ abortControllers.set(abortKey, abortController);
623
283
  signal = abortController.signal;
624
284
  abortHandler = () => {
285
+ clearResponseTimeout();
625
286
  cleanupStreamListener?.();
626
287
  cleanupStreamListener = null;
288
+ cleanupResponseListener?.();
289
+ cleanupResponseListener = null;
627
290
  cleanupAbortController();
628
291
  if (queueId) {
629
292
  removeApiQueueItem(queueId);
630
293
  }
631
- reject(new Error(`Request ${fullName} aborted`));
294
+ resolve2(normalizeApiError({
295
+ response: { status: "error", errorCode: "request.aborted" },
296
+ fallbackErrorCode: "request.aborted"
297
+ }));
632
298
  };
633
299
  signal.addEventListener("abort", abortHandler);
634
300
  }
635
301
  const runRequest = (socketInstance) => {
636
302
  if (!canSendNow(socketInstance)) {
637
303
  queueId ??= createQueueId();
304
+ let suppressOnDrop = true;
638
305
  const enqueued = enqueueApiRequest({
639
306
  id: queueId,
640
307
  key: fullName,
641
308
  run: (nextSocket) => {
642
309
  runRequest(nextSocket);
643
310
  },
644
- createdAt: Date.now()
311
+ createdAt: Date.now(),
312
+ //? CORE: mirror SYNC-09. Settle the awaiting caller when this
313
+ //? QUEUED request is later EVICTED (drop-oldest by a future
314
+ //? enqueue / age expiry) instead of ever running. Without it the
315
+ //? promise hangs forever, since `run` is the only thing that could
316
+ //? otherwise resolve it — `apiRequest` had drifted from the sibling
317
+ //? `syncRequest` which already had this fix. Also tear down the
318
+ //? abort-controller registry entry so it isn't leaked.
319
+ onDrop: (reason) => {
320
+ if (suppressOnDrop) return;
321
+ cleanupAbortController();
322
+ resolve2(normalizeApiError({
323
+ response: { status: "error", errorCode: "offline.dropped", errorParams: [{ key: "reason", value: reason }] },
324
+ fallbackErrorCode: "offline.dropped"
325
+ }));
326
+ }
645
327
  });
328
+ suppressOnDrop = false;
646
329
  if (!enqueued) {
647
330
  resolve2(normalizeApiError({
648
331
  response: { status: "error", errorCode: "offline.queueFull", httpStatus: 503 },
649
332
  fallbackErrorCode: "offline.queueFull",
650
333
  fallbackHttpStatus: 503
651
334
  }));
335
+ } else if (externalSignal && queueId) {
336
+ const capturedQueueId = queueId;
337
+ const queueAbortHandler = () => {
338
+ removeApiQueueItem(capturedQueueId);
339
+ cleanupAbortController();
340
+ resolve2(normalizeApiError({
341
+ response: { status: "error", errorCode: "request.aborted" },
342
+ fallbackErrorCode: "request.aborted"
343
+ }));
344
+ };
345
+ externalSignal.addEventListener("abort", queueAbortHandler, { once: true });
652
346
  }
653
347
  return;
654
348
  }
@@ -660,9 +354,12 @@ function apiRequest(params) {
660
354
  let cleanupExternalAbort = null;
661
355
  if (externalSignal) {
662
356
  const externalAbortHandler = () => {
357
+ clearResponseTimeout();
663
358
  socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });
664
359
  cleanupStreamListener?.();
665
360
  cleanupStreamListener = null;
361
+ cleanupResponseListener?.();
362
+ cleanupResponseListener = null;
666
363
  cleanupAbortController();
667
364
  cleanupExternalAbort?.();
668
365
  cleanupExternalAbort = null;
@@ -676,6 +373,28 @@ function apiRequest(params) {
676
373
  externalSignal.removeEventListener("abort", externalAbortHandler);
677
374
  };
678
375
  }
376
+ clearResponseTimeout();
377
+ const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;
378
+ if (typeof effectiveTimeoutMs === "number" && effectiveTimeoutMs > 0) {
379
+ responseTimeout = setTimeout(() => {
380
+ responseTimeout = null;
381
+ cleanupStreamListener?.();
382
+ cleanupStreamListener = null;
383
+ cleanupResponseListener?.();
384
+ cleanupResponseListener = null;
385
+ cleanupExternalAbort?.();
386
+ cleanupExternalAbort = null;
387
+ cleanupAbortController();
388
+ if (queueId) {
389
+ removeApiQueueItem(queueId);
390
+ }
391
+ resolve2(normalizeApiError({
392
+ response: { status: "error", errorCode: "api.timeout", httpStatus: 504 },
393
+ fallbackErrorCode: "api.timeout",
394
+ fallbackHttpStatus: 504
395
+ }));
396
+ }, effectiveTimeoutMs);
397
+ }
679
398
  if (typeof onStream === "function") {
680
399
  const streamEventName = buildApiStreamEventName(tempIndex);
681
400
  const streamListener = (streamPayload) => {
@@ -695,17 +414,23 @@ function apiRequest(params) {
695
414
  if (shouldLogDev()) {
696
415
  getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });
697
416
  }
698
- socketInstance.once(buildApiResponseEventName(tempIndex), (response) => {
417
+ const responseEventName = buildApiResponseEventName(tempIndex);
418
+ const responseListener = (response) => {
699
419
  if (signal?.aborted) {
420
+ clearResponseTimeout();
700
421
  cleanupExternalAbort?.();
701
422
  cleanupExternalAbort = null;
702
423
  return;
703
424
  }
425
+ clearResponseTimeout();
704
426
  cleanupStreamListener?.();
705
427
  cleanupStreamListener = null;
706
428
  cleanupExternalAbort?.();
707
429
  cleanupExternalAbort = null;
430
+ cleanupResponseListener?.();
431
+ cleanupResponseListener = null;
708
432
  const status = response.status;
433
+ dispatchApiResponseInterceptors({ name: sanitizedName, version, response });
709
434
  if (shouldLogDev()) {
710
435
  getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });
711
436
  }
@@ -725,154 +450,63 @@ function apiRequest(params) {
725
450
  }
726
451
  cleanupAbortController();
727
452
  resolve2(response);
728
- });
453
+ };
454
+ socketInstance.once(responseEventName, responseListener);
455
+ cleanupResponseListener = () => {
456
+ socketInstance.off(responseEventName, responseListener);
457
+ };
458
+ const disconnectHandler = () => {
459
+ if (cleanupResponseListener) {
460
+ cleanupResponseListener();
461
+ cleanupResponseListener = null;
462
+ }
463
+ cleanupStreamListener?.();
464
+ cleanupStreamListener = null;
465
+ cleanupExternalAbort?.();
466
+ cleanupExternalAbort = null;
467
+ clearResponseTimeout();
468
+ cleanupAbortController();
469
+ resolve2(normalizeApiError({
470
+ response: { status: "error", errorCode: "api.disconnected" },
471
+ fallbackErrorCode: "api.disconnected"
472
+ }));
473
+ };
474
+ socketInstance.once("disconnect", disconnectHandler);
475
+ const originalCleanupResponseListener = cleanupResponseListener;
476
+ cleanupResponseListener = () => {
477
+ socketInstance.off("disconnect", disconnectHandler);
478
+ originalCleanupResponseListener();
479
+ };
729
480
  };
481
+ await dispatchApiRequestInterceptors({
482
+ name: sanitizedName,
483
+ version,
484
+ data
485
+ });
486
+ if (externalSignal?.aborted) {
487
+ resolve2(normalizeApiError({
488
+ response: { status: "error", errorCode: "request.aborted" },
489
+ fallbackErrorCode: "request.aborted"
490
+ }));
491
+ return;
492
+ }
730
493
  runRequest(socket);
731
494
  })();
732
495
  });
733
496
  }
734
497
 
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
498
+ // src/tryCatchClient.ts
765
499
  async function tryCatch(func, params, context) {
766
500
  try {
767
501
  const response = await func(params);
768
502
  return [null, response];
769
503
  } catch (error) {
504
+ const { captureException } = await import("./sentrySetup-Q4BKUAQ3.js");
770
505
  captureException(error, context);
771
506
  return [error, null];
772
507
  }
773
508
  }
774
509
 
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
510
  // src/localesRegistry.ts
877
511
  var activeLocales = {};
878
512
  var activeLanguageSource = null;
@@ -912,98 +546,6 @@ var registerPageMiddleware = (path, middleware) => {
912
546
  var getPageMiddleware = (path) => pageMiddlewares.get(path);
913
547
  var hasPageMiddleware = (path) => pageMiddlewares.has(path);
914
548
 
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
549
  // src/react/sessionContext.ts
1008
550
  import { createContext, use } from "react";
1009
551
  var DEFAULT_VALUE = {
@@ -1119,8 +661,10 @@ function translate({ translationList, key, params }) {
1119
661
  let finalResult = result;
1120
662
  for (const param of params) {
1121
663
  if (!param.key) continue;
1122
- const regex = new RegExp(`\\{\\{${param.key}\\}\\}`, "g");
1123
- finalResult = finalResult.replace(regex, String(param.value));
664
+ const escapedKey = param.key.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
665
+ const regex = new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g");
666
+ const replacement = String(param.value);
667
+ finalResult = finalResult.replace(regex, () => replacement);
1124
668
  }
1125
669
  return finalResult;
1126
670
  }
@@ -1223,7 +767,7 @@ function Middleware({ children }) {
1223
767
  } else if (result?.redirect) {
1224
768
  void navigate(result.redirect);
1225
769
  } else {
1226
- void navigate(-1);
770
+ void navigate(getProjectConfig().loginRedirectUrl ?? "/");
1227
771
  }
1228
772
  setChecking(false);
1229
773
  })();
@@ -1312,6 +856,8 @@ export {
1312
856
  parseServiceRouteName,
1313
857
  proposeLogin,
1314
858
  registerApiMethodMap,
859
+ registerApiRequestInterceptor,
860
+ registerApiResponseInterceptor,
1315
861
  registerClientHook,
1316
862
  registerLanguageSource,
1317
863
  registerLocales,