@rebasepro/server 0.9.1-canary.d906fb7 → 0.9.1-canary.e3f810f

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.es.js CHANGED
@@ -3305,7 +3305,7 @@ var BackendCollectionRegistry = class extends CollectionRegistry {
3305
3305
  //#endregion
3306
3306
  //#region src/collections/loader.ts
3307
3307
  function isCollectionFile(file) {
3308
- return (file.endsWith(".ts") || file.endsWith(".js")) && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js";
3308
+ return (file.endsWith(".ts") || file.endsWith(".js")) && !file.startsWith(".") && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js";
3309
3309
  }
3310
3310
  async function importModule(filePath) {
3311
3311
  return await import(pathToFileURL(filePath).href);
@@ -3502,6 +3502,21 @@ function createRoutedRealtimeService(opts) {
3502
3502
  //#region src/api/errors.ts
3503
3503
  /** Tracks whether we've already shown the doctor hint (once per process). */
3504
3504
  var _schemaDriftHinted = false;
3505
+ /** 5-character SQLSTATE, e.g. `42501`, `23505`. */
3506
+ var SQLSTATE_RE = /^[0-9A-Z]{5}$/;
3507
+ /**
3508
+ * Walk the cause chain for the underlying database error, identified by a
3509
+ * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route
3510
+ * code sometimes wraps drizzle again, so the real error may sit several
3511
+ * levels down.
3512
+ */
3513
+ function extractDbError(error, depth = 0) {
3514
+ if (!error || typeof error !== "object" || depth > 8) return null;
3515
+ const e = error;
3516
+ if (typeof e.code === "string" && SQLSTATE_RE.test(e.code)) return e;
3517
+ if (e.cause && typeof e.cause === "object") return extractDbError(e.cause, depth + 1);
3518
+ return null;
3519
+ }
3505
3520
  /**
3506
3521
  * Extract the missing table or column name from a PG error.
3507
3522
  * PG 42P01 messages look like: 'relation "my_table" does not exist'
@@ -3586,26 +3601,31 @@ var errorHandler = (err, c) => {
3586
3601
  if (cause.code === "ECONNREFUSED" && !cause.address && Array.isArray(cause.errors)) resolvedCause = cause.errors.find((e) => e.address) || cause;
3587
3602
  else resolvedCause = cause;
3588
3603
  }
3589
- if (resolvedCause) {
3604
+ const dbError = extractDbError(error);
3605
+ if (resolvedCause && (resolvedCause.code === "ENETUNREACH" || resolvedCause.code === "ECONNREFUSED")) {
3590
3606
  const cause = resolvedCause;
3591
3607
  if (cause.code === "ENETUNREACH") logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;
3592
- else if (cause.code === "ECONNREFUSED") logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
3593
- else if (cause.code === "42703" || cause.code === "42P01") {
3594
- code = "SCHEMA_DRIFT";
3595
- logMessage = `Schema drift: ${cause.code === "42703" ? "column" : "table"} "${cause.table || cause.column || extractMissingIdentifier(cause.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3596
- }
3608
+ else logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
3597
3609
  } else if ("code" in error && error.code === "ENETUNREACH") {
3598
3610
  const netErr = error;
3599
3611
  logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;
3600
- } else if ("code" in error && (error.code === "42703" || error.code === "42P01")) {
3612
+ } else if (dbError && (dbError.code === "42703" || dbError.code === "42P01")) {
3601
3613
  code = "SCHEMA_DRIFT";
3602
- const issue = error.code === "42703" ? "column" : "table";
3603
- const pgErr = error;
3604
- logMessage = `Schema drift: ${issue} "${pgErr.table || pgErr.column || extractMissingIdentifier(error.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3605
- }
3606
- const causePg = error.cause && typeof error.cause === "object" ? error.cause : void 0;
3607
- const pgErrorCode = causePg?.code || error.code;
3608
- const isDbSchemaMismatch = code === "SCHEMA_DRIFT" || pgErrorCode === "42703" || pgErrorCode === "42P01";
3614
+ logMessage = `Schema drift: ${dbError.code === "42703" ? "column" : "table"} "${dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3615
+ } else if (dbError) {
3616
+ const parts = [`[PG ${dbError.code}] ${dbError.message}`];
3617
+ if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);
3618
+ if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);
3619
+ if (dbError.table) parts.push(`Table: ${dbError.table}`);
3620
+ if (dbError.column) parts.push(`Column: ${dbError.column}`);
3621
+ if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);
3622
+ if (dbError.code === "42501") {
3623
+ code = "DB_PERMISSION_DENIED";
3624
+ parts.push(`The database rejected the statement for lack of privilege — usually a row-level security policy${dbError.table ? ` on "${dbError.table}"` : ""} denying this role, or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.`);
3625
+ }
3626
+ logMessage = parts.join(". ");
3627
+ }
3628
+ const isDbSchemaMismatch = code === "SCHEMA_DRIFT";
3609
3629
  if (isDbSchemaMismatch) {
3610
3630
  logger.warn(`⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` + (reqId ? ` [${reqId}]` : ""));
3611
3631
  if (!_schemaDriftHinted && process.env.NODE_ENV !== "production") {
@@ -3624,18 +3644,27 @@ var errorHandler = (err, c) => {
3624
3644
  ].join("\n"));
3625
3645
  }
3626
3646
  } else logger.error(`❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` + (reqId ? ` [${reqId}]` : ""));
3627
- if (!(isDbSchemaMismatch || statusCode < 500 && code === "BAD_REQUEST")) logger.error(String(error.stack || error));
3647
+ if (!(isDbSchemaMismatch || dbError !== null || statusCode < 500 && code === "BAD_REQUEST")) logger.error(String(error.stack || error));
3628
3648
  let clientMessage = "An unexpected error occurred";
3629
3649
  if (statusCode < 500 && error.message) clientMessage = error.message;
3630
3650
  else if (error instanceof ApiError || error.name === "ApiError") clientMessage = error.message;
3631
3651
  else if (code === "SCHEMA_DRIFT") {
3632
- const pgErr = causePg || error;
3633
- clientMessage = `Schema drift: ${pgErr.code === "42703" || pgErrorCode === "42703" ? "column" : "table"} "${pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || "unknown"}" does not exist. Run \`pnpm db:push\` to sync your schema.`;
3634
- } else if (code === "INTERNAL_ERROR") clientMessage = "Internal Server Error";
3652
+ const pgErr = dbError || error;
3653
+ clientMessage = `Schema drift: ${pgErr.code === "42703" ? "column" : "table"} "${pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || "unknown"}" does not exist. Run \`pnpm db:push\` to sync your schema.`;
3654
+ } else if (code === "DB_PERMISSION_DENIED") clientMessage = `Permission denied by the database${dbError?.table ? ` on "${dbError.table}"` : ""} (row-level security). Check the RLS policies for this table.`;
3655
+ else if (code === "INTERNAL_ERROR") clientMessage = "Internal Server Error";
3656
+ const dbDetails = dbError ? {
3657
+ dbCode: dbError.code,
3658
+ ...process.env.NODE_ENV !== "production" && {
3659
+ dbMessage: dbError.message,
3660
+ ...dbError.detail && { detail: dbError.detail },
3661
+ ...dbError.hint && { hint: dbError.hint }
3662
+ }
3663
+ } : void 0;
3635
3664
  return c.json({ error: {
3636
3665
  message: clientMessage,
3637
3666
  code,
3638
- ...error.details !== void 0 && { details: error.details },
3667
+ ...error.details !== void 0 ? { details: error.details } : dbDetails !== void 0 ? { details: dbDetails } : {},
3639
3668
  ...reqId && { requestId: reqId }
3640
3669
  } }, statusCode);
3641
3670
  };
@@ -3657,6 +3686,7 @@ function codeToStatus(code) {
3657
3686
  EMAIL_EXISTS: 409,
3658
3687
  ROLE_EXISTS: 409,
3659
3688
  SCHEMA_DRIFT: 500,
3689
+ DB_PERMISSION_DENIED: 500,
3660
3690
  INTERNAL_ERROR: 500,
3661
3691
  NOT_CONFIGURED: 503,
3662
3692
  SERVICE_UNAVAILABLE: 503
@@ -14382,7 +14412,7 @@ async function initializeStorage(storageConfig, isProduction) {
14382
14412
  //#region src/init/docs.ts
14383
14413
  async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
14384
14414
  if (enableSwagger === false || activeCollections.length === 0) return;
14385
- const { generateOpenApiSpec } = await import("./openapi-generator-Z9oYWLf_.js");
14415
+ const { generateOpenApiSpec } = await import("./openapi-generator-DqSIwNLV.js");
14386
14416
  app.get(`${basePath}/docs`, (c) => {
14387
14417
  const spec = generateOpenApiSpec(activeCollections, {
14388
14418
  basePath,
@@ -15903,9 +15933,11 @@ var ClientStorageSourceRegistry = class ClientStorageSourceRegistry {
15903
15933
  function extractMessageError(message) {
15904
15934
  const payload = message.payload;
15905
15935
  const errPayload = payload?.error;
15936
+ const errorMessage = typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error";
15937
+ const errorCode = typeof errPayload === "object" ? errPayload.code : payload?.code;
15906
15938
  return {
15907
- errorMessage: typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error",
15908
- errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
15939
+ errorMessage: typeof errorMessage === "string" ? errorMessage : errorMessage == null ? "Unknown error" : JSON.stringify(errorMessage),
15940
+ errorCode
15909
15941
  };
15910
15942
  }
15911
15943
  /**
@@ -15941,6 +15973,7 @@ var RebaseWebSocketClient = class {
15941
15973
  isConnected = false;
15942
15974
  messageQueue = [];
15943
15975
  requestTimeoutMs = 3e4;
15976
+ subscriptionTimeoutMs = 3e4;
15944
15977
  reconnectTimeout = null;
15945
15978
  isAuthenticated = false;
15946
15979
  authPromise = null;
@@ -16046,6 +16079,7 @@ var RebaseWebSocketClient = class {
16046
16079
  this.emit(wasReconnect ? "reconnect" : "connect");
16047
16080
  this.processMessageQueue();
16048
16081
  if (wasReconnect) this.resubscribeAll();
16082
+ this.armPendingSubscribeWatchdogs();
16049
16083
  };
16050
16084
  this.ws.onmessage = (event) => {
16051
16085
  try {
@@ -16060,6 +16094,7 @@ var RebaseWebSocketClient = class {
16060
16094
  this.isConnected = false;
16061
16095
  this.isAuthenticated = false;
16062
16096
  this.authPromise = null;
16097
+ this.suspendSubscribeWatchdogs();
16063
16098
  this.emit("disconnect");
16064
16099
  for (const [reqId, request] of this.pendingRequests.entries()) {
16065
16100
  if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
@@ -16091,6 +16126,7 @@ var RebaseWebSocketClient = class {
16091
16126
  attemptReconnect() {
16092
16127
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
16093
16128
  console.error("Max reconnection attempts reached");
16129
+ this.failAllPendingSubscriptions(new RebaseApiError("Connection lost", { code: "CONNECTION_LOST" }));
16094
16130
  return;
16095
16131
  }
16096
16132
  this.reconnectAttempts++;
@@ -16145,29 +16181,18 @@ var RebaseWebSocketClient = class {
16145
16181
  subscription.backendSubscriptionId = newBackendId;
16146
16182
  backendKeyMap.delete(oldBackendId);
16147
16183
  backendKeyMap.set(newBackendId, subscriptionKey);
16148
- this.sendMessage({
16149
- type: messageType,
16150
- payload: {
16151
- ...subscription.props,
16152
- subscriptionId: newBackendId
16153
- }
16154
- }).catch((error) => {
16155
- console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
16156
- subscription.callbacks.forEach((callback) => {
16157
- if (callback.onError) callback.onError(error);
16158
- });
16159
- });
16160
- } else {
16161
- const { errorMessage, errorCode } = extractMessageError(message);
16162
- const error = new RebaseApiError(errorMessage, { code: errorCode });
16163
- subscription.callbacks.forEach((callback) => {
16164
- if (callback.onError) callback.onError(error);
16165
- });
16184
+ if (messageType === "subscribe_collection") this.sendCollectionSubscribe(subscriptionKey);
16185
+ else this.sendEntitySubscribe(subscriptionKey);
16186
+ return;
16166
16187
  }
16188
+ const { errorMessage, errorCode } = extractMessageError(message);
16189
+ const error = new RebaseApiError(errorMessage, { code: errorCode });
16190
+ if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
16191
+ else this.failEntitySubscription(subscriptionKey, error);
16167
16192
  }).catch((err) => {
16168
- subscription.callbacks.forEach((callback) => {
16169
- if (callback.onError) callback.onError(err);
16170
- });
16193
+ const error = err instanceof Error ? err : new Error(String(err));
16194
+ if (messageType === "subscribe_collection") this.failCollectionSubscription(subscriptionKey, error);
16195
+ else this.failEntitySubscription(subscriptionKey, error);
16171
16196
  });
16172
16197
  }
16173
16198
  handleWebSocketMessage(message) {
@@ -16208,6 +16233,9 @@ var RebaseWebSocketClient = class {
16208
16233
  collectionSub.latestData = rows;
16209
16234
  collectionSub.lastUpdated = Date.now();
16210
16235
  collectionSub.isInitialDataReceived = true;
16236
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
16237
+ collectionSub.subscribeTimeout = void 0;
16238
+ collectionSub.subscribeInFlight = false;
16211
16239
  collectionSub.callbacks.forEach((callback) => {
16212
16240
  try {
16213
16241
  callback.onUpdate(rows);
@@ -16263,6 +16291,9 @@ var RebaseWebSocketClient = class {
16263
16291
  entitySub.latestData = row;
16264
16292
  entitySub.lastUpdated = Date.now();
16265
16293
  entitySub.isInitialDataReceived = true;
16294
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
16295
+ entitySub.subscribeTimeout = void 0;
16296
+ entitySub.subscribeInFlight = false;
16266
16297
  entitySub.callbacks.forEach((callback) => {
16267
16298
  try {
16268
16299
  callback.onUpdate(row);
@@ -16284,6 +16315,9 @@ var RebaseWebSocketClient = class {
16284
16315
  this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
16285
16316
  return;
16286
16317
  }
16318
+ if (collectionSub.subscribeTimeout) clearTimeout(collectionSub.subscribeTimeout);
16319
+ collectionSub.subscribeTimeout = void 0;
16320
+ collectionSub.subscribeInFlight = false;
16287
16321
  const { errorMessage, errorCode } = extractMessageError(message);
16288
16322
  const error = new RebaseApiError(errorMessage, { code: errorCode });
16289
16323
  collectionSub.callbacks.forEach((callback) => {
@@ -16300,6 +16334,9 @@ var RebaseWebSocketClient = class {
16300
16334
  this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "row", this.backendToEntityKey, "subscribe_one");
16301
16335
  return;
16302
16336
  }
16337
+ if (entitySub.subscribeTimeout) clearTimeout(entitySub.subscribeTimeout);
16338
+ entitySub.subscribeTimeout = void 0;
16339
+ entitySub.subscribeInFlight = false;
16303
16340
  const { errorMessage, errorCode } = extractMessageError(message);
16304
16341
  const error = new RebaseApiError(errorMessage, { code: errorCode });
16305
16342
  entitySub.callbacks.forEach((callback) => {
@@ -16648,9 +16685,12 @@ var RebaseWebSocketClient = class {
16648
16685
  console.error("Error in collection subscription callback:", error);
16649
16686
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
16650
16687
  }
16688
+ else if (!existingSubscription.subscribeInFlight) this.sendCollectionSubscribe(subscriptionKey);
16651
16689
  return () => {
16652
16690
  callbackMap.delete(callbackId);
16653
16691
  if (callbackMap.size === 0) {
16692
+ if (this.collectionSubscriptions.get(subscriptionKey) !== existingSubscription) return;
16693
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
16654
16694
  this.collectionSubscriptions.delete(subscriptionKey);
16655
16695
  this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
16656
16696
  if (this.isConnected && this.ws) this.sendMessage({
@@ -16672,21 +16712,14 @@ var RebaseWebSocketClient = class {
16672
16712
  props
16673
16713
  });
16674
16714
  this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
16675
- this.sendMessage({
16676
- type: "subscribe_collection",
16677
- payload: {
16678
- ...props,
16679
- subscriptionId: backendSubscriptionId
16680
- }
16681
- }).catch((error) => {
16682
- if (onError) onError(error);
16683
- });
16715
+ this.sendCollectionSubscribe(subscriptionKey);
16684
16716
  return () => {
16685
16717
  const subscription = this.collectionSubscriptions.get(subscriptionKey);
16686
16718
  if (subscription) {
16687
16719
  const callbacks = subscription.callbacks;
16688
16720
  callbacks.delete(callbackId);
16689
16721
  if (callbacks.size === 0) {
16722
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
16690
16723
  this.collectionSubscriptions.delete(subscriptionKey);
16691
16724
  this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
16692
16725
  if (this.isConnected && this.ws) this.sendMessage({
@@ -16713,9 +16746,12 @@ var RebaseWebSocketClient = class {
16713
16746
  console.error("Error in row subscription callback:", error);
16714
16747
  if (onError) onError(error instanceof Error ? error : new Error(String(error)));
16715
16748
  }
16749
+ else if (!existingSubscription.subscribeInFlight) this.sendEntitySubscribe(subscriptionKey);
16716
16750
  return () => {
16717
16751
  callbackMap.delete(callbackId);
16718
16752
  if (callbackMap.size === 0) {
16753
+ if (this.singleSubscriptions.get(subscriptionKey) !== existingSubscription) return;
16754
+ if (existingSubscription.subscribeTimeout) clearTimeout(existingSubscription.subscribeTimeout);
16719
16755
  this.singleSubscriptions.delete(subscriptionKey);
16720
16756
  this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
16721
16757
  if (this.isConnected && this.ws) this.sendMessage({
@@ -16737,15 +16773,7 @@ var RebaseWebSocketClient = class {
16737
16773
  props
16738
16774
  });
16739
16775
  this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
16740
- this.sendMessage({
16741
- type: "subscribe_one",
16742
- payload: {
16743
- ...props,
16744
- subscriptionId: backendSubscriptionId
16745
- }
16746
- }).catch((error) => {
16747
- if (onError) onError(error);
16748
- });
16776
+ this.sendEntitySubscribe(subscriptionKey);
16749
16777
  return () => {
16750
16778
  const subscription = this.singleSubscriptions.get(subscriptionKey);
16751
16779
  if (subscription) {
@@ -16763,6 +16791,157 @@ var RebaseWebSocketClient = class {
16763
16791
  };
16764
16792
  }
16765
16793
  /**
16794
+ * Send a `subscribe_collection` for an already-registered subscription and
16795
+ * arm its watchdog.
16796
+ *
16797
+ * Every path that registers a collection subscription goes through here, so
16798
+ * that a subscribe which never lands — a rejected send, or a server that
16799
+ * never answers — always ends up in `failCollectionSubscription` rather than
16800
+ * leaving the entry parked with `isInitialDataReceived === false` forever.
16801
+ */
16802
+ sendCollectionSubscribe(subscriptionKey) {
16803
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
16804
+ if (!subscription) return;
16805
+ const backendSubscriptionId = subscription.backendSubscriptionId;
16806
+ subscription.subscribeInFlight = true;
16807
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
16808
+ subscription.subscribeTimeout = void 0;
16809
+ if (this.isConnected) this.sendCollectionSubscribeWatchdog(subscriptionKey);
16810
+ this.sendMessage({
16811
+ type: "subscribe_collection",
16812
+ payload: {
16813
+ ...subscription.props,
16814
+ subscriptionId: backendSubscriptionId
16815
+ }
16816
+ }).catch((error) => {
16817
+ const current = this.collectionSubscriptions.get(subscriptionKey);
16818
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
16819
+ this.failCollectionSubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
16820
+ });
16821
+ }
16822
+ /** The `listenOne` counterpart of {@link sendCollectionSubscribe}. */
16823
+ sendEntitySubscribe(subscriptionKey) {
16824
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
16825
+ if (!subscription) return;
16826
+ const backendSubscriptionId = subscription.backendSubscriptionId;
16827
+ subscription.subscribeInFlight = true;
16828
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
16829
+ subscription.subscribeTimeout = void 0;
16830
+ if (this.isConnected) this.sendEntitySubscribeWatchdog(subscriptionKey);
16831
+ this.sendMessage({
16832
+ type: "subscribe_one",
16833
+ payload: {
16834
+ ...subscription.props,
16835
+ subscriptionId: backendSubscriptionId
16836
+ }
16837
+ }).catch((error) => {
16838
+ const current = this.singleSubscriptions.get(subscriptionKey);
16839
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
16840
+ this.failEntitySubscription(subscriptionKey, error instanceof Error ? error : new Error(String(error)));
16841
+ });
16842
+ }
16843
+ /**
16844
+ * Report a subscribe failure to every listener and drop the registration.
16845
+ *
16846
+ * Dropping it is the point: the callbacks stay live (their components are
16847
+ * still mounted and have been told), but the next `listenCollection` for
16848
+ * these params finds no entry and issues a fresh subscribe instead of
16849
+ * silently attaching to a dead one.
16850
+ */
16851
+ failCollectionSubscription(subscriptionKey, error) {
16852
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
16853
+ if (!subscription) return;
16854
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
16855
+ subscription.subscribeInFlight = false;
16856
+ this.collectionSubscriptions.delete(subscriptionKey);
16857
+ this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
16858
+ subscription.callbacks.forEach((callback) => {
16859
+ if (callback.onError) try {
16860
+ callback.onError(error);
16861
+ } catch (callbackError) {
16862
+ console.error("Error in collection subscription error callback:", callbackError);
16863
+ }
16864
+ });
16865
+ }
16866
+ /** The `listenOne` counterpart of {@link failCollectionSubscription}. */
16867
+ failEntitySubscription(subscriptionKey, error) {
16868
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
16869
+ if (!subscription) return;
16870
+ if (subscription.subscribeTimeout) clearTimeout(subscription.subscribeTimeout);
16871
+ subscription.subscribeInFlight = false;
16872
+ this.singleSubscriptions.delete(subscriptionKey);
16873
+ this.backendToEntityKey.delete(subscription.backendSubscriptionId);
16874
+ subscription.callbacks.forEach((callback) => {
16875
+ if (callback.onError) try {
16876
+ callback.onError(error);
16877
+ } catch (callbackError) {
16878
+ console.error("Error in row subscription error callback:", callbackError);
16879
+ }
16880
+ });
16881
+ }
16882
+ /**
16883
+ * Stop the watchdogs without failing anything — used when the socket drops,
16884
+ * since the reconnect path re-subscribes everything anyway and a watchdog
16885
+ * firing mid-reconnect would tear down healthy subscriptions.
16886
+ */
16887
+ suspendSubscribeWatchdogs() {
16888
+ for (const sub of this.collectionSubscriptions.values()) {
16889
+ if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
16890
+ sub.subscribeTimeout = void 0;
16891
+ sub.subscribeInFlight = false;
16892
+ }
16893
+ for (const sub of this.singleSubscriptions.values()) {
16894
+ if (sub.subscribeTimeout) clearTimeout(sub.subscribeTimeout);
16895
+ sub.subscribeTimeout = void 0;
16896
+ sub.subscribeInFlight = false;
16897
+ }
16898
+ }
16899
+ /**
16900
+ * Arm watchdogs for subscribes that were requested while offline and have
16901
+ * just been flushed to the socket. Their timers were deliberately not set at
16902
+ * request time, so without this they would have no timeout at all.
16903
+ */
16904
+ armPendingSubscribeWatchdogs() {
16905
+ for (const [key, sub] of this.collectionSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendCollectionSubscribeWatchdog(key);
16906
+ for (const [key, sub] of this.singleSubscriptions.entries()) if (sub.subscribeInFlight && !sub.subscribeTimeout) this.sendEntitySubscribeWatchdog(key);
16907
+ }
16908
+ sendCollectionSubscribeWatchdog(subscriptionKey) {
16909
+ const subscription = this.collectionSubscriptions.get(subscriptionKey);
16910
+ if (!subscription) return;
16911
+ const backendSubscriptionId = subscription.backendSubscriptionId;
16912
+ subscription.subscribeTimeout = setTimeout(() => {
16913
+ const current = this.collectionSubscriptions.get(subscriptionKey);
16914
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
16915
+ if (!current.subscribeInFlight) return;
16916
+ this.failCollectionSubscription(subscriptionKey, new RebaseApiError("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
16917
+ }, this.subscriptionTimeoutMs);
16918
+ }
16919
+ sendEntitySubscribeWatchdog(subscriptionKey) {
16920
+ const subscription = this.singleSubscriptions.get(subscriptionKey);
16921
+ if (!subscription) return;
16922
+ const backendSubscriptionId = subscription.backendSubscriptionId;
16923
+ subscription.subscribeTimeout = setTimeout(() => {
16924
+ const current = this.singleSubscriptions.get(subscriptionKey);
16925
+ if (!current || current.backendSubscriptionId !== backendSubscriptionId) return;
16926
+ if (!current.subscribeInFlight) return;
16927
+ this.failEntitySubscription(subscriptionKey, new RebaseApiError("Subscription timed out", { code: "SUBSCRIPTION_TIMEOUT" }));
16928
+ }, this.subscriptionTimeoutMs);
16929
+ }
16930
+ /**
16931
+ * Fail every subscription that never received data. Called when reconnection
16932
+ * is given up on, so views surface an error instead of spinning forever.
16933
+ */
16934
+ failAllPendingSubscriptions(error) {
16935
+ for (const key of [...this.collectionSubscriptions.keys()]) {
16936
+ const sub = this.collectionSubscriptions.get(key);
16937
+ if (sub && !sub.isInitialDataReceived) this.failCollectionSubscription(key, error);
16938
+ }
16939
+ for (const key of [...this.singleSubscriptions.keys()]) {
16940
+ const sub = this.singleSubscriptions.get(key);
16941
+ if (sub && !sub.isInitialDataReceived) this.failEntitySubscription(key, error);
16942
+ }
16943
+ }
16944
+ /**
16766
16945
  * Re-send all active subscriptions to the backend after a reconnect.
16767
16946
  * The server wipes subscription state when a client disconnects, so
16768
16947
  * we need to re-register everything to resume receiving updates.
@@ -16775,15 +16954,7 @@ var RebaseWebSocketClient = class {
16775
16954
  sub.backendSubscriptionId = newBackendId;
16776
16955
  this.backendToCollectionKey.delete(oldBackendId);
16777
16956
  this.backendToCollectionKey.set(newBackendId, key);
16778
- this.sendMessage({
16779
- type: "subscribe_collection",
16780
- payload: {
16781
- ...sub.props,
16782
- subscriptionId: newBackendId
16783
- }
16784
- }).catch((error) => {
16785
- console.error("[WS] Failed to re-subscribe collection:", key, error);
16786
- });
16957
+ this.sendCollectionSubscribe(key);
16787
16958
  }
16788
16959
  for (const [key, sub] of this.singleSubscriptions.entries()) {
16789
16960
  const oldBackendId = sub.backendSubscriptionId;
@@ -16791,15 +16962,7 @@ var RebaseWebSocketClient = class {
16791
16962
  sub.backendSubscriptionId = newBackendId;
16792
16963
  this.backendToEntityKey.delete(oldBackendId);
16793
16964
  this.backendToEntityKey.set(newBackendId, key);
16794
- this.sendMessage({
16795
- type: "subscribe_one",
16796
- payload: {
16797
- ...sub.props,
16798
- subscriptionId: newBackendId
16799
- }
16800
- }).catch((error) => {
16801
- console.error("[WS] Failed to re-subscribe row:", key, error);
16802
- });
16965
+ this.sendEntitySubscribe(key);
16803
16966
  }
16804
16967
  }
16805
16968
  createCollectionSubscriptionKey(props) {
@@ -17902,7 +18065,7 @@ async function loadFunctionsFromDirectory(directory, importModule = nativeDynami
17902
18065
  const problems = [];
17903
18066
  if (!fs$2.existsSync(directory)) return functions;
17904
18067
  const files = fs$2.readdirSync(directory);
17905
- for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
18068
+ for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.startsWith(".") && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
17906
18069
  const filePath = path$1.join(directory, file);
17907
18070
  try {
17908
18071
  const fileUrl = pathToFileURL(filePath).href;
@@ -18031,7 +18194,7 @@ async function loadCronJobsFromDirectory(directory, importModule = nativeDynamic
18031
18194
  const jobs = [];
18032
18195
  if (!fs$2.existsSync(directory)) return jobs;
18033
18196
  const files = fs$2.readdirSync(directory);
18034
- for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
18197
+ for (const file of files) if ((file.endsWith(".ts") || file.endsWith(".js")) && !file.startsWith(".") && !file.includes(".test.") && !file.endsWith(".d.ts") && file !== "index.ts" && file !== "index.js") {
18035
18198
  const filePath = path$1.join(directory, file);
18036
18199
  try {
18037
18200
  const fileUrl = pathToFileURL(filePath).href;