@concavejs/runtime-node 0.0.1-alpha.6 → 0.0.1-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12514,6 +12514,30 @@ class BaseSqliteDocStore {
12514
12514
  return scored.slice(0, limit);
12515
12515
  }
12516
12516
  }
12517
+ function createSerializedTransactionRunner(hooks) {
12518
+ let queue = Promise.resolve();
12519
+ return async function runInTransaction(fn) {
12520
+ const run = queue.then(async () => {
12521
+ await hooks.begin();
12522
+ try {
12523
+ const result = await fn();
12524
+ await hooks.commit();
12525
+ return result;
12526
+ } catch (error) {
12527
+ try {
12528
+ await hooks.rollback();
12529
+ } catch {}
12530
+ throw error;
12531
+ }
12532
+ });
12533
+ queue = run.then(() => {
12534
+ return;
12535
+ }, () => {
12536
+ return;
12537
+ });
12538
+ return run;
12539
+ };
12540
+ }
12517
12541
 
12518
12542
  class Long22 {
12519
12543
  low;
@@ -12666,8 +12690,14 @@ class NodePreparedStatement {
12666
12690
 
12667
12691
  class NodeSqliteAdapter {
12668
12692
  db;
12693
+ runSerializedTransaction;
12669
12694
  constructor(db) {
12670
12695
  this.db = db;
12696
+ this.runSerializedTransaction = createSerializedTransactionRunner({
12697
+ begin: () => this.db.exec("BEGIN TRANSACTION"),
12698
+ commit: () => this.db.exec("COMMIT"),
12699
+ rollback: () => this.db.exec("ROLLBACK")
12700
+ });
12671
12701
  }
12672
12702
  exec(sql) {
12673
12703
  this.db.exec(sql);
@@ -12676,15 +12706,7 @@ class NodeSqliteAdapter {
12676
12706
  return new NodePreparedStatement(this.db.prepare(sql));
12677
12707
  }
12678
12708
  async transaction(fn) {
12679
- try {
12680
- this.db.exec("BEGIN TRANSACTION");
12681
- const result = await fn();
12682
- this.db.exec("COMMIT");
12683
- return result;
12684
- } catch (error) {
12685
- this.db.exec("ROLLBACK");
12686
- throw error;
12687
- }
12709
+ return this.runSerializedTransaction(fn);
12688
12710
  }
12689
12711
  hexToBuffer(hex) {
12690
12712
  return Buffer.from(hexToArrayBuffer32(hex));
@@ -12788,7 +12810,7 @@ var __defProp14, __export5 = (target, all) => {
12788
12810
  } catch {
12789
12811
  return;
12790
12812
  }
12791
- }, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, JWKS_CACHE5, debug5 = () => {}, Convex25, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
12813
+ }, AsyncLocalStorageCtor5, snapshotContext5, transactionContext5, idGeneratorContext5, CALL_CONTEXT_SYMBOL5, globalCallContext5, callContext5, DEFAULT_JWKS_CACHE_TTL_MS5, MAX_JWKS_CACHE_TTL_MS5, JWKS_CACHE5, debug5 = () => {}, Convex25, UZERO22, TWO_PWR_16_DBL22, TWO_PWR_32_DBL22, TWO_PWR_64_DBL22, MAX_UNSIGNED_VALUE22, SqliteDocStore;
12792
12814
  var init_dist = __esm(() => {
12793
12815
  __defProp14 = Object.defineProperty;
12794
12816
  init_base645 = __esm5(() => {
@@ -13886,6 +13908,8 @@ var init_dist = __esm(() => {
13886
13908
  init_values5();
13887
13909
  init_index_manager5();
13888
13910
  init_interface6();
13911
+ DEFAULT_JWKS_CACHE_TTL_MS5 = 5 * 60 * 1000;
13912
+ MAX_JWKS_CACHE_TTL_MS5 = 24 * 60 * 60 * 1000;
13889
13913
  JWKS_CACHE5 = new Map;
13890
13914
  init_values5();
13891
13915
  init_schema_service5();
@@ -13941,6 +13965,9 @@ class FsBlobStore {
13941
13965
  return false;
13942
13966
  }
13943
13967
  }
13968
+ isNotFoundError(error) {
13969
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
13970
+ }
13944
13971
  generateStorageId() {
13945
13972
  return crypto.randomUUID();
13946
13973
  }
@@ -13981,31 +14008,25 @@ class FsBlobStore {
13981
14008
  async get(storageId) {
13982
14009
  const objectPath = this.getObjectPath(storageId);
13983
14010
  try {
13984
- const fileExists = await this.pathExists(objectPath);
13985
- if (!fileExists) {
13986
- return null;
13987
- }
13988
14011
  const data = await readFile(objectPath);
13989
14012
  const metadata = await this.getMetadata(storageId);
13990
14013
  const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
13991
14014
  return new Blob([arrayBuffer], { type: metadata?.contentType || "application/octet-stream" });
13992
14015
  } catch (error) {
14016
+ if (this.isNotFoundError(error)) {
14017
+ return null;
14018
+ }
13993
14019
  console.error("Error reading object from filesystem:", error);
13994
- return null;
14020
+ throw error;
13995
14021
  }
13996
14022
  }
13997
14023
  async delete(storageId) {
13998
14024
  const objectPath = this.getObjectPath(storageId);
13999
14025
  const metadataPath = this.getMetadataPath(storageId);
14000
- try {
14001
- await Promise.all([
14002
- unlink(objectPath).catch(() => {}),
14003
- unlink(metadataPath).catch(() => {})
14004
- ]);
14005
- } catch (error) {
14006
- console.error("Error deleting object from filesystem:", error);
14007
- throw error;
14008
- }
14026
+ await Promise.all([
14027
+ this.unlinkIfExists(objectPath),
14028
+ this.unlinkIfExists(metadataPath)
14029
+ ]);
14009
14030
  }
14010
14031
  async getUrl(storageId) {
14011
14032
  const objectPath = this.getObjectPath(storageId);
@@ -14019,15 +14040,25 @@ class FsBlobStore {
14019
14040
  async getMetadata(storageId) {
14020
14041
  const metadataPath = this.getMetadataPath(storageId);
14021
14042
  try {
14022
- const fileExists = await this.pathExists(metadataPath);
14023
- if (!fileExists) {
14024
- return null;
14025
- }
14026
14043
  const data = await readFile(metadataPath, "utf-8");
14027
14044
  return JSON.parse(data);
14028
14045
  } catch (error) {
14046
+ if (this.isNotFoundError(error)) {
14047
+ return null;
14048
+ }
14029
14049
  console.error("Error reading metadata from filesystem:", error);
14030
- return null;
14050
+ throw error;
14051
+ }
14052
+ }
14053
+ async unlinkIfExists(path2) {
14054
+ try {
14055
+ await unlink(path2);
14056
+ } catch (error) {
14057
+ if (this.isNotFoundError(error)) {
14058
+ return;
14059
+ }
14060
+ console.error("Error deleting object from filesystem:", error);
14061
+ throw error;
14031
14062
  }
14032
14063
  }
14033
14064
  }
@@ -18555,12 +18586,15 @@ var WELL_KNOWN_JWKS_URLS = {
18555
18586
  firebase: () => "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"
18556
18587
  };
18557
18588
  var DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
18589
+ var DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
18590
+ var MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
18558
18591
  var JWKS_CACHE = new Map;
18559
18592
  var defaultValidationConfig;
18560
18593
  var adminAuthConfig;
18561
18594
  var systemAuthConfig;
18562
18595
  function setJwtValidationConfig(config) {
18563
18596
  defaultValidationConfig = config;
18597
+ JWKS_CACHE.clear();
18564
18598
  }
18565
18599
  function getJwtValidationConfig() {
18566
18600
  return defaultValidationConfig;
@@ -18688,7 +18722,8 @@ function resolveJwtValidationConfigFromEnv(env) {
18688
18722
  const secret = getEnvValue("AUTH_SECRET", env) ?? getEnvValue("CONCAVE_JWT_SECRET", env) ?? getEnvValue("JWT_SECRET", env);
18689
18723
  const skipVerification = parseBoolean(getEnvValue("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
18690
18724
  const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
18691
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
18725
+ const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
18726
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
18692
18727
  return;
18693
18728
  }
18694
18729
  return {
@@ -18697,7 +18732,8 @@ function resolveJwtValidationConfigFromEnv(env) {
18697
18732
  audience,
18698
18733
  secret,
18699
18734
  skipVerification,
18700
- clockTolerance
18735
+ clockTolerance,
18736
+ jwksCacheTtlMs
18701
18737
  };
18702
18738
  }
18703
18739
  function normalizeList(value) {
@@ -18741,15 +18777,33 @@ function validateClaims(claims, config) {
18741
18777
  throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
18742
18778
  }
18743
18779
  }
18744
- function getRemoteJwks(jwksUrl) {
18780
+ function getRemoteJwks(jwksUrl, config) {
18781
+ const now = Date.now();
18745
18782
  const cached = JWKS_CACHE.get(jwksUrl);
18783
+ if (cached && cached.expiresAtMs > now) {
18784
+ return cached.resolver;
18785
+ }
18746
18786
  if (cached) {
18747
- return cached;
18787
+ JWKS_CACHE.delete(jwksUrl);
18748
18788
  }
18749
18789
  const jwks = createRemoteJWKSet(new URL(jwksUrl));
18750
- JWKS_CACHE.set(jwksUrl, jwks);
18790
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
18791
+ const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
18792
+ JWKS_CACHE.set(jwksUrl, {
18793
+ resolver: jwks,
18794
+ expiresAtMs: now + ttlMs
18795
+ });
18751
18796
  return jwks;
18752
18797
  }
18798
+ function resolveJwksCacheTtlMs(configuredTtl) {
18799
+ if (configuredTtl === undefined) {
18800
+ return DEFAULT_JWKS_CACHE_TTL_MS;
18801
+ }
18802
+ if (!Number.isFinite(configuredTtl)) {
18803
+ return DEFAULT_JWKS_CACHE_TTL_MS;
18804
+ }
18805
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS, Math.floor(configuredTtl)));
18806
+ }
18753
18807
  function decodeJwtUnsafe(token) {
18754
18808
  if (!token)
18755
18809
  return null;
@@ -18782,7 +18836,7 @@ async function verifyJwt(token, config) {
18782
18836
  const key = new TextEncoder().encode(effectiveConfig.secret);
18783
18837
  ({ payload } = await jwtVerify(token, key, options));
18784
18838
  } else {
18785
- ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
18839
+ ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
18786
18840
  }
18787
18841
  const claims = payload;
18788
18842
  validateClaims(claims, effectiveConfig);
@@ -19870,7 +19924,7 @@ class ForbiddenInQueriesOrMutations extends Error {
19870
19924
  this.name = "ForbiddenInQueriesOrMutations";
19871
19925
  }
19872
19926
  }
19873
- async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath) {
19927
+ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, deterministicSeed, mutationTransaction, udfExecutor, componentPath, snapshotOverride) {
19874
19928
  const ambientIdentity = getAuthContext();
19875
19929
  let effectiveAuth;
19876
19930
  if (auth && typeof auth === "object" && "tokenType" in auth) {
@@ -19885,7 +19939,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
19885
19939
  const inheritedSnapshot = snapshotContext.getStore() ?? null;
19886
19940
  const existingIdGenerator = idGeneratorContext.getStore() ?? undefined;
19887
19941
  const idGenerator = existingIdGenerator ?? (deterministicSeed ? createDeterministicIdGenerator(deterministicSeed) : undefined);
19888
- const convex = new UdfKernel(docstore, effectiveAuth, storage2, inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
19942
+ const convex = new UdfKernel(docstore, effectiveAuth, storage2, snapshotOverride ?? inheritedSnapshot, mutationTransaction, udfExecutor, componentPath, idGenerator);
19889
19943
  convex.clearAccessLogs();
19890
19944
  const logLines = [];
19891
19945
  const logger = (level) => {
@@ -19941,7 +19995,7 @@ async function runUdfAndGetLogs(docstore, fn, ops, auth, udfType, storage2, dete
19941
19995
  };
19942
19996
  } finally {}
19943
19997
  }
19944
- function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
19998
+ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath, snapshotOverride) {
19945
19999
  const tnow = Date.now();
19946
20000
  const seed = resolveSeed("query", requestId, tnow);
19947
20001
  const rng = udfRng(seed);
@@ -19953,7 +20007,7 @@ function runUdfQuery(docstore, fn, auth, storage2, requestId, udfExecutor, compo
19953
20007
  fetch: forbiddenAsyncOp("fetch"),
19954
20008
  setInterval: forbiddenAsyncOp("setInterval"),
19955
20009
  setTimeout: forbiddenAsyncOp("setTimeout")
19956
- }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath);
20010
+ }, auth, "query", storage2, seed, undefined, udfExecutor, componentPath, snapshotOverride);
19957
20011
  }
19958
20012
  function runUdfMutation(docstore, fn, auth, storage2, requestId, udfExecutor, componentPath) {
19959
20013
  const tnow = Date.now();
@@ -20397,7 +20451,7 @@ class InlineUdfExecutor {
20397
20451
  this.moduleRegistry = options.moduleRegistry;
20398
20452
  this.logSink = options.logSink;
20399
20453
  }
20400
- async execute(functionPath, args, udfType, auth, componentPath, requestId) {
20454
+ async execute(functionPath, args, udfType, auth, componentPath, requestId, snapshotTimestamp) {
20401
20455
  const [moduleName, functionName2] = this.parseUdfPath(functionPath);
20402
20456
  const finalRequestId = requestId ?? this.requestIdFactory(udfType, functionPath);
20403
20457
  const isSystemFunction = moduleName === "_system" || functionPath.startsWith("_system:");
@@ -20422,7 +20476,7 @@ class InlineUdfExecutor {
20422
20476
  const runWithType = () => {
20423
20477
  switch (udfType) {
20424
20478
  case "query":
20425
- return runUdfQuery(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath);
20479
+ return runUdfQuery(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath, snapshotTimestamp);
20426
20480
  case "mutation":
20427
20481
  return runUdfMutation(this.docstore, runUdf2, auth, this.blobstore, finalRequestId, this, componentPath);
20428
20482
  case "action":
@@ -20476,7 +20530,7 @@ class InlineUdfExecutor {
20476
20530
  async executeHttp(request, auth, requestId) {
20477
20531
  const url = new URL(request.url);
20478
20532
  const runHttpUdf = async () => {
20479
- const httpModule = await this.loadModule("http", request.url);
20533
+ const httpModule = await this.loadModule("http");
20480
20534
  const router = httpModule?.default;
20481
20535
  if (!router?.isRouter || typeof router.lookup !== "function") {
20482
20536
  throw new Error("convex/http.ts must export a default httpRouter()");
@@ -20610,7 +20664,7 @@ class UdfExecutionAdapter {
20610
20664
  this.executor = executor;
20611
20665
  this.callType = callType;
20612
20666
  }
20613
- async executeUdf(path, jsonArgs, type, auth, componentPath, requestId) {
20667
+ async executeUdf(path, jsonArgs, type, auth, componentPath, requestId, snapshotTimestamp) {
20614
20668
  const convexArgs = convertClientArgs(jsonArgs);
20615
20669
  const target = normalizeExecutionTarget(path, componentPath);
20616
20670
  let authContext2;
@@ -20648,7 +20702,7 @@ class UdfExecutionAdapter {
20648
20702
  return runWithAuth(userIdentity, async () => {
20649
20703
  const executeWithContext = this.callType === "client" ? runAsClientCall : runAsServerCall;
20650
20704
  return executeWithContext(async () => {
20651
- return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath2(target.componentPath), requestId);
20705
+ return await this.executor.execute(target.path, convexArgs, type, authContext2 ?? userIdentity, normalizeComponentPath2(target.componentPath), requestId, snapshotTimestamp);
20652
20706
  });
20653
20707
  });
20654
20708
  }
@@ -20960,6 +21014,39 @@ async function resolveAuthContext(bodyAuth, headerToken, headerIdentity) {
20960
21014
  }
20961
21015
  return bodyAuth;
20962
21016
  }
21017
+ function parseTimestampInput(value) {
21018
+ if (value === undefined || value === null) {
21019
+ return;
21020
+ }
21021
+ if (typeof value === "bigint") {
21022
+ return value >= 0n ? value : undefined;
21023
+ }
21024
+ if (typeof value === "number") {
21025
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
21026
+ return;
21027
+ }
21028
+ return BigInt(value);
21029
+ }
21030
+ if (typeof value === "string") {
21031
+ const trimmed = value.trim();
21032
+ if (!/^\d+$/.test(trimmed)) {
21033
+ return;
21034
+ }
21035
+ try {
21036
+ return BigInt(trimmed);
21037
+ } catch {
21038
+ return;
21039
+ }
21040
+ }
21041
+ return;
21042
+ }
21043
+ async function resolveSnapshotTimestamp(options, request) {
21044
+ const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
21045
+ if (typeof fromCallback === "bigint") {
21046
+ return fromCallback;
21047
+ }
21048
+ return BigInt(Date.now());
21049
+ }
20963
21050
  async function handleCoreHttpApiRequest(request, options) {
20964
21051
  const url = new URL(request.url);
20965
21052
  const segments = url.pathname.split("/").filter(Boolean);
@@ -20999,6 +21086,19 @@ async function handleCoreHttpApiRequest(request, options) {
20999
21086
  throw error;
21000
21087
  }
21001
21088
  const route = routeSegments[0];
21089
+ if (route === "query_ts") {
21090
+ if (request.method !== "POST") {
21091
+ return {
21092
+ handled: true,
21093
+ response: apply(Response.json({ error: "Method not allowed" }, { status: 405 }))
21094
+ };
21095
+ }
21096
+ const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
21097
+ return {
21098
+ handled: true,
21099
+ response: apply(Response.json({ ts: snapshotTimestamp.toString() }))
21100
+ };
21101
+ }
21002
21102
  if (route === "storage") {
21003
21103
  if (!options.storage) {
21004
21104
  return {
@@ -21056,7 +21156,7 @@ async function handleCoreHttpApiRequest(request, options) {
21056
21156
  }
21057
21157
  }
21058
21158
  }
21059
- if (route === "query" || route === "mutation" || route === "action") {
21159
+ if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
21060
21160
  if (request.method !== "POST") {
21061
21161
  return {
21062
21162
  handled: true,
@@ -21079,7 +21179,7 @@ async function handleCoreHttpApiRequest(request, options) {
21079
21179
  response: apply(Response.json({ error: "Invalid request body" }, { status: 400 }))
21080
21180
  };
21081
21181
  }
21082
- const { path, args, format, auth: bodyAuth, componentPath } = body;
21182
+ const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
21083
21183
  if (!path || typeof path !== "string") {
21084
21184
  return {
21085
21185
  handled: true,
@@ -21108,6 +21208,14 @@ async function handleCoreHttpApiRequest(request, options) {
21108
21208
  };
21109
21209
  }
21110
21210
  const jsonArgs = rawArgs ?? {};
21211
+ const executionType = route === "query_at_ts" ? "query" : route;
21212
+ const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
21213
+ if (route === "query_at_ts" && snapshotTimestamp === undefined) {
21214
+ return {
21215
+ handled: true,
21216
+ response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 }))
21217
+ };
21218
+ }
21111
21219
  let authForExecution;
21112
21220
  try {
21113
21221
  authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
@@ -21121,11 +21229,12 @@ async function handleCoreHttpApiRequest(request, options) {
21121
21229
  throw error;
21122
21230
  }
21123
21231
  const executionParams = {
21124
- type: route,
21232
+ type: executionType,
21125
21233
  path,
21126
21234
  args: jsonArgs,
21127
21235
  auth: authForExecution,
21128
21236
  componentPath,
21237
+ snapshotTimestamp,
21129
21238
  request
21130
21239
  };
21131
21240
  try {
@@ -21140,7 +21249,7 @@ async function handleCoreHttpApiRequest(request, options) {
21140
21249
  throw validationError;
21141
21250
  }
21142
21251
  const result = await options.executeFunction(executionParams);
21143
- if (options.notifyWrites && (route === "mutation" || route === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
21252
+ if (options.notifyWrites && (executionType === "mutation" || executionType === "action") && (result.writtenRanges?.length || result.writtenTables?.length)) {
21144
21253
  await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges(result.writtenRanges));
21145
21254
  }
21146
21255
  return {
@@ -21191,7 +21300,7 @@ function stripApiVersionPrefix(pathname) {
21191
21300
  }
21192
21301
  function isReservedApiPath(pathname) {
21193
21302
  const normalizedPath = stripApiVersionPrefix(pathname);
21194
- if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
21303
+ if (normalizedPath === "/api/execute" || normalizedPath === "/api/sync" || normalizedPath === "/api/reset-test-state" || normalizedPath === "/api/query" || normalizedPath === "/api/query_ts" || normalizedPath === "/api/query_at_ts" || normalizedPath === "/api/mutation" || normalizedPath === "/api/action") {
21195
21304
  return true;
21196
21305
  }
21197
21306
  if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
@@ -21262,7 +21371,7 @@ class HttpHandler {
21262
21371
  }
21263
21372
  };
21264
21373
  const coreResult = await handleCoreHttpApiRequest(request, {
21265
- executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
21374
+ executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
21266
21375
  notifyWrites,
21267
21376
  storage: this.docstore && this.blobstore ? {
21268
21377
  store: async (blob) => {
@@ -21281,7 +21390,16 @@ class HttpHandler {
21281
21390
  return { blob: blob ?? null };
21282
21391
  }
21283
21392
  } : undefined,
21284
- corsHeaders
21393
+ corsHeaders,
21394
+ getSnapshotTimestamp: () => {
21395
+ const oracle = this.docstore?.timestampOracle;
21396
+ const oracleTimestamp = typeof oracle?.beginSnapshot === "function" ? oracle.beginSnapshot() : typeof oracle?.getCurrentTimestamp === "function" ? oracle.getCurrentTimestamp() : undefined;
21397
+ const wallClock = BigInt(Date.now());
21398
+ if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
21399
+ return oracleTimestamp;
21400
+ }
21401
+ return wallClock;
21402
+ }
21285
21403
  });
21286
21404
  if (coreResult?.handled) {
21287
21405
  return coreResult.response;
@@ -21656,6 +21774,18 @@ function encodeServerMessage(message2) {
21656
21774
  }
21657
21775
  }
21658
21776
  }
21777
+ // ../core/dist/docstore/index.js
21778
+ init_interface();
21779
+
21780
+ // ../core/dist/docstore/vector-index-registration.js
21781
+ async function getVectorIndexesFromSchema(schemaService) {
21782
+ try {
21783
+ return await schemaService.getAllVectorIndexes();
21784
+ } catch (error) {
21785
+ console.warn("[Vector] Failed to extract vector indexes from schema:", error);
21786
+ return [];
21787
+ }
21788
+ }
21659
21789
  // ../core/dist/scheduler/scheduled-function-executor.js
21660
21790
  var SCHEDULED_FUNCTIONS_TABLE = "_scheduled_functions";
21661
21791
 
@@ -21668,6 +21798,8 @@ class ScheduledFunctionExecutor {
21668
21798
  logger;
21669
21799
  runMutationInTransaction;
21670
21800
  tableName;
21801
+ maxConcurrentJobs;
21802
+ scanPageSize;
21671
21803
  constructor(options) {
21672
21804
  this.docstore = options.docstore;
21673
21805
  this.udfExecutor = options.udfExecutor;
@@ -21677,46 +21809,68 @@ class ScheduledFunctionExecutor {
21677
21809
  this.logger = options.logger ?? console;
21678
21810
  this.runMutationInTransaction = options.runMutationInTransaction;
21679
21811
  this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE;
21812
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
21813
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
21680
21814
  }
21681
21815
  async runDueJobs() {
21682
21816
  const tableId = stringToHex(this.tableName);
21683
- const allJobs = await this.docstore.scan(tableId);
21684
21817
  const now = this.now();
21685
- const pendingJobs = allJobs.filter((doc) => {
21686
- const value = doc.value?.value;
21687
- if (!value || typeof value !== "object") {
21688
- return false;
21689
- }
21690
- const state = value.state;
21691
- const scheduledTime = value.scheduledTime;
21692
- return state?.kind === "pending" && typeof scheduledTime === "number" && scheduledTime <= now;
21693
- });
21694
- if (pendingJobs.length === 0) {
21695
- return {
21696
- executed: 0,
21697
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
21698
- };
21699
- }
21700
- await Promise.all(pendingJobs.map((job) => {
21701
- const jobValue = job.value?.value;
21702
- if (!jobValue) {
21703
- throw new Error("Job value unexpectedly missing after filter");
21818
+ let executed = 0;
21819
+ const inFlight = new Set;
21820
+ const schedule = async (jobValue) => {
21821
+ const run = this.executeJob(jobValue, tableId).then(() => {
21822
+ executed += 1;
21823
+ }).finally(() => {
21824
+ inFlight.delete(run);
21825
+ });
21826
+ inFlight.add(run);
21827
+ if (inFlight.size >= this.maxConcurrentJobs) {
21828
+ await Promise.race(inFlight);
21704
21829
  }
21705
- return this.executeJob(jobValue, tableId);
21706
- }));
21707
- return {
21708
- executed: pendingJobs.length,
21709
- nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
21710
21830
  };
21831
+ await this.forEachScheduledJob(async (jobValue) => {
21832
+ const state = jobValue.state;
21833
+ const scheduledTime = jobValue.scheduledTime;
21834
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
21835
+ return;
21836
+ }
21837
+ if (scheduledTime <= now) {
21838
+ await schedule(jobValue);
21839
+ }
21840
+ });
21841
+ await Promise.all(inFlight);
21842
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
21711
21843
  }
21712
21844
  async getNextScheduledTime() {
21713
- const tableId = stringToHex(this.tableName);
21714
- const allJobs = await this.docstore.scan(tableId);
21715
- return this.computeNextScheduledTime(allJobs);
21845
+ let nextScheduledTime = null;
21846
+ await this.forEachScheduledJob(async (jobValue) => {
21847
+ const state = jobValue.state;
21848
+ const scheduledTime = jobValue.scheduledTime;
21849
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
21850
+ return;
21851
+ }
21852
+ if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
21853
+ nextScheduledTime = scheduledTime;
21854
+ }
21855
+ });
21856
+ return nextScheduledTime;
21716
21857
  }
21717
- computeNextScheduledTime(allJobs) {
21718
- const pending = allJobs.map((doc) => doc.value?.value).filter((value) => !!value && value.state?.kind === "pending").map((value) => value.scheduledTime).filter((time) => typeof time === "number").sort((a, b) => a - b);
21719
- return pending.length > 0 ? pending[0] : null;
21858
+ async forEachScheduledJob(visitor) {
21859
+ const tableId = stringToHex(this.tableName);
21860
+ let cursor2 = null;
21861
+ while (true) {
21862
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order.Asc);
21863
+ for (const doc of page.documents) {
21864
+ const value = doc.value?.value;
21865
+ if (value && typeof value === "object") {
21866
+ await visitor(value);
21867
+ }
21868
+ }
21869
+ if (!page.hasMore || !page.nextCursor) {
21870
+ break;
21871
+ }
21872
+ cursor2 = page.nextCursor;
21873
+ }
21720
21874
  }
21721
21875
  async executeJob(jobValue, tableId) {
21722
21876
  const jobId = jobValue?._id;
@@ -21849,6 +22003,8 @@ class CronExecutor {
21849
22003
  allocateTimestamp;
21850
22004
  now;
21851
22005
  logger;
22006
+ maxConcurrentJobs;
22007
+ scanPageSize;
21852
22008
  constructor(options) {
21853
22009
  this.docstore = options.docstore;
21854
22010
  this.udfExecutor = options.udfExecutor;
@@ -21856,6 +22012,8 @@ class CronExecutor {
21856
22012
  this.allocateTimestamp = options.allocateTimestamp;
21857
22013
  this.now = options.now ?? (() => Date.now());
21858
22014
  this.logger = options.logger ?? console;
22015
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
22016
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
21859
22017
  }
21860
22018
  async syncCronSpecs(cronSpecs) {
21861
22019
  const tableId = stringToHex(CRONS_TABLE);
@@ -21941,33 +22099,58 @@ class CronExecutor {
21941
22099
  }
21942
22100
  async runDueJobs() {
21943
22101
  const tableId = stringToHex(CRONS_TABLE);
21944
- const allJobs = await this.docstore.scan(tableId);
21945
22102
  const now = this.now();
21946
- const dueJobs = allJobs.filter((doc) => {
21947
- const value = doc.value?.value;
21948
- return value && value.nextRun <= now;
21949
- });
21950
- if (dueJobs.length === 0) {
21951
- return {
21952
- executed: 0,
21953
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
21954
- };
21955
- }
21956
- await Promise.all(dueJobs.map((job) => this.executeJob(job, tableId)));
21957
- const updatedJobs = await this.docstore.scan(tableId);
21958
- return {
21959
- executed: dueJobs.length,
21960
- nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
22103
+ let executed = 0;
22104
+ const inFlight = new Set;
22105
+ const schedule = async (job) => {
22106
+ const run = this.executeJob(job, tableId).then(() => {
22107
+ executed += 1;
22108
+ }).finally(() => {
22109
+ inFlight.delete(run);
22110
+ });
22111
+ inFlight.add(run);
22112
+ if (inFlight.size >= this.maxConcurrentJobs) {
22113
+ await Promise.race(inFlight);
22114
+ }
21961
22115
  };
22116
+ await this.forEachCronJob(async (job) => {
22117
+ const value = job.value?.value;
22118
+ if (!value || typeof value.nextRun !== "number") {
22119
+ return;
22120
+ }
22121
+ if (value.nextRun <= now) {
22122
+ await schedule(job);
22123
+ }
22124
+ });
22125
+ await Promise.all(inFlight);
22126
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
21962
22127
  }
21963
22128
  async getNextScheduledTime() {
21964
- const tableId = stringToHex(CRONS_TABLE);
21965
- const allJobs = await this.docstore.scan(tableId);
21966
- return this.computeNextScheduledTime(allJobs);
22129
+ let nextScheduledTime = null;
22130
+ await this.forEachCronJob(async (job) => {
22131
+ const nextRun = job.value?.value?.nextRun;
22132
+ if (typeof nextRun !== "number") {
22133
+ return;
22134
+ }
22135
+ if (nextScheduledTime === null || nextRun < nextScheduledTime) {
22136
+ nextScheduledTime = nextRun;
22137
+ }
22138
+ });
22139
+ return nextScheduledTime;
21967
22140
  }
21968
- computeNextScheduledTime(allJobs) {
21969
- const nextTimes = allJobs.map((doc) => doc.value?.value?.nextRun).filter((time) => typeof time === "number").sort((a, b) => a - b);
21970
- return nextTimes.length > 0 ? nextTimes[0] : null;
22141
+ async forEachCronJob(visitor) {
22142
+ const tableId = stringToHex(CRONS_TABLE);
22143
+ let cursor2 = null;
22144
+ while (true) {
22145
+ const page = await this.docstore.scanPaginated(tableId, cursor2, this.scanPageSize, Order.Asc);
22146
+ for (const job of page.documents) {
22147
+ await visitor(job);
22148
+ }
22149
+ if (!page.hasMore || !page.nextCursor) {
22150
+ break;
22151
+ }
22152
+ cursor2 = page.nextCursor;
22153
+ }
21971
22154
  }
21972
22155
  async executeJob(job, tableId) {
21973
22156
  const value = job.value?.value;
@@ -22187,18 +22370,6 @@ var import_sender = __toESM(require_sender(), 1);
22187
22370
  var import_websocket = __toESM(require_websocket(), 1);
22188
22371
  var import_websocket_server = __toESM(require_websocket_server(), 1);
22189
22372
 
22190
- // ../core/dist/docstore/index.js
22191
- init_interface();
22192
-
22193
- // ../core/dist/docstore/vector-index-registration.js
22194
- async function getVectorIndexesFromSchema(schemaService) {
22195
- try {
22196
- return await schemaService.getAllVectorIndexes();
22197
- } catch (error) {
22198
- console.warn("[Vector] Failed to extract vector indexes from schema:", error);
22199
- return [];
22200
- }
22201
- }
22202
22373
  // ../runtime-base/dist/sync/index.js
22203
22374
  import { AsyncLocalStorage as AsyncLocalStorage32 } from "node:async_hooks";
22204
22375
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "node:async_hooks";
@@ -28047,6 +28218,8 @@ class SystemAuthError2 extends Error {
28047
28218
  }
28048
28219
  }
28049
28220
  var DEFAULT_CLOCK_TOLERANCE_SECONDS2 = 60;
28221
+ var DEFAULT_JWKS_CACHE_TTL_MS2 = 5 * 60 * 1000;
28222
+ var MAX_JWKS_CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
28050
28223
  var JWKS_CACHE2 = new Map;
28051
28224
  var defaultValidationConfig2;
28052
28225
  var adminAuthConfig2;
@@ -28142,7 +28315,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
28142
28315
  const secret = getEnvValue2("AUTH_SECRET", env) ?? getEnvValue2("CONCAVE_JWT_SECRET", env) ?? getEnvValue2("JWT_SECRET", env);
28143
28316
  const skipVerification = parseBoolean2(getEnvValue2("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean2(getEnvValue2("CONCAVE_JWT_SKIP_VERIFICATION", env));
28144
28317
  const clockTolerance = parseNumber2(getEnvValue2("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_CLOCK_TOLERANCE", env));
28145
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
28318
+ const jwksCacheTtlMs = parseNumber2(getEnvValue2("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber2(getEnvValue2("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
28319
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
28146
28320
  return;
28147
28321
  }
28148
28322
  return {
@@ -28151,7 +28325,8 @@ function resolveJwtValidationConfigFromEnv2(env) {
28151
28325
  audience,
28152
28326
  secret,
28153
28327
  skipVerification,
28154
- clockTolerance
28328
+ clockTolerance,
28329
+ jwksCacheTtlMs
28155
28330
  };
28156
28331
  }
28157
28332
  function normalizeList2(value) {
@@ -28195,15 +28370,33 @@ function validateClaims2(claims, config) {
28195
28370
  throw new JWTValidationError2("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
28196
28371
  }
28197
28372
  }
28198
- function getRemoteJwks2(jwksUrl) {
28373
+ function getRemoteJwks2(jwksUrl, config) {
28374
+ const now = Date.now();
28199
28375
  const cached = JWKS_CACHE2.get(jwksUrl);
28376
+ if (cached && cached.expiresAtMs > now) {
28377
+ return cached.resolver;
28378
+ }
28200
28379
  if (cached) {
28201
- return cached;
28380
+ JWKS_CACHE2.delete(jwksUrl);
28202
28381
  }
28203
28382
  const jwks = createRemoteJWKSet2(new URL(jwksUrl));
28204
- JWKS_CACHE2.set(jwksUrl, jwks);
28383
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig2?.jwksCacheTtlMs;
28384
+ const ttlMs = resolveJwksCacheTtlMs2(configuredTtl);
28385
+ JWKS_CACHE2.set(jwksUrl, {
28386
+ resolver: jwks,
28387
+ expiresAtMs: now + ttlMs
28388
+ });
28205
28389
  return jwks;
28206
28390
  }
28391
+ function resolveJwksCacheTtlMs2(configuredTtl) {
28392
+ if (configuredTtl === undefined) {
28393
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
28394
+ }
28395
+ if (!Number.isFinite(configuredTtl)) {
28396
+ return DEFAULT_JWKS_CACHE_TTL_MS2;
28397
+ }
28398
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS2, Math.floor(configuredTtl)));
28399
+ }
28207
28400
  function decodeJwtUnsafe2(token) {
28208
28401
  if (!token)
28209
28402
  return null;
@@ -28236,7 +28429,7 @@ async function verifyJwt2(token, config) {
28236
28429
  const key = new TextEncoder().encode(effectiveConfig.secret);
28237
28430
  ({ payload } = await jwtVerify2(token, key, options));
28238
28431
  } else {
28239
- ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl), options));
28432
+ ({ payload } = await jwtVerify2(token, getRemoteJwks2(effectiveConfig.jwksUrl, effectiveConfig), options));
28240
28433
  }
28241
28434
  const claims = payload;
28242
28435
  validateClaims2(claims, effectiveConfig);
@@ -28291,7 +28484,7 @@ class UdfExecutionAdapter2 {
28291
28484
  this.executor = executor2;
28292
28485
  this.callType = callType;
28293
28486
  }
28294
- async executeUdf(path, jsonArgs, type, auth2, componentPath, requestId) {
28487
+ async executeUdf(path, jsonArgs, type, auth2, componentPath, requestId, snapshotTimestamp) {
28295
28488
  const convexArgs = convertClientArgs2(jsonArgs);
28296
28489
  const target = normalizeExecutionTarget2(path, componentPath);
28297
28490
  let authContext22;
@@ -28329,7 +28522,7 @@ class UdfExecutionAdapter2 {
28329
28522
  return runWithAuth2(userIdentity, async () => {
28330
28523
  const executeWithContext = this.callType === "client" ? runAsClientCall2 : runAsServerCall2;
28331
28524
  return executeWithContext(async () => {
28332
- return await this.executor.execute(target.path, convexArgs, type, authContext22 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId);
28525
+ return await this.executor.execute(target.path, convexArgs, type, authContext22 ?? userIdentity, normalizeComponentPath3(target.componentPath), requestId, snapshotTimestamp);
28333
28526
  });
28334
28527
  });
28335
28528
  }
@@ -30603,6 +30796,11 @@ var WEBSOCKET_READY_STATE_OPEN = 1;
30603
30796
  var BACKPRESSURE_HIGH_WATER_MARK = 100;
30604
30797
  var BACKPRESSURE_BUFFER_LIMIT = 1024 * 1024;
30605
30798
  var SLOW_CLIENT_TIMEOUT_MS = 30000;
30799
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 5000;
30800
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW = 1000;
30801
+ var DEFAULT_OPERATION_TIMEOUT_MS = 15000;
30802
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION = 1000;
30803
+ var RATE_LIMIT_HARD_MULTIPLIER = 5;
30606
30804
 
30607
30805
  class SyncSession {
30608
30806
  websocket;
@@ -30631,15 +30829,25 @@ class SyncSession {
30631
30829
  class SyncProtocolHandler {
30632
30830
  udfExecutor;
30633
30831
  sessions = new Map;
30832
+ rateLimitStates = new Map;
30634
30833
  subscriptionManager;
30635
30834
  instanceName;
30636
30835
  backpressureController;
30637
30836
  heartbeatController;
30638
30837
  isDev;
30838
+ maxMessagesPerWindow;
30839
+ rateLimitWindowMs;
30840
+ operationTimeoutMs;
30841
+ maxActiveQueriesPerSession;
30639
30842
  constructor(instanceName, udfExecutor, options) {
30640
30843
  this.udfExecutor = udfExecutor;
30641
30844
  this.instanceName = instanceName;
30642
30845
  this.isDev = options?.isDev ?? true;
30846
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW);
30847
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS);
30848
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
30849
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS;
30850
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION);
30643
30851
  this.subscriptionManager = new SubscriptionManager2;
30644
30852
  this.backpressureController = new SessionBackpressureController({
30645
30853
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN,
@@ -30657,6 +30865,10 @@ class SyncProtocolHandler {
30657
30865
  createSession(sessionId, websocket) {
30658
30866
  const session = new SyncSession(websocket);
30659
30867
  this.sessions.set(sessionId, session);
30868
+ this.rateLimitStates.set(sessionId, {
30869
+ windowStartedAt: Date.now(),
30870
+ messagesInWindow: 0
30871
+ });
30660
30872
  return session;
30661
30873
  }
30662
30874
  getSession(sessionId) {
@@ -30671,6 +30883,7 @@ class SyncProtocolHandler {
30671
30883
  session.isDraining = false;
30672
30884
  this.subscriptionManager.unsubscribeAll(sessionId);
30673
30885
  this.sessions.delete(sessionId);
30886
+ this.rateLimitStates.delete(sessionId);
30674
30887
  }
30675
30888
  }
30676
30889
  updateSessionId(oldSessionId, newSessionId) {
@@ -30678,6 +30891,11 @@ class SyncProtocolHandler {
30678
30891
  if (session) {
30679
30892
  this.sessions.delete(oldSessionId);
30680
30893
  this.sessions.set(newSessionId, session);
30894
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
30895
+ if (rateLimitState) {
30896
+ this.rateLimitStates.delete(oldSessionId);
30897
+ this.rateLimitStates.set(newSessionId, rateLimitState);
30898
+ }
30681
30899
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
30682
30900
  }
30683
30901
  }
@@ -30686,6 +30904,29 @@ class SyncProtocolHandler {
30686
30904
  if (!session && message22.type !== "Connect") {
30687
30905
  throw new Error("Session not found");
30688
30906
  }
30907
+ if (session) {
30908
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
30909
+ if (rateLimitDecision === "reject") {
30910
+ return [
30911
+ {
30912
+ type: "FatalError",
30913
+ error: "Rate limit exceeded, retry shortly"
30914
+ }
30915
+ ];
30916
+ }
30917
+ if (rateLimitDecision === "close") {
30918
+ try {
30919
+ session.websocket.close(1013, "Rate limit exceeded");
30920
+ } catch {}
30921
+ this.destroySession(sessionId);
30922
+ return [
30923
+ {
30924
+ type: "FatalError",
30925
+ error: "Rate limit exceeded"
30926
+ }
30927
+ ];
30928
+ }
30929
+ }
30689
30930
  switch (message22.type) {
30690
30931
  case "Connect":
30691
30932
  return this.handleConnect(sessionId, message22);
@@ -30742,6 +30983,15 @@ class SyncProtocolHandler {
30742
30983
  return [fatalError];
30743
30984
  }
30744
30985
  const startVersion = makeStateVersion(session.querySetVersion, session.identityVersion, session.timestamp);
30986
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
30987
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
30988
+ return [
30989
+ {
30990
+ type: "FatalError",
30991
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
30992
+ }
30993
+ ];
30994
+ }
30745
30995
  session.querySetVersion = message22.newVersion;
30746
30996
  const modifications = [];
30747
30997
  for (const mod of message22.modifications) {
@@ -31065,7 +31315,54 @@ class SyncProtocolHandler {
31065
31315
  }, () => {
31066
31316
  return;
31067
31317
  });
31068
- return run;
31318
+ return this.withOperationTimeout(run);
31319
+ }
31320
+ withOperationTimeout(promise) {
31321
+ if (this.operationTimeoutMs <= 0) {
31322
+ return promise;
31323
+ }
31324
+ let timeoutHandle;
31325
+ const timeoutPromise = new Promise((_, reject) => {
31326
+ timeoutHandle = setTimeout(() => {
31327
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
31328
+ }, this.operationTimeoutMs);
31329
+ });
31330
+ return Promise.race([promise, timeoutPromise]).finally(() => {
31331
+ if (timeoutHandle) {
31332
+ clearTimeout(timeoutHandle);
31333
+ }
31334
+ });
31335
+ }
31336
+ consumeRateLimit(sessionId) {
31337
+ const state = this.rateLimitStates.get(sessionId);
31338
+ if (!state) {
31339
+ return "allow";
31340
+ }
31341
+ const now = Date.now();
31342
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
31343
+ state.windowStartedAt = now;
31344
+ state.messagesInWindow = 0;
31345
+ }
31346
+ state.messagesInWindow += 1;
31347
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
31348
+ return "allow";
31349
+ }
31350
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER);
31351
+ if (state.messagesInWindow >= hardLimit) {
31352
+ return "close";
31353
+ }
31354
+ return "reject";
31355
+ }
31356
+ computeProjectedActiveQueryCount(session, message22) {
31357
+ const projected = new Set(session.activeQueries.keys());
31358
+ for (const mod of message22.modifications) {
31359
+ if (mod.type === "Add") {
31360
+ projected.add(mod.queryId);
31361
+ } else if (mod.type === "Remove") {
31362
+ projected.delete(mod.queryId);
31363
+ }
31364
+ }
31365
+ return projected.size;
31069
31366
  }
31070
31367
  sendPing(session) {
31071
31368
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN) {
@@ -37311,6 +37608,8 @@ function decodeJwtClaimsToken3(token) {
37311
37608
  return null;
37312
37609
  }
37313
37610
  }
37611
+ var DEFAULT_JWKS_CACHE_TTL_MS3 = 5 * 60 * 1000;
37612
+ var MAX_JWKS_CACHE_TTL_MS3 = 24 * 60 * 60 * 1000;
37314
37613
  var JWKS_CACHE3 = new Map;
37315
37614
  function decodeJwtUnsafe3(token) {
37316
37615
  if (!token)
@@ -38128,6 +38427,8 @@ class ScheduledFunctionExecutor2 {
38128
38427
  logger;
38129
38428
  runMutationInTransaction;
38130
38429
  tableName;
38430
+ maxConcurrentJobs;
38431
+ scanPageSize;
38131
38432
  constructor(options) {
38132
38433
  this.docstore = options.docstore;
38133
38434
  this.udfExecutor = options.udfExecutor;
@@ -38137,46 +38438,68 @@ class ScheduledFunctionExecutor2 {
38137
38438
  this.logger = options.logger ?? console;
38138
38439
  this.runMutationInTransaction = options.runMutationInTransaction;
38139
38440
  this.tableName = options.tableName ?? SCHEDULED_FUNCTIONS_TABLE2;
38441
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
38442
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
38140
38443
  }
38141
38444
  async runDueJobs() {
38142
38445
  const tableId = stringToHex4(this.tableName);
38143
- const allJobs = await this.docstore.scan(tableId);
38144
38446
  const now = this.now();
38145
- const pendingJobs = allJobs.filter((doc) => {
38146
- const value = doc.value?.value;
38147
- if (!value || typeof value !== "object") {
38148
- return false;
38149
- }
38150
- const state = value.state;
38151
- const scheduledTime = value.scheduledTime;
38152
- return state?.kind === "pending" && typeof scheduledTime === "number" && scheduledTime <= now;
38153
- });
38154
- if (pendingJobs.length === 0) {
38155
- return {
38156
- executed: 0,
38157
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
38158
- };
38159
- }
38160
- await Promise.all(pendingJobs.map((job) => {
38161
- const jobValue = job.value?.value;
38162
- if (!jobValue) {
38163
- throw new Error("Job value unexpectedly missing after filter");
38447
+ let executed = 0;
38448
+ const inFlight = new Set;
38449
+ const schedule = async (jobValue) => {
38450
+ const run = this.executeJob(jobValue, tableId).then(() => {
38451
+ executed += 1;
38452
+ }).finally(() => {
38453
+ inFlight.delete(run);
38454
+ });
38455
+ inFlight.add(run);
38456
+ if (inFlight.size >= this.maxConcurrentJobs) {
38457
+ await Promise.race(inFlight);
38164
38458
  }
38165
- return this.executeJob(jobValue, tableId);
38166
- }));
38167
- return {
38168
- executed: pendingJobs.length,
38169
- nextScheduledTime: this.computeNextScheduledTime(await this.docstore.scan(tableId))
38170
38459
  };
38460
+ await this.forEachScheduledJob(async (jobValue) => {
38461
+ const state = jobValue.state;
38462
+ const scheduledTime = jobValue.scheduledTime;
38463
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
38464
+ return;
38465
+ }
38466
+ if (scheduledTime <= now) {
38467
+ await schedule(jobValue);
38468
+ }
38469
+ });
38470
+ await Promise.all(inFlight);
38471
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
38171
38472
  }
38172
38473
  async getNextScheduledTime() {
38173
- const tableId = stringToHex4(this.tableName);
38174
- const allJobs = await this.docstore.scan(tableId);
38175
- return this.computeNextScheduledTime(allJobs);
38474
+ let nextScheduledTime = null;
38475
+ await this.forEachScheduledJob(async (jobValue) => {
38476
+ const state = jobValue.state;
38477
+ const scheduledTime = jobValue.scheduledTime;
38478
+ if (state?.kind !== "pending" || typeof scheduledTime !== "number") {
38479
+ return;
38480
+ }
38481
+ if (nextScheduledTime === null || scheduledTime < nextScheduledTime) {
38482
+ nextScheduledTime = scheduledTime;
38483
+ }
38484
+ });
38485
+ return nextScheduledTime;
38176
38486
  }
38177
- computeNextScheduledTime(allJobs) {
38178
- const pending = allJobs.map((doc) => doc.value?.value).filter((value) => !!value && value.state?.kind === "pending").map((value) => value.scheduledTime).filter((time) => typeof time === "number").sort((a, b) => a - b);
38179
- return pending.length > 0 ? pending[0] : null;
38487
+ async forEachScheduledJob(visitor) {
38488
+ const tableId = stringToHex4(this.tableName);
38489
+ let cursor22 = null;
38490
+ while (true) {
38491
+ const page = await this.docstore.scanPaginated(tableId, cursor22, this.scanPageSize, Order3.Asc);
38492
+ for (const doc of page.documents) {
38493
+ const value = doc.value?.value;
38494
+ if (value && typeof value === "object") {
38495
+ await visitor(value);
38496
+ }
38497
+ }
38498
+ if (!page.hasMore || !page.nextCursor) {
38499
+ break;
38500
+ }
38501
+ cursor22 = page.nextCursor;
38502
+ }
38180
38503
  }
38181
38504
  async executeJob(jobValue, tableId) {
38182
38505
  const jobId = jobValue?._id;
@@ -38308,6 +38631,8 @@ class CronExecutor2 {
38308
38631
  allocateTimestamp;
38309
38632
  now;
38310
38633
  logger;
38634
+ maxConcurrentJobs;
38635
+ scanPageSize;
38311
38636
  constructor(options) {
38312
38637
  this.docstore = options.docstore;
38313
38638
  this.udfExecutor = options.udfExecutor;
@@ -38315,6 +38640,8 @@ class CronExecutor2 {
38315
38640
  this.allocateTimestamp = options.allocateTimestamp;
38316
38641
  this.now = options.now ?? (() => Date.now());
38317
38642
  this.logger = options.logger ?? console;
38643
+ this.maxConcurrentJobs = Math.max(1, options.maxConcurrentJobs ?? 8);
38644
+ this.scanPageSize = Math.max(1, options.scanPageSize ?? 256);
38318
38645
  }
38319
38646
  async syncCronSpecs(cronSpecs) {
38320
38647
  const tableId = stringToHex4(CRONS_TABLE2);
@@ -38400,33 +38727,58 @@ class CronExecutor2 {
38400
38727
  }
38401
38728
  async runDueJobs() {
38402
38729
  const tableId = stringToHex4(CRONS_TABLE2);
38403
- const allJobs = await this.docstore.scan(tableId);
38404
38730
  const now = this.now();
38405
- const dueJobs = allJobs.filter((doc) => {
38406
- const value = doc.value?.value;
38407
- return value && value.nextRun <= now;
38408
- });
38409
- if (dueJobs.length === 0) {
38410
- return {
38411
- executed: 0,
38412
- nextScheduledTime: this.computeNextScheduledTime(allJobs)
38413
- };
38414
- }
38415
- await Promise.all(dueJobs.map((job) => this.executeJob(job, tableId)));
38416
- const updatedJobs = await this.docstore.scan(tableId);
38417
- return {
38418
- executed: dueJobs.length,
38419
- nextScheduledTime: this.computeNextScheduledTime(updatedJobs)
38731
+ let executed = 0;
38732
+ const inFlight = new Set;
38733
+ const schedule = async (job) => {
38734
+ const run = this.executeJob(job, tableId).then(() => {
38735
+ executed += 1;
38736
+ }).finally(() => {
38737
+ inFlight.delete(run);
38738
+ });
38739
+ inFlight.add(run);
38740
+ if (inFlight.size >= this.maxConcurrentJobs) {
38741
+ await Promise.race(inFlight);
38742
+ }
38420
38743
  };
38744
+ await this.forEachCronJob(async (job) => {
38745
+ const value = job.value?.value;
38746
+ if (!value || typeof value.nextRun !== "number") {
38747
+ return;
38748
+ }
38749
+ if (value.nextRun <= now) {
38750
+ await schedule(job);
38751
+ }
38752
+ });
38753
+ await Promise.all(inFlight);
38754
+ return { executed, nextScheduledTime: await this.getNextScheduledTime() };
38421
38755
  }
38422
38756
  async getNextScheduledTime() {
38423
- const tableId = stringToHex4(CRONS_TABLE2);
38424
- const allJobs = await this.docstore.scan(tableId);
38425
- return this.computeNextScheduledTime(allJobs);
38757
+ let nextScheduledTime = null;
38758
+ await this.forEachCronJob(async (job) => {
38759
+ const nextRun = job.value?.value?.nextRun;
38760
+ if (typeof nextRun !== "number") {
38761
+ return;
38762
+ }
38763
+ if (nextScheduledTime === null || nextRun < nextScheduledTime) {
38764
+ nextScheduledTime = nextRun;
38765
+ }
38766
+ });
38767
+ return nextScheduledTime;
38426
38768
  }
38427
- computeNextScheduledTime(allJobs) {
38428
- const nextTimes = allJobs.map((doc) => doc.value?.value?.nextRun).filter((time) => typeof time === "number").sort((a, b) => a - b);
38429
- return nextTimes.length > 0 ? nextTimes[0] : null;
38769
+ async forEachCronJob(visitor) {
38770
+ const tableId = stringToHex4(CRONS_TABLE2);
38771
+ let cursor22 = null;
38772
+ while (true) {
38773
+ const page = await this.docstore.scanPaginated(tableId, cursor22, this.scanPageSize, Order3.Asc);
38774
+ for (const job of page.documents) {
38775
+ await visitor(job);
38776
+ }
38777
+ if (!page.hasMore || !page.nextCursor) {
38778
+ break;
38779
+ }
38780
+ cursor22 = page.nextCursor;
38781
+ }
38430
38782
  }
38431
38783
  async executeJob(job, tableId) {
38432
38784
  const value = job.value?.value;
@@ -46034,6 +46386,8 @@ class SystemAuthError3 extends Error {
46034
46386
  }
46035
46387
  }
46036
46388
  var DEFAULT_CLOCK_TOLERANCE_SECONDS3 = 60;
46389
+ var DEFAULT_JWKS_CACHE_TTL_MS4 = 5 * 60 * 1000;
46390
+ var MAX_JWKS_CACHE_TTL_MS4 = 24 * 60 * 60 * 1000;
46037
46391
  var JWKS_CACHE4 = new Map;
46038
46392
  var defaultValidationConfig3;
46039
46393
  var adminAuthConfig3;
@@ -46129,7 +46483,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
46129
46483
  const secret = getEnvValue3("AUTH_SECRET", env) ?? getEnvValue3("CONCAVE_JWT_SECRET", env) ?? getEnvValue3("JWT_SECRET", env);
46130
46484
  const skipVerification = parseBoolean3(getEnvValue3("AUTH_SKIP_VERIFICATION", env)) ?? parseBoolean3(getEnvValue3("CONCAVE_JWT_SKIP_VERIFICATION", env));
46131
46485
  const clockTolerance = parseNumber3(getEnvValue3("AUTH_CLOCK_TOLERANCE", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_CLOCK_TOLERANCE", env));
46132
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
46486
+ const jwksCacheTtlMs = parseNumber3(getEnvValue3("AUTH_JWKS_CACHE_TTL_MS", env)) ?? parseNumber3(getEnvValue3("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
46487
+ if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined && jwksCacheTtlMs === undefined) {
46133
46488
  return;
46134
46489
  }
46135
46490
  return {
@@ -46138,7 +46493,8 @@ function resolveJwtValidationConfigFromEnv3(env) {
46138
46493
  audience,
46139
46494
  secret,
46140
46495
  skipVerification,
46141
- clockTolerance
46496
+ clockTolerance,
46497
+ jwksCacheTtlMs
46142
46498
  };
46143
46499
  }
46144
46500
  function normalizeList3(value) {
@@ -46182,15 +46538,33 @@ function validateClaims3(claims, config) {
46182
46538
  throw new JWTValidationError3("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
46183
46539
  }
46184
46540
  }
46185
- function getRemoteJwks3(jwksUrl) {
46541
+ function getRemoteJwks3(jwksUrl, config) {
46542
+ const now = Date.now();
46186
46543
  const cached = JWKS_CACHE4.get(jwksUrl);
46544
+ if (cached && cached.expiresAtMs > now) {
46545
+ return cached.resolver;
46546
+ }
46187
46547
  if (cached) {
46188
- return cached;
46548
+ JWKS_CACHE4.delete(jwksUrl);
46189
46549
  }
46190
46550
  const jwks = createRemoteJWKSet3(new URL(jwksUrl));
46191
- JWKS_CACHE4.set(jwksUrl, jwks);
46551
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig3?.jwksCacheTtlMs;
46552
+ const ttlMs = resolveJwksCacheTtlMs3(configuredTtl);
46553
+ JWKS_CACHE4.set(jwksUrl, {
46554
+ resolver: jwks,
46555
+ expiresAtMs: now + ttlMs
46556
+ });
46192
46557
  return jwks;
46193
46558
  }
46559
+ function resolveJwksCacheTtlMs3(configuredTtl) {
46560
+ if (configuredTtl === undefined) {
46561
+ return DEFAULT_JWKS_CACHE_TTL_MS4;
46562
+ }
46563
+ if (!Number.isFinite(configuredTtl)) {
46564
+ return DEFAULT_JWKS_CACHE_TTL_MS4;
46565
+ }
46566
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS4, Math.floor(configuredTtl)));
46567
+ }
46194
46568
  function decodeJwtUnsafe4(token) {
46195
46569
  if (!token)
46196
46570
  return null;
@@ -46223,7 +46597,7 @@ async function verifyJwt3(token, config) {
46223
46597
  const key = new TextEncoder().encode(effectiveConfig.secret);
46224
46598
  ({ payload } = await jwtVerify3(token, key, options));
46225
46599
  } else {
46226
- ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl), options));
46600
+ ({ payload } = await jwtVerify3(token, getRemoteJwks3(effectiveConfig.jwksUrl, effectiveConfig), options));
46227
46601
  }
46228
46602
  const claims = payload;
46229
46603
  validateClaims3(claims, effectiveConfig);
@@ -47366,6 +47740,11 @@ var WEBSOCKET_READY_STATE_OPEN2 = 1;
47366
47740
  var BACKPRESSURE_HIGH_WATER_MARK2 = 100;
47367
47741
  var BACKPRESSURE_BUFFER_LIMIT2 = 1024 * 1024;
47368
47742
  var SLOW_CLIENT_TIMEOUT_MS2 = 30000;
47743
+ var DEFAULT_RATE_LIMIT_WINDOW_MS2 = 5000;
47744
+ var DEFAULT_MAX_MESSAGES_PER_WINDOW2 = 1000;
47745
+ var DEFAULT_OPERATION_TIMEOUT_MS2 = 15000;
47746
+ var DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2 = 1000;
47747
+ var RATE_LIMIT_HARD_MULTIPLIER2 = 5;
47369
47748
 
47370
47749
  class SyncSession2 {
47371
47750
  websocket;
@@ -47394,15 +47773,25 @@ class SyncSession2 {
47394
47773
  class SyncProtocolHandler2 {
47395
47774
  udfExecutor;
47396
47775
  sessions = new Map;
47776
+ rateLimitStates = new Map;
47397
47777
  subscriptionManager;
47398
47778
  instanceName;
47399
47779
  backpressureController;
47400
47780
  heartbeatController;
47401
47781
  isDev;
47782
+ maxMessagesPerWindow;
47783
+ rateLimitWindowMs;
47784
+ operationTimeoutMs;
47785
+ maxActiveQueriesPerSession;
47402
47786
  constructor(instanceName, udfExecutor, options) {
47403
47787
  this.udfExecutor = udfExecutor;
47404
47788
  this.instanceName = instanceName;
47405
47789
  this.isDev = options?.isDev ?? true;
47790
+ this.maxMessagesPerWindow = Math.max(1, options?.maxMessagesPerWindow ?? DEFAULT_MAX_MESSAGES_PER_WINDOW2);
47791
+ this.rateLimitWindowMs = Math.max(1, options?.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS2);
47792
+ const configuredOperationTimeout = options?.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS2;
47793
+ this.operationTimeoutMs = Number.isFinite(configuredOperationTimeout) ? Math.max(0, Math.floor(configuredOperationTimeout)) : DEFAULT_OPERATION_TIMEOUT_MS2;
47794
+ this.maxActiveQueriesPerSession = Math.max(1, options?.maxActiveQueriesPerSession ?? DEFAULT_MAX_ACTIVE_QUERIES_PER_SESSION2);
47406
47795
  this.subscriptionManager = new SubscriptionManager4;
47407
47796
  this.backpressureController = new SessionBackpressureController2({
47408
47797
  websocketReadyStateOpen: WEBSOCKET_READY_STATE_OPEN2,
@@ -47420,6 +47809,10 @@ class SyncProtocolHandler2 {
47420
47809
  createSession(sessionId, websocket) {
47421
47810
  const session = new SyncSession2(websocket);
47422
47811
  this.sessions.set(sessionId, session);
47812
+ this.rateLimitStates.set(sessionId, {
47813
+ windowStartedAt: Date.now(),
47814
+ messagesInWindow: 0
47815
+ });
47423
47816
  return session;
47424
47817
  }
47425
47818
  getSession(sessionId) {
@@ -47434,6 +47827,7 @@ class SyncProtocolHandler2 {
47434
47827
  session.isDraining = false;
47435
47828
  this.subscriptionManager.unsubscribeAll(sessionId);
47436
47829
  this.sessions.delete(sessionId);
47830
+ this.rateLimitStates.delete(sessionId);
47437
47831
  }
47438
47832
  }
47439
47833
  updateSessionId(oldSessionId, newSessionId) {
@@ -47441,6 +47835,11 @@ class SyncProtocolHandler2 {
47441
47835
  if (session) {
47442
47836
  this.sessions.delete(oldSessionId);
47443
47837
  this.sessions.set(newSessionId, session);
47838
+ const rateLimitState = this.rateLimitStates.get(oldSessionId);
47839
+ if (rateLimitState) {
47840
+ this.rateLimitStates.delete(oldSessionId);
47841
+ this.rateLimitStates.set(newSessionId, rateLimitState);
47842
+ }
47444
47843
  this.subscriptionManager.updateSessionId(oldSessionId, newSessionId);
47445
47844
  }
47446
47845
  }
@@ -47449,6 +47848,29 @@ class SyncProtocolHandler2 {
47449
47848
  if (!session && message22.type !== "Connect") {
47450
47849
  throw new Error("Session not found");
47451
47850
  }
47851
+ if (session) {
47852
+ const rateLimitDecision = this.consumeRateLimit(sessionId);
47853
+ if (rateLimitDecision === "reject") {
47854
+ return [
47855
+ {
47856
+ type: "FatalError",
47857
+ error: "Rate limit exceeded, retry shortly"
47858
+ }
47859
+ ];
47860
+ }
47861
+ if (rateLimitDecision === "close") {
47862
+ try {
47863
+ session.websocket.close(1013, "Rate limit exceeded");
47864
+ } catch {}
47865
+ this.destroySession(sessionId);
47866
+ return [
47867
+ {
47868
+ type: "FatalError",
47869
+ error: "Rate limit exceeded"
47870
+ }
47871
+ ];
47872
+ }
47873
+ }
47452
47874
  switch (message22.type) {
47453
47875
  case "Connect":
47454
47876
  return this.handleConnect(sessionId, message22);
@@ -47505,6 +47927,15 @@ class SyncProtocolHandler2 {
47505
47927
  return [fatalError];
47506
47928
  }
47507
47929
  const startVersion = makeStateVersion2(session.querySetVersion, session.identityVersion, session.timestamp);
47930
+ const projectedActiveQueryCount = this.computeProjectedActiveQueryCount(session, message22);
47931
+ if (projectedActiveQueryCount > this.maxActiveQueriesPerSession) {
47932
+ return [
47933
+ {
47934
+ type: "FatalError",
47935
+ error: `Too many active queries: ${projectedActiveQueryCount} exceeds limit ${this.maxActiveQueriesPerSession}`
47936
+ }
47937
+ ];
47938
+ }
47508
47939
  session.querySetVersion = message22.newVersion;
47509
47940
  const modifications = [];
47510
47941
  for (const mod of message22.modifications) {
@@ -47828,7 +48259,54 @@ class SyncProtocolHandler2 {
47828
48259
  }, () => {
47829
48260
  return;
47830
48261
  });
47831
- return run;
48262
+ return this.withOperationTimeout(run);
48263
+ }
48264
+ withOperationTimeout(promise) {
48265
+ if (this.operationTimeoutMs <= 0) {
48266
+ return promise;
48267
+ }
48268
+ let timeoutHandle;
48269
+ const timeoutPromise = new Promise((_, reject) => {
48270
+ timeoutHandle = setTimeout(() => {
48271
+ reject(new Error(`Sync operation timed out after ${this.operationTimeoutMs}ms`));
48272
+ }, this.operationTimeoutMs);
48273
+ });
48274
+ return Promise.race([promise, timeoutPromise]).finally(() => {
48275
+ if (timeoutHandle) {
48276
+ clearTimeout(timeoutHandle);
48277
+ }
48278
+ });
48279
+ }
48280
+ consumeRateLimit(sessionId) {
48281
+ const state = this.rateLimitStates.get(sessionId);
48282
+ if (!state) {
48283
+ return "allow";
48284
+ }
48285
+ const now = Date.now();
48286
+ if (now - state.windowStartedAt >= this.rateLimitWindowMs) {
48287
+ state.windowStartedAt = now;
48288
+ state.messagesInWindow = 0;
48289
+ }
48290
+ state.messagesInWindow += 1;
48291
+ if (state.messagesInWindow <= this.maxMessagesPerWindow) {
48292
+ return "allow";
48293
+ }
48294
+ const hardLimit = Math.max(this.maxMessagesPerWindow + 1, this.maxMessagesPerWindow * RATE_LIMIT_HARD_MULTIPLIER2);
48295
+ if (state.messagesInWindow >= hardLimit) {
48296
+ return "close";
48297
+ }
48298
+ return "reject";
48299
+ }
48300
+ computeProjectedActiveQueryCount(session, message22) {
48301
+ const projected = new Set(session.activeQueries.keys());
48302
+ for (const mod of message22.modifications) {
48303
+ if (mod.type === "Add") {
48304
+ projected.add(mod.queryId);
48305
+ } else if (mod.type === "Remove") {
48306
+ projected.delete(mod.queryId);
48307
+ }
48308
+ }
48309
+ return projected.size;
47832
48310
  }
47833
48311
  sendPing(session) {
47834
48312
  if (!session.websocket || session.websocket.readyState !== WEBSOCKET_READY_STATE_OPEN2) {