@tungthedev/streams-server 0.2.0
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/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import type { Pool, PoolClient } from "pg";
|
|
2
|
+
import type { StreamRow } from "../store/rows";
|
|
3
|
+
import type {
|
|
4
|
+
LiveTemplateActivationInput,
|
|
5
|
+
LiveTemplateActivationResult,
|
|
6
|
+
LiveTemplateIdentityRow,
|
|
7
|
+
LiveTemplateLastSeenUpdate,
|
|
8
|
+
LiveTemplateStoreRow,
|
|
9
|
+
StreamTouchStateRow,
|
|
10
|
+
TouchProcessorStore,
|
|
11
|
+
} from "../store/touch_store";
|
|
12
|
+
import type { WalReadRow } from "../store/wal_store";
|
|
13
|
+
import type { PgExecutor } from "./types";
|
|
14
|
+
|
|
15
|
+
type PostgresTouchStoreDelegates = {
|
|
16
|
+
nowMs(): bigint;
|
|
17
|
+
getStream(stream: string): Promise<StreamRow | null>;
|
|
18
|
+
ensureStream(stream: string, opts?: { contentType?: string | null; streamFlags?: number }): Promise<StreamRow>;
|
|
19
|
+
isDeleted(row: StreamRow): boolean;
|
|
20
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type LiveTemplatePgRow = {
|
|
24
|
+
stream: string;
|
|
25
|
+
template_id: string;
|
|
26
|
+
entity: string;
|
|
27
|
+
fields_json: string;
|
|
28
|
+
encodings_json: string;
|
|
29
|
+
state: string;
|
|
30
|
+
created_at_ms: string | number | bigint;
|
|
31
|
+
last_seen_at_ms: string | number | bigint;
|
|
32
|
+
inactivity_ttl_ms: string | number | bigint;
|
|
33
|
+
active_from_source_offset: string | number | bigint;
|
|
34
|
+
retired_at_ms: string | number | bigint | null;
|
|
35
|
+
retired_reason: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function pgInt(value: bigint): string {
|
|
39
|
+
return value.toString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toBigInt(value: unknown): bigint {
|
|
43
|
+
return typeof value === "bigint" ? value : BigInt(value as any);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clampNumber(value: bigint): number {
|
|
47
|
+
return value > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class PostgresTouchStore implements TouchProcessorStore {
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly pool: Pool,
|
|
53
|
+
private readonly delegates: PostgresTouchStoreDelegates
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
nowMs(): bigint {
|
|
57
|
+
return this.delegates.nowMs();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getStream(stream: string): Promise<StreamRow | null> {
|
|
61
|
+
return this.delegates.getStream(stream);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): Promise<StreamRow> {
|
|
65
|
+
const row = await this.delegates.ensureStream(stream, { contentType: opts?.contentType ?? null, streamFlags: opts?.streamFlags });
|
|
66
|
+
if (opts?.streamFlags && opts.streamFlags > 0) await this.addStreamFlags(stream, opts.streamFlags);
|
|
67
|
+
return (await this.getStream(stream)) ?? row;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async addStreamFlags(stream: string, flags: number): Promise<void> {
|
|
71
|
+
if (!Number.isFinite(flags) || flags <= 0) return;
|
|
72
|
+
await this.pool.query(
|
|
73
|
+
`UPDATE streams
|
|
74
|
+
SET stream_flags = (stream_flags | $1), updated_at_ms = $2
|
|
75
|
+
WHERE stream = $3;`,
|
|
76
|
+
[Math.floor(flags), pgInt(this.nowMs()), stream]
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
isDeleted(row: StreamRow): boolean {
|
|
81
|
+
return this.delegates.isDeleted(row);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
|
|
85
|
+
return this.delegates.readWalRange(stream, startOffset, endOffset, routingKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async activateLiveTemplates(args: {
|
|
89
|
+
stream: string;
|
|
90
|
+
templates: LiveTemplateActivationInput[];
|
|
91
|
+
maxActiveTemplatesPerStream: number;
|
|
92
|
+
maxActiveTemplatesPerEntity: number;
|
|
93
|
+
maxActivationTokens: number;
|
|
94
|
+
}): Promise<LiveTemplateActivationResult> {
|
|
95
|
+
if (args.templates.length === 0) return { activated: [], invalid: [], rateLimited: [], activationTokensUsed: 0, evicted: [] };
|
|
96
|
+
const client = await this.pool.connect();
|
|
97
|
+
try {
|
|
98
|
+
await client.query("BEGIN");
|
|
99
|
+
await client.query(`SELECT stream FROM streams WHERE stream = $1 FOR UPDATE;`, [args.stream]);
|
|
100
|
+
const activated: string[] = [];
|
|
101
|
+
const invalid: string[] = [];
|
|
102
|
+
const rateLimited: string[] = [];
|
|
103
|
+
let activationTokensUsed = 0;
|
|
104
|
+
|
|
105
|
+
const templateIds = args.templates.map((template) => template.templateId);
|
|
106
|
+
const entities = args.templates.map((template) => template.entity);
|
|
107
|
+
const fieldsJson = args.templates.map((template) => template.fieldsJson);
|
|
108
|
+
const encodingsJson = args.templates.map((template) => template.encodingsJson);
|
|
109
|
+
const nowMs = args.templates.map((template) => template.nowMs);
|
|
110
|
+
const ttlMs = args.templates.map((template) => template.inactivityTtlMs);
|
|
111
|
+
const activeFrom = args.templates.map((template) => pgInt(template.activeFromSourceOffset));
|
|
112
|
+
|
|
113
|
+
await client.query(
|
|
114
|
+
`WITH input AS (
|
|
115
|
+
SELECT *
|
|
116
|
+
FROM unnest($2::text[]) WITH ORDINALITY AS i(template_id, ord)
|
|
117
|
+
)
|
|
118
|
+
SELECT t.template_id
|
|
119
|
+
FROM live_templates AS t
|
|
120
|
+
JOIN input AS i ON i.template_id = t.template_id
|
|
121
|
+
WHERE t.stream = $1
|
|
122
|
+
FOR UPDATE OF t;`,
|
|
123
|
+
[args.stream, templateIds]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const classified = await client.query<{
|
|
127
|
+
ord: string | number | bigint;
|
|
128
|
+
template_id: string;
|
|
129
|
+
entity: string;
|
|
130
|
+
fields_json: string;
|
|
131
|
+
encodings_json: string;
|
|
132
|
+
now_ms: string | number | bigint;
|
|
133
|
+
inactivity_ttl_ms: string | number | bigint;
|
|
134
|
+
active_from_source_offset: string | number | bigint;
|
|
135
|
+
existing_entity: string | null;
|
|
136
|
+
existing_fields_json: string | null;
|
|
137
|
+
existing_encodings_json: string | null;
|
|
138
|
+
existing_state: string | null;
|
|
139
|
+
}>(
|
|
140
|
+
`WITH input AS (
|
|
141
|
+
SELECT *
|
|
142
|
+
FROM unnest($2::text[], $3::text[], $4::text[], $5::text[], $6::bigint[], $7::bigint[], $8::bigint[])
|
|
143
|
+
WITH ORDINALITY AS i(template_id, entity, fields_json, encodings_json, now_ms, inactivity_ttl_ms, active_from_source_offset, ord)
|
|
144
|
+
)
|
|
145
|
+
SELECT
|
|
146
|
+
i.ord,
|
|
147
|
+
i.template_id,
|
|
148
|
+
i.entity,
|
|
149
|
+
i.fields_json,
|
|
150
|
+
i.encodings_json,
|
|
151
|
+
i.now_ms,
|
|
152
|
+
i.inactivity_ttl_ms,
|
|
153
|
+
i.active_from_source_offset,
|
|
154
|
+
t.entity AS existing_entity,
|
|
155
|
+
t.fields_json AS existing_fields_json,
|
|
156
|
+
t.encodings_json AS existing_encodings_json,
|
|
157
|
+
t.state AS existing_state
|
|
158
|
+
FROM input AS i
|
|
159
|
+
LEFT JOIN live_templates AS t ON t.stream = $1 AND t.template_id = i.template_id
|
|
160
|
+
ORDER BY i.ord ASC;`,
|
|
161
|
+
[args.stream, templateIds, entities, fieldsJson, encodingsJson, nowMs, ttlMs, activeFrom]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const accepted: typeof classified.rows = [];
|
|
165
|
+
for (const row of classified.rows) {
|
|
166
|
+
const hasExisting = row.existing_state != null;
|
|
167
|
+
if (
|
|
168
|
+
hasExisting &&
|
|
169
|
+
(row.existing_entity !== row.entity || row.existing_fields_json !== row.fields_json || row.existing_encodings_json !== row.encodings_json)
|
|
170
|
+
) {
|
|
171
|
+
invalid.push(row.template_id);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const alreadyActive = row.existing_state === "active";
|
|
175
|
+
if (!alreadyActive && activationTokensUsed >= args.maxActivationTokens) {
|
|
176
|
+
rateLimited.push(row.template_id);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!alreadyActive) activationTokensUsed += 1;
|
|
180
|
+
accepted.push(row);
|
|
181
|
+
activated.push(row.template_id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (accepted.length > 0) {
|
|
185
|
+
await client.query(
|
|
186
|
+
`INSERT INTO live_templates(
|
|
187
|
+
stream, template_id, entity, fields_json, encodings_json,
|
|
188
|
+
state, created_at_ms, last_seen_at_ms, inactivity_ttl_ms, active_from_source_offset,
|
|
189
|
+
retired_at_ms, retired_reason
|
|
190
|
+
)
|
|
191
|
+
SELECT $1, u.template_id, u.entity, u.fields_json, u.encodings_json,
|
|
192
|
+
'active', u.now_ms, u.now_ms, u.inactivity_ttl_ms, u.active_from_source_offset,
|
|
193
|
+
NULL, NULL
|
|
194
|
+
FROM unnest($2::text[], $3::text[], $4::text[], $5::text[], $6::bigint[], $7::bigint[], $8::bigint[])
|
|
195
|
+
AS u(template_id, entity, fields_json, encodings_json, now_ms, inactivity_ttl_ms, active_from_source_offset)
|
|
196
|
+
ON CONFLICT(stream, template_id) DO UPDATE SET
|
|
197
|
+
state = 'active',
|
|
198
|
+
last_seen_at_ms = excluded.last_seen_at_ms,
|
|
199
|
+
inactivity_ttl_ms = excluded.inactivity_ttl_ms,
|
|
200
|
+
active_from_source_offset = CASE
|
|
201
|
+
WHEN live_templates.state = 'active' THEN live_templates.active_from_source_offset
|
|
202
|
+
ELSE excluded.active_from_source_offset
|
|
203
|
+
END,
|
|
204
|
+
retired_at_ms = NULL,
|
|
205
|
+
retired_reason = NULL;`,
|
|
206
|
+
[
|
|
207
|
+
args.stream,
|
|
208
|
+
accepted.map((row) => row.template_id),
|
|
209
|
+
accepted.map((row) => row.entity),
|
|
210
|
+
accepted.map((row) => row.fields_json),
|
|
211
|
+
accepted.map((row) => row.encodings_json),
|
|
212
|
+
accepted.map((row) => toBigInt(row.now_ms).toString()),
|
|
213
|
+
accepted.map((row) => toBigInt(row.inactivity_ttl_ms).toString()),
|
|
214
|
+
accepted.map((row) => toBigInt(row.active_from_source_offset).toString()),
|
|
215
|
+
]
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const evicted = await this.evictToCapsInTransaction(client, {
|
|
219
|
+
stream: args.stream,
|
|
220
|
+
protectedIds: new Set(activated),
|
|
221
|
+
maxActiveTemplatesPerStream: args.maxActiveTemplatesPerStream,
|
|
222
|
+
maxActiveTemplatesPerEntity: args.maxActiveTemplatesPerEntity,
|
|
223
|
+
nowMs: args.templates.reduce((max, template) => Math.max(max, template.nowMs), 0),
|
|
224
|
+
});
|
|
225
|
+
await client.query("COMMIT");
|
|
226
|
+
return { activated, invalid, rateLimited, activationTokensUsed, evicted };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
229
|
+
throw error;
|
|
230
|
+
} finally {
|
|
231
|
+
client.release();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async ensureStreamTouchState(stream: string): Promise<void> {
|
|
236
|
+
const srow = await this.getStream(stream);
|
|
237
|
+
const initialThrough = srow ? srow.next_offset - 1n : -1n;
|
|
238
|
+
await ensurePostgresStreamTouchState(this.pool, this.nowMs(), stream, initialThrough);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async deleteStreamTouchState(stream: string): Promise<void> {
|
|
242
|
+
await deletePostgresStreamTouchState(this.pool, stream);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async getStreamTouchState(stream: string): Promise<StreamTouchStateRow | null> {
|
|
246
|
+
const res = await this.pool.query<{ stream: string; processed_through: string | number | bigint; updated_at_ms: string | number | bigint }>(
|
|
247
|
+
`SELECT stream, processed_through, updated_at_ms
|
|
248
|
+
FROM stream_touch_state
|
|
249
|
+
WHERE stream = $1
|
|
250
|
+
LIMIT 1;`,
|
|
251
|
+
[stream]
|
|
252
|
+
);
|
|
253
|
+
const row = res.rows[0];
|
|
254
|
+
return row
|
|
255
|
+
? {
|
|
256
|
+
stream: row.stream,
|
|
257
|
+
processed_through: toBigInt(row.processed_through),
|
|
258
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
259
|
+
}
|
|
260
|
+
: null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async listStreamTouchStates(): Promise<StreamTouchStateRow[]> {
|
|
264
|
+
const res = await this.pool.query<{ stream: string; processed_through: string | number | bigint; updated_at_ms: string | number | bigint }>(
|
|
265
|
+
`SELECT stream, processed_through, updated_at_ms
|
|
266
|
+
FROM stream_touch_state
|
|
267
|
+
ORDER BY stream ASC;`
|
|
268
|
+
);
|
|
269
|
+
return res.rows.map((row) => ({
|
|
270
|
+
stream: row.stream,
|
|
271
|
+
processed_through: toBigInt(row.processed_through),
|
|
272
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async listStreamsByProfile(kind: string): Promise<string[]> {
|
|
277
|
+
const res = await this.pool.query<{ stream: string }>(
|
|
278
|
+
`SELECT stream FROM streams
|
|
279
|
+
WHERE profile = $1
|
|
280
|
+
AND (stream_flags & 1) = 0
|
|
281
|
+
ORDER BY stream ASC;`,
|
|
282
|
+
[kind]
|
|
283
|
+
);
|
|
284
|
+
return res.rows.map((row) => row.stream);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async updateStreamTouchStateThrough(stream: string, processedThrough: bigint): Promise<void> {
|
|
288
|
+
await this.pool.query(
|
|
289
|
+
`INSERT INTO stream_touch_state(stream, processed_through, updated_at_ms)
|
|
290
|
+
VALUES($1, $2, $3)
|
|
291
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
292
|
+
processed_through = excluded.processed_through,
|
|
293
|
+
updated_at_ms = excluded.updated_at_ms;`,
|
|
294
|
+
[stream, pgInt(processedThrough), pgInt(this.nowMs())]
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async deleteWalThrough(stream: string, uploadedThrough: bigint): Promise<{ deletedRows: number; deletedBytes: number }> {
|
|
299
|
+
const res = await this.pool.query<{ deleted_rows: string | number | bigint; deleted_bytes: string | number | bigint }>(
|
|
300
|
+
`WITH deleted AS (
|
|
301
|
+
DELETE FROM wal
|
|
302
|
+
WHERE stream = $1 AND "offset" <= $2
|
|
303
|
+
RETURNING payload_len
|
|
304
|
+
),
|
|
305
|
+
totals AS (
|
|
306
|
+
SELECT COUNT(*)::bigint AS deleted_rows, COALESCE(SUM(payload_len), 0)::bigint AS deleted_bytes FROM deleted
|
|
307
|
+
),
|
|
308
|
+
updated AS (
|
|
309
|
+
UPDATE streams
|
|
310
|
+
SET wal_rows = GREATEST(0, wal_rows - totals.deleted_rows),
|
|
311
|
+
wal_bytes = GREATEST(0, wal_bytes - totals.deleted_bytes),
|
|
312
|
+
updated_at_ms = $3
|
|
313
|
+
FROM totals
|
|
314
|
+
WHERE stream = $1
|
|
315
|
+
RETURNING totals.deleted_rows, totals.deleted_bytes
|
|
316
|
+
)
|
|
317
|
+
SELECT deleted_rows, deleted_bytes FROM updated
|
|
318
|
+
UNION ALL
|
|
319
|
+
SELECT deleted_rows, deleted_bytes FROM totals
|
|
320
|
+
WHERE NOT EXISTS (SELECT 1 FROM updated)
|
|
321
|
+
LIMIT 1;`,
|
|
322
|
+
[stream, pgInt(uploadedThrough), pgInt(this.nowMs())]
|
|
323
|
+
);
|
|
324
|
+
const row = res.rows[0];
|
|
325
|
+
const deletedRows = row ? toBigInt(row.deleted_rows) : 0n;
|
|
326
|
+
const deletedBytes = row ? toBigInt(row.deleted_bytes) : 0n;
|
|
327
|
+
return { deletedRows: clampNumber(deletedRows), deletedBytes: clampNumber(deletedBytes) };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async getWalOldestOffset(stream: string): Promise<bigint | null> {
|
|
331
|
+
const res = await this.pool.query<{ min_off: string | number | bigint | null }>(
|
|
332
|
+
`SELECT MIN("offset") AS min_off FROM wal WHERE stream = $1;`,
|
|
333
|
+
[stream]
|
|
334
|
+
);
|
|
335
|
+
const value = res.rows[0]?.min_off;
|
|
336
|
+
return value == null ? null : toBigInt(value);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async trimWalByAge(stream: string, maxAgeMs: number): Promise<{ trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null }> {
|
|
340
|
+
const ageMs = Math.max(0, Math.floor(maxAgeMs));
|
|
341
|
+
if (!Number.isFinite(ageMs)) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: null };
|
|
342
|
+
|
|
343
|
+
const lastRes = await this.pool.query<{ offset: string | number | bigint }>(
|
|
344
|
+
`SELECT "offset" FROM wal WHERE stream = $1 ORDER BY "offset" DESC LIMIT 1;`,
|
|
345
|
+
[stream]
|
|
346
|
+
);
|
|
347
|
+
const lastOffsetRaw = lastRes.rows[0]?.offset;
|
|
348
|
+
if (lastOffsetRaw == null) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: null };
|
|
349
|
+
const lastOffset = toBigInt(lastOffsetRaw);
|
|
350
|
+
|
|
351
|
+
let keepFromOffset: bigint;
|
|
352
|
+
if (ageMs === 0) {
|
|
353
|
+
keepFromOffset = lastOffset;
|
|
354
|
+
} else {
|
|
355
|
+
const cutoff = this.nowMs() - BigInt(ageMs);
|
|
356
|
+
const keepRes = await this.pool.query<{ offset: string | number | bigint }>(
|
|
357
|
+
`SELECT "offset" FROM wal
|
|
358
|
+
WHERE stream = $1 AND ts_ms >= $2
|
|
359
|
+
ORDER BY "offset" ASC
|
|
360
|
+
LIMIT 1;`,
|
|
361
|
+
[stream, pgInt(cutoff)]
|
|
362
|
+
);
|
|
363
|
+
keepFromOffset = keepRes.rows[0]?.offset == null ? lastOffset : toBigInt(keepRes.rows[0]!.offset);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (keepFromOffset <= 0n) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
|
|
367
|
+
const deleted = await this.deleteWalThrough(stream, keepFromOffset - 1n);
|
|
368
|
+
if (deleted.deletedRows <= 0) return { trimmedRows: 0, trimmedBytes: 0, keptFromOffset: keepFromOffset };
|
|
369
|
+
await this.pool.query(
|
|
370
|
+
`UPDATE streams
|
|
371
|
+
SET pending_rows = GREATEST(0, pending_rows - $1::bigint),
|
|
372
|
+
pending_bytes = GREATEST(0, pending_bytes - $2::bigint),
|
|
373
|
+
updated_at_ms = $3
|
|
374
|
+
WHERE stream = $4;`,
|
|
375
|
+
[deleted.deletedRows, deleted.deletedBytes, pgInt(this.nowMs()), stream]
|
|
376
|
+
);
|
|
377
|
+
return { trimmedRows: deleted.deletedRows, trimmedBytes: deleted.deletedBytes, keptFromOffset: keepFromOffset };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async countActiveLiveTemplates(stream: string): Promise<number> {
|
|
381
|
+
const res = await this.pool.query<{ cnt: string | number | bigint }>(
|
|
382
|
+
`SELECT COUNT(*) AS cnt FROM live_templates WHERE stream = $1 AND state = 'active';`,
|
|
383
|
+
[stream]
|
|
384
|
+
);
|
|
385
|
+
return Number(res.rows[0]?.cnt ?? 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async listActiveLiveTemplates(stream: string): Promise<LiveTemplateStoreRow[]> {
|
|
389
|
+
const res = await this.pool.query<LiveTemplatePgRow>(
|
|
390
|
+
`SELECT stream, template_id, entity, fields_json, encodings_json, state, created_at_ms, last_seen_at_ms,
|
|
391
|
+
inactivity_ttl_ms, active_from_source_offset, retired_at_ms, retired_reason
|
|
392
|
+
FROM live_templates
|
|
393
|
+
WHERE stream = $1 AND state = 'active'
|
|
394
|
+
ORDER BY entity ASC, template_id ASC;`,
|
|
395
|
+
[stream]
|
|
396
|
+
);
|
|
397
|
+
return res.rows.map(coerceLiveTemplateRow);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async updateLiveTemplateLastSeenBatch(updates: LiveTemplateLastSeenUpdate[]): Promise<void> {
|
|
401
|
+
if (updates.length === 0) return;
|
|
402
|
+
await this.pool.query(
|
|
403
|
+
`UPDATE live_templates AS t
|
|
404
|
+
SET last_seen_at_ms = GREATEST(t.last_seen_at_ms, v.last_seen_at_ms)
|
|
405
|
+
FROM (
|
|
406
|
+
SELECT *
|
|
407
|
+
FROM unnest($1::text[], $2::text[], $3::bigint[]) AS u(stream, template_id, last_seen_at_ms)
|
|
408
|
+
) AS v
|
|
409
|
+
WHERE t.stream = v.stream
|
|
410
|
+
AND t.template_id = v.template_id
|
|
411
|
+
AND t.state = 'active';`,
|
|
412
|
+
[updates.map((update) => update.stream), updates.map((update) => update.templateId), updates.map((update) => update.lastSeenAtMs)]
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async listExpiredLiveTemplates(stream: string, nowMs: number, limit: number): Promise<LiveTemplateIdentityRow[]> {
|
|
417
|
+
const res = await this.pool.query<{
|
|
418
|
+
template_id: string;
|
|
419
|
+
entity: string;
|
|
420
|
+
fields_json: string;
|
|
421
|
+
encodings_json: string;
|
|
422
|
+
last_seen_at_ms: string | number | bigint;
|
|
423
|
+
inactivity_ttl_ms: string | number | bigint;
|
|
424
|
+
}>(
|
|
425
|
+
`SELECT template_id, entity, fields_json, encodings_json, last_seen_at_ms, inactivity_ttl_ms
|
|
426
|
+
FROM live_templates
|
|
427
|
+
WHERE stream = $1 AND state = 'active' AND (last_seen_at_ms + inactivity_ttl_ms) < $2
|
|
428
|
+
ORDER BY last_seen_at_ms ASC
|
|
429
|
+
LIMIT $3;`,
|
|
430
|
+
[stream, nowMs, Math.max(1, Math.floor(limit))]
|
|
431
|
+
);
|
|
432
|
+
return res.rows.map((row) => ({
|
|
433
|
+
template_id: row.template_id,
|
|
434
|
+
entity: row.entity,
|
|
435
|
+
fields_json: row.fields_json,
|
|
436
|
+
encodings_json: row.encodings_json,
|
|
437
|
+
last_seen_at_ms: toBigInt(row.last_seen_at_ms),
|
|
438
|
+
inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async retireLiveTemplatesForInactivity(stream: string, templateIds: string[], nowMs: number): Promise<void> {
|
|
443
|
+
if (templateIds.length === 0) return;
|
|
444
|
+
await this.pool.query(
|
|
445
|
+
`UPDATE live_templates
|
|
446
|
+
SET state = 'retired', retired_reason = 'inactivity', retired_at_ms = $1
|
|
447
|
+
WHERE stream = $2 AND state = 'active' AND template_id = ANY($3::text[]);`,
|
|
448
|
+
[nowMs, stream, templateIds]
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async listActiveLiveTemplateEntitiesByIds(stream: string, templateIds: string[]): Promise<string[]> {
|
|
453
|
+
if (templateIds.length === 0) return [];
|
|
454
|
+
const res = await this.pool.query<{ entity: string }>(
|
|
455
|
+
`SELECT DISTINCT entity
|
|
456
|
+
FROM live_templates
|
|
457
|
+
WHERE stream = $1 AND state = 'active' AND template_id = ANY($2::text[]);`,
|
|
458
|
+
[stream, templateIds]
|
|
459
|
+
);
|
|
460
|
+
return res.rows.map((row) => row.entity.trim()).filter((entity) => entity !== "");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private async evictToCapsInTransaction(
|
|
464
|
+
client: PoolClient,
|
|
465
|
+
args: {
|
|
466
|
+
stream: string;
|
|
467
|
+
protectedIds: Set<string>;
|
|
468
|
+
maxActiveTemplatesPerStream: number;
|
|
469
|
+
maxActiveTemplatesPerEntity: number;
|
|
470
|
+
nowMs: number;
|
|
471
|
+
}
|
|
472
|
+
): Promise<Array<{ templateId: string; reason: "cap_exceeded"; cap: number }>> {
|
|
473
|
+
const evicted: Array<{ templateId: string; reason: "cap_exceeded"; cap: number }> = [];
|
|
474
|
+
const protectedIds = Array.from(args.protectedIds);
|
|
475
|
+
const entityRes = await client.query<{ template_id: string }>(
|
|
476
|
+
`WITH ranked AS (
|
|
477
|
+
SELECT
|
|
478
|
+
template_id,
|
|
479
|
+
COUNT(*) OVER (PARTITION BY entity) AS total_count,
|
|
480
|
+
ROW_NUMBER() OVER (
|
|
481
|
+
PARTITION BY entity
|
|
482
|
+
ORDER BY
|
|
483
|
+
CASE WHEN template_id = ANY($4::text[]) THEN 1 ELSE 0 END ASC,
|
|
484
|
+
last_seen_at_ms ASC,
|
|
485
|
+
template_id ASC
|
|
486
|
+
) AS rn
|
|
487
|
+
FROM live_templates
|
|
488
|
+
WHERE stream = $1 AND state = 'active'
|
|
489
|
+
),
|
|
490
|
+
selected AS (
|
|
491
|
+
SELECT template_id
|
|
492
|
+
FROM ranked
|
|
493
|
+
WHERE rn <= GREATEST(total_count - $2, 0)
|
|
494
|
+
),
|
|
495
|
+
updated AS (
|
|
496
|
+
UPDATE live_templates AS t
|
|
497
|
+
SET state = 'retired', retired_reason = 'cap_exceeded', retired_at_ms = $3
|
|
498
|
+
FROM selected
|
|
499
|
+
WHERE t.stream = $1
|
|
500
|
+
AND t.state = 'active'
|
|
501
|
+
AND t.template_id = selected.template_id
|
|
502
|
+
RETURNING t.template_id
|
|
503
|
+
)
|
|
504
|
+
SELECT template_id FROM updated
|
|
505
|
+
ORDER BY template_id ASC;`,
|
|
506
|
+
[args.stream, args.maxActiveTemplatesPerEntity, args.nowMs, protectedIds]
|
|
507
|
+
);
|
|
508
|
+
for (const row of entityRes.rows) evicted.push({ templateId: row.template_id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerEntity });
|
|
509
|
+
|
|
510
|
+
const streamRes = await client.query<{ template_id: string }>(
|
|
511
|
+
`WITH ranked AS (
|
|
512
|
+
SELECT
|
|
513
|
+
template_id,
|
|
514
|
+
COUNT(*) OVER () AS total_count,
|
|
515
|
+
ROW_NUMBER() OVER (
|
|
516
|
+
ORDER BY
|
|
517
|
+
CASE WHEN template_id = ANY($4::text[]) THEN 1 ELSE 0 END ASC,
|
|
518
|
+
last_seen_at_ms ASC,
|
|
519
|
+
template_id ASC
|
|
520
|
+
) AS rn
|
|
521
|
+
FROM live_templates
|
|
522
|
+
WHERE stream = $1 AND state = 'active'
|
|
523
|
+
),
|
|
524
|
+
selected AS (
|
|
525
|
+
SELECT template_id
|
|
526
|
+
FROM ranked
|
|
527
|
+
WHERE rn <= GREATEST(total_count - $2, 0)
|
|
528
|
+
),
|
|
529
|
+
updated AS (
|
|
530
|
+
UPDATE live_templates AS t
|
|
531
|
+
SET state = 'retired', retired_reason = 'cap_exceeded', retired_at_ms = $3
|
|
532
|
+
FROM selected
|
|
533
|
+
WHERE t.stream = $1
|
|
534
|
+
AND t.state = 'active'
|
|
535
|
+
AND t.template_id = selected.template_id
|
|
536
|
+
RETURNING t.template_id
|
|
537
|
+
)
|
|
538
|
+
SELECT template_id FROM updated
|
|
539
|
+
ORDER BY template_id ASC;`,
|
|
540
|
+
[args.stream, args.maxActiveTemplatesPerStream, args.nowMs, protectedIds]
|
|
541
|
+
);
|
|
542
|
+
for (const row of streamRes.rows) evicted.push({ templateId: row.template_id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerStream });
|
|
543
|
+
return evicted;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export async function ensurePostgresStreamTouchState(
|
|
548
|
+
executor: PgExecutor,
|
|
549
|
+
nowMs: bigint,
|
|
550
|
+
stream: string,
|
|
551
|
+
initialThrough: bigint
|
|
552
|
+
): Promise<void> {
|
|
553
|
+
await executor.query(
|
|
554
|
+
`INSERT INTO stream_touch_state(stream, processed_through, updated_at_ms)
|
|
555
|
+
VALUES($1, $2, $3)
|
|
556
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
557
|
+
processed_through = stream_touch_state.processed_through,
|
|
558
|
+
updated_at_ms = stream_touch_state.updated_at_ms;`,
|
|
559
|
+
[stream, pgInt(initialThrough), pgInt(nowMs)]
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function ensurePostgresStreamTouchStateFromStream(executor: PgExecutor, nowMs: bigint, stream: string): Promise<void> {
|
|
564
|
+
const res = await executor.query<{ next_offset: string | number | bigint }>(
|
|
565
|
+
`SELECT next_offset FROM streams WHERE stream = $1 LIMIT 1;`,
|
|
566
|
+
[stream]
|
|
567
|
+
);
|
|
568
|
+
const nextOffset = toBigInt(res.rows[0]?.next_offset ?? 0);
|
|
569
|
+
await ensurePostgresStreamTouchState(executor, nowMs, stream, nextOffset - 1n);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function deletePostgresStreamTouchState(executor: PgExecutor, stream: string): Promise<void> {
|
|
573
|
+
await executor.query(`DELETE FROM stream_touch_state WHERE stream = $1;`, [stream]);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function coerceLiveTemplateRow(row: LiveTemplatePgRow): LiveTemplateStoreRow {
|
|
577
|
+
return {
|
|
578
|
+
stream: row.stream,
|
|
579
|
+
template_id: row.template_id,
|
|
580
|
+
entity: row.entity,
|
|
581
|
+
fields_json: row.fields_json,
|
|
582
|
+
encodings_json: row.encodings_json,
|
|
583
|
+
state: row.state,
|
|
584
|
+
created_at_ms: toBigInt(row.created_at_ms),
|
|
585
|
+
last_seen_at_ms: toBigInt(row.last_seen_at_ms),
|
|
586
|
+
inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
|
|
587
|
+
active_from_source_offset: toBigInt(row.active_from_source_offset),
|
|
588
|
+
retired_at_ms: row.retired_at_ms == null ? null : toBigInt(row.retired_at_ms),
|
|
589
|
+
retired_reason: row.retired_reason == null ? null : row.retired_reason,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PoolClient } from "pg";
|
|
2
|
+
|
|
3
|
+
export type PgExecutor = Pick<PoolClient, "query">;
|
|
4
|
+
|
|
5
|
+
export type PgStreamRow = {
|
|
6
|
+
stream: string;
|
|
7
|
+
created_at_ms: string | number | bigint;
|
|
8
|
+
updated_at_ms: string | number | bigint;
|
|
9
|
+
content_type: string;
|
|
10
|
+
profile: string | null;
|
|
11
|
+
stream_seq: string | null;
|
|
12
|
+
closed: number | boolean;
|
|
13
|
+
closed_producer_id: string | null;
|
|
14
|
+
closed_producer_epoch: number | null;
|
|
15
|
+
closed_producer_seq: number | null;
|
|
16
|
+
ttl_seconds: number | null;
|
|
17
|
+
epoch: number;
|
|
18
|
+
next_offset: string | number | bigint;
|
|
19
|
+
sealed_through?: string | number | bigint;
|
|
20
|
+
uploaded_through?: string | number | bigint;
|
|
21
|
+
uploaded_segment_count?: number | string;
|
|
22
|
+
pending_rows?: string | number | bigint;
|
|
23
|
+
pending_bytes?: string | number | bigint;
|
|
24
|
+
logical_size_bytes: string | number | bigint;
|
|
25
|
+
wal_rows: string | number | bigint;
|
|
26
|
+
wal_bytes: string | number | bigint;
|
|
27
|
+
last_append_ms: string | number | bigint;
|
|
28
|
+
last_segment_cut_ms?: string | number | bigint;
|
|
29
|
+
segment_in_progress?: number | string;
|
|
30
|
+
expires_at_ms: string | number | bigint | null;
|
|
31
|
+
stream_flags: number;
|
|
32
|
+
};
|