@supalive/core 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +28 -0
  2. package/dist/db-ClCicssQ.js +28 -0
  3. package/dist/db-ClCicssQ.js.map +1 -0
  4. package/dist/{index-DouKwYL6.d.ts → index-DJYvJYR4.d.ts} +214 -9
  5. package/dist/index-DJYvJYR4.d.ts.map +1 -0
  6. package/dist/{mysql-Bxurg_PH.d.ts → mysql-53W9lRh9.d.ts} +8 -3
  7. package/dist/mysql-53W9lRh9.d.ts.map +1 -0
  8. package/dist/{mysql-bvnjYWvZ.js → mysql-C0mysDQ_.js} +57 -25
  9. package/dist/mysql-C0mysDQ_.js.map +1 -0
  10. package/dist/{overlap-checker-Ct_HP_iB.js → overlap-checker-ClJC0nxW.js} +52 -4
  11. package/dist/overlap-checker-ClJC0nxW.js.map +1 -0
  12. package/dist/{postgres-Ck-xt72U.d.ts → postgres-7d6R9P7w.d.ts} +8 -3
  13. package/dist/postgres-7d6R9P7w.d.ts.map +1 -0
  14. package/dist/{postgres-B8u1YN8X.js → postgres-SvdXfb0w.js} +55 -22
  15. package/dist/postgres-SvdXfb0w.js.map +1 -0
  16. package/dist/{procedure-DX87tyef.js → procedure-Ze1GE-N9.js} +33 -2
  17. package/dist/procedure-Ze1GE-N9.js.map +1 -0
  18. package/dist/{query-Bh0RNxDz.js → query-CrbJ_W0W.js} +21 -1
  19. package/dist/query-CrbJ_W0W.js.map +1 -0
  20. package/dist/{realtime_db-DOCgqbwi.js → realtime_db-BtwBABDY.js} +76 -51
  21. package/dist/realtime_db-BtwBABDY.js.map +1 -0
  22. package/dist/{router-DlTYWpop.js → router-CiWYeWUR.js} +8 -2
  23. package/dist/router-CiWYeWUR.js.map +1 -0
  24. package/dist/src/client/index.d.ts +1 -1
  25. package/dist/src/client/index.js +44 -5
  26. package/dist/src/client/index.js.map +1 -1
  27. package/dist/src/exports/mysql.d.ts +1 -1
  28. package/dist/src/exports/mysql.js +1 -1
  29. package/dist/src/exports/postgres.d.ts +1 -1
  30. package/dist/src/exports/postgres.js +1 -1
  31. package/dist/src/exports/procedure.d.ts +2 -2
  32. package/dist/src/exports/procedure.js +4 -4
  33. package/dist/src/exports/schema-sql.d.ts +36 -2
  34. package/dist/src/exports/schema-sql.d.ts.map +1 -1
  35. package/dist/src/exports/schema-sql.js +74 -19
  36. package/dist/src/exports/schema-sql.js.map +1 -1
  37. package/dist/src/exports/server.d.ts +65 -17
  38. package/dist/src/exports/server.d.ts.map +1 -1
  39. package/dist/src/exports/server.js +724 -63
  40. package/dist/src/exports/server.js.map +1 -1
  41. package/dist/src/exports/subscription-manager-worker-entry.js +8 -5
  42. package/dist/src/exports/subscription-manager-worker-entry.js.map +1 -1
  43. package/dist/src/exports/types.d.ts +3 -3
  44. package/dist/src/exports/types.js +1 -1
  45. package/dist/{subscription-worker-CqoWp6zB.js → subscription-worker-B2XtjFWY.js} +307 -69
  46. package/dist/subscription-worker-B2XtjFWY.js.map +1 -0
  47. package/dist/{types_db-DXj_l9n-.js → types_db-CTRm_awX.js} +7 -2
  48. package/dist/types_db-CTRm_awX.js.map +1 -0
  49. package/dist/types_server-wW3ZgE7l.d.ts +420 -0
  50. package/dist/types_server-wW3ZgE7l.d.ts.map +1 -0
  51. package/package.json +5 -4
  52. package/dist/index-CaWMDqq0.d.ts +0 -1754
  53. package/dist/index-CaWMDqq0.d.ts.map +0 -1
  54. package/dist/index-DouKwYL6.d.ts.map +0 -1
  55. package/dist/mysql-Bxurg_PH.d.ts.map +0 -1
  56. package/dist/mysql-Cxd4Uq5p.d.ts +0 -104
  57. package/dist/mysql-Cxd4Uq5p.d.ts.map +0 -1
  58. package/dist/mysql-bvnjYWvZ.js.map +0 -1
  59. package/dist/overlap-checker-Ct_HP_iB.js.map +0 -1
  60. package/dist/postgres-B8u1YN8X.js.map +0 -1
  61. package/dist/postgres-CfT_aDx3.d.ts +0 -108
  62. package/dist/postgres-CfT_aDx3.d.ts.map +0 -1
  63. package/dist/postgres-Ck-xt72U.d.ts.map +0 -1
  64. package/dist/procedure-DX87tyef.js.map +0 -1
  65. package/dist/query-Bh0RNxDz.js.map +0 -1
  66. package/dist/realtime_db-DOCgqbwi.js.map +0 -1
  67. package/dist/router-DlTYWpop.js.map +0 -1
  68. package/dist/src/exports/codegen.d.ts +0 -38
  69. package/dist/src/exports/codegen.d.ts.map +0 -1
  70. package/dist/src/exports/codegen.js +0 -134
  71. package/dist/src/exports/codegen.js.map +0 -1
  72. package/dist/subscription-worker-CqoWp6zB.js.map +0 -1
  73. package/dist/subscription-worker-PtmJWEj1.js +0 -511
  74. package/dist/subscription-worker-PtmJWEj1.js.map +0 -1
  75. package/dist/subscription-worker-pahX0zxg.js +0 -511
  76. package/dist/subscription-worker-pahX0zxg.js.map +0 -1
  77. package/dist/types_db-DXj_l9n-.js.map +0 -1
  78. package/dist/types_server-BW1ys_SK.d.ts +0 -216
  79. package/dist/types_server-BW1ys_SK.d.ts.map +0 -1
  80. package/dist/types_server-Dpsi0wpG.d.ts +0 -216
  81. package/dist/types_server-Dpsi0wpG.d.ts.map +0 -1
  82. package/dist/types_server-DzrIccyO.d.ts +0 -216
  83. package/dist/types_server-DzrIccyO.d.ts.map +0 -1
  84. package/dist/types_server-TFo6SWDH.d.ts +0 -216
  85. package/dist/types_server-TFo6SWDH.d.ts.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"subscription-worker-CqoWp6zB.js","names":["uuidv4","logger","baseLogger","logger","Redis","baseLogger"],"sources":["../src/server/subscription-manager-types.ts","../src/logger.ts","../src/db/cache.ts","../src/db/init_db.ts","../src/server/subscription-worker.ts"],"sourcesContent":["import z from \"zod\";\nimport { BigIntSchema, RawReadEntry, ReadEntry, ReadEntrySchema, WriteEntrySchema } from \"../db/types_db\";\n\n// ─── RPC Schemas ──────────────────────────────────────────────────────────────\n//\n// The wire protocol intentionally has NO `_clientVersion` / `_resync` /\n// `serverVersion` fields. Ordering of concurrent `updateSubscriptionReadSet`\n// calls is enforced by `lastSnapshotTs` (which is monotonic — it comes from\n// `Database.getLatestSnapshotTimestamp()`). Recovery after a sub-manager\n// restart is driven by the app server iterating its own\n// `serverSubscriptions` map and re-registering each one using params\n// persisted in Redis under `sl:reg:<subId>`.\n\nexport const RegisterSubscriptionParamsSchema = z.object({\n /**\n * Caller-computed FNV-1a routing key + identity for this subscription.\n * The sub-manager trusts these values rather than re-deriving them from\n * (queryName, args, queryIdentity); this avoids one stringify+SHA1 per\n * register RPC. Trust is fine: the sub-manager RPC channel is fully\n * internal.\n */\n subId: z.string(),\n cacheKey: z.string(),\n queryName: z.string(),\n args: z.unknown(),\n /**\n * Segmentation key used in the subId/cacheKey hash. Defaults to the\n * authenticated user's id (\"anonymous\" when none), unless the\n * procedure overrides it via `QueryConfig.queryIdentity`.\n */\n queryIdentity: z.string(),\n instanceName: z.string(),\n lastSnapshotTs: BigIntSchema,\n});\nexport type RegisterSubscriptionParams = z.infer<typeof RegisterSubscriptionParamsSchema>;\n\nexport const RegisterSubscriptionResultSchema = z.object({\n cacheKey: z.string(),\n recompute: z.boolean(),\n readSet: z.array(ReadEntrySchema),\n});\nexport type RegisterSubscriptionResult = z.infer<typeof RegisterSubscriptionResultSchema>;\n\nexport const RegisterSubscriptionBatchParamsSchema = z.array(RegisterSubscriptionParamsSchema);\nexport type RegisterSubscriptionBatchParams = z.infer<typeof RegisterSubscriptionBatchParamsSchema>;\n\nexport const RegisterSubscriptionBatchResultSchema = z.array(RegisterSubscriptionResultSchema);\nexport type RegisterSubscriptionBatchResult = z.infer<typeof RegisterSubscriptionBatchResultSchema>;\n\nexport const UpdateSubscriptionReadSetParamsSchema = z.object({\n cacheKey: z.string(),\n subId: z.string(),\n instanceName: z.string(),\n lastSnapshotTs: BigIntSchema,\n readSet: z.array(ReadEntrySchema),\n});\nexport type UpdateSubscriptionReadSetParams = z.infer<typeof UpdateSubscriptionReadSetParamsSchema>;\n\n/**\n * `tracked: false` means the sub-manager has no record of this subId (e.g.\n * a restart wiped state and the caller hasn't re-registered yet). The app\n * server should treat this as a signal to register the sub before retrying.\n */\nexport const UpdateSubscriptionReadSetResultSchema = z.object({\n tracked: z.boolean(),\n});\nexport type UpdateSubscriptionReadSetResult = z.infer<typeof UpdateSubscriptionReadSetResultSchema>;\n\nexport const UnregisterSubscriptionParamsSchema = z.object({\n subId: z.string(),\n instanceName: z.string(),\n});\nexport type UnregisterSubscriptionParams = z.infer<typeof UnregisterSubscriptionParamsSchema>;\n\nexport const UnregisterSubscriptionsParamsSchema = z.array(UnregisterSubscriptionParamsSchema);\nexport type UnregisterSubscriptionsParams = z.infer<typeof UnregisterSubscriptionsParamsSchema>;\n\nexport const UnregisterSubscriptionResultSchema = z.object({ ok: z.boolean() });\nexport type UnregisterSubscriptionResult = z.infer<typeof UnregisterSubscriptionResultSchema>;\n\nexport const UnregisterSubscriptionsResultSchema = z.array(z.object({ ok: z.boolean() }));\nexport type UnregisterSubscriptionsResult = z.infer<typeof UnregisterSubscriptionsResultSchema>;\n\nexport const InvalidateWritesetParamsSchema = z.object({\n writeSet: z.array(WriteEntrySchema),\n});\nexport type InvalidateWritesetParams = z.infer<typeof InvalidateWritesetParamsSchema>;\n\nexport const AffectedSubscriptionSchema = z.object({\n subId: z.string(),\n queryName: z.string(),\n args: z.unknown(),\n cacheKey: z.string(),\n /**\n * Every live instance that currently holds this subId. Routing rules:\n * • If the writer is in this list, it recomputes locally and publishes\n * an `update` to the rest (its own id is filtered by the publisher).\n * • Otherwise the writer broadcasts a `recompute-race` to every entry;\n * receivers race for a Redis lock and the winner recomputes.\n */\n notifyInstances: z.array(z.string()),\n});\nexport type AffectedSubscription = z.infer<typeof AffectedSubscriptionSchema>;\n\nexport const InvalidateWritesetResultSchema = z.object({\n affected: z.array(AffectedSubscriptionSchema),\n});\nexport type InvalidateWritesetResult = z.infer<typeof InvalidateWritesetResultSchema>;\n\n// ─── Registration record persisted to Redis ──────────────────────────────────\n//\n// Stored at `sl:reg:<subId>` by the sub-manager when a brand-new subscription\n// is created, and deleted by the sub-manager when the last instance leaves.\n// The app server reads this on reconnect to know how to re-register its\n// known subIds.\n\nexport const RegistrationRecordSchema = z.object({\n queryName: z.string(),\n args: z.unknown(),\n queryIdentity: z.string(),\n cacheKey: z.string(),\n});\nexport type RegistrationRecord = z.infer<typeof RegistrationRecordSchema>;\n\nexport interface TrackedSubscription {\n subId: string;\n queryName: string;\n args: unknown;\n cacheKey: string;\n lastSnapshotTs: bigint;\n readSet: ReadEntry[];\n rawReadSet: RawReadEntry[];\n /** All app server instances that currently hold this subscription. */\n instances: Set<string>;\n timeout: NodeJS.Timeout | null;\n}","import pino, { Level } from \"pino\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport pinoPretty from \"pino-pretty\";\nconst _ = pinoPretty; // Prevents tree shaking of pino-pretty\n\nexport const appInstanceId = `instance-${uuidv4().slice(0, 8)}`;\nexport const isProduction = process.env.ENV === \"prod\";\nexport const logger = pino({\n level: process.env.LOG_LEVEL || \"info\",\n timestamp: pino.stdTimeFunctions.isoTime,\n redact: { paths: [\"user.password\"] },\n transport: isProduction ? undefined : {\n target: 'pino-pretty',\n options: {\n colorize: true\n },\n },\n serializers: {\n \"columnsTypes\": (map) => {\n if (map instanceof Map) return serializeMapValue(map);\n return map;\n },\n },\n base: {\n \"srv\": appInstanceId,\n },\n});\n\nexport function logLevelOf(name: string): Level {\n const defaultLogLevel = process.env.DEFAULT_LOG_LEVEL || \"info\";\n return (process.env[name]?.toLowerCase() || defaultLogLevel) as Level;\n}\n\nexport function serializeMapValue<K extends string | number | symbol, V>(map: Map<K, V>): Record<K, V> {\n const obj: Record<K, any> = {} as Record<K, any>;\n for (const [k, v] of map.entries()) {\n obj[k] = {} as V;\n if (v instanceof Map) {\n for (const [innerK, innerV] of v.entries()) {\n obj[k][innerK] = innerV;\n }\n } else {\n obj[k] = v;\n }\n }\n\n return obj as Record<K, V>;\n}","import type { Redis as UpstashRedis } from \"@upstash/redis\";\nimport {\n CachedPgMetadata,\n CachedPgMetadataSchema,\n QueryCacheEntry, QueryCacheEntrySchema,\n QueryCacheMetadata, QueryCacheMetadataSchema\n} from \"./types_db\";\nimport { retry, handleAll, ExponentialBackoff } from \"cockatiel\";\nimport { RegistrationRecord, RegistrationRecordSchema } from \"../server/subscription-manager-types\";\nimport Redis from \"ioredis\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\nimport { supaliveStringify } from \"../helper\";\n\nconst logger = baseLogger.child({\n \"name\": \"CACHE_LAYER\",\n},\n { level: logLevelOf(\"SERVER_LOG_LEVEL\") },\n);\n\nexport const redisRetryPolicy = retry(handleAll, {\n maxAttempts: 3,\n backoff: new ExponentialBackoff({\n initialDelay: 5,\n maxDelay: 2300,\n exponent: 2,\n }),\n});\n\nexport class CacheLayer {\n public redis: Redis | UpstashRedis;\n private readonly ttlSeconds: number;\n\n constructor(upstash: Redis | UpstashRedis, ttlSeconds: number = 3600) {\n this.redis = upstash;\n this.ttlSeconds = ttlSeconds;\n }\n\n async setQueryCacheFor(cacheKey: string, data: unknown, ttlSeconds?: number): Promise<void> {\n const combined = supaliveStringify({ data: data });\n await redisRetryPolicy.execute(async () => {\n await this.redis.setex(this.getQueryCacheDataKey(cacheKey), ttlSeconds ?? this.ttlSeconds, combined);\n });\n }\n\n async getQueryCacheFor(cacheKey: string): Promise<QueryCacheEntry | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQueryCacheDataKey(cacheKey)));\n\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return QueryCacheEntrySchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n async getQueryCacheMetaData(cacheKey: string): Promise<QueryCacheMetadata | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQeruyCacheMetaKey(cacheKey)));\n\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return QueryCacheMetadataSchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n async setQueryCacheMetadata(cacheKey: string, metaData: QueryCacheMetadata): Promise<void> {\n const metadata = supaliveStringify(metaData);\n await redisRetryPolicy.execute(() => this.redis.set(this.getQeruyCacheMetaKey(cacheKey), metadata));\n }\n\n async persistRegistrationRecord(subId: string, record: RegistrationRecord): Promise<void> {\n const data = supaliveStringify(record);\n await redisRetryPolicy.execute(() => this.redis.set(this.getRegistrationKey(subId), data));\n }\n\n async deleteRegistrationRecord(subId: string): Promise<void> {\n await redisRetryPolicy.execute(() => this.redis.del(this.getRegistrationKey(subId)));\n }\n\n async deleteRegistrationsRecord(subIds: string[]): Promise<void> {\n if (subIds.length === 0) return;\n\n await redisRetryPolicy.execute(() =>\n this.redis.del(...subIds.map((subId) => this.getRegistrationKey(subId))),\n );\n }\n\n async fetchRegistrationRecordsBatch(\n subIds: string[],\n ): Promise<Map<string, RegistrationRecord | null>> {\n const result = new Map<string, RegistrationRecord | null>();\n const CHUNK_SIZE = 100;\n\n for (let i = 0; i < subIds.length; i += CHUNK_SIZE) {\n const chunk = subIds.slice(i, i + CHUNK_SIZE);\n const keys = chunk.map((id) => `sl:reg:${id}`);\n try {\n if (keys.length === 0) continue;\n\n const rawValues = await redisRetryPolicy.execute(() =>\n this.redis.mget(...keys),\n );\n\n for (let j = 0; j < chunk.length; j++) {\n const subId = chunk[j]!;\n const raw = rawValues[j];\n const rawJson = typeof raw === \"string\" ? JSON.parse(raw) : raw;\n\n if (!raw) {\n result.set(subId, null);\n } else {\n try {\n const parsed = RegistrationRecordSchema.parse(rawJson);\n result.set(subId, parsed);\n } catch {\n logger.error(`Failed to parse sl:reg:${subId} during batch recovery`);\n result.set(subId, null);\n }\n }\n }\n } catch (err) {\n logger.error(err, \"Failed to fetch registration records batch\");\n for (const subId of chunk) {\n result.set(subId, null);\n }\n }\n }\n return result;\n }\n\n async getCachedTableColumnTypes(key: string): Promise<CachedPgMetadata | null> {\n const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(key));\n if (!dataRaw) return null;\n\n try {\n const parsed = typeof dataRaw === \"string\" ? JSON.parse(dataRaw) : dataRaw;\n return CachedPgMetadataSchema.parse(parsed);\n } catch {\n return null;\n }\n }\n\n async setCachedTableColumnsTypes(key: string, value: CachedPgMetadata): Promise<void> {\n await redisRetryPolicy.execute(() =>\n this.redis.set(key, supaliveStringify(value)),\n );\n }\n\n /**\n * Race participant: try to claim `lock:recompute:<subId>:<commitTs>`\n * with `SET NX EX`. The single Redis round-trip both arbitrates and\n * sets a TTL fallback in case the winner crashes mid-recompute.\n */\n async tryAcquireRecomputeLock(instanceId: string, subId: string, commitTs: string, ttl: number): Promise<boolean> {\n const key = `lock:recompute:${subId}:${commitTs}`;\n return redisRetryPolicy.execute(async () => {\n try {\n const res = await this.redis.set(key, instanceId, \"EX\", ttl, \"NX\");\n return res === \"OK\";\n } catch (err) {\n logger.error(err, `Failed to acquire recompute lock ${key}`);\n // Fail-closed: if Redis is misbehaving, don't run a recompute that\n // we can't coordinate. The next mutation will trigger another race.\n return false;\n }\n });\n }\n\n\n private getRegistrationKey(subId: string): string {\n return `sl:reg:${subId}`;\n }\n\n private getQueryCacheDataKey(cacheKey: string): string {\n return `sl:cache:${cacheKey}`;\n }\n\n private getQeruyCacheMetaKey(cacheKey: string): string {\n return `sl:meta:${cacheKey}`;\n }\n}","import { DatabaseConfig, SupaliveServerConfig, UpstashConfig } from \"../config\";\nimport { logger } from \"../logger\";\nimport { CacheLayer } from \"./cache\";\nimport { MySqlDatabase } from \"./mysql\";\nimport { PgDatabase } from \"./postgres\";\nimport { SupaliveDb } from \"./realtime_db\";\nimport Redis from \"ioredis\";\n\n/**\n * Default and env override for the per-statement DB timeout. PG enforces\n * this via `statement_timeout` (server cancels), MySQL via the per-query\n * `timeout` option (driver issues KILL QUERY). Set 0 to disable.\n */\nexport const DB_QUERY_TIMEOUT_MS = Number(\n process.env.SUPALIVE_DB_QUERY_TIMEOUT_MS ?? 30_000,\n);\n\nexport type InitDbOptions = {\n DbConfig: DatabaseConfig;\n CacheLayer: CacheLayer;\n forceBootstrapReload?: boolean;\n workerId?: number;\n};\n\nexport async function initDatabase(options: InitDbOptions): Promise<SupaliveDb> {\n const impl = createDatabase(options.DbConfig, options.workerId);\n const supaliveDb = SupaliveDb.create({ db: impl });\n await bootstrapDbTypes(supaliveDb, options.CacheLayer, options.forceBootstrapReload ?? false);\n return supaliveDb;\n}\n\nexport function createDatabase(config: DatabaseConfig, workerId?: number): PgDatabase | MySqlDatabase {\n const bindings: Record<string, any> = {\n \"db\": config.type,\n };\n if (workerId) {\n bindings[\"worker\"] = workerId;\n }\n\n if (config.type === \"postgres\") {\n return new PgDatabase(\n logger.child(bindings), {\n ...config,\n connectionString: config.connectionString,\n min: config.minConnections ?? 0,\n max: config.maxConnections ?? 20,\n connectionTimeoutMillis: config.connectionTimeoutMillis ?? 15_000,\n idleTimeoutMillis: config.idleTimeoutMillis ?? 30_000,\n // server-enforced per-statement timeout; pg cancels the running\n // query when it fires. 0 disables it.\n statement_timeout: DB_QUERY_TIMEOUT_MS > 0 ? DB_QUERY_TIMEOUT_MS : undefined,\n });\n } else {\n return new MySqlDatabase(\n logger.child(bindings), {\n ...config,\n connectionLimit: config.maxConnections ?? 20,\n connectTimeout: config.connectionTimeoutMillis ?? 15_000,\n // mysql2 pool: 0 = wait forever for a free conn (default). queueLimit\n // bounds the wait queue length so backpressure surfaces as an error\n // rather than unbounded growth.\n queueLimit: config.queueLimit ?? 0,\n // driver-side per-statement timeout applied uniformly in\n // MySqlDatabase.query.\n queryTimeoutMs: DB_QUERY_TIMEOUT_MS,\n });\n }\n}\n\n/**\n * Eagerly load column types into the apply-path cache at server start.\n * In production, the result is cached in Redis under\n * `supalive:schema:types:${SUPALIVE_SCHEMA_VERSION ?? \"v1\"}` so other\n * instances starting cold can skip the pg_attribute round-trip. In any\n * other env we go straight to PG every time so local schema iteration\n * is never blocked by stale Redis state. Migrations should publish a\n * `schema:reload` message (see {@link publishSchemaReload}) after\n * applying DDL — every instance picks that up and re-bootstraps.\n */\nexport async function bootstrapDbTypes(db: SupaliveDb, cache: CacheLayer, forceReload = false): Promise<void> {\n // TODO: remove this check if we needs mysql column types\n if (db.impl.dbType === \"mysql\") {\n // currently we don't use mysql column type\n return;\n }\n\n const inProd = process.env.NODE_ENV === \"production\";\n const version = process.env.SUPALIVE_SCHEMA_VERSION ?? \"v1\";\n const key = `supalive:schema:types:${version}`;\n try {\n await db.impl.bootstrapColumnTypes({\n cacheKey: key,\n redis: inProd && !forceReload ? cache : undefined,\n });\n logger.info({ columnsTypes: db.impl.columnTypes }, `Bootstrapped db column types (redis=${inProd}, key=${key})`);\n } catch (err) {\n logger.error(err, \"Failed to bootstrap db column types\");\n throw err;\n }\n}\n\nexport type InitCacheLayerOptions = {\n upstash: UpstashConfig;\n /* Default 3600 */\n cacheTtlSeconds?: number;\n /* Default 15_000 */\n connectionTimeout?: number;\n /* Default 1 */\n commandMaxRetries?: number;\n /* Default 5 */\n commandSubMaxRetries?: number;\n /* Default true */\n createSubClient?: boolean;\n};\n\nexport function initCacheLayer(config: InitCacheLayerOptions): { cacheLayer: CacheLayer, redisSubClient?: Redis } {\n const redis = new Redis(config.upstash.redisUrl, {\n connectTimeout: config.connectionTimeout ?? 15_000,\n maxRetriesPerRequest: config.commandMaxRetries ?? 1,\n keepAlive: 3000,\n });\n \n const redisSub = config.createSubClient ? new Redis(config.upstash.redisUrl, {\n connectTimeout: config.connectionTimeout ?? 15_000,\n maxRetriesPerRequest: config.commandSubMaxRetries ?? 5,\n disableClientInfo: true,\n retryStrategy: (times) => {\n if (times > 5) return null;\n return Math.min(times * 200, 2000);\n }\n }) : undefined;\n\n return {\n cacheLayer: new CacheLayer(redis, config.cacheTtlSeconds ?? 3600),\n redisSubClient: redisSub\n };\n}\n\n","import type { Database } from \"../db/db\";\nimport type { RawReadEntry, ReadEntry } from \"../db/types_db\";\nimport type { CacheLayer } from \"../db/cache\";\nimport {\n checkLogsAffectReadSet,\n checkWriteSetAffectsReadSet,\n mapReadSetToRaw,\n type RawWriteEntry,\n} from \"./overlap-checker\";\nimport {\n AffectedSubscription,\n RegisterSubscriptionParams,\n RegisterSubscriptionResult,\n RegistrationRecord,\n TrackedSubscription,\n UnregisterSubscriptionParams,\n UnregisterSubscriptionResult,\n UnregisterSubscriptionsParams,\n UnregisterSubscriptionsResult,\n UpdateSubscriptionReadSetParams,\n UpdateSubscriptionReadSetResult,\n} from \"./subscription-manager-types\";\nimport { logger as baseLogger, logLevelOf } from \"../logger\";\n\n// A logical worker that owns a disjoint slice of subscriptions. The dispatcher\n// routes each operation here by FNV-1a(subId) % numWorkers. Each worker is\n// independent: no shared state, no locks, no cross-worker coordination.\n// invalidate() is a pure function of the worker's own state + the writeSet.\n\nexport class SubscriptionWorker {\n readonly id: number;\n private subscriptions = new Map<string, TrackedSubscription>();\n private cache: CacheLayer;\n private db: Database;\n public logger: ReturnType<typeof baseLogger.child>;\n\n constructor(id: number, cache: CacheLayer, db: Database) {\n this.id = id;\n this.cache = cache;\n this.db = db;\n this.logger = baseLogger.child(\n { name: \"SUBS_WRK\", wid: id },\n { level: logLevelOf(\"SUBS_MANAGER_SERVER_LOG_LEVEL\") },\n );\n }\n\n size(): number {\n return this.subscriptions.size;\n }\n\n has(subId: string): boolean {\n return this.subscriptions.has(subId);\n }\n\n /**\n * Lowest `lastSnapshotTs` across this worker's subscriptions, or `null`\n * when there are none. Used by the dispatcher to derive a safe prune floor.\n */\n minSubscriptionTs(): bigint | null {\n let min: bigint | null = null;\n for (const sub of this.subscriptions.values()) {\n if (min === null || sub.lastSnapshotTs < min) min = sub.lastSnapshotTs;\n }\n return min;\n }\n\n async register(\n params: RegisterSubscriptionParams,\n ): Promise<RegisterSubscriptionResult> {\n const { subId, cacheKey, queryName, args, queryIdentity, instanceName, lastSnapshotTs } = params;\n this.logger.info(`registering subscription: ${subId} from ${instanceName}`);\n\n const existing = this.subscriptions.get(subId);\n\n if (existing && !existing.timeout) {\n existing.instances.add(instanceName);\n return {\n cacheKey,\n recompute: false,\n readSet: existing.readSet,\n };\n }\n\n if (existing?.timeout) {\n this.logger.info(`reactivating subscription: ${subId} from: ${instanceName}`);\n clearTimeout(existing.timeout);\n existing.timeout = null;\n }\n\n const metadata = await this.cache.getQueryCacheMetaData(cacheKey);\n let needsRecompute = true;\n let readSet: ReadEntry[];\n let rawReadSet: RawReadEntry[];\n\n if (metadata) {\n readSet = metadata.readSet;\n rawReadSet = mapReadSetToRaw(readSet);\n\n const tableIds = Array.from(new Set(readSet.map((r) => r.tableId)));\n const logs = await this.db.getCommitLogsBetweenTs(metadata.lastSnapshotTs, lastSnapshotTs, tableIds);\n needsRecompute = checkLogsAffectReadSet(logs, rawReadSet);\n\n if (!needsRecompute) {\n if (lastSnapshotTs !== metadata.lastSnapshotTs) {\n this.cache\n .setQueryCacheMetadata(cacheKey, { lastSnapshotTs: lastSnapshotTs, readSet })\n .catch((err) => this.logger.error(err, `Failed to update cache metadata for ${cacheKey}`));\n }\n metadata.lastSnapshotTs = lastSnapshotTs;\n }\n } else {\n readSet = [];\n rawReadSet = [];\n }\n\n let newSubscription: TrackedSubscription = {\n subId,\n queryName,\n args,\n cacheKey,\n lastSnapshotTs: lastSnapshotTs,\n readSet,\n rawReadSet,\n instances: new Set([instanceName]),\n timeout: null,\n };\n\n const createdWhileChecking = this.subscriptions.get(subId);\n if (createdWhileChecking) {\n if (createdWhileChecking.timeout) {\n clearTimeout(createdWhileChecking.timeout);\n createdWhileChecking.timeout = null;\n }\n\n if (createdWhileChecking.lastSnapshotTs > lastSnapshotTs) {\n newSubscription = {\n ...newSubscription,\n ...createdWhileChecking,\n instances: new Set([...createdWhileChecking.instances, instanceName]),\n };\n needsRecompute = false;\n }\n }\n\n this.subscriptions.set(subId, newSubscription);\n\n this.persistRegistration(subId, { queryName, args, queryIdentity, cacheKey });\n\n return { cacheKey, recompute: needsRecompute, readSet };\n }\n\n async updateReadSet(\n params: UpdateSubscriptionReadSetParams,\n ): Promise<UpdateSubscriptionReadSetResult> {\n const { cacheKey, subId, instanceName, lastSnapshotTs, readSet } = params;\n this.logger.info(`updating subscription readSet: ${subId} from ${instanceName}`);\n\n const existing = this.subscriptions.get(subId);\n if (!existing) {\n return { tracked: false };\n }\n\n if (lastSnapshotTs < existing.lastSnapshotTs) {\n return { tracked: true };\n }\n\n const rawReadSet = mapReadSetToRaw(readSet);\n this.subscriptions.set(subId, { ...existing, readSet, rawReadSet, lastSnapshotTs });\n await this.cache.setQueryCacheMetadata(cacheKey, { lastSnapshotTs, readSet });\n\n return { tracked: true };\n }\n\n async unregister(\n params: UnregisterSubscriptionParams,\n jitter: number = 0,\n ): Promise<UnregisterSubscriptionResult> {\n const { subId, instanceName } = params;\n this.logger.info(`unregistering subscription: ${subId} from: ${instanceName}`);\n\n const sub = this.subscriptions.get(subId);\n if (!sub) return { ok: true };\n\n if (sub.timeout) {\n clearTimeout(sub.timeout);\n }\n\n if (sub.instances.size > 1) {\n sub.instances.delete(instanceName);\n return { ok: true };\n }\n\n this.logger.debug(`scheduling unregister subscription: ${subId} from: ${instanceName} in 15s`);\n sub.timeout = setTimeout(async () => {\n sub.instances.delete(instanceName);\n\n if (sub.instances.size === 0) {\n this.logger.info(`unregistered subscription: ${subId} from: ${instanceName}`);\n this.subscriptions.delete(subId);\n await this.deleteRegistration(subId);\n }\n }, 15_000 + jitter);\n\n return { ok: true };\n }\n\n async unregisters(\n params: UnregisterSubscriptionsParams,\n ): Promise<UnregisterSubscriptionsResult> {\n this.logger.info({ subIds: params }, `scheduling unregistering subscriptions`);\n for (const param of params) {\n this.unregister(param, Math.floor(Math.random() * 100));\n }\n\n return params.map(() => ({ ok: true }));\n }\n\n /**\n * Synchronous, CPU-bound. Scans this worker's slice of subscriptions and\n * returns the ones affected by the writeSet.\n */\n invalidate(rawWriteSet: RawWriteEntry[]): AffectedSubscription[] {\n const affected: AffectedSubscription[] = [];\n\n for (const [, sub] of this.subscriptions) {\n if (sub.instances.size === 0) continue;\n if (!checkWriteSetAffectsReadSet(rawWriteSet, sub.rawReadSet)) continue;\n\n affected.push({\n subId: sub.subId,\n queryName: sub.queryName,\n args: sub.args,\n cacheKey: sub.cacheKey,\n notifyInstances: Array.from(sub.instances),\n });\n }\n\n return affected;\n }\n\n private async persistRegistration(subId: string, record: RegistrationRecord): Promise<void> {\n try {\n await this.cache.persistRegistrationRecord(subId, record);\n } catch (err) {\n this.logger.error(err, `[worker ${this.id}] Failed to persist registration for ${subId}`);\n }\n }\n\n private async deleteRegistration(subId: string): Promise<void> {\n try {\n await this.cache.deleteRegistrationRecord(subId);\n } catch (err) {\n this.logger.error(err, `[worker ${this.id}] Failed to delete registration for ${subId}`);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,mCAAmC,EAAE,OAAO;;;;;;;;CAQrD,OAAO,EAAE,OAAO;CAChB,UAAU,EAAE,OAAO;CACnB,WAAW,EAAE,OAAO;CACpB,MAAM,EAAE,QAAQ;;;;;;CAMhB,eAAe,EAAE,OAAO;CACxB,cAAc,EAAE,OAAO;CACvB,gBAAgB;AACpB,CAAC;AAGD,MAAa,mCAAmC,EAAE,OAAO;CACrD,UAAU,EAAE,OAAO;CACnB,WAAW,EAAE,QAAQ;CACrB,SAAS,EAAE,MAAM,eAAe;AACpC,CAAC;AAGD,MAAa,wCAAwC,EAAE,MAAM,gCAAgC;AAG7F,MAAa,wCAAwC,EAAE,MAAM,gCAAgC;AAG7F,MAAa,wCAAwC,EAAE,OAAO;CAC1D,UAAU,EAAE,OAAO;CACnB,OAAO,EAAE,OAAO;CAChB,cAAc,EAAE,OAAO;CACvB,gBAAgB;CAChB,SAAS,EAAE,MAAM,eAAe;AACpC,CAAC;;;;;;AAQD,MAAa,wCAAwC,EAAE,OAAO,EAC1D,SAAS,EAAE,QAAQ,EACvB,CAAC;AAGD,MAAa,qCAAqC,EAAE,OAAO;CACvD,OAAO,EAAE,OAAO;CAChB,cAAc,EAAE,OAAO;AAC3B,CAAC;AAGD,MAAa,sCAAsC,EAAE,MAAM,kCAAkC;AAG7F,MAAa,qCAAqC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAG9E,MAAa,sCAAsC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;AAGxF,MAAa,iCAAiC,EAAE,OAAO,EACnD,UAAU,EAAE,MAAM,gBAAgB,EACtC,CAAC;AAGD,MAAa,6BAA6B,EAAE,OAAO;CAC/C,OAAO,EAAE,OAAO;CAChB,WAAW,EAAE,OAAO;CACpB,MAAM,EAAE,QAAQ;CAChB,UAAU,EAAE,OAAO;;;;;;;;CAQnB,iBAAiB,EAAE,MAAM,EAAE,OAAO,CAAC;AACvC,CAAC;AAGD,MAAa,iCAAiC,EAAE,OAAO,EACnD,UAAU,EAAE,MAAM,0BAA0B,EAChD,CAAC;AAUD,MAAa,2BAA2B,EAAE,OAAO;CAC7C,WAAW,EAAE,OAAO;CACpB,MAAM,EAAE,QAAQ;CAChB,eAAe,EAAE,OAAO;CACxB,UAAU,EAAE,OAAO;AACvB,CAAC;;;ACpHD,MAAa,gBAAgB,YAAYA,GAAO,CAAC,CAAC,MAAM,GAAG,CAAC;AAC5D,MAAa,eAAe,QAAQ,IAAI,QAAQ;AAChD,MAAaC,WAAS,KAAK;CACvB,OAAO,QAAQ,IAAI,aAAa;CAChC,WAAW,KAAK,iBAAiB;CACjC,QAAQ,EAAE,OAAO,CAAC,eAAe,EAAE;CACnC,WAAW,eAAe,KAAA,IAAY;EAClC,QAAQ;EACR,SAAS,EACL,UAAU,KACd;CACJ;CACA,aAAa,EACT,iBAAiB,QAAQ;EACrB,IAAI,eAAe,KAAK,OAAO,kBAAkB,GAAG;EACpD,OAAO;CACX,EACJ;CACA,MAAM,EACF,OAAO,cACX;AACJ,CAAC;AAED,SAAgB,WAAW,MAAqB;CAC5C,MAAM,kBAAkB,QAAQ,IAAI,qBAAqB;CACzD,OAAQ,QAAQ,IAAI,KAAK,EAAE,YAAY,KAAK;AAChD;AAEA,SAAgB,kBAAyD,KAA8B;CACnG,MAAM,MAAsB,CAAC;CAC7B,KAAK,MAAM,CAAC,GAAG,MAAM,IAAI,QAAQ,GAAG;EAChC,IAAI,KAAK,CAAC;EACV,IAAI,aAAa,KACb,KAAK,MAAM,CAAC,QAAQ,WAAW,EAAE,QAAQ,GACrC,IAAI,EAAE,CAAC,UAAU;OAGrB,IAAI,KAAK;CAEjB;CAEA,OAAO;AACX;;;AClCA,MAAM,SAASC,SAAW,MAAM,EAC9B,QAAQ,cACV,GACE,EAAE,OAAO,WAAW,kBAAkB,EAAE,CAC1C;AAEA,MAAa,mBAAmB,MAAM,WAAW;CAC/C,aAAa;CACb,SAAS,IAAI,mBAAmB;EAC9B,cAAc;EACd,UAAU;EACV,UAAU;CACZ,CAAC;AACH,CAAC;AAED,IAAa,aAAb,MAAwB;CACtB;CACA;CAEA,YAAY,SAA+B,aAAqB,MAAM;EACpE,KAAK,QAAQ;EACb,KAAK,aAAa;CACpB;CAEA,MAAM,iBAAiB,UAAkB,MAAe,YAAoC;EAC1F,MAAM,WAAW,kBAAkB,EAAQ,KAAK,CAAC;EACjD,MAAM,iBAAiB,QAAQ,YAAY;GACzC,MAAM,KAAK,MAAM,MAAM,KAAK,qBAAqB,QAAQ,GAAG,cAAc,KAAK,YAAY,QAAQ;EACrG,CAAC;CACH;CAEA,MAAM,iBAAiB,UAAmD;EACxE,MAAM,UAAU,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,KAAK,qBAAqB,QAAQ,CAAC,CAAC;EAExG,IAAI,CAAC,SAAS,OAAO;EAErB,IAAI;GACF,MAAM,SAAS,OAAO,YAAY,WAAW,KAAK,MAAM,OAAO,IAAI;GACnE,OAAO,sBAAsB,MAAM,MAAM;EAC3C,QAAQ;GACN,OAAO;EACT;CACF;CAEA,MAAM,sBAAsB,UAAsD;EAChF,MAAM,UAAU,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,KAAK,qBAAqB,QAAQ,CAAC,CAAC;EAExG,IAAI,CAAC,SAAS,OAAO;EAErB,IAAI;GACF,MAAM,SAAS,OAAO,YAAY,WAAW,KAAK,MAAM,OAAO,IAAI;GACnE,OAAO,yBAAyB,MAAM,MAAM;EAC9C,QAAQ;GACN,OAAO;EACT;CACF;CAEA,MAAM,sBAAsB,UAAkB,UAA6C;EACzF,MAAM,WAAW,kBAAkB,QAAQ;EAC3C,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,KAAK,qBAAqB,QAAQ,GAAG,QAAQ,CAAC;CACpG;CAEA,MAAM,0BAA0B,OAAe,QAA2C;EACxF,MAAM,OAAO,kBAAkB,MAAM;EACrC,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,KAAK,mBAAmB,KAAK,GAAG,IAAI,CAAC;CAC3F;CAEA,MAAM,yBAAyB,OAA8B;EAC3D,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,KAAK,mBAAmB,KAAK,CAAC,CAAC;CACrF;CAEA,MAAM,0BAA0B,QAAiC;EAC/D,IAAI,OAAO,WAAW,GAAG;EAEzB,MAAM,iBAAiB,cACrB,KAAK,MAAM,IAAI,GAAG,OAAO,KAAK,UAAU,KAAK,mBAAmB,KAAK,CAAC,CAAC,CACzE;CACF;CAEA,MAAM,8BACJ,QACiD;EACjD,MAAM,yBAAS,IAAI,IAAuC;EAC1D,MAAM,aAAa;EAEnB,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,YAAY;GAClD,MAAM,QAAQ,OAAO,MAAM,GAAG,IAAI,UAAU;GAC5C,MAAM,OAAO,MAAM,KAAK,OAAO,UAAU,IAAI;GAC7C,IAAI;IACF,IAAI,KAAK,WAAW,GAAG;IAEvB,MAAM,YAAY,MAAM,iBAAiB,cACvC,KAAK,MAAM,KAAK,GAAG,IAAI,CACzB;IAEA,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;KACrC,MAAM,QAAQ,MAAM;KACpB,MAAM,MAAM,UAAU;KACtB,MAAM,UAAU,OAAO,QAAQ,WAAW,KAAK,MAAM,GAAG,IAAI;KAE5D,IAAI,CAAC,KACH,OAAO,IAAI,OAAO,IAAI;UAEtB,IAAI;MACF,MAAM,SAAS,yBAAyB,MAAM,OAAO;MACrD,OAAO,IAAI,OAAO,MAAM;KAC1B,QAAQ;MACN,OAAO,MAAM,0BAA0B,MAAM,uBAAuB;MACpE,OAAO,IAAI,OAAO,IAAI;KACxB;IAEJ;GACF,SAAS,KAAK;IACZ,OAAO,MAAM,KAAK,4CAA4C;IAC9D,KAAK,MAAM,SAAS,OAClB,OAAO,IAAI,OAAO,IAAI;GAE1B;EACF;EACA,OAAO;CACT;CAEA,MAAM,0BAA0B,KAA+C;EAC7E,MAAM,UAAU,MAAM,iBAAiB,cAAc,KAAK,MAAM,IAAI,GAAG,CAAC;EACxE,IAAI,CAAC,SAAS,OAAO;EAErB,IAAI;GACF,MAAM,SAAS,OAAO,YAAY,WAAW,KAAK,MAAM,OAAO,IAAI;GACnE,OAAO,uBAAuB,MAAM,MAAM;EAC5C,QAAQ;GACN,OAAO;EACT;CACF;CAEA,MAAM,2BAA2B,KAAa,OAAwC;EACpF,MAAM,iBAAiB,cACrB,KAAK,MAAM,IAAI,KAAK,kBAAkB,KAAK,CAAC,CAC9C;CACF;;;;;;CAOA,MAAM,wBAAwB,YAAoB,OAAe,UAAkB,KAA+B;EAChH,MAAM,MAAM,kBAAkB,MAAM,GAAG;EACvC,OAAO,iBAAiB,QAAQ,YAAY;GAC1C,IAAI;IAEF,OAAO,MADW,KAAK,MAAM,IAAI,KAAK,YAAY,MAAM,KAAK,IAAI,MAClD;GACjB,SAAS,KAAK;IACZ,OAAO,MAAM,KAAK,oCAAoC,KAAK;IAG3D,OAAO;GACT;EACF,CAAC;CACH;CAGA,mBAA2B,OAAuB;EAChD,OAAO,UAAU;CACnB;CAEA,qBAA6B,UAA0B;EACrD,OAAO,YAAY;CACrB;CAEA,qBAA6B,UAA0B;EACrD,OAAO,WAAW;CACpB;AACF;;;;;;;;AC5KA,MAAa,sBAAsB,OAC/B,QAAQ,IAAI,gCAAgC,GAChD;AASA,eAAsB,aAAa,SAA6C;CAC5E,MAAM,OAAO,eAAe,QAAQ,UAAU,QAAQ,QAAQ;CAC9D,MAAM,aAAa,WAAW,OAAO,EAAE,IAAI,KAAK,CAAC;CACjD,MAAM,iBAAiB,YAAY,QAAQ,YAAY,QAAQ,wBAAwB,KAAK;CAC5F,OAAO;AACX;AAEA,SAAgB,eAAe,QAAwB,UAA+C;CAClG,MAAM,WAAgC,EAClC,MAAM,OAAO,KACjB;CACA,IAAI,UACA,SAAS,YAAY;CAGzB,IAAI,OAAO,SAAS,YAChB,OAAO,IAAI,WACPC,SAAO,MAAM,QAAQ,GAAG;EACxB,GAAG;EACH,kBAAkB,OAAO;EACzB,KAAK,OAAO,kBAAkB;EAC9B,KAAK,OAAO,kBAAkB;EAC9B,yBAAyB,OAAO,2BAA2B;EAC3D,mBAAmB,OAAO,qBAAqB;EAG/C,mBAAmB,sBAAsB,IAAI,sBAAsB,KAAA;CACvE,CAAC;MAED,OAAO,IAAI,cACPA,SAAO,MAAM,QAAQ,GAAG;EACxB,GAAG;EACH,iBAAiB,OAAO,kBAAkB;EAC1C,gBAAgB,OAAO,2BAA2B;EAIlD,YAAY,OAAO,cAAc;EAGjC,gBAAgB;CACpB,CAAC;AAET;;;;;;;;;;;AAYA,eAAsB,iBAAiB,IAAgB,OAAmB,cAAc,OAAsB;CAE1G,IAAI,GAAG,KAAK,WAAW,SAEnB;CAGJ,MAAM,SAAS,QAAQ,IAAI,aAAa;CAExC,MAAM,MAAM,yBADI,QAAQ,IAAI,2BAA2B;CAEvD,IAAI;EACA,MAAM,GAAG,KAAK,qBAAqB;GAC/B,UAAU;GACV,OAAO,UAAU,CAAC,cAAc,QAAQ,KAAA;EAC5C,CAAC;EACD,SAAO,KAAK,EAAE,cAAc,GAAG,KAAK,YAAY,GAAG,uCAAuC,OAAO,QAAQ,IAAI,EAAE;CACnH,SAAS,KAAK;EACV,SAAO,MAAM,KAAK,qCAAqC;EACvD,MAAM;CACV;AACJ;AAgBA,SAAgB,eAAe,QAAmF;CAC9G,MAAM,QAAQ,IAAIC,QAAM,OAAO,QAAQ,UAAU;EAC7C,gBAAgB,OAAO,qBAAqB;EAC5C,sBAAsB,OAAO,qBAAqB;EAClD,WAAW;CACf,CAAC;CAED,MAAM,WAAW,OAAO,kBAAkB,IAAIA,QAAM,OAAO,QAAQ,UAAU;EACzE,gBAAgB,OAAO,qBAAqB;EAC5C,sBAAsB,OAAO,wBAAwB;EACrD,mBAAmB;EACnB,gBAAgB,UAAU;GACtB,IAAI,QAAQ,GAAG,OAAO;GACtB,OAAO,KAAK,IAAI,QAAQ,KAAK,GAAI;EACrC;CACJ,CAAC,IAAI,KAAA;CAEL,OAAO;EACH,YAAY,IAAI,WAAW,OAAO,OAAO,mBAAmB,IAAI;EAChE,gBAAgB;CACpB;AACJ;;;AC3GA,IAAa,qBAAb,MAAgC;CAC9B;CACA,gCAAwB,IAAI,IAAiC;CAC7D;CACA;CACA;CAEA,YAAY,IAAY,OAAmB,IAAc;EACvD,KAAK,KAAK;EACV,KAAK,QAAQ;EACb,KAAK,KAAK;EACV,KAAK,SAASC,SAAW,MACvB;GAAE,MAAM;GAAY,KAAK;EAAG,GAC5B,EAAE,OAAO,WAAW,+BAA+B,EAAE,CACvD;CACF;CAEA,OAAe;EACb,OAAO,KAAK,cAAc;CAC5B;CAEA,IAAI,OAAwB;EAC1B,OAAO,KAAK,cAAc,IAAI,KAAK;CACrC;;;;;CAMA,oBAAmC;EACjC,IAAI,MAAqB;EACzB,KAAK,MAAM,OAAO,KAAK,cAAc,OAAO,GAC1C,IAAI,QAAQ,QAAQ,IAAI,iBAAiB,KAAK,MAAM,IAAI;EAE1D,OAAO;CACT;CAEA,MAAM,SACJ,QACqC;EACrC,MAAM,EAAE,OAAO,UAAU,WAAW,MAAM,eAAe,cAAc,mBAAmB;EAC1F,KAAK,OAAO,KAAK,6BAA6B,MAAM,QAAQ,cAAc;EAE1E,MAAM,WAAW,KAAK,cAAc,IAAI,KAAK;EAE7C,IAAI,YAAY,CAAC,SAAS,SAAS;GACjC,SAAS,UAAU,IAAI,YAAY;GACnC,OAAO;IACL;IACA,WAAW;IACX,SAAS,SAAS;GACpB;EACF;EAEA,IAAI,UAAU,SAAS;GACrB,KAAK,OAAO,KAAK,8BAA8B,MAAM,SAAS,cAAc;GAC5E,aAAa,SAAS,OAAO;GAC7B,SAAS,UAAU;EACrB;EAEA,MAAM,WAAW,MAAM,KAAK,MAAM,sBAAsB,QAAQ;EAChE,IAAI,iBAAiB;EACrB,IAAI;EACJ,IAAI;EAEJ,IAAI,UAAU;GACZ,UAAU,SAAS;GACnB,aAAa,gBAAgB,OAAO;GAEpC,MAAM,WAAW,MAAM,KAAK,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC;GAElE,iBAAiB,uBAAuB,MADrB,KAAK,GAAG,uBAAuB,SAAS,gBAAgB,gBAAgB,QAAQ,GACrD,UAAU;GAExD,IAAI,CAAC,gBAAgB;IACnB,IAAI,mBAAmB,SAAS,gBAC9B,KAAK,MACF,sBAAsB,UAAU;KAAkB;KAAgB;IAAQ,CAAC,CAAC,CAC5E,OAAO,QAAQ,KAAK,OAAO,MAAM,KAAK,uCAAuC,UAAU,CAAC;IAE7F,SAAS,iBAAiB;GAC5B;EACF,OAAO;GACL,UAAU,CAAC;GACX,aAAa,CAAC;EAChB;EAEA,IAAI,kBAAuC;GACzC;GACA;GACA;GACA;GACgB;GAChB;GACA;GACA,2BAAW,IAAI,IAAI,CAAC,YAAY,CAAC;GACjC,SAAS;EACX;EAEA,MAAM,uBAAuB,KAAK,cAAc,IAAI,KAAK;EACzD,IAAI,sBAAsB;GACxB,IAAI,qBAAqB,SAAS;IAChC,aAAa,qBAAqB,OAAO;IACzC,qBAAqB,UAAU;GACjC;GAEA,IAAI,qBAAqB,iBAAiB,gBAAgB;IACxD,kBAAkB;KAChB,GAAG;KACH,GAAG;KACH,2BAAW,IAAI,IAAI,CAAC,GAAG,qBAAqB,WAAW,YAAY,CAAC;IACtE;IACA,iBAAiB;GACnB;EACF;EAEA,KAAK,cAAc,IAAI,OAAO,eAAe;EAE7C,KAAK,oBAAoB,OAAO;GAAE;GAAW;GAAM;GAAe;EAAS,CAAC;EAE5E,OAAO;GAAE;GAAU,WAAW;GAAgB;EAAQ;CACxD;CAEA,MAAM,cACJ,QAC0C;EAC1C,MAAM,EAAE,UAAU,OAAO,cAAc,gBAAgB,YAAY;EACnE,KAAK,OAAO,KAAK,kCAAkC,MAAM,QAAQ,cAAc;EAE/E,MAAM,WAAW,KAAK,cAAc,IAAI,KAAK;EAC7C,IAAI,CAAC,UACH,OAAO,EAAE,SAAS,MAAM;EAG1B,IAAI,iBAAiB,SAAS,gBAC5B,OAAO,EAAE,SAAS,KAAK;EAGzB,MAAM,aAAa,gBAAgB,OAAO;EAC1C,KAAK,cAAc,IAAI,OAAO;GAAE,GAAG;GAAU;GAAS;GAAY;EAAe,CAAC;EAClF,MAAM,KAAK,MAAM,sBAAsB,UAAU;GAAE;GAAgB;EAAQ,CAAC;EAE5E,OAAO,EAAE,SAAS,KAAK;CACzB;CAEA,MAAM,WACJ,QACA,SAAiB,GACsB;EACvC,MAAM,EAAE,OAAO,iBAAiB;EAChC,KAAK,OAAO,KAAK,+BAA+B,MAAM,SAAS,cAAc;EAE7E,MAAM,MAAM,KAAK,cAAc,IAAI,KAAK;EACxC,IAAI,CAAC,KAAK,OAAO,EAAE,IAAI,KAAK;EAE5B,IAAI,IAAI,SACN,aAAa,IAAI,OAAO;EAG1B,IAAI,IAAI,UAAU,OAAO,GAAG;GAC1B,IAAI,UAAU,OAAO,YAAY;GACjC,OAAO,EAAE,IAAI,KAAK;EACpB;EAEA,KAAK,OAAO,MAAM,uCAAuC,MAAM,SAAS,aAAa,QAAQ;EAC7F,IAAI,UAAU,WAAW,YAAY;GACnC,IAAI,UAAU,OAAO,YAAY;GAEjC,IAAI,IAAI,UAAU,SAAS,GAAG;IAC5B,KAAK,OAAO,KAAK,8BAA8B,MAAM,SAAS,cAAc;IAC5E,KAAK,cAAc,OAAO,KAAK;IAC/B,MAAM,KAAK,mBAAmB,KAAK;GACrC;EACF,GAAG,OAAS,MAAM;EAElB,OAAO,EAAE,IAAI,KAAK;CACpB;CAEA,MAAM,YACJ,QACwC;EACxC,KAAK,OAAO,KAAK,EAAE,QAAQ,OAAO,GAAG,wCAAwC;EAC7E,KAAK,MAAM,SAAS,QAClB,KAAK,WAAW,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG,CAAC;EAGxD,OAAO,OAAO,WAAW,EAAE,IAAI,KAAK,EAAE;CACxC;;;;;CAMA,WAAW,aAAsD;EAC/D,MAAM,WAAmC,CAAC;EAE1C,KAAK,MAAM,GAAG,QAAQ,KAAK,eAAe;GACxC,IAAI,IAAI,UAAU,SAAS,GAAG;GAC9B,IAAI,CAAC,4BAA4B,aAAa,IAAI,UAAU,GAAG;GAE/D,SAAS,KAAK;IACZ,OAAO,IAAI;IACX,WAAW,IAAI;IACf,MAAM,IAAI;IACV,UAAU,IAAI;IACd,iBAAiB,MAAM,KAAK,IAAI,SAAS;GAC3C,CAAC;EACH;EAEA,OAAO;CACT;CAEA,MAAc,oBAAoB,OAAe,QAA2C;EAC1F,IAAI;GACF,MAAM,KAAK,MAAM,0BAA0B,OAAO,MAAM;EAC1D,SAAS,KAAK;GACZ,KAAK,OAAO,MAAM,KAAK,WAAW,KAAK,GAAG,uCAAuC,OAAO;EAC1F;CACF;CAEA,MAAc,mBAAmB,OAA8B;EAC7D,IAAI;GACF,MAAM,KAAK,MAAM,yBAAyB,KAAK;EACjD,SAAS,KAAK;GACZ,KAAK,OAAO,MAAM,KAAK,WAAW,KAAK,GAAG,sCAAsC,OAAO;EACzF;CACF;AACF"}
@@ -1,511 +0,0 @@
1
- import { r as supaliveStringify } from "./helper-wGbzz6zo.js";
2
- import { b as WriteEntrySchema, m as QueryCacheMetadataSchema, n as BigIntSchema, p as QueryCacheEntrySchema, r as CachedPgMetadataSchema, y as ReadEntrySchema } from "./types_db-DXj_l9n-.js";
3
- import { n as checkWriteSetAffectsReadSet, r as mapReadSetToRaw, t as checkLogsAffectReadSet } from "./overlap-checker-Ct_HP_iB.js";
4
- import { t as MySqlDatabase } from "./mysql-bvnjYWvZ.js";
5
- import { t as PgDatabase } from "./postgres-B8u1YN8X.js";
6
- import { t as SupaliveDb } from "./realtime_db-DOCgqbwi.js";
7
- import z from "zod";
8
- import { v4 } from "uuid";
9
- import { ExponentialBackoff, handleAll, retry } from "cockatiel";
10
- import pino from "pino";
11
- import "pino-pretty";
12
- import Redis$1 from "ioredis";
13
- //#region src/server/subscription-manager-types.ts
14
- const RegisterSubscriptionParamsSchema = z.object({
15
- /**
16
- * Caller-computed FNV-1a routing key + identity for this subscription.
17
- * The sub-manager trusts these values rather than re-deriving them from
18
- * (queryName, args, queryIdentity); this avoids one stringify+SHA1 per
19
- * register RPC. Trust is fine: the sub-manager RPC channel is fully
20
- * internal.
21
- */
22
- subId: z.string(),
23
- cacheKey: z.string(),
24
- queryName: z.string(),
25
- args: z.unknown(),
26
- /**
27
- * Segmentation key used in the subId/cacheKey hash. Defaults to the
28
- * authenticated user's id ("anonymous" when none), unless the
29
- * procedure overrides it via `QueryConfig.queryIdentity`.
30
- */
31
- queryIdentity: z.string(),
32
- instanceName: z.string(),
33
- lastSnapshotTs: BigIntSchema
34
- });
35
- const RegisterSubscriptionResultSchema = z.object({
36
- cacheKey: z.string(),
37
- recompute: z.boolean(),
38
- readSet: z.array(ReadEntrySchema)
39
- });
40
- const RegisterSubscriptionBatchParamsSchema = z.array(RegisterSubscriptionParamsSchema);
41
- const RegisterSubscriptionBatchResultSchema = z.array(RegisterSubscriptionResultSchema);
42
- const UpdateSubscriptionReadSetParamsSchema = z.object({
43
- cacheKey: z.string(),
44
- subId: z.string(),
45
- instanceName: z.string(),
46
- lastSnapshotTs: BigIntSchema,
47
- readSet: z.array(ReadEntrySchema)
48
- });
49
- /**
50
- * `tracked: false` means the sub-manager has no record of this subId (e.g.
51
- * a restart wiped state and the caller hasn't re-registered yet). The app
52
- * server should treat this as a signal to register the sub before retrying.
53
- */
54
- const UpdateSubscriptionReadSetResultSchema = z.object({ tracked: z.boolean() });
55
- const UnregisterSubscriptionParamsSchema = z.object({
56
- subId: z.string(),
57
- instanceName: z.string()
58
- });
59
- const UnregisterSubscriptionsParamsSchema = z.array(UnregisterSubscriptionParamsSchema);
60
- const UnregisterSubscriptionResultSchema = z.object({ ok: z.boolean() });
61
- const UnregisterSubscriptionsResultSchema = z.array(z.object({ ok: z.boolean() }));
62
- const InvalidateWritesetParamsSchema = z.object({ writeSet: z.array(WriteEntrySchema) });
63
- const AffectedSubscriptionSchema = z.object({
64
- subId: z.string(),
65
- queryName: z.string(),
66
- args: z.unknown(),
67
- cacheKey: z.string(),
68
- /**
69
- * Every live instance that currently holds this subId. Routing rules:
70
- * • If the writer is in this list, it recomputes locally and publishes
71
- * an `update` to the rest (its own id is filtered by the publisher).
72
- * • Otherwise the writer broadcasts a `recompute-race` to every entry;
73
- * receivers race for a Redis lock and the winner recomputes.
74
- */
75
- notifyInstances: z.array(z.string())
76
- });
77
- const InvalidateWritesetResultSchema = z.object({ affected: z.array(AffectedSubscriptionSchema) });
78
- const RegistrationRecordSchema = z.object({
79
- queryName: z.string(),
80
- args: z.unknown(),
81
- queryIdentity: z.string(),
82
- cacheKey: z.string()
83
- });
84
- //#endregion
85
- //#region src/logger.ts
86
- const appInstanceId = `instance-${v4().slice(0, 8)}`;
87
- const isProduction = process.env.ENV === "prod";
88
- const logger$1 = pino({
89
- level: process.env.LOG_LEVEL || "info",
90
- timestamp: pino.stdTimeFunctions.isoTime,
91
- redact: { paths: ["user.password"] },
92
- transport: isProduction ? void 0 : {
93
- target: "pino-pretty",
94
- options: { colorize: true }
95
- },
96
- serializers: { "columnsTypes": (map) => {
97
- if (map instanceof Map) return serializeMapValue(map);
98
- return map;
99
- } },
100
- base: { "srv": appInstanceId }
101
- });
102
- function logLevelOf(name) {
103
- const defaultLogLevel = process.env.DEFAULT_LOG_LEVEL || "info";
104
- return process.env[name]?.toLowerCase() || defaultLogLevel;
105
- }
106
- function serializeMapValue(map) {
107
- const obj = {};
108
- for (const [k, v] of map.entries()) {
109
- obj[k] = {};
110
- if (v instanceof Map) for (const [innerK, innerV] of v.entries()) obj[k][innerK] = innerV;
111
- else obj[k] = v;
112
- }
113
- return obj;
114
- }
115
- //#endregion
116
- //#region src/db/cache.ts
117
- const logger = logger$1.child({ "name": "CACHE_LAYER" }, { level: logLevelOf("SERVER_LOG_LEVEL") });
118
- const redisRetryPolicy = retry(handleAll, {
119
- maxAttempts: 3,
120
- backoff: new ExponentialBackoff({
121
- initialDelay: 5,
122
- maxDelay: 2300,
123
- exponent: 2
124
- })
125
- });
126
- var CacheLayer = class {
127
- redis;
128
- ttlSeconds;
129
- constructor(upstash, ttlSeconds = 3600) {
130
- this.redis = upstash;
131
- this.ttlSeconds = ttlSeconds;
132
- }
133
- async setQueryCacheFor(cacheKey, data, ttlSeconds) {
134
- const combined = supaliveStringify({ data });
135
- await redisRetryPolicy.execute(async () => {
136
- await this.redis.setex(this.getQueryCacheDataKey(cacheKey), ttlSeconds ?? this.ttlSeconds, combined);
137
- });
138
- }
139
- async getQueryCacheFor(cacheKey) {
140
- const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQueryCacheDataKey(cacheKey)));
141
- if (!dataRaw) return null;
142
- try {
143
- const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
144
- return QueryCacheEntrySchema.parse(parsed);
145
- } catch {
146
- return null;
147
- }
148
- }
149
- async getQueryCacheMetaData(cacheKey) {
150
- const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(this.getQeruyCacheMetaKey(cacheKey)));
151
- if (!dataRaw) return null;
152
- try {
153
- const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
154
- return QueryCacheMetadataSchema.parse(parsed);
155
- } catch {
156
- return null;
157
- }
158
- }
159
- async setQueryCacheMetadata(cacheKey, metaData) {
160
- const metadata = supaliveStringify(metaData);
161
- await redisRetryPolicy.execute(() => this.redis.set(this.getQeruyCacheMetaKey(cacheKey), metadata));
162
- }
163
- async persistRegistrationRecord(subId, record) {
164
- const data = supaliveStringify(record);
165
- await redisRetryPolicy.execute(() => this.redis.set(this.getRegistrationKey(subId), data));
166
- }
167
- async deleteRegistrationRecord(subId) {
168
- await redisRetryPolicy.execute(() => this.redis.del(this.getRegistrationKey(subId)));
169
- }
170
- async deleteRegistrationsRecord(subIds) {
171
- if (subIds.length === 0) return;
172
- await redisRetryPolicy.execute(() => this.redis.del(...subIds.map((subId) => this.getRegistrationKey(subId))));
173
- }
174
- async fetchRegistrationRecordsBatch(subIds) {
175
- const result = /* @__PURE__ */ new Map();
176
- const CHUNK_SIZE = 100;
177
- for (let i = 0; i < subIds.length; i += CHUNK_SIZE) {
178
- const chunk = subIds.slice(i, i + CHUNK_SIZE);
179
- const keys = chunk.map((id) => `sl:reg:${id}`);
180
- try {
181
- if (keys.length === 0) continue;
182
- const rawValues = await redisRetryPolicy.execute(() => this.redis.mget(...keys));
183
- for (let j = 0; j < chunk.length; j++) {
184
- const subId = chunk[j];
185
- const raw = rawValues[j];
186
- const rawJson = typeof raw === "string" ? JSON.parse(raw) : raw;
187
- if (!raw) result.set(subId, null);
188
- else try {
189
- const parsed = RegistrationRecordSchema.parse(rawJson);
190
- result.set(subId, parsed);
191
- } catch {
192
- logger.error(`Failed to parse sl:reg:${subId} during batch recovery`);
193
- result.set(subId, null);
194
- }
195
- }
196
- } catch (err) {
197
- logger.error(err, "Failed to fetch registration records batch");
198
- for (const subId of chunk) result.set(subId, null);
199
- }
200
- }
201
- return result;
202
- }
203
- async getCachedTableColumnTypes(key) {
204
- const dataRaw = await redisRetryPolicy.execute(() => this.redis.get(key));
205
- if (!dataRaw) return null;
206
- try {
207
- const parsed = typeof dataRaw === "string" ? JSON.parse(dataRaw) : dataRaw;
208
- return CachedPgMetadataSchema.parse(parsed);
209
- } catch {
210
- return null;
211
- }
212
- }
213
- async setCachedTableColumnsTypes(key, value) {
214
- await redisRetryPolicy.execute(() => this.redis.set(key, supaliveStringify(value)));
215
- }
216
- /**
217
- * Race participant: try to claim `lock:recompute:<subId>:<commitTs>`
218
- * with `SET NX EX`. The single Redis round-trip both arbitrates and
219
- * sets a TTL fallback in case the winner crashes mid-recompute.
220
- */
221
- async tryAcquireRecomputeLock(instanceId, subId, commitTs, ttl) {
222
- const key = `lock:recompute:${subId}:${commitTs}`;
223
- return redisRetryPolicy.execute(async () => {
224
- try {
225
- return await this.redis.set(key, instanceId, "EX", ttl, "NX") === "OK";
226
- } catch (err) {
227
- logger.error(err, `Failed to acquire recompute lock ${key}`);
228
- return false;
229
- }
230
- });
231
- }
232
- getRegistrationKey(subId) {
233
- return `sl:reg:${subId}`;
234
- }
235
- getQueryCacheDataKey(cacheKey) {
236
- return `sl:cache:${cacheKey}`;
237
- }
238
- getQeruyCacheMetaKey(cacheKey) {
239
- return `sl:meta:${cacheKey}`;
240
- }
241
- };
242
- //#endregion
243
- //#region src/db/init_db.ts
244
- /**
245
- * Default and env override for the per-statement DB timeout. PG enforces
246
- * this via `statement_timeout` (server cancels), MySQL via the per-query
247
- * `timeout` option (driver issues KILL QUERY). Set 0 to disable.
248
- */
249
- const DB_QUERY_TIMEOUT_MS = Number(process.env.SUPALIVE_DB_QUERY_TIMEOUT_MS ?? 3e4);
250
- async function initDatabase(options, bool) {
251
- const impl = createDatabase(options.DbConfig, options.workerId);
252
- const supaliveDb = SupaliveDb.create({ db: impl });
253
- await bootstrapDbTypes(supaliveDb, options.CacheLayer, options.forceBootstrapReload ?? false);
254
- return supaliveDb;
255
- }
256
- function createDatabase(config, workerId) {
257
- const bindings = { "db": config.type };
258
- if (workerId) bindings["worker"] = workerId;
259
- if (config.type === "postgres") return new PgDatabase(logger$1.child(bindings), {
260
- ...config,
261
- connectionString: config.connectionString,
262
- min: config.minConnections ?? 0,
263
- max: config.maxConnections ?? 20,
264
- connectionTimeoutMillis: config.connectionTimeoutMillis ?? 15e3,
265
- idleTimeoutMillis: config.idleTimeoutMillis ?? 3e4,
266
- statement_timeout: DB_QUERY_TIMEOUT_MS > 0 ? DB_QUERY_TIMEOUT_MS : void 0
267
- });
268
- else return new MySqlDatabase(logger$1.child(bindings), {
269
- ...config,
270
- connectionLimit: config.maxConnections ?? 20,
271
- connectTimeout: config.connectionTimeoutMillis ?? 15e3,
272
- queueLimit: config.queueLimit ?? 0,
273
- queryTimeoutMs: DB_QUERY_TIMEOUT_MS
274
- });
275
- }
276
- /**
277
- * Eagerly load column types into the apply-path cache at server start.
278
- * In production, the result is cached in Redis under
279
- * `supalive:schema:types:${SUPALIVE_SCHEMA_VERSION ?? "v1"}` so other
280
- * instances starting cold can skip the pg_attribute round-trip. In any
281
- * other env we go straight to PG every time so local schema iteration
282
- * is never blocked by stale Redis state. Migrations should publish a
283
- * `schema:reload` message (see {@link publishSchemaReload}) after
284
- * applying DDL — every instance picks that up and re-bootstraps.
285
- */
286
- async function bootstrapDbTypes(db, cache, forceReload = false) {
287
- if (db.impl.dbType === "mysql") return;
288
- const inProd = process.env.NODE_ENV === "production";
289
- const key = `supalive:schema:types:${process.env.SUPALIVE_SCHEMA_VERSION ?? "v1"}`;
290
- try {
291
- await db.impl.bootstrapColumnTypes({
292
- cacheKey: key,
293
- redis: inProd && !forceReload ? cache : void 0
294
- });
295
- logger$1.info({ columnsTypes: db.impl.columnTypes }, `Bootstrapped db column types (redis=${inProd}, key=${key})`);
296
- } catch (err) {
297
- logger$1.error(err, "Failed to bootstrap db column types");
298
- throw err;
299
- }
300
- }
301
- function initCacheLayer(config) {
302
- const redis = new Redis$1(config.upstash.redisUrl, {
303
- connectTimeout: config.connectionTimeout ?? 15e3,
304
- maxRetriesPerRequest: config.commandMaxRetries ?? 1,
305
- keepAlive: 3e3
306
- });
307
- const redisSub = config.createSubClient ? new Redis$1(config.upstash.redisUrl, {
308
- connectTimeout: config.connectionTimeout ?? 15e3,
309
- maxRetriesPerRequest: config.commandSubMaxRetries ?? 5,
310
- disableClientInfo: true,
311
- retryStrategy: (times) => {
312
- if (times > 5) return null;
313
- return Math.min(times * 200, 2e3);
314
- }
315
- }) : void 0;
316
- return {
317
- cacheLayer: new CacheLayer(redis, config.cacheTtlSeconds ?? 3600),
318
- redisSubClient: redisSub
319
- };
320
- }
321
- //#endregion
322
- //#region src/server/subscription-worker.ts
323
- var SubscriptionWorker = class {
324
- id;
325
- subscriptions = /* @__PURE__ */ new Map();
326
- cache;
327
- db;
328
- logger;
329
- constructor(id, cache, db) {
330
- this.id = id;
331
- this.cache = cache;
332
- this.db = db;
333
- this.logger = logger$1.child({
334
- name: "SUBS_WRK",
335
- wid: id
336
- }, { level: logLevelOf("SUBS_MANAGER_SERVER_LOG_LEVEL") });
337
- }
338
- size() {
339
- return this.subscriptions.size;
340
- }
341
- has(subId) {
342
- return this.subscriptions.has(subId);
343
- }
344
- /**
345
- * Lowest `lastSnapshotTs` across this worker's subscriptions, or `null`
346
- * when there are none. Used by the dispatcher to derive a safe prune floor.
347
- */
348
- minSubscriptionTs() {
349
- let min = null;
350
- for (const sub of this.subscriptions.values()) if (min === null || sub.lastSnapshotTs < min) min = sub.lastSnapshotTs;
351
- return min;
352
- }
353
- async register(params) {
354
- const { subId, cacheKey, queryName, args, queryIdentity, instanceName, lastSnapshotTs } = params;
355
- this.logger.info(`registering subscription: ${subId} from ${instanceName}`);
356
- const existing = this.subscriptions.get(subId);
357
- if (existing && !existing.timeout) {
358
- existing.instances.add(instanceName);
359
- return {
360
- cacheKey,
361
- recompute: false,
362
- readSet: existing.readSet
363
- };
364
- }
365
- if (existing?.timeout) {
366
- this.logger.info(`reactivating subscription: ${subId} from: ${instanceName}`);
367
- clearTimeout(existing.timeout);
368
- existing.timeout = null;
369
- }
370
- const metadata = await this.cache.getQueryCacheMetaData(cacheKey);
371
- let needsRecompute = true;
372
- let readSet;
373
- let rawReadSet;
374
- if (metadata) {
375
- readSet = metadata.readSet;
376
- rawReadSet = mapReadSetToRaw(readSet);
377
- const tableIds = Array.from(new Set(readSet.map((r) => r.tableId)));
378
- needsRecompute = checkLogsAffectReadSet(await this.db.getCommitLogsBetweenTs(metadata.lastSnapshotTs, lastSnapshotTs, tableIds), rawReadSet);
379
- if (!needsRecompute) {
380
- if (lastSnapshotTs !== metadata.lastSnapshotTs) this.cache.setQueryCacheMetadata(cacheKey, {
381
- lastSnapshotTs,
382
- readSet
383
- }).catch((err) => this.logger.error(err, `Failed to update cache metadata for ${cacheKey}`));
384
- metadata.lastSnapshotTs = lastSnapshotTs;
385
- }
386
- } else {
387
- readSet = [];
388
- rawReadSet = [];
389
- }
390
- let newSubscription = {
391
- subId,
392
- queryName,
393
- args,
394
- cacheKey,
395
- lastSnapshotTs,
396
- readSet,
397
- rawReadSet,
398
- instances: /* @__PURE__ */ new Set([instanceName]),
399
- timeout: null
400
- };
401
- const createdWhileChecking = this.subscriptions.get(subId);
402
- if (createdWhileChecking) {
403
- if (createdWhileChecking.timeout) {
404
- clearTimeout(createdWhileChecking.timeout);
405
- createdWhileChecking.timeout = null;
406
- }
407
- if (createdWhileChecking.lastSnapshotTs > lastSnapshotTs) {
408
- newSubscription = {
409
- ...newSubscription,
410
- ...createdWhileChecking,
411
- instances: /* @__PURE__ */ new Set([...createdWhileChecking.instances, instanceName])
412
- };
413
- needsRecompute = false;
414
- }
415
- }
416
- this.subscriptions.set(subId, newSubscription);
417
- this.persistRegistration(subId, {
418
- queryName,
419
- args,
420
- queryIdentity,
421
- cacheKey
422
- });
423
- return {
424
- cacheKey,
425
- recompute: needsRecompute,
426
- readSet
427
- };
428
- }
429
- async updateReadSet(params) {
430
- const { cacheKey, subId, instanceName, lastSnapshotTs, readSet } = params;
431
- this.logger.info(`updating subscription readSet: ${subId} from ${instanceName}`);
432
- const existing = this.subscriptions.get(subId);
433
- if (!existing) return { tracked: false };
434
- if (lastSnapshotTs < existing.lastSnapshotTs) return { tracked: true };
435
- const rawReadSet = mapReadSetToRaw(readSet);
436
- this.subscriptions.set(subId, {
437
- ...existing,
438
- readSet,
439
- rawReadSet,
440
- lastSnapshotTs
441
- });
442
- await this.cache.setQueryCacheMetadata(cacheKey, {
443
- lastSnapshotTs,
444
- readSet
445
- });
446
- return { tracked: true };
447
- }
448
- async unregister(params, jitter = 0) {
449
- const { subId, instanceName } = params;
450
- this.logger.info(`unregistering subscription: ${subId} from: ${instanceName}`);
451
- const sub = this.subscriptions.get(subId);
452
- if (!sub) return { ok: true };
453
- if (sub.timeout) clearTimeout(sub.timeout);
454
- if (sub.instances.size > 1) {
455
- sub.instances.delete(instanceName);
456
- return { ok: true };
457
- }
458
- this.logger.debug(`scheduling unregister subscription: ${subId} from: ${instanceName} in 15s`);
459
- sub.timeout = setTimeout(async () => {
460
- sub.instances.delete(instanceName);
461
- if (sub.instances.size === 0) {
462
- this.logger.info(`unregistered subscription: ${subId} from: ${instanceName}`);
463
- this.subscriptions.delete(subId);
464
- await this.deleteRegistration(subId);
465
- }
466
- }, 15e3 + jitter);
467
- return { ok: true };
468
- }
469
- async unregisters(params) {
470
- this.logger.info({ subIds: params }, `scheduling unregistering subscriptions`);
471
- for (const param of params) this.unregister(param, Math.floor(Math.random() * 100));
472
- return params.map(() => ({ ok: true }));
473
- }
474
- /**
475
- * Synchronous, CPU-bound. Scans this worker's slice of subscriptions and
476
- * returns the ones affected by the writeSet.
477
- */
478
- invalidate(rawWriteSet) {
479
- const affected = [];
480
- for (const [, sub] of this.subscriptions) {
481
- if (sub.instances.size === 0) continue;
482
- if (!checkWriteSetAffectsReadSet(rawWriteSet, sub.rawReadSet)) continue;
483
- affected.push({
484
- subId: sub.subId,
485
- queryName: sub.queryName,
486
- args: sub.args,
487
- cacheKey: sub.cacheKey,
488
- notifyInstances: Array.from(sub.instances)
489
- });
490
- }
491
- return affected;
492
- }
493
- async persistRegistration(subId, record) {
494
- try {
495
- await this.cache.persistRegistrationRecord(subId, record);
496
- } catch (err) {
497
- this.logger.error(err, `[worker ${this.id}] Failed to persist registration for ${subId}`);
498
- }
499
- }
500
- async deleteRegistration(subId) {
501
- try {
502
- await this.cache.deleteRegistrationRecord(subId);
503
- } catch (err) {
504
- this.logger.error(err, `[worker ${this.id}] Failed to delete registration for ${subId}`);
505
- }
506
- }
507
- };
508
- //#endregion
509
- export { UpdateSubscriptionReadSetParamsSchema as C, UnregisterSubscriptionsResultSchema as S, RegisterSubscriptionParamsSchema as _, initCacheLayer as a, UnregisterSubscriptionResultSchema as b, appInstanceId as c, logger$1 as d, serializeMapValue as f, RegisterSubscriptionBatchResultSchema as g, RegisterSubscriptionBatchParamsSchema as h, createDatabase as i, isProduction as l, InvalidateWritesetResultSchema as m, DB_QUERY_TIMEOUT_MS as n, initDatabase as o, InvalidateWritesetParamsSchema as p, bootstrapDbTypes as r, CacheLayer as s, SubscriptionWorker as t, logLevelOf as u, RegisterSubscriptionResultSchema as v, UpdateSubscriptionReadSetResultSchema as w, UnregisterSubscriptionsParamsSchema as x, UnregisterSubscriptionParamsSchema as y };
510
-
511
- //# sourceMappingURL=subscription-worker-PtmJWEj1.js.map