@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,129 @@
|
|
|
1
|
+
import type { MaybePromise } from "./capabilities";
|
|
2
|
+
import type { StreamReadRow } from "./segment_read_store";
|
|
3
|
+
import type { WalReadRow } from "./wal_store";
|
|
4
|
+
import type {
|
|
5
|
+
IndexRunRow,
|
|
6
|
+
IndexStateRow,
|
|
7
|
+
LexiconIndexRunRow,
|
|
8
|
+
LexiconIndexStateRow,
|
|
9
|
+
SearchCompanionPlanRow,
|
|
10
|
+
SearchSegmentCompanionRow,
|
|
11
|
+
SecondaryIndexRunRow,
|
|
12
|
+
SecondaryIndexStateRow,
|
|
13
|
+
SegmentMetaRow,
|
|
14
|
+
SegmentRow,
|
|
15
|
+
} from "./rows";
|
|
16
|
+
export type {
|
|
17
|
+
IndexRunRow,
|
|
18
|
+
IndexStateRow,
|
|
19
|
+
LexiconIndexRunRow,
|
|
20
|
+
LexiconIndexStateRow,
|
|
21
|
+
SearchCompanionPlanRow,
|
|
22
|
+
SearchSegmentCompanionRow,
|
|
23
|
+
SecondaryIndexRunRow,
|
|
24
|
+
SecondaryIndexStateRow,
|
|
25
|
+
SegmentMetaRow,
|
|
26
|
+
SegmentRow,
|
|
27
|
+
} from "./rows";
|
|
28
|
+
|
|
29
|
+
export type SegmentCandidateRow = {
|
|
30
|
+
stream: string;
|
|
31
|
+
pending_bytes: bigint;
|
|
32
|
+
pending_rows: bigint;
|
|
33
|
+
last_segment_cut_ms: bigint;
|
|
34
|
+
sealed_through: bigint;
|
|
35
|
+
next_offset: bigint;
|
|
36
|
+
epoch: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type SegmentClaim = {
|
|
40
|
+
token: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type SealedSegmentCommit = {
|
|
44
|
+
segmentId: string;
|
|
45
|
+
stream: string;
|
|
46
|
+
segmentIndex: number;
|
|
47
|
+
startOffset: bigint;
|
|
48
|
+
endOffset: bigint;
|
|
49
|
+
blockCount: number;
|
|
50
|
+
lastAppendMs: bigint;
|
|
51
|
+
payloadBytes: bigint;
|
|
52
|
+
sizeBytes: number;
|
|
53
|
+
localPath: string;
|
|
54
|
+
rowsSealed: bigint;
|
|
55
|
+
claimToken?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ManifestRow = {
|
|
59
|
+
stream: string;
|
|
60
|
+
generation: number;
|
|
61
|
+
uploaded_generation: number;
|
|
62
|
+
last_uploaded_at_ms: bigint | null;
|
|
63
|
+
last_uploaded_etag: string | null;
|
|
64
|
+
last_uploaded_size_bytes: bigint | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type StreamProfileRow = {
|
|
68
|
+
stream: string;
|
|
69
|
+
profile_json: string;
|
|
70
|
+
updated_at_ms: bigint;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type ManifestPublicationSnapshot = {
|
|
74
|
+
publicationToken?: string;
|
|
75
|
+
streamRow: StreamReadRow;
|
|
76
|
+
prevUploadedSegmentCount: number;
|
|
77
|
+
uploadedPrefixCount: number;
|
|
78
|
+
uploadedThrough: bigint;
|
|
79
|
+
publishedLogicalSizeBytes: bigint;
|
|
80
|
+
generation: number;
|
|
81
|
+
segmentMeta: SegmentMetaRow;
|
|
82
|
+
profileJson: Record<string, any> | null;
|
|
83
|
+
indexState: IndexStateRow | null;
|
|
84
|
+
indexRuns: IndexRunRow[];
|
|
85
|
+
retiredRuns: IndexRunRow[];
|
|
86
|
+
secondaryIndexStates: SecondaryIndexStateRow[];
|
|
87
|
+
secondaryIndexRuns: SecondaryIndexRunRow[];
|
|
88
|
+
retiredSecondaryIndexRuns: SecondaryIndexRunRow[];
|
|
89
|
+
lexiconIndexStates: LexiconIndexStateRow[];
|
|
90
|
+
lexiconIndexRuns: LexiconIndexRunRow[];
|
|
91
|
+
retiredLexiconIndexRuns: LexiconIndexRunRow[];
|
|
92
|
+
searchCompanionPlan: SearchCompanionPlanRow | null;
|
|
93
|
+
searchSegmentCompanions: SearchSegmentCompanionRow[];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type ManifestPublicationOptions = {
|
|
97
|
+
wait?: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export interface SegmentStore {
|
|
101
|
+
getSegmentStreamState(stream: string): MaybePromise<StreamReadRow | null>;
|
|
102
|
+
isDeleted(row: StreamReadRow): boolean;
|
|
103
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
104
|
+
candidates(minPendingBytes: bigint, minPendingRows: bigint, maxIntervalMs: bigint, limit: number): MaybePromise<SegmentCandidateRow[]>;
|
|
105
|
+
recentSegmentCompressionRatio(stream: string, limit?: number): MaybePromise<number | null>;
|
|
106
|
+
tryClaimSegment(stream: string): MaybePromise<SegmentClaim | null>;
|
|
107
|
+
setSegmentInProgress(stream: string, inProgress: number, claim?: SegmentClaim): MaybePromise<void>;
|
|
108
|
+
nextSegmentIndexForStream(stream: string): MaybePromise<number>;
|
|
109
|
+
commitSealedSegment(row: SealedSegmentCommit): MaybePromise<void>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ManifestStore {
|
|
113
|
+
nowMs(): bigint;
|
|
114
|
+
countPendingSegments(): MaybePromise<number>;
|
|
115
|
+
pendingUploadHeads(limit: number): MaybePromise<SegmentRow[]>;
|
|
116
|
+
markSegmentUploaded(segmentId: string, etag: string, uploadedAtMs: bigint): MaybePromise<void>;
|
|
117
|
+
loadManifestPublicationSnapshot(stream: string, opts?: ManifestPublicationOptions): MaybePromise<ManifestPublicationSnapshot | null>;
|
|
118
|
+
commitManifest(
|
|
119
|
+
stream: string,
|
|
120
|
+
generation: number,
|
|
121
|
+
etag: string,
|
|
122
|
+
uploadedAtMs: bigint,
|
|
123
|
+
uploadedThrough: bigint,
|
|
124
|
+
sizeBytes: number,
|
|
125
|
+
publicationToken?: string
|
|
126
|
+
): MaybePromise<void>;
|
|
127
|
+
releaseManifestPublication?(publicationToken: string): MaybePromise<void>;
|
|
128
|
+
getSegmentForManifestCleanup(stream: string, segmentIndex: number): MaybePromise<SegmentRow | null>;
|
|
129
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SearchCompanionPlanRow, SearchSegmentCompanionRow, SegmentRow, StreamRow } from "./rows";
|
|
2
|
+
|
|
3
|
+
export type StreamReadRow = StreamRow;
|
|
4
|
+
export type SegmentReadRow = SegmentRow;
|
|
5
|
+
export type SearchCompanionPlanReadRow = SearchCompanionPlanRow;
|
|
6
|
+
export type SearchSegmentCompanionReadRow = SearchSegmentCompanionRow;
|
|
7
|
+
|
|
8
|
+
export interface StreamReadStore {
|
|
9
|
+
nowMsForRead(): Promise<bigint>;
|
|
10
|
+
getStreamForRead(stream: string): Promise<StreamReadRow | null>;
|
|
11
|
+
isDeleted(row: StreamReadRow): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SegmentReadStore {
|
|
15
|
+
listSegmentsForRead(stream: string): Promise<SegmentReadRow[]>;
|
|
16
|
+
getSegmentByIndexForRead(stream: string, segmentIndex: number): Promise<SegmentReadRow | null>;
|
|
17
|
+
findSegmentForOffsetForRead(stream: string, offset: bigint): Promise<SegmentReadRow | null>;
|
|
18
|
+
countSegmentsForRead(stream: string): Promise<number>;
|
|
19
|
+
getSearchCompanionPlanForRead(stream: string): Promise<SearchCompanionPlanReadRow | null>;
|
|
20
|
+
listSearchSegmentCompanionsForRead(stream: string): Promise<SearchSegmentCompanionReadRow[]>;
|
|
21
|
+
getSearchSegmentCompanionForRead(stream: string, segmentIndex: number): Promise<SearchSegmentCompanionReadRow | null>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { MaybePromise } from "./capabilities";
|
|
2
|
+
|
|
3
|
+
export interface StorageStatsStore {
|
|
4
|
+
countStreams(): MaybePromise<number>;
|
|
5
|
+
getWalDbSizeBytes(): MaybePromise<number>;
|
|
6
|
+
getMetaDbSizeBytes(): MaybePromise<number>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ObjectStoreRequestSummary = {
|
|
10
|
+
puts: bigint;
|
|
11
|
+
reads: bigint;
|
|
12
|
+
gets: bigint;
|
|
13
|
+
heads: bigint;
|
|
14
|
+
lists: bigint;
|
|
15
|
+
deletes: bigint;
|
|
16
|
+
by_artifact: Array<{
|
|
17
|
+
artifact: string;
|
|
18
|
+
puts: bigint;
|
|
19
|
+
gets: bigint;
|
|
20
|
+
heads: bigint;
|
|
21
|
+
lists: bigint;
|
|
22
|
+
deletes: bigint;
|
|
23
|
+
reads: bigint;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ObjectStoreRequestCountRow = {
|
|
28
|
+
artifact: string;
|
|
29
|
+
op: string;
|
|
30
|
+
count: bigint;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function summarizeObjectStoreRequestCounts(rows: ObjectStoreRequestCountRow[]): ObjectStoreRequestSummary {
|
|
34
|
+
const byArtifact = new Map<string, { puts: bigint; gets: bigint; heads: bigint; lists: bigint; deletes: bigint; reads: bigint }>();
|
|
35
|
+
let puts = 0n;
|
|
36
|
+
let gets = 0n;
|
|
37
|
+
let heads = 0n;
|
|
38
|
+
let lists = 0n;
|
|
39
|
+
let deletes = 0n;
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const artifact = String(row.artifact);
|
|
42
|
+
const op = String(row.op);
|
|
43
|
+
const count = row.count;
|
|
44
|
+
const entry = byArtifact.get(artifact) ?? { puts: 0n, gets: 0n, heads: 0n, lists: 0n, deletes: 0n, reads: 0n };
|
|
45
|
+
if (op === "put") {
|
|
46
|
+
entry.puts += count;
|
|
47
|
+
puts += count;
|
|
48
|
+
} else if (op === "get") {
|
|
49
|
+
entry.gets += count;
|
|
50
|
+
entry.reads += count;
|
|
51
|
+
gets += count;
|
|
52
|
+
} else if (op === "head") {
|
|
53
|
+
entry.heads += count;
|
|
54
|
+
entry.reads += count;
|
|
55
|
+
heads += count;
|
|
56
|
+
} else if (op === "list") {
|
|
57
|
+
entry.lists += count;
|
|
58
|
+
entry.reads += count;
|
|
59
|
+
lists += count;
|
|
60
|
+
} else if (op === "delete") {
|
|
61
|
+
entry.deletes += count;
|
|
62
|
+
deletes += count;
|
|
63
|
+
}
|
|
64
|
+
byArtifact.set(artifact, entry);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
puts,
|
|
68
|
+
reads: gets + heads + lists,
|
|
69
|
+
gets,
|
|
70
|
+
heads,
|
|
71
|
+
lists,
|
|
72
|
+
deletes,
|
|
73
|
+
by_artifact: Array.from(byArtifact.entries()).map(([artifact, entry]) => ({ artifact, ...entry })),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ObjectStoreAccountingRecorder {
|
|
78
|
+
recordObjectStoreRequestByHash(streamHash: string, artifact: string, op: string, bytes?: number, count?: number): MaybePromise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ObjectStoreAccountingStore extends ObjectStoreAccountingRecorder {
|
|
82
|
+
getObjectStoreRequestSummaryByHash(streamHash: string): MaybePromise<ObjectStoreRequestSummary>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { StreamRow } from "./rows";
|
|
2
|
+
import type { MaybePromise } from "./capabilities";
|
|
3
|
+
import type { WalReadRow } from "./wal_store";
|
|
4
|
+
|
|
5
|
+
export type StreamTouchStateRow = {
|
|
6
|
+
stream: string;
|
|
7
|
+
processed_through: bigint;
|
|
8
|
+
updated_at_ms: bigint;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LiveTemplateStoreRow = {
|
|
12
|
+
stream: string;
|
|
13
|
+
template_id: string;
|
|
14
|
+
entity: string;
|
|
15
|
+
fields_json: string;
|
|
16
|
+
encodings_json: string;
|
|
17
|
+
state: string;
|
|
18
|
+
created_at_ms: bigint;
|
|
19
|
+
last_seen_at_ms: bigint;
|
|
20
|
+
inactivity_ttl_ms: bigint;
|
|
21
|
+
active_from_source_offset: bigint;
|
|
22
|
+
retired_at_ms: bigint | null;
|
|
23
|
+
retired_reason: string | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type LiveTemplateIdentityRow = {
|
|
27
|
+
template_id: string;
|
|
28
|
+
entity: string;
|
|
29
|
+
fields_json: string;
|
|
30
|
+
encodings_json?: string;
|
|
31
|
+
last_seen_at_ms: bigint;
|
|
32
|
+
inactivity_ttl_ms?: bigint;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type LiveTemplateLastSeenUpdate = {
|
|
36
|
+
stream: string;
|
|
37
|
+
templateId: string;
|
|
38
|
+
lastSeenAtMs: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type LiveTemplateActivationInput = {
|
|
42
|
+
templateId: string;
|
|
43
|
+
entity: string;
|
|
44
|
+
fieldsJson: string;
|
|
45
|
+
encodingsJson: string;
|
|
46
|
+
nowMs: number;
|
|
47
|
+
inactivityTtlMs: number;
|
|
48
|
+
activeFromSourceOffset: bigint;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type LiveTemplateActivationResult = {
|
|
52
|
+
activated: string[];
|
|
53
|
+
invalid: string[];
|
|
54
|
+
rateLimited: string[];
|
|
55
|
+
activationTokensUsed: number;
|
|
56
|
+
evicted: Array<{ templateId: string; reason: "cap_exceeded"; cap: number }>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface ProfileTouchControlStore {
|
|
60
|
+
ensureStreamTouchState(stream: string): MaybePromise<void>;
|
|
61
|
+
deleteStreamTouchState(stream: string): MaybePromise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TouchRouteStore {
|
|
65
|
+
countActiveLiveTemplates(stream: string): MaybePromise<number>;
|
|
66
|
+
getStreamTouchState(stream: string): MaybePromise<StreamTouchStateRow | null>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LiveTemplateStore {
|
|
70
|
+
activateLiveTemplates(args: {
|
|
71
|
+
stream: string;
|
|
72
|
+
templates: LiveTemplateActivationInput[];
|
|
73
|
+
maxActiveTemplatesPerStream: number;
|
|
74
|
+
maxActiveTemplatesPerEntity: number;
|
|
75
|
+
maxActivationTokens: number;
|
|
76
|
+
}): MaybePromise<LiveTemplateActivationResult>;
|
|
77
|
+
countActiveLiveTemplates(stream: string): MaybePromise<number>;
|
|
78
|
+
listActiveLiveTemplates(stream: string): MaybePromise<LiveTemplateStoreRow[]>;
|
|
79
|
+
updateLiveTemplateLastSeenBatch(updates: LiveTemplateLastSeenUpdate[]): MaybePromise<void>;
|
|
80
|
+
listExpiredLiveTemplates(stream: string, nowMs: number, limit: number): MaybePromise<LiveTemplateIdentityRow[]>;
|
|
81
|
+
retireLiveTemplatesForInactivity(stream: string, templateIds: string[], nowMs: number): MaybePromise<void>;
|
|
82
|
+
listActiveLiveTemplateEntitiesByIds(stream: string, templateIds: string[]): MaybePromise<string[]>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TouchProcessorStore extends ProfileTouchControlStore, TouchRouteStore, LiveTemplateStore {
|
|
86
|
+
nowMs(): bigint;
|
|
87
|
+
getStream(stream: string): MaybePromise<StreamRow | null>;
|
|
88
|
+
ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): MaybePromise<StreamRow>;
|
|
89
|
+
addStreamFlags(stream: string, flags: number): MaybePromise<void>;
|
|
90
|
+
isDeleted(row: StreamRow): boolean;
|
|
91
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
92
|
+
listStreamTouchStates(): MaybePromise<StreamTouchStateRow[]>;
|
|
93
|
+
listStreamsByProfile(kind: string): MaybePromise<string[]>;
|
|
94
|
+
updateStreamTouchStateThrough(stream: string, processedThrough: bigint): MaybePromise<void>;
|
|
95
|
+
deleteWalThrough(stream: string, uploadedThrough: bigint): MaybePromise<{ deletedRows: number; deletedBytes: number }>;
|
|
96
|
+
getWalOldestOffset(stream: string): MaybePromise<bigint | null>;
|
|
97
|
+
trimWalByAge(stream: string, maxAgeMs: number): MaybePromise<{ trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null }>;
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StoreAppendBatch, StoreAppendTask } from "./append";
|
|
2
|
+
|
|
3
|
+
export type WalReadRow = {
|
|
4
|
+
offset: bigint;
|
|
5
|
+
tsMs: bigint;
|
|
6
|
+
routingKey: Uint8Array | null;
|
|
7
|
+
contentType: string | null;
|
|
8
|
+
payload: Uint8Array;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface WalAppendStore {
|
|
12
|
+
appendBatch(tasks: StoreAppendTask[]): Promise<StoreAppendBatch>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WalReadStore {
|
|
16
|
+
readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
17
|
+
readWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
|
|
18
|
+
getWalOldestTimestampMsForRead(stream: string): Promise<bigint | null>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WalStore extends WalAppendStore, WalReadStore {}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
import type { SqliteDurableStore, SegmentRow } from "./db/db";
|
|
5
|
+
import type { ObjectStore } from "./objectstore/interface";
|
|
6
|
+
import type { SegmentDiskCache } from "./segment/cache";
|
|
7
|
+
import { loadSegmentBytesCached } from "./segment/cached_segment";
|
|
8
|
+
import { iterateBlocksResult } from "./segment/format";
|
|
9
|
+
import { dsError } from "./util/ds_error";
|
|
10
|
+
import { yieldToEventLoop } from "./util/yield";
|
|
11
|
+
|
|
12
|
+
export class StreamSizeReconciler {
|
|
13
|
+
private stopped = false;
|
|
14
|
+
private running: Promise<void> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly db: SqliteDurableStore,
|
|
18
|
+
private readonly os: ObjectStore,
|
|
19
|
+
private readonly segmentCache?: SegmentDiskCache,
|
|
20
|
+
private readonly onMetadataChanged?: (stream: string) => void
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
start(): void {
|
|
24
|
+
if (this.running) return;
|
|
25
|
+
this.running = this.run().finally(() => {
|
|
26
|
+
this.running = null;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stop(): void {
|
|
31
|
+
this.stopped = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async run(): Promise<void> {
|
|
35
|
+
while (!this.stopped) {
|
|
36
|
+
const streams = this.db.listStreamsMissingLogicalSize(8);
|
|
37
|
+
if (streams.length === 0) return;
|
|
38
|
+
for (const stream of streams) {
|
|
39
|
+
if (this.stopped) return;
|
|
40
|
+
try {
|
|
41
|
+
await this.reconcileStream(stream);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
const msg = String((e as any)?.message ?? e);
|
|
44
|
+
if (!this.stopped && !msg.includes("Statement has finalized")) {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.error("stream size reconcile failed", stream, e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async reconcileStream(stream: string): Promise<void> {
|
|
54
|
+
for (let attempt = 0; attempt < 3 && !this.stopped; attempt++) {
|
|
55
|
+
const before = this.db.getStream(stream);
|
|
56
|
+
if (!before || this.db.isDeleted(before) || before.next_offset <= 0n) return;
|
|
57
|
+
if (before.logical_size_bytes > 0n) return;
|
|
58
|
+
|
|
59
|
+
const segments = this.db.listSegmentsForStream(stream);
|
|
60
|
+
let total = 0n;
|
|
61
|
+
for (const segment of segments) {
|
|
62
|
+
if (this.stopped) return;
|
|
63
|
+
total += await this.sumSegmentPayloadBytes(segment);
|
|
64
|
+
await yieldToEventLoop();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const after = this.db.getStream(stream);
|
|
68
|
+
if (!after || this.db.isDeleted(after)) return;
|
|
69
|
+
if (after.logical_size_bytes > 0n) return;
|
|
70
|
+
|
|
71
|
+
if (segments.length !== this.db.countSegmentsForStream(stream)) continue;
|
|
72
|
+
|
|
73
|
+
const finalTotal = total + after.wal_bytes;
|
|
74
|
+
if (finalTotal > after.logical_size_bytes) {
|
|
75
|
+
this.db.setStreamLogicalSizeBytes(stream, finalTotal);
|
|
76
|
+
this.onMetadataChanged?.(stream);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async sumSegmentPayloadBytes(segment: SegmentRow): Promise<bigint> {
|
|
83
|
+
const bytes = await this.loadSegmentBytes(segment);
|
|
84
|
+
let total = 0n;
|
|
85
|
+
for (const blockRes of iterateBlocksResult(bytes)) {
|
|
86
|
+
if (Result.isError(blockRes)) throw dsError(blockRes.error.message);
|
|
87
|
+
for (const record of blockRes.value.decoded.records) {
|
|
88
|
+
total += BigInt(record.payload.byteLength);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return total;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async loadSegmentBytes(segment: SegmentRow): Promise<Uint8Array> {
|
|
95
|
+
if (existsSync(segment.local_path)) {
|
|
96
|
+
return new Uint8Array(await readFile(segment.local_path));
|
|
97
|
+
}
|
|
98
|
+
return loadSegmentBytesCached(this.os, segment, this.segmentCache);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { xxh3BigInt, xxh3Hex } from "../runtime/hash.ts";
|
|
2
|
+
export type TemplateEncoding = "string" | "int64" | "bool" | "datetime" | "bytes";
|
|
3
|
+
|
|
4
|
+
function utf8(s: string): Uint8Array {
|
|
5
|
+
return new TextEncoder().encode(s);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function encodeU64Be(v: bigint): Uint8Array {
|
|
9
|
+
const out = new Uint8Array(8);
|
|
10
|
+
let x = v;
|
|
11
|
+
for (let i = 7; i >= 0; i--) {
|
|
12
|
+
out[i] = Number(x & 0xffn);
|
|
13
|
+
x >>= 8n;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function xxh3Low32(bytes: Uint8Array): number {
|
|
19
|
+
const h = xxh3BigInt(bytes);
|
|
20
|
+
return Number(h & 0xffffffffn) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function canonicalizeTemplateFields(fields: Array<{ name: string; encoding: TemplateEncoding }>): Array<{ name: string; encoding: TemplateEncoding }> {
|
|
24
|
+
const out = [...fields].map((f) => ({ name: f.name, encoding: f.encoding }));
|
|
25
|
+
out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function templateIdFor(entity: string, fieldNamesSorted: string[]): string {
|
|
30
|
+
const parts: Uint8Array[] = [utf8("tpl\0"), utf8(entity), utf8("\0")];
|
|
31
|
+
for (let i = 0; i < fieldNamesSorted.length; i++) {
|
|
32
|
+
if (i > 0) parts.push(utf8("\0"));
|
|
33
|
+
parts.push(utf8(fieldNamesSorted[i]));
|
|
34
|
+
}
|
|
35
|
+
return xxh3Hex(concat(parts));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function tableKeyFor(entity: string): string {
|
|
39
|
+
return xxh3Hex(concat([utf8("tbl\0"), utf8(entity)]));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function tableKeyIdFor(entity: string): number {
|
|
43
|
+
return xxh3Low32(concat([utf8("tbl\0"), utf8(entity)]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function templateKeyFor(templateIdHex16: string): string {
|
|
47
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
48
|
+
return xxh3Hex(concat([utf8("tpl\0"), tplBytes]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function templateKeyIdFor(templateIdHex16: string): number {
|
|
52
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
53
|
+
return xxh3Low32(concat([utf8("tpl\0"), tplBytes]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function membershipKeyFor(templateIdHex16: string, encodedArgs: string[]): string {
|
|
57
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
58
|
+
const parts: Uint8Array[] = [utf8("mem\0"), tplBytes];
|
|
59
|
+
for (const a of encodedArgs) {
|
|
60
|
+
parts.push(utf8("\0"));
|
|
61
|
+
parts.push(utf8(a));
|
|
62
|
+
}
|
|
63
|
+
return xxh3Hex(concat(parts));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function membershipKeyIdFor(templateIdHex16: string, encodedArgs: string[]): number {
|
|
67
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
68
|
+
const parts: Uint8Array[] = [utf8("mem\0"), tplBytes];
|
|
69
|
+
for (const a of encodedArgs) {
|
|
70
|
+
parts.push(utf8("\0"));
|
|
71
|
+
parts.push(utf8(a));
|
|
72
|
+
}
|
|
73
|
+
return xxh3Low32(concat(parts));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function projectedFieldKeyFor(templateIdHex16: string, fieldName: string, encodedArgs: string[]): string {
|
|
77
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
78
|
+
const parts: Uint8Array[] = [utf8("fld\0"), tplBytes, utf8("\0"), utf8(fieldName)];
|
|
79
|
+
for (const a of encodedArgs) {
|
|
80
|
+
parts.push(utf8("\0"));
|
|
81
|
+
parts.push(utf8(a));
|
|
82
|
+
}
|
|
83
|
+
return xxh3Hex(concat(parts));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function projectedFieldKeyIdFor(templateIdHex16: string, fieldName: string, encodedArgs: string[]): number {
|
|
87
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
88
|
+
const parts: Uint8Array[] = [utf8("fld\0"), tplBytes, utf8("\0"), utf8(fieldName)];
|
|
89
|
+
for (const a of encodedArgs) {
|
|
90
|
+
parts.push(utf8("\0"));
|
|
91
|
+
parts.push(utf8(a));
|
|
92
|
+
}
|
|
93
|
+
return xxh3Low32(concat(parts));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function watchKeyFor(templateIdHex16: string, encodedArgs: string[]): string {
|
|
97
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
98
|
+
const parts: Uint8Array[] = [utf8("key\0"), tplBytes];
|
|
99
|
+
for (const a of encodedArgs) {
|
|
100
|
+
parts.push(utf8("\0"));
|
|
101
|
+
parts.push(utf8(a));
|
|
102
|
+
}
|
|
103
|
+
return xxh3Hex(concat(parts));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function watchKeyIdFor(templateIdHex16: string, encodedArgs: string[]): number {
|
|
107
|
+
const tplBytes = encodeU64Be(BigInt(`0x${templateIdHex16}`));
|
|
108
|
+
const parts: Uint8Array[] = [utf8("key\0"), tplBytes];
|
|
109
|
+
for (const a of encodedArgs) {
|
|
110
|
+
parts.push(utf8("\0"));
|
|
111
|
+
parts.push(utf8(a));
|
|
112
|
+
}
|
|
113
|
+
return xxh3Low32(concat(parts));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function encodeTemplateArg(value: unknown, encoding: TemplateEncoding): string | null {
|
|
117
|
+
if (value === null || value === undefined) return null;
|
|
118
|
+
switch (encoding) {
|
|
119
|
+
case "string": {
|
|
120
|
+
if (typeof value === "string") return value;
|
|
121
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
122
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
case "int64": {
|
|
126
|
+
if (typeof value === "bigint") return value.toString();
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value) && Number.isInteger(value)) return String(value);
|
|
128
|
+
if (typeof value === "string" && /^-?(0|[1-9][0-9]*)$/.test(value.trim())) return value.trim();
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
case "bool": {
|
|
132
|
+
if (typeof value !== "boolean") return null;
|
|
133
|
+
return value ? "1" : "0";
|
|
134
|
+
}
|
|
135
|
+
case "datetime": {
|
|
136
|
+
if (typeof value !== "string") return null;
|
|
137
|
+
const d = new Date(value);
|
|
138
|
+
if (!Number.isFinite(d.getTime())) return null;
|
|
139
|
+
return d.toISOString();
|
|
140
|
+
}
|
|
141
|
+
case "bytes": {
|
|
142
|
+
if (typeof value !== "string") return null;
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function concat(parts: Uint8Array[]): Uint8Array {
|
|
149
|
+
let total = 0;
|
|
150
|
+
for (const p of parts) total += p.byteLength;
|
|
151
|
+
const out = new Uint8Array(total);
|
|
152
|
+
let off = 0;
|
|
153
|
+
for (const p of parts) {
|
|
154
|
+
out.set(p, off);
|
|
155
|
+
off += p.byteLength;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|