Skip to content

Commit a7a4ec8

Browse files
committed
refactor(webapp): rename the realtime backend to native and tune it via env vars
The new realtime runs backend is now named native instead of notifier: the feature flag selects electric | native | shadow, and all of its env vars share the REALTIME_BACKEND_NATIVE_ prefix, including the dedicated pub/sub Redis and ClickHouse pool configs. Remaining hardcoded tunables (live-poll jitter, working-set TTL, single-run cache, backend flag cache, default concurrency limit) moved to env vars with unchanged defaults. Backend selection reuses the org feature flags already loaded by request auth instead of an extra organization read, and its caches survive dev-mode reloads. Oversized comment blocks trimmed throughout.
1 parent 6a78c3d commit a7a4ec8

27 files changed

Lines changed: 325 additions & 624 deletions

apps/webapp/app/entry.server.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ process.on("uncaughtException", (error, origin) => {
270270

271271
singleton("RunEngineEventBusHandlers", registerRunEngineEventBusHandlers);
272272
singleton("SetupBatchQueueCallbacks", setupBatchQueueCallbacks);
273-
// Attach the run-changed notifier delegations to the engine event bus.
274-
// No-ops (registers nothing) unless REALTIME_NOTIFIER_ENABLED=1.
273+
// Attach the realtime run-changed publish delegations to the engine event bus.
274+
// No-ops (registers nothing) unless REALTIME_BACKEND_NATIVE_ENABLED=1.
275275
singleton("RunChangeNotifierHandlers", registerRunChangeNotifierHandlers);
276276

277277
// Wrapped in singleton() so Remix's dev-mode CJS reloads don't append

apps/webapp/app/env.server.ts

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -300,46 +300,36 @@ const EnvironmentSchema = z
300300
.int()
301301
.default(24 * 60 * 60 * 1000), // 1 day in milliseconds
302302

303-
// Master switch for the notifier-backed realtime feed.
304-
// "0" (default) = the existing realtime path serves everything, publishes are
305-
// no-ops, and no notifier Redis connections are opened (zero-overhead off).
306-
// "1" = run-changed signals are published and the per-org `realtimeBackend`
307-
// feature flag selects the backend per request.
308-
REALTIME_NOTIFIER_ENABLED: z.string().default("0"),
309-
// Backstop wait before a live notifier request refetches the run (ms). Matches
310-
// Electric's ~20s live long-poll hold so the client polling cadence is unchanged
311-
// across backends (a ±15% jitter is applied per request to avoid refetch herds).
312-
REALTIME_NOTIFIER_LIVE_POLL_TIMEOUT_MS: z.coerce.number().int().default(20_000),
313-
// Hard cap on the tag-list snapshot size served by the notifier feed.
314-
REALTIME_NOTIFIER_MAX_LIST_RESULTS: z.coerce.number().int().default(1_000),
315-
// Short-TTL coalescing cache for the multi-run (tag-list/batch) resolve+hydrate.
316-
// Concurrent same-filter feeds share one ClickHouse resolve + Postgres hydrate
317-
// within this window, so an env-wide wake doesn't fan out into per-feed queries.
318-
// Staleness budget: a newly-matching run is visible within ~ttl + poll interval.
319-
REALTIME_NOTIFIER_RUNSET_CACHE_TTL_MS: z.coerce.number().int().default(1_000),
320-
REALTIME_NOTIFIER_RUNSET_CACHE_MAX_ENTRIES: z.coerce.number().int().default(5_000),
321-
// Cap on the per-handle working-set cache (runId -> updatedAt) the notifier keeps
322-
// for diffing multi-run live polls.
323-
REALTIME_NOTIFIER_WORKING_SET_MAX_ENTRIES: z.coerce.number().int().default(10_000),
324-
// Quantize the tag-list createdAt lower bound to this epoch-aligned bucket (ms) so
325-
// same-tag feeds that pin their window within the same bucket share one resolve+
326-
// hydrate cache entry. Floored, so the window only ever widens by < bucket. 0
327-
// disables bucketing (each feed keeps its exact lower bound).
328-
REALTIME_NOTIFIER_RUNSET_CREATED_AT_BUCKET_MS: z.coerce.number().int().default(60_000),
329-
// Leading-edge throttle (ms) on the per-env wake channel: a busy env's run-change
330-
// firehose is collapsed to at most one feed-wake per window, decoupling wake load
331-
// from run throughput. Lossless because consumers refetch current state on a wake.
332-
// 0 disables coalescing (every change wakes immediately).
333-
REALTIME_NOTIFIER_ENV_WAKE_COALESCE_WINDOW_MS: z.coerce.number().int().default(100),
334-
// When "1", a multi-run live poll woken by a change irrelevant to its filter keeps
335-
// holding the long-poll (re-resolving cheaply) instead of returning an empty
336-
// up-to-date the client would immediately re-issue. "0" reverts to per-wake replies.
337-
REALTIME_NOTIFIER_HOLD_ON_EMPTY: z.string().default("1"),
338-
// Max concurrent fresh ClickHouse resolves (cache misses) per instance. Caps the
339-
// distinct-filter reconnect stampede: a mass reconnect of N feeds on N different filters
340-
// queues to this many concurrent CH queries instead of firing all N at once. Same-filter
341-
// bursts collapse via the single-flight cache before taking a permit. 0 disables the gate.
342-
REALTIME_NOTIFIER_RESOLVE_ADMISSION_LIMIT: z.coerce.number().int().default(16),
303+
// Master switch for the native realtime backend; off = Electric serves everything, publishes no-op.
304+
REALTIME_BACKEND_NATIVE_ENABLED: z.string().default("0"),
305+
// Live long-poll backstop hold (ms); matches Electric's ~20s cadence.
306+
REALTIME_BACKEND_NATIVE_LIVE_POLL_TIMEOUT_MS: z.coerce.number().int().default(20_000),
307+
// Jitter ratio on the live-poll hold (0.15 = ±15%) to avoid synchronized refetch herds.
308+
REALTIME_BACKEND_NATIVE_LIVE_POLL_JITTER_RATIO: z.coerce.number().default(0.15),
309+
// Hard cap on the tag-list snapshot size.
310+
REALTIME_BACKEND_NATIVE_MAX_LIST_RESULTS: z.coerce.number().int().default(1_000),
311+
// TTL/size of the coalescing cache for the multi-run resolve+hydrate (same-filter feeds share one query).
312+
REALTIME_BACKEND_NATIVE_RUNSET_CACHE_TTL_MS: z.coerce.number().int().default(1_000),
313+
REALTIME_BACKEND_NATIVE_RUNSET_CACHE_MAX_ENTRIES: z.coerce.number().int().default(5_000),
314+
// Size/TTL of the per-handle working-set cache used to diff multi-run live polls.
315+
REALTIME_BACKEND_NATIVE_WORKING_SET_MAX_ENTRIES: z.coerce.number().int().default(10_000),
316+
REALTIME_BACKEND_NATIVE_WORKING_SET_TTL_MS: z.coerce.number().int().default(300_000),
317+
// Bucket (ms) the tag-list createdAt floor is quantized to so same-tag feeds share a cache entry; 0 disables.
318+
REALTIME_BACKEND_NATIVE_RUNSET_CREATED_AT_BUCKET_MS: z.coerce.number().int().default(60_000),
319+
// Leading-edge throttle (ms) on per-env wake delivery; 0 wakes on every change.
320+
REALTIME_BACKEND_NATIVE_ENV_WAKE_COALESCE_WINDOW_MS: z.coerce.number().int().default(100),
321+
// "1" holds a multi-run live poll open on a non-matching wake instead of replying up-to-date.
322+
REALTIME_BACKEND_NATIVE_HOLD_ON_EMPTY: z.string().default("1"),
323+
// Max concurrent fresh ClickHouse resolves per instance (reconnect-stampede gate); 0 disables.
324+
REALTIME_BACKEND_NATIVE_RESOLVE_ADMISSION_LIMIT: z.coerce.number().int().default(16),
325+
// Fallback per-env concurrent-connection limit when the org has none configured.
326+
REALTIME_BACKEND_NATIVE_DEFAULT_CONCURRENCY_LIMIT: z.coerce.number().int().default(100_000),
327+
// TTL/size of the single-run read-through cache that collapses duplicate refetch bursts.
328+
REALTIME_BACKEND_NATIVE_RUN_CACHE_TTL_MS: z.coerce.number().int().default(250),
329+
REALTIME_BACKEND_NATIVE_RUN_CACHE_MAX_ENTRIES: z.coerce.number().int().default(5_000),
330+
// TTL/size of the per-org realtimeBackend flag cache used to pick the serving backend.
331+
REALTIME_BACKEND_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000),
332+
REALTIME_BACKEND_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(50_000),
343333

344334
PUBSUB_REDIS_HOST: z
345335
.string()
@@ -373,40 +363,35 @@ const EnvironmentSchema = z
373363
PUBSUB_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"),
374364
PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"),
375365

376-
// Dedicated pub/sub Redis for the realtime runs feed's run-changed notifier, so
377-
// its publish/subscribe traffic can run on its own instance. Each value falls
378-
// back to the shared PUBSUB_REDIS_* (then REDIS_*) when unset, so the default is
379-
// unchanged until explicitly pointed at a dedicated instance.
380-
REALTIME_RUNS_PUBSUB_REDIS_HOST: z
366+
// Dedicated pub/sub Redis for the native realtime backend; falls back to PUBSUB_REDIS_* then REDIS_*.
367+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_HOST: z
381368
.string()
382369
.optional()
383370
.transform((v) => v ?? process.env.PUBSUB_REDIS_HOST ?? process.env.REDIS_HOST),
384-
REALTIME_RUNS_PUBSUB_REDIS_PORT: z.coerce
371+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PORT: z.coerce
385372
.number()
386373
.optional()
387374
.transform((v) => {
388375
if (v !== undefined) return v;
389376
const raw = process.env.PUBSUB_REDIS_PORT ?? process.env.REDIS_PORT;
390377
return raw ? parseInt(raw) : undefined;
391378
}),
392-
REALTIME_RUNS_PUBSUB_REDIS_USERNAME: z
379+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_USERNAME: z
393380
.string()
394381
.optional()
395382
.transform((v) => v ?? process.env.PUBSUB_REDIS_USERNAME ?? process.env.REDIS_USERNAME),
396-
REALTIME_RUNS_PUBSUB_REDIS_PASSWORD: z
383+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PASSWORD: z
397384
.string()
398385
.optional()
399386
.transform((v) => v ?? process.env.PUBSUB_REDIS_PASSWORD ?? process.env.REDIS_PASSWORD),
400-
REALTIME_RUNS_PUBSUB_REDIS_TLS_DISABLED: z
387+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_TLS_DISABLED: z
401388
.string()
402389
.default(process.env.PUBSUB_REDIS_TLS_DISABLED ?? process.env.REDIS_TLS_DISABLED ?? "false"),
403-
REALTIME_RUNS_PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z
390+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z
404391
.string()
405392
.default(process.env.PUBSUB_REDIS_CLUSTER_MODE_ENABLED ?? "0"),
406-
// Use sharded pub/sub (SSUBSCRIBE/SPUBLISH) when in cluster mode, so a busy env's
407-
// traffic stays on one shard instead of broadcasting to every node. Only takes
408-
// effect alongside CLUSTER_MODE_ENABLED. "0" forces classic pub/sub on the cluster.
409-
REALTIME_RUNS_PUBSUB_REDIS_SHARDED_ENABLED: z.string().default("1"),
393+
// Use sharded pub/sub (SSUBSCRIBE/SPUBLISH) in cluster mode; "0" forces classic pub/sub.
394+
REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_SHARDED_ENABLED: z.string().default("1"),
410395

411396
DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100),
412397
DEFAULT_ENV_EXECUTION_CONCURRENCY_BURST_FACTOR: z.coerce.number().default(1.0),
@@ -1684,20 +1669,18 @@ const EnvironmentSchema = z
16841669
.enum(["log", "error", "warn", "info", "debug"])
16851670
.default("info"),
16861671
RUN_ENGINE_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
1687-
// ClickHouse client used by the realtime runs feed for tag/batch id resolution.
1688-
// Kept on its own URL + pool so the feed's reads can't contend with the main
1689-
// analytics client (CLICKHOUSE_URL). Falls back to the main URL when unset.
1690-
REALTIME_RUNS_CLICKHOUSE_URL: z
1672+
// Dedicated ClickHouse pool for the native backend's tag/batch id resolution; falls back to CLICKHOUSE_URL.
1673+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL: z
16911674
.string()
16921675
.optional()
16931676
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
1694-
REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
1695-
REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
1696-
REALTIME_RUNS_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
1697-
REALTIME_RUNS_CLICKHOUSE_LOG_LEVEL: z
1677+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
1678+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
1679+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
1680+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_LOG_LEVEL: z
16981681
.enum(["log", "error", "warn", "info", "debug"])
16991682
.default("info"),
1700-
REALTIME_RUNS_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
1683+
REALTIME_BACKEND_NATIVE_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
17011684
EVENTS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(1000),
17021685
EVENTS_CLICKHOUSE_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000),
17031686
METRICS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(10000),

apps/webapp/app/routes/realtime.v1.batches.$batchId.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ export const loader = createLoaderApiRoute(
3333
},
3434
},
3535
async ({ authentication, request, resource: batchRun, apiVersion }) => {
36-
// Pick the Electric proxy or the notifier-backed batch feed
37-
// per org (defaults to Electric). Both implement streamBatch.
36+
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamBatch.
3837
const client = await resolveRealtimeStreamClient(authentication.environment);
3938

4039
return client.streamBatch(

apps/webapp/app/routes/realtime.v1.runs.$runId.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ export const loader = createLoaderApiRoute(
4848
},
4949
},
5050
async ({ authentication, request, resource: run, apiVersion }) => {
51-
// Pick the Electric proxy or the notifier-backed shim per org (defaults to
52-
// Electric; controlled by REALTIME_NOTIFIER_ENABLED + the realtimeBackend
53-
// feature flag). Both implement the same streamRun contract.
51+
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamRun.
5452
const client = await resolveRealtimeStreamClient(authentication.environment);
5553

5654
return client.streamRun(
@@ -60,10 +58,7 @@ export const loader = createLoaderApiRoute(
6058
apiVersion,
6159
authentication.realtime,
6260
request.headers.get("x-trigger-electric-version") ?? undefined,
63-
// Propagate abort on client disconnect so the upstream Electric long-poll
64-
// fetch is cancelled too. Without this, undici buffers from the unconsumed
65-
// upstream response body accumulate until Electric's poll timeout, causing
66-
// steady RSS growth on api (see docs/runbooks for the H1 isolation test).
61+
// Propagate abort on client disconnect so the upstream Electric long-poll is cancelled too, else undici buffers grow RSS until the poll timeout.
6762
getRequestAbortSignal()
6863
);
6964
}

apps/webapp/app/routes/realtime.v1.runs.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@ export const loader = createLoaderApiRoute(
2525
authorization: {
2626
action: "read",
2727
resource: (_, __, searchParams) =>
28-
// Pre-RBAC, the resource was the searchParams object itself and
29-
// the legacy `checkAuthorization` iterated `Object.keys`, so a
30-
// JWT with type-level `read:tags` (no id) granted access to the
31-
// unfiltered runs stream. Including `{ type: "tags" }` here
32-
// preserves that — per-id `read:tags:<tag>` still grants only
33-
// when the filter includes that tag.
28+
// `{ type: "tags" }` preserves pre-RBAC type-level `read:tags` access to the unfiltered stream; per-id `read:tags:<tag>` still grants only when the filter includes that tag.
3429
anyResource([
3530
{ type: "runs" },
3631
{ type: "tags" },
@@ -39,8 +34,7 @@ export const loader = createLoaderApiRoute(
3934
},
4035
},
4136
async ({ searchParams, authentication, request, apiVersion }) => {
42-
// Pick the Electric proxy or the notifier-backed tag-list feed per org
43-
// (defaults to Electric). Both implement streamRuns.
37+
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamRuns.
4438
const client = await resolveRealtimeStreamClient(authentication.environment);
4539

4640
return client.streamRuns(

apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -211,33 +211,33 @@ function initializeRunEngineClickhouseClient(): ClickHouse {
211211
});
212212
}
213213

214-
/** Realtime runs feed tag/batch id resolution (`REALTIME_RUNS_CLICKHOUSE_URL`);
214+
/** Realtime runs feed tag/batch id resolution (`REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL`);
215215
* falls back to the default client if unset. */
216216
const defaultRealtimeClickhouseClient = singleton(
217217
"realtimeClickhouseClient",
218218
initializeRealtimeClickhouseClient
219219
);
220220

221221
function initializeRealtimeClickhouseClient(): ClickHouse {
222-
if (!env.REALTIME_RUNS_CLICKHOUSE_URL) {
222+
if (!env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL) {
223223
return defaultClickhouseClient;
224224
}
225225

226-
const url = new URL(env.REALTIME_RUNS_CLICKHOUSE_URL);
226+
const url = new URL(env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL);
227227
url.searchParams.delete("secure");
228228

229229
return new ClickHouse({
230230
url: url.toString(),
231231
name: "realtime-runs-clickhouse",
232232
keepAlive: {
233-
enabled: env.REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
234-
idleSocketTtl: env.REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
233+
enabled: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
234+
idleSocketTtl: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
235235
},
236-
logLevel: env.REALTIME_RUNS_CLICKHOUSE_LOG_LEVEL,
236+
logLevel: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_LOG_LEVEL,
237237
compression: {
238-
request: env.REALTIME_RUNS_CLICKHOUSE_COMPRESSION_REQUEST === "1",
238+
request: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_COMPRESSION_REQUEST === "1",
239239
},
240-
maxOpenConnections: env.REALTIME_RUNS_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
240+
maxOpenConnections: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
241241
});
242242
}
243243

@@ -366,14 +366,14 @@ function buildOrgClickhouseClient(url: string, clientType: ClientType): ClickHou
366366
url: parsed.toString(),
367367
name,
368368
keepAlive: {
369-
enabled: env.REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
370-
idleSocketTtl: env.REALTIME_RUNS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
369+
enabled: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
370+
idleSocketTtl: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
371371
},
372-
logLevel: env.REALTIME_RUNS_CLICKHOUSE_LOG_LEVEL,
372+
logLevel: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_LOG_LEVEL,
373373
compression: {
374-
request: env.REALTIME_RUNS_CLICKHOUSE_COMPRESSION_REQUEST === "1",
374+
request: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_COMPRESSION_REQUEST === "1",
375375
},
376-
maxOpenConnections: env.REALTIME_RUNS_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
376+
maxOpenConnections: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
377377
});
378378
case "standard":
379379
case "query":

apps/webapp/app/services/realtime/boundedTtlCache.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
/**
2-
* Tiny in-process bounded TTL cache shared by the realtime feeds.
3-
*
4-
* Entries expire after `ttlMs`. An expired entry is evicted when read (`get`); on
5-
* write, if the cache is at `maxEntries`, expired entries are swept and, if it's
6-
* still full (pathologically all live), the oldest insertion is dropped. Node is
7-
* single-threaded so no locking is needed. Used where a miss is cheap and
8-
* correctness-safe (read-through hydration, per-handle working sets, per-org flag
9-
* resolution).
10-
*
11-
* A stored value of `undefined` cannot be distinguished from a miss; callers that
12-
* need to cache "absence" should store an explicit sentinel (e.g. `null`).
2+
* Tiny in-process bounded TTL cache shared by the realtime feeds: entries expire after `ttlMs` (evicted on read),
3+
* and at-capacity writes sweep expired entries then drop the oldest. A stored `undefined` is indistinguishable from a miss (use `null` for absence).
134
*/
145
export class BoundedTtlCache<V> {
156
readonly #entries = new Map<string, { value: V; expiresAt: number }>();

apps/webapp/app/services/realtime/clickHouseRunListResolver.server.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,9 @@ export type ClickHouseRunListResolverOptions = {
1010
};
1111

1212
/**
13-
* Resolves the realtime tag/list filter into matching run ids via ClickHouse
14-
* `listRunIds`. Tag matching is contains-ANY (OR), the same
15-
* semantics the dashboard runs list uses. Filter-only: ids only, hydrated from
16-
* Postgres by id afterward. This keeps the realtime tag feed off the Postgres
17-
* `runTags` GIN index entirely.
18-
*
19-
* (Multi-tag subscribeToRunsWithTag is therefore OR, not the AND that Electric's
20-
* `runTags @> ARRAY[...]` shape used. Restoring AND is a follow-up: add a
21-
* `hasAll` mode to the ClickHouse runs filter and use it here.)
13+
* Resolves the realtime tag/list filter into matching run ids via ClickHouse `listRunIds` (filter-only;
14+
* rows hydrated from Postgres by id afterward). Tag matching is contains-ANY (OR) — note this differs from
15+
* Electric's `runTags @> ARRAY[...]` AND shape; restoring AND needs a `hasAll` mode on the ClickHouse filter.
2216
*/
2317
export class ClickHouseRunListResolver implements RunListResolver {
2418
constructor(private readonly options: ClickHouseRunListResolverOptions) {}

0 commit comments

Comments
 (0)