@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,81 @@
|
|
|
1
|
+
import type { SqliteDurableStore } from "./db";
|
|
2
|
+
import type { ManifestPublicationSnapshot } from "../store/segment_manifest_store";
|
|
3
|
+
import { dsError } from "../util/ds_error.ts";
|
|
4
|
+
import { readU64LE } from "../util/endian";
|
|
5
|
+
|
|
6
|
+
export function loadSqliteManifestPublicationSnapshot(db: SqliteDurableStore, stream: string): ManifestPublicationSnapshot | null {
|
|
7
|
+
const streamRow = db.getStream(stream);
|
|
8
|
+
if (!streamRow) return null;
|
|
9
|
+
|
|
10
|
+
const prevUploadedSegmentCount = streamRow.uploaded_segment_count ?? 0;
|
|
11
|
+
let uploadedPrefixCount = db.advanceUploadedSegmentCount(stream);
|
|
12
|
+
|
|
13
|
+
const segmentCount = db.countSegmentsForStream(stream);
|
|
14
|
+
let segmentMeta = db.getSegmentMeta(stream);
|
|
15
|
+
const needsRebuild =
|
|
16
|
+
!segmentMeta ||
|
|
17
|
+
segmentMeta.segment_count !== segmentCount ||
|
|
18
|
+
segmentMeta.segment_offsets.byteLength !== segmentCount * 8 ||
|
|
19
|
+
segmentMeta.segment_blocks.byteLength !== segmentCount * 4 ||
|
|
20
|
+
segmentMeta.segment_last_ts.byteLength !== segmentCount * 8;
|
|
21
|
+
if (needsRebuild) {
|
|
22
|
+
segmentMeta = db.rebuildSegmentMeta(stream);
|
|
23
|
+
}
|
|
24
|
+
if (!segmentMeta) return null;
|
|
25
|
+
if (uploadedPrefixCount > segmentMeta.segment_count) {
|
|
26
|
+
uploadedPrefixCount = segmentMeta.segment_count;
|
|
27
|
+
db.setUploadedSegmentCount(stream, uploadedPrefixCount);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const uploadedThrough =
|
|
31
|
+
uploadedPrefixCount === 0 ? -1n : readU64LE(segmentMeta.segment_offsets, (uploadedPrefixCount - 1) * 8) - 1n;
|
|
32
|
+
const unpublishedWalBytes = db.getWalBytesAfterOffset(stream, uploadedThrough);
|
|
33
|
+
const publishedLogicalSizeBytes =
|
|
34
|
+
streamRow.logical_size_bytes > unpublishedWalBytes ? streamRow.logical_size_bytes - unpublishedWalBytes : 0n;
|
|
35
|
+
|
|
36
|
+
const manifestRow = db.getManifestRow(stream);
|
|
37
|
+
const secondaryIndexStates = db.listSecondaryIndexStates(stream);
|
|
38
|
+
const secondaryIndexRuns = secondaryIndexStates.flatMap((state) => db.listSecondaryIndexRuns(stream, state.index_name));
|
|
39
|
+
const retiredSecondaryIndexRuns = secondaryIndexStates.flatMap((state) =>
|
|
40
|
+
db.listRetiredSecondaryIndexRuns(stream, state.index_name)
|
|
41
|
+
);
|
|
42
|
+
const lexiconIndexStates = db.listLexiconIndexStates(stream);
|
|
43
|
+
const lexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
|
|
44
|
+
db.listLexiconIndexRuns(stream, state.source_kind, state.source_name)
|
|
45
|
+
);
|
|
46
|
+
const retiredLexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
|
|
47
|
+
db.listRetiredLexiconIndexRuns(stream, state.source_kind, state.source_name)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let profileJson: Record<string, any> | null = null;
|
|
51
|
+
const profileRow = db.getStreamProfile(stream);
|
|
52
|
+
if (profileRow) {
|
|
53
|
+
try {
|
|
54
|
+
profileJson = JSON.parse(profileRow.profile_json);
|
|
55
|
+
} catch {
|
|
56
|
+
throw dsError(`invalid profile_json for ${stream}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
streamRow,
|
|
62
|
+
prevUploadedSegmentCount,
|
|
63
|
+
uploadedPrefixCount,
|
|
64
|
+
uploadedThrough,
|
|
65
|
+
publishedLogicalSizeBytes,
|
|
66
|
+
generation: manifestRow.generation + 1,
|
|
67
|
+
segmentMeta,
|
|
68
|
+
profileJson,
|
|
69
|
+
indexState: db.getIndexState(stream),
|
|
70
|
+
indexRuns: db.listIndexRuns(stream),
|
|
71
|
+
retiredRuns: db.listRetiredIndexRuns(stream),
|
|
72
|
+
secondaryIndexStates,
|
|
73
|
+
secondaryIndexRuns,
|
|
74
|
+
retiredSecondaryIndexRuns,
|
|
75
|
+
lexiconIndexStates,
|
|
76
|
+
lexiconIndexRuns,
|
|
77
|
+
retiredLexiconIndexRuns,
|
|
78
|
+
searchCompanionPlan: db.getSearchCompanionPlan(stream),
|
|
79
|
+
searchSegmentCompanions: db.listSearchSegmentCompanions(stream),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import type { SqliteDatabase, SqliteStatement } from "../sqlite/adapter";
|
|
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
|
+
|
|
14
|
+
type SqliteTouchStoreDelegates = {
|
|
15
|
+
nowMs(): bigint;
|
|
16
|
+
getStream(stream: string): StreamRow | null;
|
|
17
|
+
ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): StreamRow;
|
|
18
|
+
addStreamFlags(stream: string, flags: number): void;
|
|
19
|
+
isDeleted(row: StreamRow): boolean;
|
|
20
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
21
|
+
deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number };
|
|
22
|
+
getWalOldestOffset(stream: string): bigint | null;
|
|
23
|
+
trimWalByAge(stream: string, maxAgeMs: number): { trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function toBigInt(v: unknown): bigint {
|
|
27
|
+
return typeof v === "bigint" ? v : BigInt(v as any);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bindInt(v: bigint): number | string {
|
|
31
|
+
const max = BigInt(Number.MAX_SAFE_INTEGER);
|
|
32
|
+
const min = BigInt(Number.MIN_SAFE_INTEGER);
|
|
33
|
+
if (v <= max && v >= min) return Number(v);
|
|
34
|
+
return v.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SqliteTouchStore implements TouchProcessorStore {
|
|
38
|
+
private readonly stmts: {
|
|
39
|
+
getStreamTouchState: ReturnType<SqliteDatabase["query"]>;
|
|
40
|
+
upsertStreamTouchState: ReturnType<SqliteDatabase["query"]>;
|
|
41
|
+
deleteStreamTouchState: ReturnType<SqliteDatabase["query"]>;
|
|
42
|
+
listStreamTouchStates: ReturnType<SqliteDatabase["query"]>;
|
|
43
|
+
listStreamsByProfile: ReturnType<SqliteDatabase["query"]>;
|
|
44
|
+
countActiveLiveTemplates: SqliteStatement;
|
|
45
|
+
listActiveLiveTemplates: SqliteStatement;
|
|
46
|
+
getLiveTemplate: SqliteStatement;
|
|
47
|
+
updateLiveTemplateHeartbeat: SqliteStatement;
|
|
48
|
+
reactivateLiveTemplate: SqliteStatement;
|
|
49
|
+
insertLiveTemplate: SqliteStatement;
|
|
50
|
+
updateLiveTemplateLastSeen: SqliteStatement;
|
|
51
|
+
listExpiredLiveTemplates: SqliteStatement;
|
|
52
|
+
retireLiveTemplateForInactivity: SqliteStatement;
|
|
53
|
+
listActiveLiveTemplateEntityCounts: SqliteStatement;
|
|
54
|
+
retireLiveTemplateForCap: SqliteStatement;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private readonly db: SqliteDatabase,
|
|
59
|
+
private readonly delegates: SqliteTouchStoreDelegates
|
|
60
|
+
) {
|
|
61
|
+
this.stmts = {
|
|
62
|
+
getStreamTouchState: this.db.query(
|
|
63
|
+
`SELECT stream, processed_through, updated_at_ms
|
|
64
|
+
FROM stream_touch_state WHERE stream=? LIMIT 1;`
|
|
65
|
+
),
|
|
66
|
+
upsertStreamTouchState: this.db.query(
|
|
67
|
+
`INSERT INTO stream_touch_state(stream, processed_through, updated_at_ms)
|
|
68
|
+
VALUES(?, ?, ?)
|
|
69
|
+
ON CONFLICT(stream) DO UPDATE SET
|
|
70
|
+
processed_through=excluded.processed_through,
|
|
71
|
+
updated_at_ms=excluded.updated_at_ms;`
|
|
72
|
+
),
|
|
73
|
+
deleteStreamTouchState: this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`),
|
|
74
|
+
listStreamTouchStates: this.db.query(
|
|
75
|
+
`SELECT stream, processed_through, updated_at_ms
|
|
76
|
+
FROM stream_touch_state
|
|
77
|
+
ORDER BY stream ASC;`
|
|
78
|
+
),
|
|
79
|
+
listStreamsByProfile: this.db.query(`SELECT stream FROM streams WHERE profile=? ORDER BY stream ASC;`),
|
|
80
|
+
countActiveLiveTemplates: this.db.query(`SELECT COUNT(*) as cnt FROM live_templates WHERE stream=? AND state='active';`),
|
|
81
|
+
listActiveLiveTemplates: this.db.query(
|
|
82
|
+
`SELECT stream, template_id, entity, fields_json, encodings_json, state, created_at_ms, last_seen_at_ms,
|
|
83
|
+
inactivity_ttl_ms, active_from_source_offset, retired_at_ms, retired_reason
|
|
84
|
+
FROM live_templates
|
|
85
|
+
WHERE stream=? AND state='active'
|
|
86
|
+
ORDER BY entity ASC, template_id ASC;`
|
|
87
|
+
),
|
|
88
|
+
getLiveTemplate: this.db.query(
|
|
89
|
+
`SELECT stream, template_id, entity, fields_json, encodings_json, state, created_at_ms, last_seen_at_ms,
|
|
90
|
+
inactivity_ttl_ms, active_from_source_offset, retired_at_ms, retired_reason
|
|
91
|
+
FROM live_templates
|
|
92
|
+
WHERE stream=? AND template_id=? LIMIT 1;`
|
|
93
|
+
),
|
|
94
|
+
updateLiveTemplateHeartbeat: this.db.query(
|
|
95
|
+
`UPDATE live_templates
|
|
96
|
+
SET last_seen_at_ms=?, inactivity_ttl_ms=?
|
|
97
|
+
WHERE stream=? AND template_id=?;`
|
|
98
|
+
),
|
|
99
|
+
reactivateLiveTemplate: this.db.query(
|
|
100
|
+
`UPDATE live_templates
|
|
101
|
+
SET state='active',
|
|
102
|
+
last_seen_at_ms=?,
|
|
103
|
+
inactivity_ttl_ms=?,
|
|
104
|
+
active_from_source_offset=?,
|
|
105
|
+
retired_at_ms=NULL,
|
|
106
|
+
retired_reason=NULL
|
|
107
|
+
WHERE stream=? AND template_id=?;`
|
|
108
|
+
),
|
|
109
|
+
insertLiveTemplate: this.db.query(
|
|
110
|
+
`INSERT INTO live_templates(
|
|
111
|
+
stream, template_id, entity, fields_json, encodings_json,
|
|
112
|
+
state, created_at_ms, last_seen_at_ms, inactivity_ttl_ms, active_from_source_offset,
|
|
113
|
+
retired_at_ms, retired_reason
|
|
114
|
+
) VALUES(?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, NULL, NULL);`
|
|
115
|
+
),
|
|
116
|
+
updateLiveTemplateLastSeen: this.db.query(
|
|
117
|
+
`UPDATE live_templates
|
|
118
|
+
SET last_seen_at_ms=?
|
|
119
|
+
WHERE stream=? AND template_id=? AND state='active';`
|
|
120
|
+
),
|
|
121
|
+
listExpiredLiveTemplates: this.db.query(
|
|
122
|
+
`SELECT template_id, entity, fields_json, encodings_json, last_seen_at_ms, inactivity_ttl_ms
|
|
123
|
+
FROM live_templates
|
|
124
|
+
WHERE stream=? AND state='active' AND (last_seen_at_ms + inactivity_ttl_ms) < ?
|
|
125
|
+
ORDER BY last_seen_at_ms ASC
|
|
126
|
+
LIMIT ?;`
|
|
127
|
+
),
|
|
128
|
+
retireLiveTemplateForInactivity: this.db.query(
|
|
129
|
+
`UPDATE live_templates
|
|
130
|
+
SET state='retired', retired_reason='inactivity', retired_at_ms=?
|
|
131
|
+
WHERE stream=? AND template_id=? AND state='active';`
|
|
132
|
+
),
|
|
133
|
+
listActiveLiveTemplateEntityCounts: this.db.query(
|
|
134
|
+
`SELECT entity, COUNT(*) as cnt
|
|
135
|
+
FROM live_templates
|
|
136
|
+
WHERE stream=? AND state='active'
|
|
137
|
+
GROUP BY entity;`
|
|
138
|
+
),
|
|
139
|
+
retireLiveTemplateForCap: this.db.query(
|
|
140
|
+
`UPDATE live_templates
|
|
141
|
+
SET state='retired', retired_reason='cap_exceeded', retired_at_ms=?
|
|
142
|
+
WHERE stream=? AND template_id=? AND state='active';`
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
nowMs(): bigint {
|
|
148
|
+
return this.delegates.nowMs();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getStream(stream: string): StreamRow | null {
|
|
152
|
+
return this.delegates.getStream(stream);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): StreamRow {
|
|
156
|
+
return this.delegates.ensureStream(stream, opts);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
addStreamFlags(stream: string, flags: number): void {
|
|
160
|
+
this.delegates.addStreamFlags(stream, flags);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
isDeleted(row: StreamRow): boolean {
|
|
164
|
+
return this.delegates.isDeleted(row);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
|
|
168
|
+
return this.delegates.readWalRange(stream, startOffset, endOffset, routingKey);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number } {
|
|
172
|
+
return this.delegates.deleteWalThrough(stream, uploadedThrough);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getWalOldestOffset(stream: string): bigint | null {
|
|
176
|
+
return this.delegates.getWalOldestOffset(stream);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
trimWalByAge(stream: string, maxAgeMs: number): { trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null } {
|
|
180
|
+
return this.delegates.trimWalByAge(stream, maxAgeMs);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getStreamTouchState(stream: string): StreamTouchStateRow | null {
|
|
184
|
+
const row = this.stmts.getStreamTouchState.get(stream) as any;
|
|
185
|
+
if (!row) return null;
|
|
186
|
+
return {
|
|
187
|
+
stream: String(row.stream),
|
|
188
|
+
processed_through: toBigInt(row.processed_through),
|
|
189
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
listStreamTouchStates(): StreamTouchStateRow[] {
|
|
194
|
+
const rows = this.stmts.listStreamTouchStates.all() as any[];
|
|
195
|
+
return rows.map((row) => ({
|
|
196
|
+
stream: String(row.stream),
|
|
197
|
+
processed_through: toBigInt(row.processed_through),
|
|
198
|
+
updated_at_ms: toBigInt(row.updated_at_ms),
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
listStreamsByProfile(kind: string): string[] {
|
|
203
|
+
const rows = this.stmts.listStreamsByProfile.all(kind) as any[];
|
|
204
|
+
return rows.map((row) => String(row.stream));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ensureStreamTouchState(stream: string): void {
|
|
208
|
+
const existing = this.getStreamTouchState(stream);
|
|
209
|
+
if (existing) return;
|
|
210
|
+
const srow = this.getStream(stream);
|
|
211
|
+
const initialThrough = srow ? srow.next_offset - 1n : -1n;
|
|
212
|
+
this.stmts.upsertStreamTouchState.run(stream, bindInt(initialThrough), this.nowMs());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateStreamTouchStateThrough(stream: string, processedThrough: bigint): void {
|
|
216
|
+
this.stmts.upsertStreamTouchState.run(stream, bindInt(processedThrough), this.nowMs());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
deleteStreamTouchState(stream: string): void {
|
|
220
|
+
this.stmts.deleteStreamTouchState.run(stream);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
countActiveLiveTemplates(stream: string): number {
|
|
224
|
+
const row = this.stmts.countActiveLiveTemplates.get(stream) as any;
|
|
225
|
+
return Number(row?.cnt ?? 0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
activateLiveTemplates(args: {
|
|
229
|
+
stream: string;
|
|
230
|
+
templates: LiveTemplateActivationInput[];
|
|
231
|
+
maxActiveTemplatesPerStream: number;
|
|
232
|
+
maxActiveTemplatesPerEntity: number;
|
|
233
|
+
maxActivationTokens: number;
|
|
234
|
+
}): LiveTemplateActivationResult {
|
|
235
|
+
if (args.templates.length === 0) return { activated: [], invalid: [], rateLimited: [], activationTokensUsed: 0, evicted: [] };
|
|
236
|
+
const tx = this.db.transaction(() => {
|
|
237
|
+
const activated: string[] = [];
|
|
238
|
+
const invalid: string[] = [];
|
|
239
|
+
const rateLimited: string[] = [];
|
|
240
|
+
let activationTokensUsed = 0;
|
|
241
|
+
for (const template of args.templates) {
|
|
242
|
+
const existing = this.stmts.getLiveTemplate.get(args.stream, template.templateId) as any;
|
|
243
|
+
if (
|
|
244
|
+
existing &&
|
|
245
|
+
(String(existing.entity) !== template.entity ||
|
|
246
|
+
String(existing.fields_json) !== template.fieldsJson ||
|
|
247
|
+
String(existing.encodings_json) !== template.encodingsJson)
|
|
248
|
+
) {
|
|
249
|
+
invalid.push(template.templateId);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const alreadyActive = existing && String(existing.state) === "active";
|
|
253
|
+
if (!alreadyActive && activationTokensUsed >= args.maxActivationTokens) {
|
|
254
|
+
rateLimited.push(template.templateId);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (existing) {
|
|
258
|
+
const state = String(existing.state);
|
|
259
|
+
if (state === "active") {
|
|
260
|
+
this.stmts.updateLiveTemplateHeartbeat.run(template.nowMs, template.inactivityTtlMs, args.stream, template.templateId);
|
|
261
|
+
} else {
|
|
262
|
+
this.stmts.reactivateLiveTemplate.run(
|
|
263
|
+
template.nowMs,
|
|
264
|
+
template.inactivityTtlMs,
|
|
265
|
+
bindInt(template.activeFromSourceOffset),
|
|
266
|
+
args.stream,
|
|
267
|
+
template.templateId
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
this.stmts.insertLiveTemplate.run(
|
|
272
|
+
args.stream,
|
|
273
|
+
template.templateId,
|
|
274
|
+
template.entity,
|
|
275
|
+
template.fieldsJson,
|
|
276
|
+
template.encodingsJson,
|
|
277
|
+
template.nowMs,
|
|
278
|
+
template.nowMs,
|
|
279
|
+
template.inactivityTtlMs,
|
|
280
|
+
bindInt(template.activeFromSourceOffset)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (!alreadyActive) activationTokensUsed += 1;
|
|
284
|
+
activated.push(template.templateId);
|
|
285
|
+
}
|
|
286
|
+
const evicted = this.evictToCapsInTransaction({
|
|
287
|
+
stream: args.stream,
|
|
288
|
+
protectedIds: new Set(activated),
|
|
289
|
+
maxActiveTemplatesPerStream: args.maxActiveTemplatesPerStream,
|
|
290
|
+
maxActiveTemplatesPerEntity: args.maxActiveTemplatesPerEntity,
|
|
291
|
+
nowMs: args.templates.reduce((max, template) => Math.max(max, template.nowMs), 0),
|
|
292
|
+
});
|
|
293
|
+
return { activated, invalid, rateLimited, activationTokensUsed, evicted };
|
|
294
|
+
});
|
|
295
|
+
return tx();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
listActiveLiveTemplates(stream: string): LiveTemplateStoreRow[] {
|
|
299
|
+
const rows = this.stmts.listActiveLiveTemplates.all(stream) as any[];
|
|
300
|
+
return rows.map((row) => this.coerceLiveTemplateRow(row));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getLiveTemplate(stream: string, templateId: string): LiveTemplateStoreRow | null {
|
|
304
|
+
const row = this.stmts.getLiveTemplate.get(stream, templateId) as any;
|
|
305
|
+
return row ? this.coerceLiveTemplateRow(row) : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private updateLiveTemplateHeartbeat(stream: string, templateId: string, nowMs: number, inactivityTtlMs: number): void {
|
|
309
|
+
this.stmts.updateLiveTemplateHeartbeat.run(nowMs, inactivityTtlMs, stream, templateId);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private reactivateLiveTemplate(stream: string, templateId: string, nowMs: number, inactivityTtlMs: number, activeFromSourceOffset: bigint): void {
|
|
313
|
+
this.stmts.reactivateLiveTemplate.run(nowMs, inactivityTtlMs, activeFromSourceOffset, stream, templateId);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private insertLiveTemplate(args: {
|
|
317
|
+
stream: string;
|
|
318
|
+
templateId: string;
|
|
319
|
+
entity: string;
|
|
320
|
+
fieldsJson: string;
|
|
321
|
+
encodingsJson: string;
|
|
322
|
+
nowMs: number;
|
|
323
|
+
inactivityTtlMs: number;
|
|
324
|
+
activeFromSourceOffset: bigint;
|
|
325
|
+
}): void {
|
|
326
|
+
this.stmts.insertLiveTemplate.run(
|
|
327
|
+
args.stream,
|
|
328
|
+
args.templateId,
|
|
329
|
+
args.entity,
|
|
330
|
+
args.fieldsJson,
|
|
331
|
+
args.encodingsJson,
|
|
332
|
+
args.nowMs,
|
|
333
|
+
args.nowMs,
|
|
334
|
+
args.inactivityTtlMs,
|
|
335
|
+
args.activeFromSourceOffset
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
updateLiveTemplateLastSeenBatch(updates: LiveTemplateLastSeenUpdate[]): void {
|
|
340
|
+
for (const update of updates) {
|
|
341
|
+
this.stmts.updateLiveTemplateLastSeen.run(update.lastSeenAtMs, update.stream, update.templateId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
listExpiredLiveTemplates(stream: string, nowMs: number, limit: number): LiveTemplateIdentityRow[] {
|
|
346
|
+
const rows = this.stmts.listExpiredLiveTemplates.all(stream, nowMs, Math.max(1, Math.floor(limit))) as any[];
|
|
347
|
+
return rows.map((row) => ({
|
|
348
|
+
template_id: String(row.template_id),
|
|
349
|
+
entity: String(row.entity),
|
|
350
|
+
fields_json: String(row.fields_json),
|
|
351
|
+
encodings_json: String(row.encodings_json),
|
|
352
|
+
last_seen_at_ms: toBigInt(row.last_seen_at_ms),
|
|
353
|
+
inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
retireLiveTemplatesForInactivity(stream: string, templateIds: string[], nowMs: number): void {
|
|
358
|
+
for (const templateId of templateIds) {
|
|
359
|
+
this.stmts.retireLiveTemplateForInactivity.run(nowMs, stream, templateId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private listActiveLiveTemplateEntityCounts(stream: string): Array<{ entity: string; count: number }> {
|
|
364
|
+
const rows = this.stmts.listActiveLiveTemplateEntityCounts.all(stream) as any[];
|
|
365
|
+
return rows.map((row) => ({ entity: String(row.entity), count: Number(row.cnt) }));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private listLiveTemplateLruIds(args: { stream: string; entity?: string; excludeTemplateIds?: string[]; limit: number }): string[] {
|
|
369
|
+
const params: any[] = [args.stream];
|
|
370
|
+
let where = `stream=? AND state='active'`;
|
|
371
|
+
if (args.entity) {
|
|
372
|
+
where += ` AND entity=?`;
|
|
373
|
+
params.push(args.entity);
|
|
374
|
+
}
|
|
375
|
+
const excludeTemplateIds = args.excludeTemplateIds ?? [];
|
|
376
|
+
if (excludeTemplateIds.length > 0) {
|
|
377
|
+
const placeholders = excludeTemplateIds.map(() => "?").join(", ");
|
|
378
|
+
where += ` AND template_id NOT IN (${placeholders})`;
|
|
379
|
+
params.push(...excludeTemplateIds);
|
|
380
|
+
}
|
|
381
|
+
const q = `SELECT template_id FROM live_templates WHERE ${where} ORDER BY last_seen_at_ms ASC, template_id ASC LIMIT ?;`;
|
|
382
|
+
params.push(Math.max(1, Math.floor(args.limit)));
|
|
383
|
+
const rows = this.db.query(q).all(...params) as any[];
|
|
384
|
+
return rows.map((row) => String(row.template_id));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private retireLiveTemplateForCap(stream: string, templateId: string, nowMs: number): void {
|
|
388
|
+
this.stmts.retireLiveTemplateForCap.run(nowMs, stream, templateId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private retireLiveTemplatesForCap(stream: string, templateIds: string[], nowMs: number): void {
|
|
392
|
+
for (const templateId of templateIds) {
|
|
393
|
+
this.stmts.retireLiveTemplateForCap.run(nowMs, stream, templateId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
listActiveLiveTemplateEntitiesByIds(stream: string, templateIds: string[]): string[] {
|
|
398
|
+
if (templateIds.length === 0) return [];
|
|
399
|
+
const entities = new Set<string>();
|
|
400
|
+
const chunkSize = 200;
|
|
401
|
+
for (let i = 0; i < templateIds.length; i += chunkSize) {
|
|
402
|
+
const chunk = templateIds.slice(i, i + chunkSize);
|
|
403
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
404
|
+
const rows = this.db
|
|
405
|
+
.query(
|
|
406
|
+
`SELECT DISTINCT entity
|
|
407
|
+
FROM live_templates
|
|
408
|
+
WHERE stream=? AND state='active' AND template_id IN (${placeholders});`
|
|
409
|
+
)
|
|
410
|
+
.all(stream, ...chunk) as any[];
|
|
411
|
+
for (const row of rows) {
|
|
412
|
+
const entity = String(row?.entity ?? "").trim();
|
|
413
|
+
if (entity !== "") entities.add(entity);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return Array.from(entities);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private evictToCapsInTransaction(args: {
|
|
420
|
+
stream: string;
|
|
421
|
+
protectedIds: Set<string>;
|
|
422
|
+
maxActiveTemplatesPerStream: number;
|
|
423
|
+
maxActiveTemplatesPerEntity: number;
|
|
424
|
+
nowMs: number;
|
|
425
|
+
}): Array<{ templateId: string; reason: "cap_exceeded"; cap: number }> {
|
|
426
|
+
const evicted: Array<{ templateId: string; reason: "cap_exceeded"; cap: number }> = [];
|
|
427
|
+
const entities = this.listActiveLiveTemplateEntityCounts(args.stream);
|
|
428
|
+
for (const row of entities) {
|
|
429
|
+
if (row.count <= args.maxActiveTemplatesPerEntity) continue;
|
|
430
|
+
const ids = this.pickLruTemplatesForCap({
|
|
431
|
+
stream: args.stream,
|
|
432
|
+
entity: row.entity,
|
|
433
|
+
protectedIds: args.protectedIds,
|
|
434
|
+
count: row.count - args.maxActiveTemplatesPerEntity,
|
|
435
|
+
});
|
|
436
|
+
this.retireLiveTemplatesForCap(args.stream, ids, args.nowMs);
|
|
437
|
+
for (const id of ids) evicted.push({ templateId: id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerEntity });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const streamCount = this.countActiveLiveTemplates(args.stream);
|
|
441
|
+
if (streamCount > args.maxActiveTemplatesPerStream) {
|
|
442
|
+
const ids = this.pickLruTemplatesForCap({
|
|
443
|
+
stream: args.stream,
|
|
444
|
+
protectedIds: args.protectedIds,
|
|
445
|
+
count: streamCount - args.maxActiveTemplatesPerStream,
|
|
446
|
+
});
|
|
447
|
+
this.retireLiveTemplatesForCap(args.stream, ids, args.nowMs);
|
|
448
|
+
for (const id of ids) evicted.push({ templateId: id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerStream });
|
|
449
|
+
}
|
|
450
|
+
return evicted;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private pickLruTemplatesForCap(args: { stream: string; entity?: string; protectedIds: Set<string>; count: number }): string[] {
|
|
454
|
+
if (args.count <= 0) return [];
|
|
455
|
+
const pick = (excludeProtected: boolean): string[] =>
|
|
456
|
+
this.listLiveTemplateLruIds({
|
|
457
|
+
stream: args.stream,
|
|
458
|
+
entity: args.entity,
|
|
459
|
+
excludeTemplateIds: excludeProtected ? Array.from(args.protectedIds) : [],
|
|
460
|
+
limit: args.count,
|
|
461
|
+
});
|
|
462
|
+
const ids = pick(true);
|
|
463
|
+
if (ids.length >= args.count) return ids;
|
|
464
|
+
const merged: string[] = [];
|
|
465
|
+
const seen = new Set<string>();
|
|
466
|
+
for (const id of [...ids, ...pick(false)]) {
|
|
467
|
+
if (seen.has(id)) continue;
|
|
468
|
+
seen.add(id);
|
|
469
|
+
merged.push(id);
|
|
470
|
+
if (merged.length >= args.count) break;
|
|
471
|
+
}
|
|
472
|
+
return merged;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private coerceLiveTemplateRow(row: any): LiveTemplateStoreRow {
|
|
476
|
+
return {
|
|
477
|
+
stream: String(row.stream),
|
|
478
|
+
template_id: String(row.template_id),
|
|
479
|
+
entity: String(row.entity),
|
|
480
|
+
fields_json: String(row.fields_json),
|
|
481
|
+
encodings_json: String(row.encodings_json),
|
|
482
|
+
state: String(row.state),
|
|
483
|
+
created_at_ms: toBigInt(row.created_at_ms),
|
|
484
|
+
last_seen_at_ms: toBigInt(row.last_seen_at_ms),
|
|
485
|
+
inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
|
|
486
|
+
active_from_source_offset: toBigInt(row.active_from_source_offset),
|
|
487
|
+
retired_at_ms: row.retired_at_ms == null ? null : toBigInt(row.retired_at_ms),
|
|
488
|
+
retired_reason: row.retired_reason == null ? null : String(row.retired_reason),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|