@luckystack/sync 0.1.9 → 0.2.1
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 -14
- package/CLAUDE.md +122 -104
- package/LICENSE +21 -21
- package/README.md +155 -155
- package/dist/client.d.ts +3 -1
- package/dist/client.js +235 -87
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +1251 -635
- package/dist/index.js.map +1 -1
- package/docs/error-states.md +5 -4
- package/docs/server-vs-client-handlers.md +2 -0
- package/docs/streaming.md +349 -349
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,45 +1,57 @@
|
|
|
1
1
|
// src/handleSyncRequest.ts
|
|
2
|
-
import {
|
|
3
|
-
import { getProjectConfig as getProjectConfig2 } from "@luckystack/core";
|
|
4
|
-
import { getRuntimeSyncMaps } from "@luckystack/core";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
5
3
|
import {
|
|
4
|
+
readSession,
|
|
5
|
+
getProjectConfig as getProjectConfig3,
|
|
6
|
+
getRuntimeSyncMaps,
|
|
6
7
|
validateRequest,
|
|
7
8
|
extractTokenFromSocket,
|
|
9
|
+
formatRoomName,
|
|
8
10
|
getIoInstance as getIoInstance2,
|
|
9
|
-
tryCatch,
|
|
11
|
+
tryCatch as tryCatch2,
|
|
10
12
|
parseTransportRouteName,
|
|
11
13
|
checkRateLimit,
|
|
12
14
|
buildSyncProgressEventName,
|
|
13
15
|
buildSyncResponseEventName,
|
|
14
|
-
socketEventNames as
|
|
16
|
+
socketEventNames as socketEventNames3,
|
|
15
17
|
dispatchHook as dispatchHook2,
|
|
16
18
|
validateInputByType,
|
|
17
|
-
getLogger as
|
|
19
|
+
getLogger as getLogger3,
|
|
20
|
+
resolveClientIp,
|
|
21
|
+
isLoopbackIp,
|
|
22
|
+
extractLanguageFromHeader,
|
|
23
|
+
normalizeErrorResponse as normalizeErrorResponse2,
|
|
24
|
+
runWithErrorTrackerIdentityScope,
|
|
25
|
+
setCurrentErrorTrackerIdentity,
|
|
26
|
+
registerSyncAbortController,
|
|
27
|
+
unregisterSyncAbortController,
|
|
28
|
+
deriveTokenBucketId
|
|
18
29
|
} from "@luckystack/core";
|
|
19
|
-
import { extractLanguageFromHeader, normalizeErrorResponse, applyErrorFormatter } from "@luckystack/core";
|
|
20
30
|
|
|
21
31
|
// src/_shared/streamEmitters.ts
|
|
22
32
|
import {
|
|
23
33
|
dispatchHook,
|
|
24
34
|
getIoInstance,
|
|
25
35
|
getLogger,
|
|
26
|
-
getProjectConfig,
|
|
36
|
+
getProjectConfig as getProjectConfig2,
|
|
27
37
|
socketEventNames
|
|
28
38
|
} from "@luckystack/core";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
};
|
|
39
|
+
|
|
40
|
+
// src/_shared/logFlags.ts
|
|
41
|
+
import { getProjectConfig } from "@luckystack/core";
|
|
42
|
+
var shouldLogDev = () => getProjectConfig().logging.devLogs;
|
|
42
43
|
var shouldLogStream = () => getProjectConfig().logging.stream;
|
|
44
|
+
|
|
45
|
+
// src/_shared/redactToken.ts
|
|
46
|
+
var VISIBLE_PREFIX = 8;
|
|
47
|
+
var redactToken = (token) => {
|
|
48
|
+
if (!token) return token;
|
|
49
|
+
if (token.length <= VISIBLE_PREFIX) return token;
|
|
50
|
+
return `${token.slice(0, VISIBLE_PREFIX)}\u2026`;
|
|
51
|
+
};
|
|
52
|
+
var redactTokens = (tokens) => tokens.map((t) => redactToken(t) ?? t);
|
|
53
|
+
|
|
54
|
+
// src/_shared/streamEmitters.ts
|
|
43
55
|
var DEFAULT_THRESHOLD_BYTES = 1048576;
|
|
44
56
|
var AVG_PACKET_BYTES = 1024;
|
|
45
57
|
var POLL_INTERVAL_MS = 10;
|
|
@@ -94,6 +106,17 @@ var buildSyncStreamEmitters = ({
|
|
|
94
106
|
signal,
|
|
95
107
|
originatorSocket
|
|
96
108
|
}) => {
|
|
109
|
+
const chunkCounters = /* @__PURE__ */ new Map();
|
|
110
|
+
const bumpChunkIndex = (recipient) => {
|
|
111
|
+
const next = (chunkCounters.get(recipient) ?? 0) + 1;
|
|
112
|
+
chunkCounters.set(recipient, next);
|
|
113
|
+
return next;
|
|
114
|
+
};
|
|
115
|
+
const dispatchStreamHooks = (recipient, chunk) => {
|
|
116
|
+
void dispatchHook("preSyncStream", { routeName: resolvedName, chunk, recipient });
|
|
117
|
+
const chunkIndex = bumpChunkIndex(recipient);
|
|
118
|
+
void dispatchHook("postSyncStream", { routeName: resolvedName, chunk, recipient, chunkIndex });
|
|
119
|
+
};
|
|
97
120
|
const buildBroadcastFrame = (payload) => ({
|
|
98
121
|
...payload,
|
|
99
122
|
cb,
|
|
@@ -114,7 +137,7 @@ var buildSyncStreamEmitters = ({
|
|
|
114
137
|
if (shouldLogStream()) {
|
|
115
138
|
getLogger().debug(`${logLabel}: ${resolvedName} server stream`, { payload });
|
|
116
139
|
}
|
|
117
|
-
dispatchStreamHooks(
|
|
140
|
+
dispatchStreamHooks("originator", payload);
|
|
118
141
|
emitOriginatorChunk(payload);
|
|
119
142
|
};
|
|
120
143
|
const emitBroadcastSyncStream = (payload = {}) => {
|
|
@@ -128,7 +151,7 @@ var buildSyncStreamEmitters = ({
|
|
|
128
151
|
if (!receiver) return;
|
|
129
152
|
const io = getIoInstance();
|
|
130
153
|
if (!io) return;
|
|
131
|
-
dispatchStreamHooks(
|
|
154
|
+
dispatchStreamHooks(receiver, payload);
|
|
132
155
|
io.to(receiver).emit(socketEventNames.sync, buildBroadcastFrame(payload));
|
|
133
156
|
};
|
|
134
157
|
const emitStreamToTokens = (tokens, payload = {}) => {
|
|
@@ -140,19 +163,20 @@ var buildSyncStreamEmitters = ({
|
|
|
140
163
|
const filtered = list.filter((t) => typeof t === "string" && t.length > 0);
|
|
141
164
|
if (filtered.length === 0) return;
|
|
142
165
|
if (shouldLogStream()) {
|
|
143
|
-
getLogger().debug(`${logLabel}: ${resolvedName} streamTo`, { tokens: filtered, payload });
|
|
166
|
+
getLogger().debug(`${logLabel}: ${resolvedName} streamTo`, { tokens: redactTokens(filtered), payload });
|
|
144
167
|
}
|
|
145
168
|
const io = getIoInstance();
|
|
146
169
|
if (!io) return;
|
|
147
170
|
for (const recipient of filtered) {
|
|
148
|
-
dispatchStreamHooks(
|
|
171
|
+
dispatchStreamHooks(recipient, payload);
|
|
149
172
|
}
|
|
150
173
|
const frame = buildBroadcastFrame(payload);
|
|
151
174
|
io.to(filtered).emit(socketEventNames.sync, frame);
|
|
152
175
|
};
|
|
153
176
|
const flushPressure = async ({ thresholdBytes } = {}) => {
|
|
154
177
|
if (isAborted()) return;
|
|
155
|
-
const
|
|
178
|
+
const configuredBytes = getProjectConfig2().sync.flushPressure.maxBufferedBytes;
|
|
179
|
+
const effectiveThresholdBytes = typeof thresholdBytes === "number" && thresholdBytes > 0 ? thresholdBytes : typeof configuredBytes === "number" && configuredBytes > 0 ? configuredBytes : DEFAULT_THRESHOLD_BYTES;
|
|
156
180
|
const packetThreshold = Math.max(1, Math.ceil(effectiveThresholdBytes / AVG_PACKET_BYTES));
|
|
157
181
|
const targets = [];
|
|
158
182
|
if (originatorSocket) targets.push(originatorSocket);
|
|
@@ -171,13 +195,171 @@ var buildSyncStreamEmitters = ({
|
|
|
171
195
|
};
|
|
172
196
|
};
|
|
173
197
|
|
|
174
|
-
// src/
|
|
198
|
+
// src/_shared/errorBuilders.ts
|
|
175
199
|
import {
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
applyErrorFormatter,
|
|
201
|
+
normalizeErrorResponse
|
|
178
202
|
} from "@luckystack/core";
|
|
179
|
-
var
|
|
180
|
-
|
|
203
|
+
var buildFormattedError = ({
|
|
204
|
+
response,
|
|
205
|
+
preferred,
|
|
206
|
+
userLanguage,
|
|
207
|
+
routeName,
|
|
208
|
+
transport,
|
|
209
|
+
userId,
|
|
210
|
+
perRouteFormatter
|
|
211
|
+
}) => {
|
|
212
|
+
const normalized = normalizeErrorResponse({
|
|
213
|
+
response,
|
|
214
|
+
preferredLocale: preferred,
|
|
215
|
+
userLanguage
|
|
216
|
+
});
|
|
217
|
+
const baseEnvelope = {
|
|
218
|
+
status: "error",
|
|
219
|
+
message: normalized.message,
|
|
220
|
+
errorCode: normalized.errorCode,
|
|
221
|
+
errorParams: normalized.errorParams,
|
|
222
|
+
httpStatus: normalized.httpStatus
|
|
223
|
+
};
|
|
224
|
+
return applyErrorFormatter({
|
|
225
|
+
response: baseEnvelope,
|
|
226
|
+
routeName: routeName ?? "sync/unknown",
|
|
227
|
+
transport,
|
|
228
|
+
userId,
|
|
229
|
+
perRouteFormatter
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
var ensureSyncErrorShape = (response) => {
|
|
233
|
+
if (typeof response.errorCode === "string" && response.errorCode.trim().length > 0) {
|
|
234
|
+
return response;
|
|
235
|
+
}
|
|
236
|
+
return { ...response, errorCode: "sync.clientRejected" };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/_shared/clientFanout.ts
|
|
240
|
+
import { tryCatch, socketEventNames as socketEventNames2, getLogger as getLogger2 } from "@luckystack/core";
|
|
241
|
+
var processClientSyncForRecipient = async ({
|
|
242
|
+
tempSocket,
|
|
243
|
+
tempToken,
|
|
244
|
+
clientSyncHandler,
|
|
245
|
+
data,
|
|
246
|
+
functionsObject,
|
|
247
|
+
serverOutput,
|
|
248
|
+
receiver,
|
|
249
|
+
resolvedName,
|
|
250
|
+
callbackKey,
|
|
251
|
+
transport,
|
|
252
|
+
handlerName,
|
|
253
|
+
logLabel,
|
|
254
|
+
shouldLogDev: shouldLogDev2,
|
|
255
|
+
shouldLogStream: shouldLogStream2,
|
|
256
|
+
buildSyncError,
|
|
257
|
+
resolvePreferredLocale,
|
|
258
|
+
sourceUserId
|
|
259
|
+
}) => {
|
|
260
|
+
const emitClientSyncStream = (payload = {}) => {
|
|
261
|
+
if (shouldLogStream2()) {
|
|
262
|
+
getLogger2().debug(`${logLabel}: ${resolvedName} client stream`, { payload });
|
|
263
|
+
}
|
|
264
|
+
tempSocket.emit(socketEventNames2.sync, {
|
|
265
|
+
...payload,
|
|
266
|
+
cb: callbackKey,
|
|
267
|
+
fullName: resolvedName,
|
|
268
|
+
status: "stream"
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
const [clientSyncError, clientSyncResult] = await tryCatch(
|
|
272
|
+
async () => await clientSyncHandler({ clientInput: data, token: tempToken, functions: functionsObject, serverOutput, roomCode: receiver, stream: emitClientSyncStream }),
|
|
273
|
+
void 0,
|
|
274
|
+
{
|
|
275
|
+
handler: handlerName,
|
|
276
|
+
sync: resolvedName,
|
|
277
|
+
stage: "client",
|
|
278
|
+
sourceUserId,
|
|
279
|
+
//? Redact the raw bearer session token before it lands in the
|
|
280
|
+
//? error-tracker context (`captureException`) — a verbatim token there is
|
|
281
|
+
//? a usable credential (defeats HttpOnly-cookie mode). The 8-char prefix
|
|
282
|
+
//? still correlates error events.
|
|
283
|
+
targetToken: redactToken(tempToken),
|
|
284
|
+
receiver,
|
|
285
|
+
transport
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
const preferred = resolvePreferredLocale(tempSocket.handshake.headers);
|
|
289
|
+
if (clientSyncError) {
|
|
290
|
+
tempSocket.emit(socketEventNames2.sync, {
|
|
291
|
+
cb: callbackKey,
|
|
292
|
+
fullName: resolvedName,
|
|
293
|
+
...buildSyncError({
|
|
294
|
+
response: { status: "error", errorCode: "sync.clientExecutionFailed" },
|
|
295
|
+
preferred
|
|
296
|
+
})
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (clientSyncResult?.status === "error") {
|
|
301
|
+
tempSocket.emit(socketEventNames2.sync, {
|
|
302
|
+
cb: callbackKey,
|
|
303
|
+
fullName: resolvedName,
|
|
304
|
+
...buildSyncError({
|
|
305
|
+
response: ensureSyncErrorShape(clientSyncResult),
|
|
306
|
+
preferred
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (clientSyncResult?.status !== "success") {
|
|
312
|
+
tempSocket.emit(socketEventNames2.sync, {
|
|
313
|
+
cb: callbackKey,
|
|
314
|
+
fullName: resolvedName,
|
|
315
|
+
...buildSyncError({
|
|
316
|
+
response: { status: "error", errorCode: "sync.invalidClientResponse" },
|
|
317
|
+
preferred
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const result = {
|
|
323
|
+
cb: callbackKey,
|
|
324
|
+
fullName: resolvedName,
|
|
325
|
+
serverOutput,
|
|
326
|
+
clientOutput: clientSyncResult,
|
|
327
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty message should fall back to the default too
|
|
328
|
+
message: clientSyncResult.message || `${resolvedName} sync success`,
|
|
329
|
+
status: "success"
|
|
330
|
+
};
|
|
331
|
+
if (shouldLogDev2()) {
|
|
332
|
+
getLogger2().debug(`${logLabel}: ${resolvedName} client success`, { result });
|
|
333
|
+
}
|
|
334
|
+
tempSocket.emit(socketEventNames2.sync, result);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/_shared/validationMode.ts
|
|
338
|
+
var resolveSyncValidationMode = (validation) => {
|
|
339
|
+
if (!validation) return "strict";
|
|
340
|
+
if (typeof validation === "string") {
|
|
341
|
+
return validation === "relaxed" ? "relaxed" : "strict";
|
|
342
|
+
}
|
|
343
|
+
return validation.input === "skip" ? "relaxed" : "strict";
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/_shared/receiverAuth.ts
|
|
347
|
+
var authorizeSyncReceiver = ({
|
|
348
|
+
receiver,
|
|
349
|
+
allowClientReceiverAll,
|
|
350
|
+
requireRoomMembership,
|
|
351
|
+
isMember
|
|
352
|
+
}) => {
|
|
353
|
+
if (receiver === "all" && !allowClientReceiverAll) {
|
|
354
|
+
return { allowed: false, errorCode: "sync.receiverNotAllowed" };
|
|
355
|
+
}
|
|
356
|
+
if (requireRoomMembership && receiver !== "all" && !isMember?.()) {
|
|
357
|
+
return { allowed: false, errorCode: "sync.notRoomMember" };
|
|
358
|
+
}
|
|
359
|
+
return { allowed: true };
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/handleSyncRequest.ts
|
|
181
363
|
var applySyncRateLimits = async ({
|
|
182
364
|
resolvedName,
|
|
183
365
|
token,
|
|
@@ -185,13 +367,22 @@ var applySyncRateLimits = async ({
|
|
|
185
367
|
user,
|
|
186
368
|
responseIndex,
|
|
187
369
|
buildSyncError,
|
|
188
|
-
preferredLocale
|
|
370
|
+
preferredLocale,
|
|
371
|
+
routeLimit
|
|
189
372
|
}) => {
|
|
190
|
-
const config =
|
|
191
|
-
const
|
|
373
|
+
const config = getProjectConfig3();
|
|
374
|
+
const resolvedIp = resolveClientIp({
|
|
375
|
+
rawAddress: socket.handshake.address,
|
|
376
|
+
headers: socket.handshake.headers,
|
|
377
|
+
trustProxy: config.http.trustProxy,
|
|
378
|
+
trustedProxyHopCount: config.http.trustedProxyHopCount
|
|
379
|
+
});
|
|
380
|
+
const defaultApiLimit = routeLimit === void 0 ? config.rateLimiting.defaultApiLimit : routeLimit;
|
|
192
381
|
if (defaultApiLimit !== false && defaultApiLimit > 0) {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
382
|
+
const identityCb = config.rateLimiting.identity;
|
|
383
|
+
const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: resolvedIp, transport: "socket" }) ?? null;
|
|
384
|
+
const requesterIdentity = customIdentity?.id ?? (token ? deriveTokenBucketId(token) : resolvedIp);
|
|
385
|
+
const keyPrefix = customIdentity?.scope ?? (token ? "token" : "ip");
|
|
195
386
|
const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
|
|
196
387
|
const { allowed, resetIn } = await checkRateLimit({
|
|
197
388
|
key: rateLimitKey,
|
|
@@ -200,13 +391,17 @@ var applySyncRateLimits = async ({
|
|
|
200
391
|
});
|
|
201
392
|
if (!allowed) {
|
|
202
393
|
void dispatchHook2("rateLimitExceeded", {
|
|
203
|
-
|
|
394
|
+
//? Parity with the API handler: an anonymous per-route bucket is IP-keyed,
|
|
395
|
+
//? so the scope is `ip` (with `route` still set to mark it a per-route
|
|
396
|
+
//? bucket vs the global `:sync:all` IP bucket), never `route`.
|
|
397
|
+
scope: token ? "user" : "ip",
|
|
204
398
|
key: rateLimitKey,
|
|
205
399
|
limit: defaultApiLimit,
|
|
206
400
|
windowMs: config.rateLimiting.windowMs,
|
|
207
401
|
count: defaultApiLimit + 1,
|
|
208
402
|
route: resolvedName,
|
|
209
|
-
userId: user?.id
|
|
403
|
+
userId: user?.id,
|
|
404
|
+
ip: token ? void 0 : resolvedIp
|
|
210
405
|
});
|
|
211
406
|
if (typeof responseIndex === "number") {
|
|
212
407
|
socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
|
|
@@ -224,8 +419,9 @@ var applySyncRateLimits = async ({
|
|
|
224
419
|
}
|
|
225
420
|
}
|
|
226
421
|
const defaultIpLimit = config.rateLimiting.defaultIpLimit;
|
|
227
|
-
|
|
228
|
-
|
|
422
|
+
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev && process.env.NODE_ENV !== "production" && isLoopbackIp(resolvedIp);
|
|
423
|
+
if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {
|
|
424
|
+
const requesterIp = resolvedIp;
|
|
229
425
|
const ipKey = `ip:${requesterIp}:sync:all`;
|
|
230
426
|
const { allowed, resetIn } = await checkRateLimit({
|
|
231
427
|
key: ipKey,
|
|
@@ -258,176 +454,237 @@ var applySyncRateLimits = async ({
|
|
|
258
454
|
}
|
|
259
455
|
return true;
|
|
260
456
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
457
|
+
function rejectSync({
|
|
458
|
+
socket,
|
|
459
|
+
responseIndex,
|
|
460
|
+
buildSyncError,
|
|
461
|
+
response,
|
|
462
|
+
preferred,
|
|
463
|
+
userLanguage
|
|
464
|
+
}) {
|
|
465
|
+
if (typeof responseIndex === "number") {
|
|
466
|
+
socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
|
|
467
|
+
response,
|
|
468
|
+
preferred,
|
|
469
|
+
userLanguage
|
|
470
|
+
}));
|
|
265
471
|
}
|
|
472
|
+
}
|
|
473
|
+
function validateSyncMessage({
|
|
474
|
+
msg,
|
|
475
|
+
socket,
|
|
476
|
+
preferredLocale
|
|
477
|
+
}) {
|
|
266
478
|
if (typeof msg != "object") {
|
|
267
479
|
if (shouldLogDev()) {
|
|
268
|
-
|
|
480
|
+
getLogger3().warn("sync: socket message was not a json object");
|
|
269
481
|
}
|
|
270
|
-
const normalized =
|
|
482
|
+
const normalized = normalizeErrorResponse2({
|
|
271
483
|
response: { status: "error", errorCode: "sync.invalidRequest" },
|
|
272
|
-
preferredLocale
|
|
484
|
+
preferredLocale
|
|
273
485
|
});
|
|
274
|
-
|
|
486
|
+
socket.emit(socketEventNames3.sync, {
|
|
275
487
|
status: normalized.status,
|
|
276
488
|
message: normalized.message,
|
|
277
489
|
errorCode: normalized.errorCode,
|
|
278
490
|
errorParams: normalized.errorParams,
|
|
279
491
|
httpStatus: normalized.httpStatus
|
|
280
492
|
});
|
|
493
|
+
return false;
|
|
281
494
|
}
|
|
282
|
-
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
function parseSyncFields({
|
|
498
|
+
msg,
|
|
499
|
+
socket,
|
|
500
|
+
responseIndex,
|
|
501
|
+
preferredLocale,
|
|
502
|
+
buildSyncError
|
|
503
|
+
}) {
|
|
504
|
+
const { name, data, cb, receiver: rawReceiver, ignoreSelf } = msg;
|
|
283
505
|
const receiver = typeof rawReceiver === "string" ? rawReceiver.trim() : "";
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
}));
|
|
506
|
+
if (!name || !data || typeof name != "string" || typeof data != "object" || Array.isArray(data)) {
|
|
507
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.invalidRequest" }, preferred: preferredLocale });
|
|
508
|
+
return { ok: false };
|
|
327
509
|
}
|
|
328
|
-
const normalizedData = data;
|
|
329
510
|
const parsedRoute = parseTransportRouteName({ value: name, prefix: "sync" });
|
|
330
511
|
if (parsedRoute.status === "error") {
|
|
331
|
-
|
|
512
|
+
rejectSync({
|
|
513
|
+
socket,
|
|
514
|
+
responseIndex,
|
|
515
|
+
buildSyncError,
|
|
332
516
|
response: {
|
|
333
517
|
status: "error",
|
|
334
518
|
errorCode: "routing.invalidServiceRouteName",
|
|
335
519
|
errorParams: [{ key: "name", value: name }]
|
|
336
520
|
},
|
|
337
521
|
preferred: preferredLocale
|
|
338
|
-
})
|
|
522
|
+
});
|
|
523
|
+
return { ok: false };
|
|
339
524
|
}
|
|
340
525
|
const resolvedName = parsedRoute.normalizedFullName;
|
|
341
|
-
currentRouteName = resolvedName;
|
|
342
526
|
if (!cb || typeof cb != "string") {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
preferred: preferredLocale
|
|
346
|
-
}));
|
|
527
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.invalidCallback" }, preferred: preferredLocale });
|
|
528
|
+
return { ok: false };
|
|
347
529
|
}
|
|
348
530
|
if (!receiver) {
|
|
349
531
|
if (shouldLogDev()) {
|
|
350
|
-
|
|
532
|
+
getLogger3().warn("sync: missing receiver / roomCode", { receiver });
|
|
351
533
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
preferred: preferredLocale
|
|
355
|
-
}));
|
|
356
|
-
}
|
|
357
|
-
if (shouldLogDev()) {
|
|
358
|
-
getLogger2().debug(`sync: ${resolvedName} called`, { sync: resolvedName });
|
|
534
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.missingReceiver" }, preferred: preferredLocale });
|
|
535
|
+
return { ok: false };
|
|
359
536
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
537
|
+
return {
|
|
538
|
+
ok: true,
|
|
539
|
+
name,
|
|
540
|
+
normalizedData: data,
|
|
541
|
+
cb,
|
|
542
|
+
receiver,
|
|
543
|
+
ignoreSelf: typeof ignoreSelf === "boolean" ? ignoreSelf : void 0,
|
|
544
|
+
resolvedName
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function setupAbortController({
|
|
548
|
+
socket,
|
|
549
|
+
responseIndex
|
|
550
|
+
}) {
|
|
551
|
+
const cancelId = randomUUID();
|
|
363
552
|
const abortController = new AbortController();
|
|
364
|
-
const abortKey = registerSyncAbortController(socket.id,
|
|
553
|
+
const abortKey = registerSyncAbortController(socket.id, cancelId, abortController);
|
|
554
|
+
if (typeof responseIndex === "number") {
|
|
555
|
+
socket.emit(buildSyncProgressEventName(responseIndex), { __cancelId: cancelId });
|
|
556
|
+
}
|
|
365
557
|
const onSocketDisconnect = () => {
|
|
366
558
|
abortController.abort();
|
|
367
559
|
};
|
|
368
|
-
socket.once(
|
|
560
|
+
socket.once(socketEventNames3.disconnect, onSocketDisconnect);
|
|
369
561
|
let cleanupDone = false;
|
|
370
562
|
const cleanupRequest = () => {
|
|
371
563
|
if (cleanupDone) return;
|
|
372
564
|
cleanupDone = true;
|
|
373
|
-
socket.off(
|
|
565
|
+
socket.off(socketEventNames3.disconnect, onSocketDisconnect);
|
|
374
566
|
unregisterSyncAbortController(abortKey);
|
|
375
567
|
};
|
|
568
|
+
return { abortController, abortKey, cleanupRequest };
|
|
569
|
+
}
|
|
570
|
+
function checkRouteExists({
|
|
571
|
+
syncObject,
|
|
572
|
+
resolvedName,
|
|
573
|
+
name,
|
|
574
|
+
socket,
|
|
575
|
+
responseIndex,
|
|
576
|
+
buildSyncError,
|
|
577
|
+
preferredLocale,
|
|
578
|
+
user,
|
|
579
|
+
cleanupRequest
|
|
580
|
+
}) {
|
|
376
581
|
if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {
|
|
377
582
|
if (shouldLogDev()) {
|
|
378
|
-
|
|
583
|
+
getLogger3().warn(`sync: ${name} has no _client or _server file`, { sync: name });
|
|
584
|
+
}
|
|
585
|
+
cleanupRequest();
|
|
586
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.notFound" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
if (!syncObject[`${resolvedName}_server`]) {
|
|
590
|
+
if (shouldLogDev()) {
|
|
591
|
+
getLogger3().warn(`sync: ${resolvedName} has a _client file but no required _server file`, { sync: resolvedName });
|
|
592
|
+
}
|
|
593
|
+
cleanupRequest();
|
|
594
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.notFound" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
function runSyncAuth({
|
|
600
|
+
serverSyncEntry,
|
|
601
|
+
user,
|
|
602
|
+
resolvedName,
|
|
603
|
+
socket,
|
|
604
|
+
responseIndex,
|
|
605
|
+
buildSyncError,
|
|
606
|
+
preferredLocale,
|
|
607
|
+
cleanupRequest
|
|
608
|
+
}) {
|
|
609
|
+
if (!serverSyncEntry) return true;
|
|
610
|
+
const { auth } = serverSyncEntry;
|
|
611
|
+
if (auth.login && !user?.id) {
|
|
612
|
+
if (shouldLogDev()) {
|
|
613
|
+
getLogger3().warn(`sync: ${resolvedName} requires login`, { sync: resolvedName });
|
|
614
|
+
}
|
|
615
|
+
cleanupRequest();
|
|
616
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "auth.required" }, preferred: preferredLocale });
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if (auth.additional && auth.additional.length > 0 && !user) {
|
|
620
|
+
if (shouldLogDev()) {
|
|
621
|
+
getLogger3().warn(`sync: ${resolvedName} requires a session for additional auth`, { sync: resolvedName });
|
|
622
|
+
}
|
|
623
|
+
cleanupRequest();
|
|
624
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "auth.required" }, preferred: preferredLocale });
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
const validationResult = validateRequest({ auth, user });
|
|
628
|
+
if (validationResult.status === "error") {
|
|
629
|
+
if (shouldLogDev()) {
|
|
630
|
+
getLogger3().warn(`sync: auth failed for ${resolvedName}`, { sync: resolvedName, errorCode: validationResult.errorCode });
|
|
379
631
|
}
|
|
380
632
|
cleanupRequest();
|
|
381
|
-
|
|
382
|
-
|
|
633
|
+
rejectSync({
|
|
634
|
+
socket,
|
|
635
|
+
responseIndex,
|
|
636
|
+
buildSyncError,
|
|
637
|
+
response: {
|
|
638
|
+
status: "error",
|
|
639
|
+
errorCode: validationResult.errorCode || "auth.forbidden",
|
|
640
|
+
errorParams: validationResult.errorParams,
|
|
641
|
+
httpStatus: validationResult.httpStatus
|
|
642
|
+
},
|
|
383
643
|
preferred: preferredLocale,
|
|
384
644
|
userLanguage: user?.language
|
|
385
|
-
})
|
|
645
|
+
});
|
|
646
|
+
return false;
|
|
386
647
|
}
|
|
387
|
-
|
|
388
|
-
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
function runReceiverAuth({
|
|
651
|
+
receiver,
|
|
652
|
+
resolvedName,
|
|
653
|
+
socket,
|
|
654
|
+
responseIndex,
|
|
655
|
+
buildSyncError,
|
|
656
|
+
preferredLocale,
|
|
657
|
+
user,
|
|
658
|
+
cleanupRequest
|
|
659
|
+
}) {
|
|
660
|
+
const syncConfig = getProjectConfig3().sync;
|
|
661
|
+
const receiverAuth = authorizeSyncReceiver({
|
|
389
662
|
receiver,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
originatorSocket: socket,
|
|
394
|
-
emitOriginatorChunk: (payload) => {
|
|
395
|
-
if (typeof responseIndex !== "number") return;
|
|
396
|
-
socket.emit(buildSyncProgressEventName(responseIndex), payload);
|
|
397
|
-
}
|
|
663
|
+
allowClientReceiverAll: syncConfig.allowClientReceiverAll,
|
|
664
|
+
requireRoomMembership: syncConfig.requireRoomMembership,
|
|
665
|
+
isMember: () => socket.rooms.has(receiver)
|
|
398
666
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
}));
|
|
667
|
+
if (!receiverAuth.allowed) {
|
|
668
|
+
if (shouldLogDev()) {
|
|
669
|
+
getLogger3().warn(`sync: receiver authorization failed for ${resolvedName}`, { sync: resolvedName, receiver, errorCode: receiverAuth.errorCode });
|
|
429
670
|
}
|
|
671
|
+
cleanupRequest();
|
|
672
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: receiverAuth.errorCode, httpStatus: 403 }, preferred: preferredLocale, userLanguage: user?.language });
|
|
673
|
+
return false;
|
|
430
674
|
}
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
async function runPreAuthorizeHook({
|
|
678
|
+
resolvedName,
|
|
679
|
+
normalizedData,
|
|
680
|
+
user,
|
|
681
|
+
receiver,
|
|
682
|
+
socket,
|
|
683
|
+
responseIndex,
|
|
684
|
+
buildSyncError,
|
|
685
|
+
preferredLocale,
|
|
686
|
+
cleanupRequest
|
|
687
|
+
}) {
|
|
431
688
|
const preAuthorizeResult = await dispatchHook2("preSyncAuthorize", {
|
|
432
689
|
routeName: resolvedName,
|
|
433
690
|
data: normalizedData,
|
|
@@ -437,10 +694,13 @@ async function handleSyncRequest({ msg, socket, token }) {
|
|
|
437
694
|
});
|
|
438
695
|
if (preAuthorizeResult.stopped) {
|
|
439
696
|
if (shouldLogDev()) {
|
|
440
|
-
|
|
697
|
+
getLogger3().warn(`sync: preSyncAuthorize stopped ${resolvedName}`, { sync: resolvedName, errorCode: preAuthorizeResult.signal.errorCode });
|
|
441
698
|
}
|
|
442
699
|
cleanupRequest();
|
|
443
|
-
|
|
700
|
+
rejectSync({
|
|
701
|
+
socket,
|
|
702
|
+
responseIndex,
|
|
703
|
+
buildSyncError,
|
|
444
704
|
response: {
|
|
445
705
|
status: "error",
|
|
446
706
|
errorCode: preAuthorizeResult.signal.errorCode,
|
|
@@ -448,7 +708,8 @@ async function handleSyncRequest({ msg, socket, token }) {
|
|
|
448
708
|
},
|
|
449
709
|
preferred: preferredLocale,
|
|
450
710
|
userLanguage: user?.language
|
|
451
|
-
})
|
|
711
|
+
});
|
|
712
|
+
return false;
|
|
452
713
|
}
|
|
453
714
|
void dispatchHook2("postSyncAuthorize", {
|
|
454
715
|
routeName: resolvedName,
|
|
@@ -457,108 +718,198 @@ async function handleSyncRequest({ msg, socket, token }) {
|
|
|
457
718
|
receiver,
|
|
458
719
|
transport: "socket"
|
|
459
720
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
async function runSyncServerExecution({
|
|
724
|
+
serverSyncEntry,
|
|
725
|
+
resolvedName,
|
|
726
|
+
normalizedData,
|
|
727
|
+
user,
|
|
728
|
+
receiver,
|
|
729
|
+
socket,
|
|
730
|
+
responseIndex,
|
|
731
|
+
buildSyncError,
|
|
732
|
+
preferredLocale,
|
|
733
|
+
cleanupRequest,
|
|
734
|
+
functionsObject,
|
|
735
|
+
emitServerSyncStream,
|
|
736
|
+
emitBroadcastSyncStream,
|
|
737
|
+
emitStreamToTokens,
|
|
738
|
+
flushPressure,
|
|
739
|
+
abortController
|
|
740
|
+
}) {
|
|
741
|
+
if (!serverSyncEntry) return { ok: true, serverOutput: {} };
|
|
742
|
+
const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;
|
|
743
|
+
const preValidateResult = await dispatchHook2("preSyncValidate", {
|
|
744
|
+
routeName: resolvedName,
|
|
745
|
+
data: normalizedData,
|
|
464
746
|
user,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
preferredLocale
|
|
747
|
+
receiver,
|
|
748
|
+
transport: "socket"
|
|
468
749
|
});
|
|
469
|
-
if (
|
|
750
|
+
if (preValidateResult.stopped) {
|
|
470
751
|
cleanupRequest();
|
|
471
|
-
|
|
752
|
+
rejectSync({
|
|
753
|
+
socket,
|
|
754
|
+
responseIndex,
|
|
755
|
+
buildSyncError,
|
|
756
|
+
response: {
|
|
757
|
+
status: "error",
|
|
758
|
+
errorCode: preValidateResult.signal.errorCode,
|
|
759
|
+
httpStatus: preValidateResult.signal.httpStatus
|
|
760
|
+
},
|
|
761
|
+
preferred: preferredLocale,
|
|
762
|
+
userLanguage: user?.language
|
|
763
|
+
});
|
|
764
|
+
return { ok: false };
|
|
472
765
|
}
|
|
473
|
-
|
|
474
|
-
if (serverSyncEntry) {
|
|
475
|
-
const { main: serverMain, inputType, inputTypeFilePath } = serverSyncEntry;
|
|
766
|
+
if (resolveSyncValidationMode(serverSyncEntry.validation) === "strict") {
|
|
476
767
|
const inputValidation = await validateInputByType({
|
|
477
768
|
typeText: inputType,
|
|
478
769
|
value: normalizedData,
|
|
479
770
|
rootKey: "clientInput",
|
|
480
771
|
filePath: inputTypeFilePath
|
|
481
772
|
});
|
|
773
|
+
void dispatchHook2("postSyncValidate", {
|
|
774
|
+
routeName: resolvedName,
|
|
775
|
+
data: normalizedData,
|
|
776
|
+
user,
|
|
777
|
+
receiver,
|
|
778
|
+
transport: "socket",
|
|
779
|
+
validation: inputValidation.status === "error" ? { status: "error", message: inputValidation.message } : { status: "success" }
|
|
780
|
+
});
|
|
482
781
|
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
782
|
if (shouldLogDev()) {
|
|
518
|
-
|
|
783
|
+
getLogger3().warn(`sync: input validation failed for ${resolvedName}`, { sync: resolvedName, message: inputValidation.message });
|
|
519
784
|
}
|
|
520
785
|
cleanupRequest();
|
|
521
|
-
|
|
522
|
-
|
|
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;
|
|
786
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.invalidInputType" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
787
|
+
return { ok: false };
|
|
549
788
|
}
|
|
789
|
+
} else {
|
|
790
|
+
void dispatchHook2("postSyncValidate", {
|
|
791
|
+
routeName: resolvedName,
|
|
792
|
+
data: normalizedData,
|
|
793
|
+
user,
|
|
794
|
+
receiver,
|
|
795
|
+
transport: "socket",
|
|
796
|
+
validation: { status: "success" }
|
|
797
|
+
});
|
|
550
798
|
}
|
|
551
|
-
const
|
|
552
|
-
|
|
799
|
+
const executePayload = {
|
|
800
|
+
routeName: resolvedName,
|
|
801
|
+
data: normalizedData,
|
|
802
|
+
user,
|
|
803
|
+
receiver,
|
|
804
|
+
transport: "socket",
|
|
805
|
+
result: void 0,
|
|
806
|
+
error: null,
|
|
807
|
+
durationMs: 0
|
|
808
|
+
};
|
|
809
|
+
const preExecuteResult = await dispatchHook2("preSyncExecute", executePayload);
|
|
810
|
+
if (preExecuteResult.stopped) {
|
|
811
|
+
cleanupRequest();
|
|
812
|
+
rejectSync({
|
|
813
|
+
socket,
|
|
814
|
+
responseIndex,
|
|
815
|
+
buildSyncError,
|
|
816
|
+
response: {
|
|
817
|
+
status: "error",
|
|
818
|
+
errorCode: preExecuteResult.signal.errorCode,
|
|
819
|
+
httpStatus: preExecuteResult.signal.httpStatus
|
|
820
|
+
},
|
|
821
|
+
preferred: preferredLocale,
|
|
822
|
+
userLanguage: user?.language
|
|
823
|
+
});
|
|
824
|
+
return { ok: false };
|
|
825
|
+
}
|
|
826
|
+
const executeStart = Date.now();
|
|
827
|
+
const [serverSyncError, serverSyncResult] = await tryCatch2(
|
|
828
|
+
async () => await serverMain({
|
|
829
|
+
clientInput: normalizedData,
|
|
830
|
+
user,
|
|
831
|
+
functions: functionsObject,
|
|
832
|
+
roomCode: receiver,
|
|
833
|
+
stream: emitServerSyncStream,
|
|
834
|
+
broadcastStream: emitBroadcastSyncStream,
|
|
835
|
+
streamTo: emitStreamToTokens,
|
|
836
|
+
abortSignal: abortController.signal,
|
|
837
|
+
flushPressure
|
|
838
|
+
}),
|
|
839
|
+
void 0,
|
|
840
|
+
{
|
|
841
|
+
handler: "handleSyncRequest",
|
|
842
|
+
sync: resolvedName,
|
|
843
|
+
stage: "server",
|
|
844
|
+
userId: user?.id,
|
|
845
|
+
receiver,
|
|
846
|
+
transport: "socket"
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
executePayload.result = serverSyncResult;
|
|
850
|
+
executePayload.error = serverSyncError ?? null;
|
|
851
|
+
executePayload.durationMs = Date.now() - executeStart;
|
|
852
|
+
await dispatchHook2("postSyncExecute", executePayload);
|
|
853
|
+
if (serverSyncError) {
|
|
553
854
|
if (shouldLogDev()) {
|
|
554
|
-
|
|
855
|
+
getLogger3().error(`sync: server execution failed for ${resolvedName}`, serverSyncError, { sync: resolvedName });
|
|
555
856
|
}
|
|
556
857
|
cleanupRequest();
|
|
557
|
-
|
|
558
|
-
|
|
858
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.serverExecutionFailed" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
859
|
+
return { ok: false };
|
|
860
|
+
}
|
|
861
|
+
if (serverSyncResult?.status == "error") {
|
|
862
|
+
const normalizedServerError = buildSyncError({
|
|
863
|
+
response: serverSyncResult,
|
|
559
864
|
preferred: preferredLocale,
|
|
560
865
|
userLanguage: user?.language
|
|
561
|
-
})
|
|
866
|
+
});
|
|
867
|
+
if (shouldLogDev()) {
|
|
868
|
+
getLogger3().warn(`sync: server returned error for ${resolvedName}`, { sync: resolvedName, message: normalizedServerError.message });
|
|
869
|
+
}
|
|
870
|
+
cleanupRequest();
|
|
871
|
+
if (typeof responseIndex === "number") {
|
|
872
|
+
socket.emit(buildSyncResponseEventName(responseIndex), normalizedServerError);
|
|
873
|
+
}
|
|
874
|
+
return { ok: false };
|
|
875
|
+
}
|
|
876
|
+
if (serverSyncResult?.status !== "success") {
|
|
877
|
+
if (shouldLogDev()) {
|
|
878
|
+
getLogger3().warn(`sync: ${resolvedName}_server returned invalid response`, { sync: resolvedName });
|
|
879
|
+
}
|
|
880
|
+
cleanupRequest();
|
|
881
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.invalidServerResponse" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
882
|
+
return { ok: false };
|
|
883
|
+
}
|
|
884
|
+
return { ok: true, serverOutput: serverSyncResult };
|
|
885
|
+
}
|
|
886
|
+
async function runSyncFanout({
|
|
887
|
+
ioInstance,
|
|
888
|
+
receiver,
|
|
889
|
+
resolvedName,
|
|
890
|
+
normalizedData,
|
|
891
|
+
serverOutput,
|
|
892
|
+
user,
|
|
893
|
+
socket,
|
|
894
|
+
responseIndex,
|
|
895
|
+
buildSyncError,
|
|
896
|
+
preferredLocale,
|
|
897
|
+
cleanupRequest,
|
|
898
|
+
ignoreSelf,
|
|
899
|
+
token,
|
|
900
|
+
cb,
|
|
901
|
+
syncObject,
|
|
902
|
+
functionsObject
|
|
903
|
+
}) {
|
|
904
|
+
const physicalReceiver = receiver === "all" ? "all" : formatRoomName(receiver, { purpose: "broadcast", userId: user?.id ?? null });
|
|
905
|
+
const sockets = physicalReceiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(physicalReceiver).fetchSockets();
|
|
906
|
+
if (sockets.length === 0) {
|
|
907
|
+
if (shouldLogDev()) {
|
|
908
|
+
getLogger3().warn("sync: no sockets found for receiver", { receiver, sync: resolvedName });
|
|
909
|
+
}
|
|
910
|
+
cleanupRequest();
|
|
911
|
+
rejectSync({ socket, responseIndex, buildSyncError, response: { status: "error", errorCode: "sync.noReceiversFound" }, preferred: preferredLocale, userLanguage: user?.language });
|
|
912
|
+
return false;
|
|
562
913
|
}
|
|
563
914
|
const fanoutPayload = {
|
|
564
915
|
routeName: resolvedName,
|
|
@@ -572,7 +923,10 @@ async function handleSyncRequest({ msg, socket, token }) {
|
|
|
572
923
|
const preFanoutResult = await dispatchHook2("preSyncFanout", fanoutPayload);
|
|
573
924
|
if (preFanoutResult.stopped) {
|
|
574
925
|
cleanupRequest();
|
|
575
|
-
|
|
926
|
+
rejectSync({
|
|
927
|
+
socket,
|
|
928
|
+
responseIndex,
|
|
929
|
+
buildSyncError,
|
|
576
930
|
response: {
|
|
577
931
|
status: "error",
|
|
578
932
|
errorCode: preFanoutResult.signal.errorCode,
|
|
@@ -580,152 +934,277 @@ async function handleSyncRequest({ msg, socket, token }) {
|
|
|
580
934
|
},
|
|
581
935
|
preferred: preferredLocale,
|
|
582
936
|
userLanguage: user?.language
|
|
583
|
-
})
|
|
937
|
+
});
|
|
938
|
+
return false;
|
|
584
939
|
}
|
|
585
|
-
|
|
940
|
+
let originatorAcked = false;
|
|
941
|
+
if (typeof responseIndex === "number") {
|
|
942
|
+
socket.emit(buildSyncResponseEventName(responseIndex), {
|
|
943
|
+
status: "success",
|
|
944
|
+
message: `sync ${resolvedName} success`,
|
|
945
|
+
result: serverOutput
|
|
946
|
+
});
|
|
947
|
+
originatorAcked = true;
|
|
948
|
+
}
|
|
949
|
+
const { fanoutYieldEvery, fanoutYieldMs } = getProjectConfig3().sync;
|
|
950
|
+
const yieldEvery = Math.max(1, fanoutYieldEvery);
|
|
586
951
|
let recipientCount = 0;
|
|
587
952
|
let tempCount = 1;
|
|
588
953
|
for (const tempSocket of sockets) {
|
|
589
954
|
tempCount++;
|
|
590
|
-
if (tempCount %
|
|
955
|
+
if (tempCount % yieldEvery === 0) {
|
|
591
956
|
await new Promise((resolve) => setTimeout(resolve, fanoutYieldMs));
|
|
592
957
|
}
|
|
593
958
|
const tempToken = extractTokenFromSocket(tempSocket);
|
|
594
|
-
if (ignoreSelf && typeof ignoreSelf
|
|
959
|
+
if (ignoreSelf && typeof ignoreSelf === "boolean" && token && token === tempToken) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const preRecipientResult = await dispatchHook2("preSyncRecipient", {
|
|
963
|
+
routeName: resolvedName,
|
|
964
|
+
receiver,
|
|
965
|
+
recipientSocketId: tempSocket.id,
|
|
966
|
+
recipientUserId: null,
|
|
967
|
+
serverOutput
|
|
968
|
+
});
|
|
969
|
+
if (preRecipientResult.stopped && preRecipientResult.signal.overrideOutput === void 0) {
|
|
595
970
|
continue;
|
|
596
971
|
}
|
|
972
|
+
const effectiveServerOutput = preRecipientResult.stopped ? preRecipientResult.signal.overrideOutput : serverOutput;
|
|
597
973
|
recipientCount++;
|
|
598
974
|
if (syncObject[`${resolvedName}_client`]) {
|
|
599
975
|
const clientSyncHandler = syncObject[`${resolvedName}_client`];
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
}
|
|
976
|
+
await processClientSyncForRecipient({
|
|
977
|
+
tempSocket,
|
|
978
|
+
tempToken,
|
|
979
|
+
clientSyncHandler,
|
|
980
|
+
data: normalizedData,
|
|
981
|
+
functionsObject,
|
|
982
|
+
serverOutput: effectiveServerOutput,
|
|
983
|
+
receiver,
|
|
984
|
+
resolvedName,
|
|
985
|
+
callbackKey: cb,
|
|
986
|
+
transport: "socket",
|
|
987
|
+
handlerName: "handleSyncRequest",
|
|
988
|
+
logLabel: "sync",
|
|
989
|
+
shouldLogDev,
|
|
990
|
+
shouldLogStream,
|
|
991
|
+
buildSyncError,
|
|
992
|
+
//? Socket transport prefers `x-language` over `accept-language` —
|
|
993
|
+
//? preserved verbatim from the previous inlined error path.
|
|
994
|
+
resolvePreferredLocale: (headers) => extractLanguageFromHeader(headers["x-language"]) || extractLanguageFromHeader(headers["accept-language"]),
|
|
995
|
+
sourceUserId: user?.id
|
|
996
|
+
});
|
|
671
997
|
} else {
|
|
672
998
|
const result = {
|
|
673
999
|
cb,
|
|
674
1000
|
fullName: resolvedName,
|
|
675
|
-
serverOutput,
|
|
1001
|
+
serverOutput: effectiveServerOutput,
|
|
676
1002
|
clientOutput: {},
|
|
677
|
-
// No client file, so empty output
|
|
678
1003
|
message: `${resolvedName} sync success`,
|
|
679
1004
|
status: "success"
|
|
680
1005
|
};
|
|
681
1006
|
if (shouldLogDev()) {
|
|
682
|
-
|
|
1007
|
+
getLogger3().debug(`sync: ${resolvedName} server-only success`, { result });
|
|
683
1008
|
}
|
|
684
|
-
tempSocket.emit(
|
|
1009
|
+
tempSocket.emit(socketEventNames3.sync, result);
|
|
685
1010
|
}
|
|
686
1011
|
}
|
|
687
1012
|
fanoutPayload.recipientCount = recipientCount;
|
|
688
1013
|
await dispatchHook2("postSyncFanout", fanoutPayload);
|
|
689
1014
|
cleanupRequest();
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1015
|
+
return originatorAcked;
|
|
1016
|
+
}
|
|
1017
|
+
async function handleSyncRequest(args) {
|
|
1018
|
+
await runWithErrorTrackerIdentityScope(() => handleSyncRequestInner(args));
|
|
1019
|
+
}
|
|
1020
|
+
async function handleSyncRequestInner({ msg, socket, token }) {
|
|
1021
|
+
const ioInstance = getIoInstance2();
|
|
1022
|
+
if (!ioInstance) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const preferredLocale = extractLanguageFromHeader(socket.handshake.headers["x-language"]) || extractLanguageFromHeader(socket.handshake.headers["accept-language"]);
|
|
1026
|
+
if (!validateSyncMessage({ msg, socket, preferredLocale })) return;
|
|
1027
|
+
const { responseIndex } = msg;
|
|
1028
|
+
const preMessageResult = await dispatchHook2("preSocketMessage", {
|
|
1029
|
+
channel: "sync",
|
|
1030
|
+
socketId: socket.id,
|
|
1031
|
+
ip: socket.handshake.address,
|
|
1032
|
+
authenticated: Boolean(token),
|
|
1033
|
+
routeName: typeof msg.name === "string" ? msg.name : void 0
|
|
1034
|
+
});
|
|
1035
|
+
if (preMessageResult.stopped) {
|
|
1036
|
+
return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), normalizeErrorResponse2({
|
|
1037
|
+
response: {
|
|
1038
|
+
status: "error",
|
|
1039
|
+
errorCode: preMessageResult.signal.errorCode,
|
|
1040
|
+
httpStatus: preMessageResult.signal.httpStatus
|
|
1041
|
+
},
|
|
1042
|
+
preferredLocale
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
let currentRouteName;
|
|
1046
|
+
let currentPerRouteFormatter;
|
|
1047
|
+
let currentUserId;
|
|
1048
|
+
const buildSyncError = ({
|
|
1049
|
+
response,
|
|
1050
|
+
preferred,
|
|
1051
|
+
userLanguage
|
|
1052
|
+
}) => buildFormattedError({
|
|
1053
|
+
response,
|
|
1054
|
+
preferred,
|
|
1055
|
+
userLanguage,
|
|
1056
|
+
routeName: currentRouteName,
|
|
1057
|
+
transport: "socket",
|
|
1058
|
+
userId: currentUserId,
|
|
1059
|
+
perRouteFormatter: currentPerRouteFormatter
|
|
1060
|
+
});
|
|
1061
|
+
const parsedFields = parseSyncFields({ msg, socket, responseIndex, preferredLocale, buildSyncError });
|
|
1062
|
+
if (!parsedFields.ok) return;
|
|
1063
|
+
const { name, normalizedData, cb, receiver, resolvedName } = parsedFields;
|
|
1064
|
+
currentRouteName = resolvedName;
|
|
1065
|
+
if (shouldLogDev()) {
|
|
1066
|
+
getLogger3().debug(`sync: ${resolvedName} called`, { sync: resolvedName });
|
|
1067
|
+
}
|
|
1068
|
+
const user = await readSession(token);
|
|
1069
|
+
currentUserId = user?.id;
|
|
1070
|
+
setCurrentErrorTrackerIdentity(user?.id ? { id: user.id, email: user.email ?? void 0, username: user.name ?? void 0 } : null);
|
|
1071
|
+
const { syncObject, functionsObject } = await getRuntimeSyncMaps();
|
|
1072
|
+
const { abortController, cleanupRequest } = setupAbortController({ socket, responseIndex });
|
|
1073
|
+
let originatorAcked = false;
|
|
1074
|
+
const [handlerError] = await tryCatch2(async () => {
|
|
1075
|
+
if (!checkRouteExists({ syncObject, resolvedName, name, socket, responseIndex, buildSyncError, preferredLocale, user, cleanupRequest })) return;
|
|
1076
|
+
const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
|
|
1077
|
+
cb,
|
|
1078
|
+
receiver,
|
|
1079
|
+
resolvedName,
|
|
1080
|
+
logLabel: "sync",
|
|
1081
|
+
signal: abortController.signal,
|
|
1082
|
+
originatorSocket: socket,
|
|
1083
|
+
emitOriginatorChunk: (payload) => {
|
|
1084
|
+
if (typeof responseIndex !== "number") return;
|
|
1085
|
+
socket.emit(buildSyncProgressEventName(responseIndex), payload);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
const serverSyncEntry = syncObject[`${resolvedName}_server`];
|
|
1089
|
+
currentPerRouteFormatter = serverSyncEntry?.errorFormatter;
|
|
1090
|
+
if (!runSyncAuth({ serverSyncEntry, user, resolvedName, socket, responseIndex, buildSyncError, preferredLocale, cleanupRequest })) return;
|
|
1091
|
+
if (!runReceiverAuth({ receiver, resolvedName, socket, responseIndex, buildSyncError, preferredLocale, user, cleanupRequest })) return;
|
|
1092
|
+
const rateLimitOk = await applySyncRateLimits({
|
|
1093
|
+
resolvedName,
|
|
1094
|
+
token,
|
|
1095
|
+
socket,
|
|
1096
|
+
user,
|
|
1097
|
+
responseIndex,
|
|
1098
|
+
buildSyncError,
|
|
1099
|
+
preferredLocale,
|
|
1100
|
+
routeLimit: serverSyncEntry?.rateLimit
|
|
1101
|
+
});
|
|
1102
|
+
if (!rateLimitOk) {
|
|
1103
|
+
cleanupRequest();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (!await runPreAuthorizeHook({ resolvedName, normalizedData, user, receiver, socket, responseIndex, buildSyncError, preferredLocale, cleanupRequest })) return;
|
|
1107
|
+
const executionResult = await runSyncServerExecution({
|
|
1108
|
+
serverSyncEntry,
|
|
1109
|
+
resolvedName,
|
|
1110
|
+
normalizedData,
|
|
1111
|
+
user,
|
|
1112
|
+
receiver,
|
|
1113
|
+
socket,
|
|
1114
|
+
responseIndex,
|
|
1115
|
+
buildSyncError,
|
|
1116
|
+
preferredLocale,
|
|
1117
|
+
cleanupRequest,
|
|
1118
|
+
functionsObject,
|
|
1119
|
+
emitServerSyncStream,
|
|
1120
|
+
emitBroadcastSyncStream,
|
|
1121
|
+
emitStreamToTokens,
|
|
1122
|
+
flushPressure,
|
|
1123
|
+
abortController
|
|
1124
|
+
});
|
|
1125
|
+
if (!executionResult.ok) return;
|
|
1126
|
+
const { serverOutput } = executionResult;
|
|
1127
|
+
const fanoutAcked = await runSyncFanout({
|
|
1128
|
+
ioInstance,
|
|
1129
|
+
receiver,
|
|
1130
|
+
resolvedName,
|
|
1131
|
+
normalizedData,
|
|
1132
|
+
serverOutput,
|
|
1133
|
+
user,
|
|
1134
|
+
socket,
|
|
1135
|
+
responseIndex,
|
|
1136
|
+
buildSyncError,
|
|
1137
|
+
preferredLocale,
|
|
1138
|
+
cleanupRequest,
|
|
1139
|
+
ignoreSelf: parsedFields.ignoreSelf,
|
|
1140
|
+
token,
|
|
1141
|
+
cb,
|
|
1142
|
+
syncObject,
|
|
1143
|
+
functionsObject
|
|
1144
|
+
});
|
|
1145
|
+
if (typeof fanoutAcked === "boolean") {
|
|
1146
|
+
originatorAcked = fanoutAcked || originatorAcked;
|
|
1147
|
+
}
|
|
1148
|
+
}, void 0, {
|
|
1149
|
+
handler: "handleSyncRequest",
|
|
1150
|
+
sync: currentRouteName,
|
|
1151
|
+
stage: "handler",
|
|
1152
|
+
userId: currentUserId,
|
|
1153
|
+
receiver,
|
|
1154
|
+
transport: "socket"
|
|
694
1155
|
});
|
|
1156
|
+
if (handlerError) {
|
|
1157
|
+
if (shouldLogDev()) {
|
|
1158
|
+
getLogger3().error(`sync: unhandled error for ${currentRouteName ?? msg.name}`, handlerError, { sync: currentRouteName });
|
|
1159
|
+
}
|
|
1160
|
+
cleanupRequest();
|
|
1161
|
+
if (!originatorAcked) {
|
|
1162
|
+
return typeof responseIndex == "number" && socket.emit(buildSyncResponseEventName(responseIndex), buildSyncError({
|
|
1163
|
+
response: { status: "error", errorCode: "sync.serverExecutionFailed" },
|
|
1164
|
+
preferred: preferredLocale
|
|
1165
|
+
}));
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
695
1168
|
}
|
|
696
1169
|
|
|
697
1170
|
// 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
1171
|
import {
|
|
1172
|
+
readSession as readSession2,
|
|
1173
|
+
getProjectConfig as getProjectConfig4,
|
|
1174
|
+
getRuntimeSyncMaps as getRuntimeSyncMapsFromSource,
|
|
702
1175
|
validateRequest as validateRequest2,
|
|
703
1176
|
extractTokenFromSocket as extractTokenFromSocket2,
|
|
1177
|
+
formatRoomName as formatRoomName2,
|
|
704
1178
|
getIoInstance as getIoInstance3,
|
|
705
|
-
tryCatch as
|
|
1179
|
+
tryCatch as tryCatch3,
|
|
706
1180
|
parseTransportRouteName as parseTransportRouteName2,
|
|
707
1181
|
checkRateLimit as checkRateLimit2,
|
|
708
|
-
socketEventNames as
|
|
1182
|
+
socketEventNames as socketEventNames4,
|
|
709
1183
|
validateInputByType as validateInputByType2,
|
|
710
1184
|
dispatchHook as dispatchHook3,
|
|
711
|
-
getLogger as
|
|
1185
|
+
getLogger as getLogger4,
|
|
1186
|
+
runWithErrorTrackerIdentityScope as runWithErrorTrackerIdentityScope2,
|
|
1187
|
+
setCurrentErrorTrackerIdentity as setCurrentErrorTrackerIdentity2,
|
|
1188
|
+
deriveTokenBucketId as deriveTokenBucketId2,
|
|
1189
|
+
isLoopbackIp as isLoopbackIp2,
|
|
1190
|
+
extractLanguageFromHeader as extractLanguageFromHeader2
|
|
712
1191
|
} 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
1192
|
var applyHttpSyncRateLimits = async ({
|
|
717
1193
|
resolvedName,
|
|
718
1194
|
token,
|
|
719
1195
|
requesterIp,
|
|
720
1196
|
user,
|
|
721
1197
|
buildSyncError,
|
|
722
|
-
preferredLocale
|
|
1198
|
+
preferredLocale,
|
|
1199
|
+
routeLimit
|
|
723
1200
|
}) => {
|
|
724
|
-
const config =
|
|
725
|
-
const effectiveSyncLimit = config.rateLimiting.defaultApiLimit;
|
|
1201
|
+
const config = getProjectConfig4();
|
|
1202
|
+
const effectiveSyncLimit = routeLimit === void 0 ? config.rateLimiting.defaultApiLimit : routeLimit;
|
|
726
1203
|
if (effectiveSyncLimit !== false && effectiveSyncLimit > 0) {
|
|
727
|
-
const
|
|
728
|
-
const
|
|
1204
|
+
const identityCb = config.rateLimiting.identity;
|
|
1205
|
+
const customIdentity = identityCb?.({ routeName: resolvedName, userId: user?.id ?? null, ip: requesterIp ?? "", transport: "http" }) ?? null;
|
|
1206
|
+
const requesterIdentity = customIdentity?.id ?? (token ? deriveTokenBucketId2(token) : requesterIp ?? "unknown");
|
|
1207
|
+
const keyPrefix = customIdentity?.scope ?? (token ? "token" : "ip");
|
|
729
1208
|
const rateLimitKey = `${keyPrefix}:${requesterIdentity}:sync:${resolvedName}`;
|
|
730
1209
|
const { allowed, resetIn } = await checkRateLimit2({
|
|
731
1210
|
key: rateLimitKey,
|
|
@@ -734,13 +1213,17 @@ var applyHttpSyncRateLimits = async ({
|
|
|
734
1213
|
});
|
|
735
1214
|
if (!allowed) {
|
|
736
1215
|
void dispatchHook3("rateLimitExceeded", {
|
|
737
|
-
|
|
1216
|
+
//? Parity with the API handler: an anonymous per-route bucket is IP-keyed,
|
|
1217
|
+
//? so the scope is `ip` (with `route` still set to mark it a per-route
|
|
1218
|
+
//? bucket vs the global `:sync:all` IP bucket), never `route`.
|
|
1219
|
+
scope: token ? "user" : "ip",
|
|
738
1220
|
key: rateLimitKey,
|
|
739
1221
|
limit: effectiveSyncLimit,
|
|
740
1222
|
windowMs: config.rateLimiting.windowMs,
|
|
741
1223
|
count: effectiveSyncLimit + 1,
|
|
742
1224
|
route: resolvedName,
|
|
743
|
-
userId: user?.id
|
|
1225
|
+
userId: user?.id,
|
|
1226
|
+
ip: token ? void 0 : requesterIp
|
|
744
1227
|
});
|
|
745
1228
|
return buildSyncError({
|
|
746
1229
|
response: {
|
|
@@ -755,7 +1238,7 @@ var applyHttpSyncRateLimits = async ({
|
|
|
755
1238
|
}
|
|
756
1239
|
}
|
|
757
1240
|
const defaultIpLimit = config.rateLimiting.defaultIpLimit;
|
|
758
|
-
const requesterIsLoopback = process.env.NODE_ENV !== "production" && (requesterIp
|
|
1241
|
+
const requesterIsLoopback = config.rateLimiting.skipLoopbackInDev && process.env.NODE_ENV !== "production" && isLoopbackIp2(requesterIp ?? "");
|
|
759
1242
|
if (!requesterIsLoopback && defaultIpLimit !== false && defaultIpLimit > 0) {
|
|
760
1243
|
const ipBucket = requesterIp ?? "unknown";
|
|
761
1244
|
const ipKey = `ip:${ipBucket}:sync:all`;
|
|
@@ -787,7 +1270,379 @@ var applyHttpSyncRateLimits = async ({
|
|
|
787
1270
|
}
|
|
788
1271
|
return null;
|
|
789
1272
|
};
|
|
790
|
-
async function
|
|
1273
|
+
async function stageResolveRoute(ctx) {
|
|
1274
|
+
const { name, cb, normalizedReceiver, user, buildSyncError, preferredLocale } = ctx;
|
|
1275
|
+
if (!name || typeof name !== "string") {
|
|
1276
|
+
return {
|
|
1277
|
+
error: buildSyncError({
|
|
1278
|
+
response: { status: "error", errorCode: "sync.invalidRequest" },
|
|
1279
|
+
preferred: preferredLocale,
|
|
1280
|
+
userLanguage: user?.language
|
|
1281
|
+
})
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const parsedRoute = parseTransportRouteName2({ value: name, prefix: "sync" });
|
|
1285
|
+
if (parsedRoute.status === "error") {
|
|
1286
|
+
return {
|
|
1287
|
+
error: buildSyncError({
|
|
1288
|
+
response: {
|
|
1289
|
+
status: "error",
|
|
1290
|
+
errorCode: "routing.invalidServiceRouteName",
|
|
1291
|
+
errorParams: [{ key: "name", value: name }]
|
|
1292
|
+
},
|
|
1293
|
+
preferred: preferredLocale,
|
|
1294
|
+
userLanguage: user?.language
|
|
1295
|
+
})
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const resolvedName = parsedRoute.normalizedFullName;
|
|
1299
|
+
const callbackName = typeof cb === "string" && cb.trim().length > 0 ? cb.trim() : `${parsedRoute.serviceRoute.normalizedRouteName}/${parsedRoute.version}`;
|
|
1300
|
+
if (!normalizedReceiver) {
|
|
1301
|
+
return {
|
|
1302
|
+
error: buildSyncError({
|
|
1303
|
+
response: { status: "error", errorCode: "sync.missingReceiver" },
|
|
1304
|
+
preferred: preferredLocale,
|
|
1305
|
+
userLanguage: user?.language
|
|
1306
|
+
})
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
const { syncObject } = await getRuntimeSyncMapsFromSource();
|
|
1310
|
+
if (!syncObject[`${resolvedName}_client`] && !syncObject[`${resolvedName}_server`]) {
|
|
1311
|
+
return {
|
|
1312
|
+
error: buildSyncError({
|
|
1313
|
+
response: { status: "error", errorCode: "sync.notFound" },
|
|
1314
|
+
preferred: preferredLocale,
|
|
1315
|
+
userLanguage: user?.language
|
|
1316
|
+
})
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
if (!syncObject[`${resolvedName}_server`]) {
|
|
1320
|
+
if (shouldLogDev()) {
|
|
1321
|
+
getLogger4().warn(`http sync: ${resolvedName} has a _client file but no required _server file`, { sync: resolvedName, transport: "http" });
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
error: buildSyncError({
|
|
1325
|
+
response: { status: "error", errorCode: "sync.notFound" },
|
|
1326
|
+
preferred: preferredLocale,
|
|
1327
|
+
userLanguage: user?.language
|
|
1328
|
+
})
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
return { resolvedName, callbackName };
|
|
1332
|
+
}
|
|
1333
|
+
function stageCheckAuth(serverSyncEntry, ctx) {
|
|
1334
|
+
const { user, preferredLocale, buildSyncError } = ctx;
|
|
1335
|
+
const { auth } = serverSyncEntry;
|
|
1336
|
+
if (auth.login && !user?.id) {
|
|
1337
|
+
return buildSyncError({
|
|
1338
|
+
response: { status: "error", errorCode: "auth.required" },
|
|
1339
|
+
preferred: preferredLocale
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
if (auth.additional && auth.additional.length > 0 && !user) {
|
|
1343
|
+
return buildSyncError({
|
|
1344
|
+
response: { status: "error", errorCode: "auth.required" },
|
|
1345
|
+
preferred: preferredLocale
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
const validationResult = validateRequest2({ auth, user });
|
|
1349
|
+
if (validationResult.status === "error") {
|
|
1350
|
+
return buildSyncError({
|
|
1351
|
+
response: {
|
|
1352
|
+
status: "error",
|
|
1353
|
+
errorCode: validationResult.errorCode || "auth.forbidden",
|
|
1354
|
+
errorParams: validationResult.errorParams,
|
|
1355
|
+
httpStatus: validationResult.httpStatus
|
|
1356
|
+
},
|
|
1357
|
+
preferred: preferredLocale,
|
|
1358
|
+
userLanguage: user?.language
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
function stageAuthorizeReceiver(resolvedName, ctx) {
|
|
1364
|
+
const { normalizedReceiver, user, preferredLocale, buildSyncError } = ctx;
|
|
1365
|
+
const syncConfig = getProjectConfig4().sync;
|
|
1366
|
+
const receiverAuth = authorizeSyncReceiver({
|
|
1367
|
+
receiver: normalizedReceiver,
|
|
1368
|
+
allowClientReceiverAll: syncConfig.allowClientReceiverAll,
|
|
1369
|
+
requireRoomMembership: syncConfig.requireRoomMembership,
|
|
1370
|
+
isMember: user ? () => Boolean(user.roomCodes?.includes(normalizedReceiver)) : null
|
|
1371
|
+
});
|
|
1372
|
+
if (!receiverAuth.allowed) {
|
|
1373
|
+
if (shouldLogDev()) {
|
|
1374
|
+
getLogger4().warn(`http sync: receiver authorization failed for ${resolvedName}`, { sync: resolvedName, receiver: normalizedReceiver, errorCode: receiverAuth.errorCode, transport: "http" });
|
|
1375
|
+
}
|
|
1376
|
+
return buildSyncError({
|
|
1377
|
+
response: { status: "error", errorCode: receiverAuth.errorCode, httpStatus: 403 },
|
|
1378
|
+
preferred: preferredLocale,
|
|
1379
|
+
userLanguage: user?.language
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
async function stageRunAuthorizeHooks(resolvedName, ctx) {
|
|
1385
|
+
const { data, normalizedReceiver, user, preferredLocale, buildSyncError } = ctx;
|
|
1386
|
+
const preAuthorizeResult = await dispatchHook3("preSyncAuthorize", {
|
|
1387
|
+
routeName: resolvedName,
|
|
1388
|
+
data,
|
|
1389
|
+
user,
|
|
1390
|
+
receiver: normalizedReceiver,
|
|
1391
|
+
transport: "http"
|
|
1392
|
+
});
|
|
1393
|
+
if (preAuthorizeResult.stopped) {
|
|
1394
|
+
return buildSyncError({
|
|
1395
|
+
response: {
|
|
1396
|
+
status: "error",
|
|
1397
|
+
errorCode: preAuthorizeResult.signal.errorCode,
|
|
1398
|
+
httpStatus: preAuthorizeResult.signal.httpStatus
|
|
1399
|
+
},
|
|
1400
|
+
preferred: preferredLocale,
|
|
1401
|
+
userLanguage: user?.language
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
void dispatchHook3("postSyncAuthorize", {
|
|
1405
|
+
routeName: resolvedName,
|
|
1406
|
+
data,
|
|
1407
|
+
user,
|
|
1408
|
+
receiver: normalizedReceiver,
|
|
1409
|
+
transport: "http"
|
|
1410
|
+
});
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
async function stageValidateInput(resolvedName, serverSyncEntry, ctx) {
|
|
1414
|
+
const { data, normalizedReceiver, user, preferredLocale, buildSyncError } = ctx;
|
|
1415
|
+
const preValidateResult = await dispatchHook3("preSyncValidate", {
|
|
1416
|
+
routeName: resolvedName,
|
|
1417
|
+
data,
|
|
1418
|
+
user,
|
|
1419
|
+
receiver: normalizedReceiver,
|
|
1420
|
+
transport: "http"
|
|
1421
|
+
});
|
|
1422
|
+
if (preValidateResult.stopped) {
|
|
1423
|
+
return buildSyncError({
|
|
1424
|
+
response: {
|
|
1425
|
+
status: "error",
|
|
1426
|
+
errorCode: preValidateResult.signal.errorCode,
|
|
1427
|
+
httpStatus: preValidateResult.signal.httpStatus
|
|
1428
|
+
},
|
|
1429
|
+
preferred: preferredLocale,
|
|
1430
|
+
userLanguage: user?.language
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
const { inputType, inputTypeFilePath } = serverSyncEntry;
|
|
1434
|
+
if (resolveSyncValidationMode(serverSyncEntry.validation) === "strict") {
|
|
1435
|
+
const inputValidation = await validateInputByType2({
|
|
1436
|
+
typeText: inputType,
|
|
1437
|
+
value: data,
|
|
1438
|
+
rootKey: "clientInput",
|
|
1439
|
+
filePath: inputTypeFilePath
|
|
1440
|
+
});
|
|
1441
|
+
void dispatchHook3("postSyncValidate", {
|
|
1442
|
+
routeName: resolvedName,
|
|
1443
|
+
data,
|
|
1444
|
+
user,
|
|
1445
|
+
receiver: normalizedReceiver,
|
|
1446
|
+
transport: "http",
|
|
1447
|
+
validation: inputValidation.status === "error" ? { status: "error", message: inputValidation.message } : { status: "success" }
|
|
1448
|
+
});
|
|
1449
|
+
if (inputValidation.status === "error") {
|
|
1450
|
+
if (shouldLogDev()) {
|
|
1451
|
+
getLogger4().warn(`http sync: input validation failed for ${resolvedName}`, { sync: resolvedName, message: inputValidation.message, transport: "http" });
|
|
1452
|
+
}
|
|
1453
|
+
return buildSyncError({
|
|
1454
|
+
response: {
|
|
1455
|
+
status: "error",
|
|
1456
|
+
errorCode: "sync.invalidInputType"
|
|
1457
|
+
},
|
|
1458
|
+
preferred: preferredLocale,
|
|
1459
|
+
userLanguage: user?.language
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
} else {
|
|
1463
|
+
void dispatchHook3("postSyncValidate", {
|
|
1464
|
+
routeName: resolvedName,
|
|
1465
|
+
data,
|
|
1466
|
+
user,
|
|
1467
|
+
receiver: normalizedReceiver,
|
|
1468
|
+
transport: "http",
|
|
1469
|
+
validation: { status: "success" }
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
async function stageExecuteServer(resolvedName, serverSyncEntry, functionsObject, ctx) {
|
|
1475
|
+
const { data, normalizedReceiver, name, user, preferredLocale, buildSyncError, effectiveAbortSignal, cb, stream } = ctx;
|
|
1476
|
+
const { main: serverMain } = serverSyncEntry;
|
|
1477
|
+
const { emitServerSyncStream, emitBroadcastSyncStream, emitStreamToTokens, flushPressure } = buildSyncStreamEmitters({
|
|
1478
|
+
cb,
|
|
1479
|
+
receiver: normalizedReceiver,
|
|
1480
|
+
resolvedName,
|
|
1481
|
+
logLabel: "http sync",
|
|
1482
|
+
signal: effectiveAbortSignal,
|
|
1483
|
+
//? No originatorSocket for HTTP/SSE — `flushPressure` falls back
|
|
1484
|
+
//? to room-socket measurement only. SSE backpressure is the
|
|
1485
|
+
//? caller's responsibility (Node's `res.write` returns a bool).
|
|
1486
|
+
//? Originator chunks travel back via SSE; broadcast / targeted
|
|
1487
|
+
//? chunks still flow over Socket.io to recipients in the receiver room.
|
|
1488
|
+
emitOriginatorChunk: (payload) => {
|
|
1489
|
+
stream?.(payload);
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
const executePayload = {
|
|
1493
|
+
routeName: resolvedName,
|
|
1494
|
+
data,
|
|
1495
|
+
user,
|
|
1496
|
+
receiver: normalizedReceiver,
|
|
1497
|
+
transport: "http",
|
|
1498
|
+
result: void 0,
|
|
1499
|
+
error: null,
|
|
1500
|
+
durationMs: 0
|
|
1501
|
+
};
|
|
1502
|
+
const preExecuteResult = await dispatchHook3("preSyncExecute", executePayload);
|
|
1503
|
+
if (preExecuteResult.stopped) {
|
|
1504
|
+
return {
|
|
1505
|
+
error: buildSyncError({
|
|
1506
|
+
response: {
|
|
1507
|
+
status: "error",
|
|
1508
|
+
errorCode: preExecuteResult.signal.errorCode,
|
|
1509
|
+
httpStatus: preExecuteResult.signal.httpStatus
|
|
1510
|
+
},
|
|
1511
|
+
preferred: preferredLocale,
|
|
1512
|
+
userLanguage: user?.language
|
|
1513
|
+
})
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
const executeStart = Date.now();
|
|
1517
|
+
const [serverSyncError, serverSyncResult] = await tryCatch3(
|
|
1518
|
+
async () => await serverMain({
|
|
1519
|
+
clientInput: data,
|
|
1520
|
+
user,
|
|
1521
|
+
functions: functionsObject,
|
|
1522
|
+
roomCode: normalizedReceiver,
|
|
1523
|
+
stream: emitServerSyncStream,
|
|
1524
|
+
broadcastStream: emitBroadcastSyncStream,
|
|
1525
|
+
streamTo: emitStreamToTokens,
|
|
1526
|
+
abortSignal: effectiveAbortSignal,
|
|
1527
|
+
flushPressure
|
|
1528
|
+
}),
|
|
1529
|
+
void 0,
|
|
1530
|
+
{
|
|
1531
|
+
handler: "handleHttpSyncRequest",
|
|
1532
|
+
sync: resolvedName,
|
|
1533
|
+
stage: "server",
|
|
1534
|
+
userId: user?.id,
|
|
1535
|
+
receiver: name,
|
|
1536
|
+
transport: "http"
|
|
1537
|
+
}
|
|
1538
|
+
);
|
|
1539
|
+
executePayload.result = serverSyncResult;
|
|
1540
|
+
executePayload.error = serverSyncError ?? null;
|
|
1541
|
+
executePayload.durationMs = Date.now() - executeStart;
|
|
1542
|
+
await dispatchHook3("postSyncExecute", executePayload);
|
|
1543
|
+
if (serverSyncError) {
|
|
1544
|
+
return {
|
|
1545
|
+
error: buildSyncError({
|
|
1546
|
+
response: { status: "error", errorCode: "sync.serverExecutionFailed" },
|
|
1547
|
+
preferred: preferredLocale,
|
|
1548
|
+
userLanguage: user?.language
|
|
1549
|
+
})
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
if (serverSyncResult?.status == "error") {
|
|
1553
|
+
return {
|
|
1554
|
+
error: buildSyncError({
|
|
1555
|
+
response: serverSyncResult,
|
|
1556
|
+
preferred: preferredLocale,
|
|
1557
|
+
userLanguage: user?.language
|
|
1558
|
+
})
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
if (serverSyncResult?.status !== "success") {
|
|
1562
|
+
return {
|
|
1563
|
+
error: buildSyncError({
|
|
1564
|
+
response: { status: "error", errorCode: "sync.invalidServerResponse" },
|
|
1565
|
+
preferred: preferredLocale,
|
|
1566
|
+
userLanguage: user?.language
|
|
1567
|
+
})
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
return { serverOutput: serverSyncResult };
|
|
1571
|
+
}
|
|
1572
|
+
async function stageFanout(resolvedName, callbackName, serverOutput, syncObject, functionsObject, ctx) {
|
|
1573
|
+
const { data, normalizedReceiver, token, ignoreSelf, user, buildSyncError } = ctx;
|
|
1574
|
+
const ioInstance = getIoInstance3();
|
|
1575
|
+
const fanoutPayload = {
|
|
1576
|
+
routeName: resolvedName,
|
|
1577
|
+
data,
|
|
1578
|
+
user,
|
|
1579
|
+
receiver: normalizedReceiver,
|
|
1580
|
+
serverOutput,
|
|
1581
|
+
transport: "http",
|
|
1582
|
+
recipientCount: 0
|
|
1583
|
+
};
|
|
1584
|
+
await dispatchHook3("preSyncFanout", fanoutPayload);
|
|
1585
|
+
const physicalReceiver = normalizedReceiver === "all" ? "all" : formatRoomName2(normalizedReceiver, { purpose: "broadcast", userId: user?.id ?? null });
|
|
1586
|
+
const sockets = physicalReceiver === "all" ? await ioInstance.fetchSockets() : await ioInstance.in(physicalReceiver).fetchSockets();
|
|
1587
|
+
let recipientCount = 0;
|
|
1588
|
+
for (const tempSocket of sockets) {
|
|
1589
|
+
const tempToken = extractTokenFromSocket2(tempSocket);
|
|
1590
|
+
if (ignoreSelf && typeof ignoreSelf === "boolean" && token && token === tempToken) {
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const preRecipientResult = await dispatchHook3("preSyncRecipient", {
|
|
1594
|
+
routeName: resolvedName,
|
|
1595
|
+
receiver: normalizedReceiver,
|
|
1596
|
+
recipientSocketId: tempSocket.id,
|
|
1597
|
+
recipientUserId: null,
|
|
1598
|
+
serverOutput
|
|
1599
|
+
});
|
|
1600
|
+
if (preRecipientResult.stopped) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
recipientCount++;
|
|
1604
|
+
if (syncObject[`${resolvedName}_client`]) {
|
|
1605
|
+
const clientSyncHandler = syncObject[`${resolvedName}_client`];
|
|
1606
|
+
await processClientSyncForRecipient({
|
|
1607
|
+
tempSocket,
|
|
1608
|
+
tempToken,
|
|
1609
|
+
clientSyncHandler,
|
|
1610
|
+
data,
|
|
1611
|
+
functionsObject,
|
|
1612
|
+
serverOutput,
|
|
1613
|
+
receiver: normalizedReceiver,
|
|
1614
|
+
resolvedName,
|
|
1615
|
+
callbackKey: callbackName,
|
|
1616
|
+
transport: "http",
|
|
1617
|
+
handlerName: "handleHttpSyncRequest",
|
|
1618
|
+
logLabel: "http sync",
|
|
1619
|
+
shouldLogDev,
|
|
1620
|
+
shouldLogStream,
|
|
1621
|
+
buildSyncError,
|
|
1622
|
+
//? HTTP transport prefers `accept-language` over `x-language`
|
|
1623
|
+
//? (reversed vs. the socket handler) — preserved verbatim from the
|
|
1624
|
+
//? previous inlined error path.
|
|
1625
|
+
resolvePreferredLocale: (headers) => extractLanguageFromHeader2(headers["accept-language"] || headers["x-language"]),
|
|
1626
|
+
sourceUserId: user?.id
|
|
1627
|
+
});
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
tempSocket.emit(socketEventNames4.sync, {
|
|
1631
|
+
cb: callbackName,
|
|
1632
|
+
fullName: resolvedName,
|
|
1633
|
+
serverOutput,
|
|
1634
|
+
clientOutput: {},
|
|
1635
|
+
message: `${resolvedName} sync success`,
|
|
1636
|
+
status: "success"
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
fanoutPayload.recipientCount = recipientCount;
|
|
1640
|
+
await dispatchHook3("postSyncFanout", fanoutPayload);
|
|
1641
|
+
}
|
|
1642
|
+
async function handleHttpSyncRequest(args) {
|
|
1643
|
+
return runWithErrorTrackerIdentityScope2(() => handleHttpSyncRequestScoped(args));
|
|
1644
|
+
}
|
|
1645
|
+
async function handleHttpSyncRequestScoped({
|
|
791
1646
|
name,
|
|
792
1647
|
cb,
|
|
793
1648
|
data,
|
|
@@ -800,51 +1655,31 @@ async function handleHttpSyncRequest({
|
|
|
800
1655
|
stream,
|
|
801
1656
|
abortSignal
|
|
802
1657
|
}) {
|
|
803
|
-
if (
|
|
804
|
-
|
|
1658
|
+
if (shouldLogDev()) {
|
|
1659
|
+
getLogger4().debug(`http sync: ${name} called`);
|
|
805
1660
|
}
|
|
806
1661
|
const effectiveAbortSignal = abortSignal ?? new AbortController().signal;
|
|
807
1662
|
const normalizedReceiver = typeof receiver === "string" ? receiver.trim() : "";
|
|
808
1663
|
const preferredLocale = extractLanguageFromHeader2(xLanguageHeader) || extractLanguageFromHeader2(acceptLanguageHeader);
|
|
809
|
-
const user = await
|
|
1664
|
+
const user = await readSession2(token);
|
|
1665
|
+
setCurrentErrorTrackerIdentity2(user?.id ? { id: user.id, email: user.email ?? void 0, username: user.name ?? void 0 } : null);
|
|
810
1666
|
let currentRouteName;
|
|
811
1667
|
let currentPerRouteFormatter;
|
|
812
1668
|
const buildSyncError = ({
|
|
813
1669
|
response,
|
|
814
1670
|
preferred,
|
|
815
1671
|
userLanguage
|
|
816
|
-
}) => {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
};
|
|
1672
|
+
}) => buildFormattedError({
|
|
1673
|
+
response,
|
|
1674
|
+
preferred,
|
|
1675
|
+
userLanguage,
|
|
1676
|
+
routeName: currentRouteName,
|
|
1677
|
+
transport: "http",
|
|
1678
|
+
userId: user?.id,
|
|
1679
|
+
perRouteFormatter: currentPerRouteFormatter
|
|
1680
|
+
});
|
|
846
1681
|
const ioInstance = getIoInstance3();
|
|
847
|
-
const [bodyError, bodyResult] = await
|
|
1682
|
+
const [bodyError, bodyResult] = await tryCatch3(async () => {
|
|
848
1683
|
if (!ioInstance) {
|
|
849
1684
|
return buildSyncError({
|
|
850
1685
|
response: { status: "error", errorCode: "sync.ioUnavailable" },
|
|
@@ -852,285 +1687,66 @@ async function handleHttpSyncRequest({
|
|
|
852
1687
|
userLanguage: user?.language
|
|
853
1688
|
});
|
|
854
1689
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
const resolvedName = parsedRoute.normalizedFullName;
|
|
1690
|
+
const ctx = {
|
|
1691
|
+
name,
|
|
1692
|
+
cb,
|
|
1693
|
+
data,
|
|
1694
|
+
normalizedReceiver,
|
|
1695
|
+
ignoreSelf,
|
|
1696
|
+
token,
|
|
1697
|
+
requesterIp,
|
|
1698
|
+
stream,
|
|
1699
|
+
effectiveAbortSignal,
|
|
1700
|
+
preferredLocale,
|
|
1701
|
+
user,
|
|
1702
|
+
buildSyncError
|
|
1703
|
+
};
|
|
1704
|
+
const routeResult = await stageResolveRoute(ctx);
|
|
1705
|
+
if (routeResult.error) return routeResult.error;
|
|
1706
|
+
const { resolvedName, callbackName } = routeResult;
|
|
875
1707
|
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
1708
|
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
1709
|
const serverSyncEntry = syncObject[`${resolvedName}_server`];
|
|
893
1710
|
currentPerRouteFormatter = serverSyncEntry?.errorFormatter;
|
|
894
1711
|
if (serverSyncEntry) {
|
|
895
|
-
const
|
|
896
|
-
if (
|
|
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
|
-
});
|
|
1712
|
+
const authError = stageCheckAuth(serverSyncEntry, ctx);
|
|
1713
|
+
if (authError) return authError;
|
|
933
1714
|
}
|
|
1715
|
+
const receiverError = stageAuthorizeReceiver(resolvedName, ctx);
|
|
1716
|
+
if (receiverError) return receiverError;
|
|
934
1717
|
const rateLimitResult = await applyHttpSyncRateLimits({
|
|
935
1718
|
resolvedName,
|
|
936
1719
|
token,
|
|
937
1720
|
requesterIp,
|
|
938
1721
|
user,
|
|
939
1722
|
buildSyncError,
|
|
940
|
-
preferredLocale
|
|
1723
|
+
preferredLocale,
|
|
1724
|
+
routeLimit: serverSyncEntry?.rateLimit
|
|
941
1725
|
});
|
|
942
1726
|
if (rateLimitResult) return rateLimitResult;
|
|
1727
|
+
const authorizeHookError = await stageRunAuthorizeHooks(resolvedName, ctx);
|
|
1728
|
+
if (authorizeHookError) return authorizeHookError;
|
|
943
1729
|
let serverOutput = {};
|
|
944
1730
|
if (serverSyncEntry) {
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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++;
|
|
1731
|
+
const validationError = await stageValidateInput(resolvedName, serverSyncEntry, ctx);
|
|
1732
|
+
if (validationError) return validationError;
|
|
1733
|
+
const executeResult = await stageExecuteServer(resolvedName, serverSyncEntry, functionsObject, ctx);
|
|
1734
|
+
if (executeResult.error) return executeResult.error;
|
|
1735
|
+
serverOutput = executeResult.serverOutput;
|
|
1119
1736
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
getLogger3().debug(`http sync: ${resolvedName} completed`);
|
|
1737
|
+
await stageFanout(resolvedName, callbackName, serverOutput, syncObject, functionsObject, ctx);
|
|
1738
|
+
if (shouldLogDev()) {
|
|
1739
|
+
getLogger4().debug(`http sync: ${resolvedName} completed`);
|
|
1124
1740
|
}
|
|
1125
1741
|
const serverMessage = serverOutput.message;
|
|
1126
1742
|
return {
|
|
1127
|
-
...serverOutput,
|
|
1128
1743
|
status: "success",
|
|
1129
|
-
message: typeof serverMessage === "string" ? serverMessage : `${resolvedName} sync success
|
|
1744
|
+
message: typeof serverMessage === "string" ? serverMessage : `${resolvedName} sync success`,
|
|
1745
|
+
result: serverOutput
|
|
1130
1746
|
};
|
|
1131
1747
|
});
|
|
1132
1748
|
if (bodyError) {
|
|
1133
|
-
|
|
1749
|
+
getLogger4().error(`http sync: ${name} threw`, bodyError, { sync: name });
|
|
1134
1750
|
return buildSyncError({
|
|
1135
1751
|
response: { status: "error", errorCode: "sync.serverExecutionFailed" },
|
|
1136
1752
|
preferred: preferredLocale,
|
|
@@ -1145,9 +1761,9 @@ async function handleHttpSyncRequest({
|
|
|
1145
1761
|
}
|
|
1146
1762
|
|
|
1147
1763
|
// src/streamThrottle.ts
|
|
1148
|
-
import { getProjectConfig as
|
|
1764
|
+
import { getProjectConfig as getProjectConfig5 } from "@luckystack/core";
|
|
1149
1765
|
var createStreamThrottle = (options = {}) => {
|
|
1150
|
-
const defaults =
|
|
1766
|
+
const defaults = getProjectConfig5().sync.streamThrottle;
|
|
1151
1767
|
const flushAtChars = options.flushAtChars ?? defaults.flushAtChars;
|
|
1152
1768
|
const flushEveryMs = options.flushEveryMs ?? defaults.flushEveryMs;
|
|
1153
1769
|
const field = options.field ?? defaults.field;
|