@luckystack/sync 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/dist/index.js ADDED
@@ -0,0 +1,1203 @@
1
+ // src/handleSyncRequest.ts
2
+ import { getSession } from "@luckystack/login";
3
+ import { getProjectConfig as getProjectConfig2 } from "@luckystack/core";
4
+ import { getRuntimeSyncMaps } from "@luckystack/core";
5
+ import {
6
+ validateRequest,
7
+ extractTokenFromSocket,
8
+ getIoInstance as getIoInstance2,
9
+ tryCatch,
10
+ parseTransportRouteName,
11
+ checkRateLimit,
12
+ buildSyncProgressEventName,
13
+ buildSyncResponseEventName,
14
+ socketEventNames as socketEventNames2,
15
+ dispatchHook as dispatchHook2,
16
+ validateInputByType,
17
+ getLogger as getLogger2
18
+ } from "@luckystack/core";
19
+ import { extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter } from "@luckystack/core";
20
+
21
+ // src/_shared/streamEmitters.ts
22
+ import {
23
+ dispatchHook,
24
+ getIoInstance,
25
+ getLogger,
26
+ getProjectConfig,
27
+ socketEventNames
28
+ } from "@luckystack/core";
29
+ var chunkCounters = /* @__PURE__ */ new Map();
30
+ var counterKey = (routeName, recipient) => `${routeName}|${recipient}`;
31
+ var bumpChunkIndex = (routeName, recipient) => {
32
+ const key = counterKey(routeName, recipient);
33
+ const next = (chunkCounters.get(key) ?? 0) + 1;
34
+ chunkCounters.set(key, next);
35
+ return next;
36
+ };
37
+ var dispatchStreamHooks = (routeName, recipient, chunk) => {
38
+ void dispatchHook("preSyncStream", { routeName, chunk, recipient });
39
+ const chunkIndex = bumpChunkIndex(routeName, recipient);
40
+ void dispatchHook("postSyncStream", { routeName, chunk, recipient, chunkIndex });
41
+ };
42
+ var shouldLogStream = () => getProjectConfig().logging.stream;
43
+ var DEFAULT_THRESHOLD_BYTES = 1048576;
44
+ var AVG_PACKET_BYTES = 1024;
45
+ var POLL_INTERVAL_MS = 10;
46
+ var MAX_SOCKETS_FOR_PRESSURE_SAMPLE = 32;
47
+ var isEngineConnLike = (value) => typeof value === "object" && value !== null;
48
+ var readSocketPressure = (socket) => {
49
+ const maybeConn = socket.conn;
50
+ if (!isEngineConnLike(maybeConn)) return { packets: 0, writable: true };
51
+ const packets = maybeConn.writeBuffer?.length ?? 0;
52
+ const writable = maybeConn.transport?.writable ?? true;
53
+ return { packets, writable };
54
+ };
55
+ var waitUntilSocketDrained = async (socket, packetThreshold, isAborted) => {
56
+ let { packets, writable } = readSocketPressure(socket);
57
+ while (writable && packets >= packetThreshold && !isAborted()) {
58
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
59
+ ({ packets, writable } = readSocketPressure(socket));
60
+ }
61
+ };
62
+ var collectRoomSocketsForPressure = (receiver) => {
63
+ const io = getIoInstance();
64
+ if (!io) return [];
65
+ if (!receiver) return [];
66
+ if (receiver === "all") {
67
+ const out2 = [];
68
+ let i2 = 0;
69
+ for (const [, sock] of io.sockets.sockets) {
70
+ if (i2 >= MAX_SOCKETS_FOR_PRESSURE_SAMPLE) break;
71
+ out2.push(sock);
72
+ i2++;
73
+ }
74
+ return out2;
75
+ }
76
+ const ids = io.sockets.adapter.rooms.get(receiver);
77
+ if (!ids || ids.size === 0) return [];
78
+ const out = [];
79
+ let i = 0;
80
+ for (const id of ids) {
81
+ if (i >= MAX_SOCKETS_FOR_PRESSURE_SAMPLE) break;
82
+ const sock = io.sockets.sockets.get(id);
83
+ if (sock) out.push(sock);
84
+ i++;
85
+ }
86
+ return out;
87
+ };
88
+ var buildSyncStreamEmitters = ({
89
+ cb,
90
+ receiver,
91
+ resolvedName,
92
+ emitOriginatorChunk,
93
+ logLabel,
94
+ signal,
95
+ originatorSocket
96
+ }) => {
97
+ const buildBroadcastFrame = (payload) => ({
98
+ ...payload,
99
+ cb,
100
+ fullName: resolvedName,
101
+ status: "stream"
102
+ });
103
+ const isAborted = () => signal?.aborted === true;
104
+ const logAbortedDrop = (kind) => {
105
+ if (shouldLogStream()) {
106
+ getLogger().debug(`${logLabel}: ${resolvedName} ${kind} skipped \u2014 request aborted`);
107
+ }
108
+ };
109
+ const emitServerSyncStream = (payload = {}) => {
110
+ if (isAborted()) {
111
+ logAbortedDrop("server stream");
112
+ return;
113
+ }
114
+ if (shouldLogStream()) {
115
+ getLogger().debug(`${logLabel}: ${resolvedName} server stream`, { payload });
116
+ }
117
+ dispatchStreamHooks(resolvedName, "originator", payload);
118
+ emitOriginatorChunk(payload);
119
+ };
120
+ const emitBroadcastSyncStream = (payload = {}) => {
121
+ if (isAborted()) {
122
+ logAbortedDrop("broadcastStream");
123
+ return;
124
+ }
125
+ if (shouldLogStream()) {
126
+ getLogger().debug(`${logLabel}: ${resolvedName} broadcastStream`, { payload });
127
+ }
128
+ if (!receiver) return;
129
+ const io = getIoInstance();
130
+ if (!io) return;
131
+ dispatchStreamHooks(resolvedName, receiver, payload);
132
+ io.to(receiver).emit(socketEventNames.sync, buildBroadcastFrame(payload));
133
+ };
134
+ const emitStreamToTokens = (tokens, payload = {}) => {
135
+ if (isAborted()) {
136
+ logAbortedDrop("streamTo");
137
+ return;
138
+ }
139
+ const list = Array.isArray(tokens) ? tokens : [tokens];
140
+ const filtered = list.filter((t) => typeof t === "string" && t.length > 0);
141
+ if (filtered.length === 0) return;
142
+ if (shouldLogStream()) {
143
+ getLogger().debug(`${logLabel}: ${resolvedName} streamTo`, { tokens: filtered, payload });
144
+ }
145
+ const io = getIoInstance();
146
+ if (!io) return;
147
+ for (const recipient of filtered) {
148
+ dispatchStreamHooks(resolvedName, recipient, payload);
149
+ }
150
+ const frame = buildBroadcastFrame(payload);
151
+ io.to(filtered).emit(socketEventNames.sync, frame);
152
+ };
153
+ const flushPressure = async ({ thresholdBytes } = {}) => {
154
+ if (isAborted()) return;
155
+ const effectiveThresholdBytes = typeof thresholdBytes === "number" && thresholdBytes > 0 ? thresholdBytes : DEFAULT_THRESHOLD_BYTES;
156
+ const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / AVG_PACKET_BYTES));
157
+ const targets = [];
158
+ if (originatorSocket) targets.push(originatorSocket);
159
+ for (const sock of collectRoomSocketsForPressure(receiver)) {
160
+ if (sock !== originatorSocket) targets.push(sock);
161
+ }
162
+ if (targets.length === 0) return;
163
+ await Promise.all(targets.map((sock) => waitUntilSocketDrained(sock, packetThreshold, isAborted)));
164
+ };
165
+ return {
166
+ emitServerSyncStream,
167
+ emitBroadcastSyncStream,
168
+ emitStreamToTokens,
169
+ buildBroadcastFrame,
170
+ flushPressure
171
+ };
172
+ };
173
+
174
+ // src/handleSyncRequest.ts
175
+ import {
176
+ registerSyncAbortController,
177
+ unregisterSyncAbortController
178
+ } from "@luckystack/core";
179
+ var shouldLogDev = () => getProjectConfig2().logging.devLogs;
180
+ var shouldLogStream2 = () => getProjectConfig2().logging.stream;
181
+ var applySyncRateLimits = async ({
182
+ resolvedName,
183
+ token,
184
+ socket,
185
+ user,
186
+ responseIndex,
187
+ buildSyncError,
188
+ preferredLocale
189
+ }) => {
190
+ const config = getProjectConfig2();
191
+ const defaultApiLimit = config.rateLimiting.defaultApiLimit;
192
+ if (defaultApiLimit !== false && defaultApiLimit > 0) {
193
+ const requesterIdentity = token ?? socket.handshake.address ?? "unknown";
194
+ const keyPrefix = token ? "token" : "ip";
195
+ const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
196
+ const { allowed, resetIn } = await checkRateLimit({
197
+ key: rateLimitKey,
198
+ limit: defaultApiLimit,
199
+ windowMs: config.rateLimiting.windowMs
200
+ });
201
+ if (!allowed) {
202
+ void dispatchHook2("rateLimitExceeded", {
203
+ scope: token ? "user" : "route",
204
+ key: rateLimitKey,
205
+ limit: defaultApiLimit,
206
+ windowMs: config.rateLimiting.windowMs,
207
+ count: defaultApiLimit + 1,
208
+ route: resolvedName,
209
+ userId: user?.id
210
+ });
211
+ if (typeof responseIndex === "number") {
212
+ socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
213
+ response: {
214
+ status: "error",
215
+ errorCode: "sync.rateLimitExceeded",
216
+ errorParams: [{ key: "seconds", value: resetIn }],
217
+ httpStatus: 429
218
+ },
219
+ preferred: preferredLocale,
220
+ userLanguage: user?.language
221
+ }));
222
+ }
223
+ return false;
224
+ }
225
+ }
226
+ const defaultIpLimit = config.rateLimiting.defaultIpLimit;
227
+ if (defaultIpLimit !== false && defaultIpLimit > 0) {
228
+ const requesterIp = socket.handshake.address ?? "unknown";
229
+ const ipKey = `ip:${requesterIp}:sync:all`;
230
+ const { allowed, resetIn } = await checkRateLimit({
231
+ key: ipKey,
232
+ limit: defaultIpLimit,
233
+ windowMs: config.rateLimiting.windowMs
234
+ });
235
+ if (!allowed) {
236
+ void dispatchHook2("rateLimitExceeded", {
237
+ scope: "ip",
238
+ key: ipKey,
239
+ limit: defaultIpLimit,
240
+ windowMs: config.rateLimiting.windowMs,
241
+ count: defaultIpLimit + 1,
242
+ ip: requesterIp
243
+ });
244
+ if (typeof responseIndex === "number") {
245
+ socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
246
+ response: {
247
+ status: "error",
248
+ errorCode: "sync.rateLimitExceeded",
249
+ errorParams: [{ key: "seconds", value: resetIn }],
250
+ httpStatus: 429
251
+ },
252
+ preferred: preferredLocale,
253
+ userLanguage: user?.language
254
+ }));
255
+ }
256
+ return false;
257
+ }
258
+ }
259
+ return true;
260
+ };
261
+ async function handleSyncRequest({ msg, socket, token }) {
262
+ const ioInstance = getIoInstance2();
263
+ if (!ioInstance) {
264
+ return;
265
+ }
266
+ if (typeof msg != "object") {
267
+ if (shouldLogDev()) {
268
+ getLogger2().warn("sync: socket message was not a json object");
269
+ }
270
+ const normalized = normalizeErrorResponse({
271
+ response: { status: "error", errorCode: "sync.invalidRequest" },
272
+ preferredLocale: extractLanguageFromHeader(socket.handshake.headers["x-language"]) || extractLanguageFromHeader(socket.handshake.headers["accept-language"])
273
+ });
274
+ return socket.emit(socketEventNames2.sync, {
275
+ status: normalized.status,
276
+ message: normalized.message,
277
+ errorCode: normalized.errorCode,
278
+ errorParams: normalized.errorParams,
279
+ httpStatus: normalized.httpStatus
280
+ });
281
+ }
282
+ const { name, data, cb, receiver: rawReceiver, responseIndex, ignoreSelf } = msg;
283
+ const receiver = typeof rawReceiver === "string" ? rawReceiver.trim() : "";
284
+ const preferredLocale = extractLanguageFromHeader(socket.handshake.headers["x-language"]) || extractLanguageFromHeader(socket.handshake.headers["accept-language"]);
285
+ let currentRouteName;
286
+ let currentPerRouteFormatter;
287
+ let currentUserId;
288
+ const buildSyncError = ({
289
+ response,
290
+ preferred,
291
+ userLanguage
292
+ }) => {
293
+ const normalized = normalizeErrorResponse({
294
+ response,
295
+ preferredLocale: preferred,
296
+ userLanguage
297
+ });
298
+ const baseEnvelope = {
299
+ status: normalized.status,
300
+ message: normalized.message,
301
+ errorCode: normalized.errorCode,
302
+ errorParams: normalized.errorParams,
303
+ httpStatus: normalized.httpStatus
304
+ };
305
+ return applyErrorFormatter({
306
+ response: baseEnvelope,
307
+ routeName: currentRouteName ?? "sync/unknown",
308
+ transport: "socket",
309
+ userId: currentUserId,
310
+ perRouteFormatter: currentPerRouteFormatter
311
+ });
312
+ };
313
+ const ensureSyncErrorShape = (response) => {
314
+ if (typeof response.errorCode === "string" && response.errorCode.trim().length > 0) {
315
+ return response;
316
+ }
317
+ return {
318
+ ...response,
319
+ errorCode: "sync.clientRejected"
320
+ };
321
+ };
322
+ if (!name || !data || typeof name != "string" || typeof data != "object") {
323
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
324
+ response: { status: "error", errorCode: "sync.invalidRequest" },
325
+ preferred: preferredLocale
326
+ }));
327
+ }
328
+ const normalizedData = data;
329
+ const parsedRoute = parseTransportRouteName({ value: name, prefix: "sync" });
330
+ if (parsedRoute.status === "error") {
331
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
332
+ response: {
333
+ status: "error",
334
+ errorCode: "routing.invalidServiceRouteName",
335
+ errorParams: [{ key: "name", value: name }]
336
+ },
337
+ preferred: preferredLocale
338
+ }));
339
+ }
340
+ const resolvedName = parsedRoute.normalizedFullName;
341
+ currentRouteName = resolvedName;
342
+ if (!cb || typeof cb != "string") {
343
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
344
+ response: { status: "error", errorCode: "sync.invalidCallback" },
345
+ preferred: preferredLocale
346
+ }));
347
+ }
348
+ if (!receiver) {
349
+ if (shouldLogDev()) {
350
+ getLogger2().warn("sync: missing receiver / roomCode", { receiver });
351
+ }
352
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
353
+ response: { status: "error", errorCode: "sync.missingReceiver" },
354
+ preferred: preferredLocale
355
+ }));
356
+ }
357
+ if (shouldLogDev()) {
358
+ getLogger2().debug(`sync: ${resolvedName} called`, { sync: resolvedName });
359
+ }
360
+ const user = await getSession(token);
361
+ currentUserId = user?.id;
362
+ const { syncObject, functionsObject } = await getRuntimeSyncMaps();
363
+ const abortController = new AbortController();
364
+ const abortKey = registerSyncAbortController(socket.id, cb, abortController);
365
+ const onSocketDisconnect = () => {
366
+ abortController.abort();
367
+ };
368
+ socket.once(socketEventNames2.disconnect, onSocketDisconnect);
369
+ let cleanupDone = false;
370
+ const cleanupRequest = () => {
371
+ if (cleanupDone) return;
372
+ cleanupDone = true;
373
+ socket.off(socketEventNames2.disconnect, onSocketDisconnect);
374
+ unregisterSyncAbortController(abortKey);
375
+ };
376
+ if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {
377
+ if (shouldLogDev()) {
378
+ getLogger2().warn(`sync: ${name} has no _client or _server file`, { sync: name });
379
+ }
380
+ cleanupRequest();
381
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
382
+ response: { status: "error", errorCode: "sync.notFound" },
383
+ preferred: preferredLocale,
384
+ userLanguage: user?.language
385
+ }));
386
+ }
387
+ const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
388
+ cb,
389
+ receiver,
390
+ resolvedName,
391
+ logLabel: "sync",
392
+ signal: abortController.signal,
393
+ originatorSocket: socket,
394
+ emitOriginatorChunk: (payload) => {
395
+ if (typeof responseIndex !== "number") return;
396
+ socket.emit(buildSyncProgressEventName(responseIndex), payload);
397
+ }
398
+ });
399
+ const serverSyncEntry = syncObject[`${resolvedName}_server`];
400
+ currentPerRouteFormatter = serverSyncEntry?.errorFormatter;
401
+ if (serverSyncEntry) {
402
+ const { auth } = serverSyncEntry;
403
+ if (auth.login && !user?.id) {
404
+ if (shouldLogDev()) {
405
+ getLogger2().warn(`sync: ${resolvedName} requires login`, { sync: resolvedName });
406
+ }
407
+ cleanupRequest();
408
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
409
+ response: { status: "error", errorCode: "auth.required" },
410
+ preferred: preferredLocale
411
+ }));
412
+ }
413
+ const validationResult = validateRequest({ auth, user });
414
+ if (validationResult.status === "error") {
415
+ if (shouldLogDev()) {
416
+ getLogger2().warn(`sync: auth failed for ${resolvedName}`, { sync: resolvedName, errorCode: validationResult.errorCode });
417
+ }
418
+ cleanupRequest();
419
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
420
+ response: {
421
+ status: "error",
422
+ errorCode: validationResult.errorCode || "auth.forbidden",
423
+ errorParams: validationResult.errorParams,
424
+ httpStatus: validationResult.httpStatus
425
+ },
426
+ preferred: preferredLocale,
427
+ userLanguage: user?.language
428
+ }));
429
+ }
430
+ }
431
+ const preAuthorizeResult = await dispatchHook2("preSyncAuthorize", {
432
+ routeName: resolvedName,
433
+ data: normalizedData,
434
+ user,
435
+ receiver,
436
+ transport: "socket"
437
+ });
438
+ if (preAuthorizeResult.stopped) {
439
+ if (shouldLogDev()) {
440
+ getLogger2().warn(`sync: preSyncAuthorize stopped ${resolvedName}`, { sync: resolvedName, errorCode: preAuthorizeResult.signal.errorCode });
441
+ }
442
+ cleanupRequest();
443
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
444
+ response: {
445
+ status: "error",
446
+ errorCode: preAuthorizeResult.signal.errorCode,
447
+ httpStatus: preAuthorizeResult.signal.httpStatus
448
+ },
449
+ preferred: preferredLocale,
450
+ userLanguage: user?.language
451
+ }));
452
+ }
453
+ void dispatchHook2("postSyncAuthorize", {
454
+ routeName: resolvedName,
455
+ data: normalizedData,
456
+ user,
457
+ receiver,
458
+ transport: "socket"
459
+ });
460
+ const rateLimitOk = await applySyncRateLimits({
461
+ resolvedName,
462
+ token,
463
+ socket,
464
+ user,
465
+ responseIndex,
466
+ buildSyncError,
467
+ preferredLocale
468
+ });
469
+ if (!rateLimitOk) {
470
+ cleanupRequest();
471
+ return;
472
+ }
473
+ let serverOutput = {};
474
+ if (serverSyncEntry) {
475
+ const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;
476
+ const inputValidation = await validateInputByType({
477
+ typeText: inputType,
478
+ value: normalizedData,
479
+ rootKey: "clientInput",
480
+ filePath: inputTypeFilePath
481
+ });
482
+ if (inputValidation.status === "error") {
483
+ cleanupRequest();
484
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
485
+ response: {
486
+ status: "error",
487
+ errorCode: "sync.invalidInputType",
488
+ errorParams: [{ key: "message", value: inputValidation.message }]
489
+ },
490
+ preferred: preferredLocale,
491
+ userLanguage: user?.language
492
+ }));
493
+ }
494
+ const [serverSyncError, serverSyncResult] = await tryCatch(
495
+ async () => await serverMain({
496
+ clientInput: normalizedData,
497
+ user,
498
+ functions: functionsObject,
499
+ roomCode: receiver,
500
+ stream: emitServerSyncStream,
501
+ broadcastStream: emitBroadcastSyncStream,
502
+ streamTo: emitStreamToTokens,
503
+ abortSignal: abortController.signal,
504
+ flushPressure
505
+ }),
506
+ void 0,
507
+ {
508
+ handler: "handleSyncRequest",
509
+ sync: resolvedName,
510
+ stage: "server",
511
+ userId: user?.id,
512
+ receiver,
513
+ transport: "socket"
514
+ }
515
+ );
516
+ if (serverSyncError) {
517
+ if (shouldLogDev()) {
518
+ getLogger2().error(`sync: server execution failed for ${resolvedName}`, serverSyncError, { sync: resolvedName });
519
+ }
520
+ cleanupRequest();
521
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
522
+ response: { status: "error", errorCode: "sync.serverExecutionFailed" },
523
+ preferred: preferredLocale,
524
+ userLanguage: user?.language
525
+ }));
526
+ } else if (serverSyncResult?.status == "error") {
527
+ const normalizedServerError = buildSyncError({
528
+ response: serverSyncResult,
529
+ preferred: preferredLocale,
530
+ userLanguage: user?.language
531
+ });
532
+ if (shouldLogDev()) {
533
+ getLogger2().warn(`sync: server returned error for ${resolvedName}`, { sync: resolvedName, message: normalizedServerError.message });
534
+ }
535
+ cleanupRequest();
536
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), normalizedServerError);
537
+ } else if (serverSyncResult?.status !== "success") {
538
+ if (shouldLogDev()) {
539
+ getLogger2().warn(`sync: ${resolvedName}_server returned invalid response`, { sync: resolvedName });
540
+ }
541
+ cleanupRequest();
542
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
543
+ response: { status: "error", errorCode: "sync.invalidServerResponse" },
544
+ preferred: preferredLocale,
545
+ userLanguage: user?.language
546
+ }));
547
+ } else if (serverSyncResult?.status == "success") {
548
+ serverOutput = serverSyncResult;
549
+ }
550
+ }
551
+ const sockets = receiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(receiver).fetchSockets();
552
+ if (sockets.length === 0) {
553
+ if (shouldLogDev()) {
554
+ getLogger2().warn("sync: no sockets found for receiver", { receiver, sync: resolvedName });
555
+ }
556
+ cleanupRequest();
557
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
558
+ response: { status: "error", errorCode: "sync.noReceiversFound" },
559
+ preferred: preferredLocale,
560
+ userLanguage: user?.language
561
+ }));
562
+ }
563
+ const fanoutPayload = {
564
+ routeName: resolvedName,
565
+ data: normalizedData,
566
+ user,
567
+ receiver,
568
+ serverOutput,
569
+ transport: "socket",
570
+ recipientCount: 0
571
+ };
572
+ const preFanoutResult = await dispatchHook2("preSyncFanout", fanoutPayload);
573
+ if (preFanoutResult.stopped) {
574
+ cleanupRequest();
575
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
576
+ response: {
577
+ status: "error",
578
+ errorCode: preFanoutResult.signal.errorCode,
579
+ httpStatus: preFanoutResult.signal.httpStatus
580
+ },
581
+ preferred: preferredLocale,
582
+ userLanguage: user?.language
583
+ }));
584
+ }
585
+ const { fanoutYieldEvery, fanoutYieldMs } = getProjectConfig2().sync;
586
+ let recipientCount = 0;
587
+ let tempCount = 1;
588
+ for (const tempSocket of sockets) {
589
+ tempCount++;
590
+ if (tempCount % fanoutYieldEvery === 0) {
591
+ await new Promise((resolve) => setTimeout(resolve, fanoutYieldMs));
592
+ }
593
+ const tempToken = extractTokenFromSocket(tempSocket);
594
+ if (ignoreSelf && typeof ignoreSelf == "boolean" && token == tempToken) {
595
+ continue;
596
+ }
597
+ recipientCount++;
598
+ if (syncObject[`${resolvedName}_client`]) {
599
+ const clientSyncHandler = syncObject[`${resolvedName}_client`];
600
+ const emitClientSyncStream = (payload = {}) => {
601
+ if (shouldLogStream2()) {
602
+ getLogger2().debug(`sync: ${resolvedName} client stream`, { payload });
603
+ }
604
+ tempSocket.emit(socketEventNames2.sync, {
605
+ ...payload,
606
+ cb,
607
+ fullName: resolvedName,
608
+ status: "stream"
609
+ });
610
+ };
611
+ const [clientSyncError, clientSyncResult] = await tryCatch(
612
+ async () => await clientSyncHandler({ clientInput: normalizedData, token: tempToken, functions: functionsObject, serverOutput, roomCode: receiver, stream: emitClientSyncStream }),
613
+ void 0,
614
+ {
615
+ handler: "handleSyncRequest",
616
+ sync: resolvedName,
617
+ stage: "client",
618
+ sourceUserId: user?.id,
619
+ targetToken: tempToken,
620
+ receiver,
621
+ transport: "socket"
622
+ }
623
+ );
624
+ if (clientSyncError) {
625
+ tempSocket.emit(socketEventNames2.sync, {
626
+ cb,
627
+ fullName: resolvedName,
628
+ ...buildSyncError({
629
+ response: { status: "error", errorCode: "sync.clientExecutionFailed" },
630
+ preferred: extractLanguageFromHeader(tempSocket.handshake.headers["x-language"]) || extractLanguageFromHeader(tempSocket.handshake.headers["accept-language"])
631
+ })
632
+ });
633
+ continue;
634
+ }
635
+ if (clientSyncResult?.status == "error") {
636
+ tempSocket.emit(socketEventNames2.sync, {
637
+ cb,
638
+ fullName: resolvedName,
639
+ ...buildSyncError({
640
+ response: ensureSyncErrorShape(clientSyncResult),
641
+ preferred: extractLanguageFromHeader(tempSocket.handshake.headers["x-language"]) || extractLanguageFromHeader(tempSocket.handshake.headers["accept-language"])
642
+ })
643
+ });
644
+ continue;
645
+ }
646
+ if (clientSyncResult?.status !== "success") {
647
+ tempSocket.emit(socketEventNames2.sync, {
648
+ cb,
649
+ fullName: resolvedName,
650
+ ...buildSyncError({
651
+ response: { status: "error", errorCode: "sync.invalidClientResponse" },
652
+ preferred: extractLanguageFromHeader(tempSocket.handshake.headers["x-language"]) || extractLanguageFromHeader(tempSocket.handshake.headers["accept-language"])
653
+ })
654
+ });
655
+ continue;
656
+ } else if (clientSyncResult?.status == "success") {
657
+ const result = {
658
+ cb,
659
+ fullName: resolvedName,
660
+ serverOutput,
661
+ clientOutput: clientSyncResult,
662
+ // Return from _client file (success only)
663
+ message: clientSyncResult.message || `${resolvedName} sync success`,
664
+ status: "success"
665
+ };
666
+ if (shouldLogDev()) {
667
+ getLogger2().debug(`sync: ${resolvedName} client success`, { result });
668
+ }
669
+ tempSocket.emit(socketEventNames2.sync, result);
670
+ }
671
+ } else {
672
+ const result = {
673
+ cb,
674
+ fullName: resolvedName,
675
+ serverOutput,
676
+ clientOutput: {},
677
+ // No client file, so empty output
678
+ message: `${resolvedName} sync success`,
679
+ status: "success"
680
+ };
681
+ if (shouldLogDev()) {
682
+ getLogger2().debug(`sync: ${resolvedName} server-only success`, { result });
683
+ }
684
+ tempSocket.emit(socketEventNames2.sync, result);
685
+ }
686
+ }
687
+ fanoutPayload.recipientCount = recipientCount;
688
+ await dispatchHook2("postSyncFanout", fanoutPayload);
689
+ cleanupRequest();
690
+ return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), {
691
+ status: "success",
692
+ message: `sync ${resolvedName} success`,
693
+ result: serverOutput
694
+ });
695
+ }
696
+
697
+ // src/handleHttpSyncRequest.ts
698
+ import { getSession as getSession2 } from "@luckystack/login";
699
+ import { getProjectConfig as getProjectConfig3 } from "@luckystack/core";
700
+ import { getRuntimeSyncMaps as getRuntimeSyncMapsFromSource } from "@luckystack/core";
701
+ import {
702
+ validateRequest as validateRequest2,
703
+ extractTokenFromSocket as extractTokenFromSocket2,
704
+ getIoInstance as getIoInstance3,
705
+ tryCatch as tryCatch2,
706
+ parseTransportRouteName as parseTransportRouteName2,
707
+ checkRateLimit as checkRateLimit2,
708
+ socketEventNames as socketEventNames3,
709
+ validateInputByType as validateInputByType2,
710
+ dispatchHook as dispatchHook3,
711
+ getLogger as getLogger3
712
+ } from "@luckystack/core";
713
+ import { extractLanguageFromHeader as extractLanguageFromHeader2, normalizeErrorResponse as normalizeErrorResponse2, applyErrorFormatter as applyErrorFormatter2 } from "@luckystack/core";
714
+ var shouldLogDev2 = () => getProjectConfig3().logging.devLogs;
715
+ var shouldLogStream3 = () => getProjectConfig3().logging.stream;
716
+ var applyHttpSyncRateLimits = async ({
717
+ resolvedName,
718
+ token,
719
+ requesterIp,
720
+ user,
721
+ buildSyncError,
722
+ preferredLocale
723
+ }) => {
724
+ const config = getProjectConfig3();
725
+ const effectiveSyncLimit = config.rateLimiting.defaultApiLimit;
726
+ if (effectiveSyncLimit !== false && effectiveSyncLimit > 0) {
727
+ const requesterIdentity = token ?? requesterIp ?? "anonymous";
728
+ const keyPrefix = token ? "token" : "ip";
729
+ const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
730
+ const { allowed, resetIn } = await checkRateLimit2({
731
+ key: rateLimitKey,
732
+ limit: effectiveSyncLimit,
733
+ windowMs: config.rateLimiting.windowMs
734
+ });
735
+ if (!allowed) {
736
+ void dispatchHook3("rateLimitExceeded", {
737
+ scope: token ? "user" : "route",
738
+ key: rateLimitKey,
739
+ limit: effectiveSyncLimit,
740
+ windowMs: config.rateLimiting.windowMs,
741
+ count: effectiveSyncLimit + 1,
742
+ route: resolvedName,
743
+ userId: user?.id
744
+ });
745
+ return buildSyncError({
746
+ response: {
747
+ status: "error",
748
+ errorCode: "sync.rateLimitExceeded",
749
+ errorParams: [{ key: "seconds", value: resetIn }],
750
+ httpStatus: 429
751
+ },
752
+ preferred: preferredLocale,
753
+ userLanguage: user?.language
754
+ });
755
+ }
756
+ }
757
+ const defaultIpLimit = config.rateLimiting.defaultIpLimit;
758
+ const requesterIsLoopback = process.env.NODE_ENV !== "production" && (requesterIp === "127.0.0.1" || requesterIp === "::1" || requesterIp === "::ffff:127.0.0.1" || typeof requesterIp === "string" && requesterIp.startsWith("127."));
759
+ if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {
760
+ const ipBucket = requesterIp ?? "unknown";
761
+ const ipKey = `ip:${ipBucket}:sync:all`;
762
+ const { allowed, resetIn } = await checkRateLimit2({
763
+ key: ipKey,
764
+ limit: defaultIpLimit,
765
+ windowMs: config.rateLimiting.windowMs
766
+ });
767
+ if (!allowed) {
768
+ void dispatchHook3("rateLimitExceeded", {
769
+ scope: "ip",
770
+ key: ipKey,
771
+ limit: defaultIpLimit,
772
+ windowMs: config.rateLimiting.windowMs,
773
+ count: defaultIpLimit + 1,
774
+ ip: ipBucket
775
+ });
776
+ return buildSyncError({
777
+ response: {
778
+ status: "error",
779
+ errorCode: "sync.rateLimitExceeded",
780
+ errorParams: [{ key: "seconds", value: resetIn }],
781
+ httpStatus: 429
782
+ },
783
+ preferred: preferredLocale,
784
+ userLanguage: user?.language
785
+ });
786
+ }
787
+ }
788
+ return null;
789
+ };
790
+ async function handleHttpSyncRequest({
791
+ name,
792
+ cb,
793
+ data,
794
+ receiver,
795
+ ignoreSelf,
796
+ token,
797
+ requesterIp,
798
+ xLanguageHeader,
799
+ acceptLanguageHeader,
800
+ stream,
801
+ abortSignal
802
+ }) {
803
+ if (shouldLogDev2()) {
804
+ getLogger3().debug(`http sync: ${name} called`);
805
+ }
806
+ const effectiveAbortSignal = abortSignal ?? new AbortController().signal;
807
+ const normalizedReceiver = typeof receiver === "string" ? receiver.trim() : "";
808
+ const preferredLocale = extractLanguageFromHeader2(xLanguageHeader) || extractLanguageFromHeader2(acceptLanguageHeader);
809
+ const user = await getSession2(token);
810
+ let currentRouteName;
811
+ let currentPerRouteFormatter;
812
+ const buildSyncError = ({
813
+ response,
814
+ preferred,
815
+ userLanguage
816
+ }) => {
817
+ const normalized = normalizeErrorResponse2({
818
+ response,
819
+ preferredLocale: preferred,
820
+ userLanguage
821
+ });
822
+ const baseEnvelope = {
823
+ status: normalized.status,
824
+ message: normalized.message,
825
+ errorCode: normalized.errorCode,
826
+ errorParams: normalized.errorParams,
827
+ httpStatus: normalized.httpStatus
828
+ };
829
+ return applyErrorFormatter2({
830
+ response: baseEnvelope,
831
+ routeName: currentRouteName ?? "sync/unknown",
832
+ transport: "http",
833
+ userId: user?.id,
834
+ perRouteFormatter: currentPerRouteFormatter
835
+ });
836
+ };
837
+ const ensureSyncErrorShape = (response) => {
838
+ if (typeof response.errorCode === "string" && response.errorCode.trim().length > 0) {
839
+ return response;
840
+ }
841
+ return {
842
+ ...response,
843
+ errorCode: "sync.clientRejected"
844
+ };
845
+ };
846
+ const ioInstance = getIoInstance3();
847
+ const [bodyError, bodyResult] = await tryCatch2(async () => {
848
+ if (!ioInstance) {
849
+ return buildSyncError({
850
+ response: { status: "error", errorCode: "sync.ioUnavailable" },
851
+ preferred: preferredLocale,
852
+ userLanguage: user?.language
853
+ });
854
+ }
855
+ if (!name || typeof name !== "string") {
856
+ return buildSyncError({
857
+ response: { status: "error", errorCode: "sync.invalidRequest" },
858
+ preferred: preferredLocale,
859
+ userLanguage: user?.language
860
+ });
861
+ }
862
+ const parsedRoute = parseTransportRouteName2({ value: name, prefix: "sync" });
863
+ if (parsedRoute.status === "error") {
864
+ return buildSyncError({
865
+ response: {
866
+ status: "error",
867
+ errorCode: "routing.invalidServiceRouteName",
868
+ errorParams: [{ key: "name", value: name }]
869
+ },
870
+ preferred: preferredLocale,
871
+ userLanguage: user?.language
872
+ });
873
+ }
874
+ const resolvedName = parsedRoute.normalizedFullName;
875
+ currentRouteName = resolvedName;
876
+ const callbackName = typeof cb === "string" && cb.trim().length > 0 ? cb.trim() : `${parsedRoute.serviceRoute.normalizedRouteName}/${parsedRoute.version}`;
877
+ if (!normalizedReceiver) {
878
+ return buildSyncError({
879
+ response: { status: "error", errorCode: "sync.missingReceiver" },
880
+ preferred: preferredLocale,
881
+ userLanguage: user?.language
882
+ });
883
+ }
884
+ const { syncObject, functionsObject } = await getRuntimeSyncMapsFromSource();
885
+ if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {
886
+ return buildSyncError({
887
+ response: { status: "error", errorCode: "sync.notFound" },
888
+ preferred: preferredLocale,
889
+ userLanguage: user?.language
890
+ });
891
+ }
892
+ const serverSyncEntry = syncObject[`${resolvedName}_server`];
893
+ currentPerRouteFormatter = serverSyncEntry?.errorFormatter;
894
+ if (serverSyncEntry) {
895
+ const { auth } = serverSyncEntry;
896
+ if (auth.login && !user?.id) {
897
+ return buildSyncError({
898
+ response: { status: "error", errorCode: "auth.required" },
899
+ preferred: preferredLocale
900
+ });
901
+ }
902
+ const validationResult = validateRequest2({ auth, user });
903
+ if (validationResult.status === "error") {
904
+ return buildSyncError({
905
+ response: {
906
+ status: "error",
907
+ errorCode: validationResult.errorCode || "auth.forbidden",
908
+ errorParams: validationResult.errorParams,
909
+ httpStatus: validationResult.httpStatus
910
+ },
911
+ preferred: preferredLocale,
912
+ userLanguage: user?.language
913
+ });
914
+ }
915
+ }
916
+ const preAuthorizeResult = await dispatchHook3("preSyncAuthorize", {
917
+ routeName: resolvedName,
918
+ data,
919
+ user,
920
+ receiver: normalizedReceiver,
921
+ transport: "http"
922
+ });
923
+ if (preAuthorizeResult.stopped) {
924
+ return buildSyncError({
925
+ response: {
926
+ status: "error",
927
+ errorCode: preAuthorizeResult.signal.errorCode,
928
+ httpStatus: preAuthorizeResult.signal.httpStatus
929
+ },
930
+ preferred: preferredLocale,
931
+ userLanguage: user?.language
932
+ });
933
+ }
934
+ const rateLimitResult = await applyHttpSyncRateLimits({
935
+ resolvedName,
936
+ token,
937
+ requesterIp,
938
+ user,
939
+ buildSyncError,
940
+ preferredLocale
941
+ });
942
+ if (rateLimitResult) return rateLimitResult;
943
+ let serverOutput = {};
944
+ if (serverSyncEntry) {
945
+ const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;
946
+ const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
947
+ cb,
948
+ receiver: normalizedReceiver,
949
+ resolvedName,
950
+ logLabel: "http sync",
951
+ signal: effectiveAbortSignal,
952
+ //? No originatorSocket for HTTP/SSE — `flushPressure` falls back
953
+ //? to room-socket measurement only. SSE backpressure is the
954
+ //? caller's responsibility (Node's `res.write` returns a bool).
955
+ //? Originator chunks travel back via SSE; broadcast / targeted
956
+ //? chunks still flow over Socket.io to recipients in the receiver room.
957
+ emitOriginatorChunk: (payload) => {
958
+ stream?.(payload);
959
+ }
960
+ });
961
+ const inputValidation = await validateInputByType2({
962
+ typeText: inputType,
963
+ value: data,
964
+ rootKey: "clientInput",
965
+ filePath: inputTypeFilePath
966
+ });
967
+ if (inputValidation.status === "error") {
968
+ return buildSyncError({
969
+ response: {
970
+ status: "error",
971
+ errorCode: "sync.invalidInputType",
972
+ errorParams: [{ key: "message", value: inputValidation.message }]
973
+ },
974
+ preferred: preferredLocale,
975
+ userLanguage: user?.language
976
+ });
977
+ }
978
+ const [serverSyncError, serverSyncResult] = await tryCatch2(
979
+ async () => await serverMain({
980
+ clientInput: data,
981
+ user,
982
+ functions: functionsObject,
983
+ roomCode: normalizedReceiver,
984
+ stream: emitServerSyncStream,
985
+ broadcastStream: emitBroadcastSyncStream,
986
+ streamTo: emitStreamToTokens,
987
+ abortSignal: effectiveAbortSignal,
988
+ flushPressure
989
+ }),
990
+ void 0,
991
+ {
992
+ handler: "handleHttpSyncRequest",
993
+ sync: resolvedName,
994
+ stage: "server",
995
+ userId: user?.id,
996
+ receiver,
997
+ transport: "http"
998
+ }
999
+ );
1000
+ if (serverSyncError) {
1001
+ return buildSyncError({
1002
+ response: { status: "error", errorCode: "sync.serverExecutionFailed" },
1003
+ preferred: preferredLocale,
1004
+ userLanguage: user?.language
1005
+ });
1006
+ }
1007
+ if (serverSyncResult?.status == "error") {
1008
+ return buildSyncError({
1009
+ response: serverSyncResult,
1010
+ preferred: preferredLocale,
1011
+ userLanguage: user?.language
1012
+ });
1013
+ }
1014
+ if (serverSyncResult?.status !== "success") {
1015
+ return buildSyncError({
1016
+ response: { status: "error", errorCode: "sync.invalidServerResponse" },
1017
+ preferred: preferredLocale,
1018
+ userLanguage: user?.language
1019
+ });
1020
+ }
1021
+ serverOutput = serverSyncResult;
1022
+ }
1023
+ const fanoutPayload = {
1024
+ routeName: resolvedName,
1025
+ data,
1026
+ user,
1027
+ receiver: normalizedReceiver,
1028
+ serverOutput,
1029
+ transport: "http",
1030
+ recipientCount: 0
1031
+ };
1032
+ await dispatchHook3("preSyncFanout", fanoutPayload);
1033
+ const sockets = receiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(normalizedReceiver).fetchSockets();
1034
+ let recipientCount = 0;
1035
+ for (const tempSocket of sockets) {
1036
+ const tempToken = extractTokenFromSocket2(tempSocket);
1037
+ if (ignoreSelf && token && token === tempToken) {
1038
+ continue;
1039
+ }
1040
+ if (syncObject[`${resolvedName}_client`]) {
1041
+ const clientSyncHandler = syncObject[`${resolvedName}_client`];
1042
+ const emitClientSyncStream = (payload = {}) => {
1043
+ if (shouldLogStream3()) {
1044
+ getLogger3().debug(`http sync: ${resolvedName} client stream`, { payload });
1045
+ }
1046
+ tempSocket.emit(socketEventNames3.sync, {
1047
+ ...payload,
1048
+ cb: callbackName,
1049
+ fullName: resolvedName,
1050
+ status: "stream"
1051
+ });
1052
+ };
1053
+ const [clientSyncError, clientSyncResult] = await tryCatch2(
1054
+ async () => await clientSyncHandler({ clientInput: data, token: tempToken, functions: functionsObject, serverOutput, roomCode: normalizedReceiver, stream: emitClientSyncStream }),
1055
+ void 0,
1056
+ {
1057
+ handler: "handleHttpSyncRequest",
1058
+ sync: resolvedName,
1059
+ stage: "client",
1060
+ sourceUserId: user?.id,
1061
+ targetToken: tempToken,
1062
+ receiver,
1063
+ transport: "http"
1064
+ }
1065
+ );
1066
+ if (clientSyncError) {
1067
+ tempSocket.emit(socketEventNames3.sync, {
1068
+ cb: callbackName,
1069
+ fullName: resolvedName,
1070
+ ...buildSyncError({
1071
+ response: { status: "error", errorCode: "sync.clientExecutionFailed" },
1072
+ preferred: extractLanguageFromHeader2(tempSocket.handshake.headers["accept-language"] || tempSocket.handshake.headers["x-language"])
1073
+ })
1074
+ });
1075
+ continue;
1076
+ }
1077
+ if (clientSyncResult?.status === "error") {
1078
+ tempSocket.emit(socketEventNames3.sync, {
1079
+ cb: callbackName,
1080
+ fullName: resolvedName,
1081
+ ...buildSyncError({
1082
+ response: ensureSyncErrorShape(clientSyncResult),
1083
+ preferred: extractLanguageFromHeader2(tempSocket.handshake.headers["accept-language"] || tempSocket.handshake.headers["x-language"])
1084
+ })
1085
+ });
1086
+ continue;
1087
+ }
1088
+ if (clientSyncResult?.status !== "success") {
1089
+ tempSocket.emit(socketEventNames3.sync, {
1090
+ cb: callbackName,
1091
+ fullName: resolvedName,
1092
+ ...buildSyncError({
1093
+ response: { status: "error", errorCode: "sync.invalidClientResponse" },
1094
+ preferred: extractLanguageFromHeader2(tempSocket.handshake.headers["accept-language"] || tempSocket.handshake.headers["x-language"])
1095
+ })
1096
+ });
1097
+ continue;
1098
+ }
1099
+ tempSocket.emit(socketEventNames3.sync, {
1100
+ cb: callbackName,
1101
+ fullName: resolvedName,
1102
+ serverOutput,
1103
+ clientOutput: clientSyncResult,
1104
+ message: clientSyncResult.message || `${resolvedName} sync success`,
1105
+ status: "success"
1106
+ });
1107
+ recipientCount++;
1108
+ continue;
1109
+ }
1110
+ tempSocket.emit(socketEventNames3.sync, {
1111
+ cb: callbackName,
1112
+ fullName: resolvedName,
1113
+ serverOutput,
1114
+ clientOutput: {},
1115
+ message: `${resolvedName} sync success`,
1116
+ status: "success"
1117
+ });
1118
+ recipientCount++;
1119
+ }
1120
+ fanoutPayload.recipientCount = recipientCount;
1121
+ await dispatchHook3("postSyncFanout", fanoutPayload);
1122
+ if (shouldLogDev2()) {
1123
+ getLogger3().debug(`http sync: ${resolvedName} completed`);
1124
+ }
1125
+ const serverMessage = serverOutput.message;
1126
+ return {
1127
+ ...serverOutput,
1128
+ status: "success",
1129
+ message: typeof serverMessage === "string" ? serverMessage : `${resolvedName} sync success`
1130
+ };
1131
+ });
1132
+ if (bodyError) {
1133
+ getLogger3().error(`http sync: ${name} threw`, bodyError, { sync: name });
1134
+ return buildSyncError({
1135
+ response: { status: "error", errorCode: "sync.serverExecutionFailed" },
1136
+ preferred: preferredLocale,
1137
+ userLanguage: user?.language
1138
+ });
1139
+ }
1140
+ return bodyResult ?? buildSyncError({
1141
+ response: { status: "error", errorCode: "sync.serverExecutionFailed" },
1142
+ preferred: preferredLocale,
1143
+ userLanguage: user?.language
1144
+ });
1145
+ }
1146
+
1147
+ // src/streamThrottle.ts
1148
+ import { getProjectConfig as getProjectConfig4 } from "@luckystack/core";
1149
+ var createStreamThrottle = (options = {}) => {
1150
+ const defaults = getProjectConfig4().sync.streamThrottle;
1151
+ const flushAtChars = options.flushAtChars ?? defaults.flushAtChars;
1152
+ const flushEveryMs = options.flushEveryMs ?? defaults.flushEveryMs;
1153
+ const field = options.field ?? defaults.field;
1154
+ let buffer = "";
1155
+ let timer = null;
1156
+ const clearTimer = () => {
1157
+ if (timer !== null) {
1158
+ clearTimeout(timer);
1159
+ timer = null;
1160
+ }
1161
+ };
1162
+ const flushNow = (emit) => {
1163
+ if (buffer.length === 0) {
1164
+ clearTimer();
1165
+ return;
1166
+ }
1167
+ const payload = { [field]: buffer };
1168
+ buffer = "";
1169
+ clearTimer();
1170
+ emit(payload);
1171
+ };
1172
+ return {
1173
+ push: (text, emit) => {
1174
+ if (!text) return;
1175
+ buffer += text;
1176
+ if (buffer.length >= flushAtChars) {
1177
+ flushNow(emit);
1178
+ return;
1179
+ }
1180
+ if (flushEveryMs !== false && timer === null) {
1181
+ timer = setTimeout(() => {
1182
+ flushNow(emit);
1183
+ }, flushEveryMs);
1184
+ if (typeof timer === "object" && "unref" in timer) {
1185
+ timer.unref();
1186
+ }
1187
+ }
1188
+ },
1189
+ flush: (emit) => {
1190
+ flushNow(emit);
1191
+ },
1192
+ reset: () => {
1193
+ buffer = "";
1194
+ clearTimer();
1195
+ }
1196
+ };
1197
+ };
1198
+ export {
1199
+ createStreamThrottle,
1200
+ handleHttpSyncRequest,
1201
+ handleSyncRequest
1202
+ };
1203
+ //# sourceMappingURL=index.js.map