@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,51 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import type {
|
|
3
|
+
StreamProfileDefinition,
|
|
4
|
+
StreamProfilePersistResult,
|
|
5
|
+
StreamProfileReadResult,
|
|
6
|
+
} from "./profile";
|
|
7
|
+
import { cloneStreamProfileSpec, expectPlainObjectResult, rejectUnknownKeysResult, type StreamProfileSpec } from "./profile";
|
|
8
|
+
|
|
9
|
+
export type GenericStreamProfile = {
|
|
10
|
+
kind: "generic";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function cloneGenericProfile(): GenericStreamProfile {
|
|
14
|
+
return { kind: "generic" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const GENERIC_STREAM_PROFILE_DEFINITION: StreamProfileDefinition = {
|
|
18
|
+
kind: "generic",
|
|
19
|
+
usesStoredProfileRow: false,
|
|
20
|
+
|
|
21
|
+
defaultProfile(): GenericStreamProfile {
|
|
22
|
+
return cloneGenericProfile();
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
validateResult(raw, path) {
|
|
26
|
+
const objRes = expectPlainObjectResult(raw, path);
|
|
27
|
+
if (Result.isError(objRes)) return objRes;
|
|
28
|
+
if (objRes.value.kind !== "generic") {
|
|
29
|
+
return Result.err({ message: `${path}.kind must be generic` });
|
|
30
|
+
}
|
|
31
|
+
const keyCheck = rejectUnknownKeysResult(objRes.value, ["kind"], path);
|
|
32
|
+
if (Result.isError(keyCheck)) return keyCheck;
|
|
33
|
+
return Result.ok(cloneGenericProfile());
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
readProfileResult(): Result<StreamProfileReadResult, { message: string }> {
|
|
37
|
+
return Result.ok({ profile: cloneGenericProfile(), cache: null });
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
persistProfileResult({ streamRow }): Result<StreamProfilePersistResult, { kind: "bad_request"; message: string; code?: string }> {
|
|
41
|
+
const profile: StreamProfileSpec = cloneStreamProfileSpec(cloneGenericProfile());
|
|
42
|
+
return Result.ok({
|
|
43
|
+
profile,
|
|
44
|
+
cache: null,
|
|
45
|
+
schemaRegistry: null,
|
|
46
|
+
streamProfile: "generic",
|
|
47
|
+
profileJson: null,
|
|
48
|
+
touchState: streamRow.profile === "state-protocol" ? "delete" : "preserve",
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import type { SchemaRegistry } from "../schema/registry";
|
|
3
|
+
import type { StreamReadRow as StreamRow, StreamReadStore } from "../store/segment_read_store";
|
|
4
|
+
import type { ProfileStore } from "../store/schema_profile_store";
|
|
5
|
+
import { LruCache } from "../util/lru";
|
|
6
|
+
import { dsError } from "../util/ds_error.ts";
|
|
7
|
+
import { GENERIC_STREAM_PROFILE_DEFINITION } from "./generic";
|
|
8
|
+
import { EVLOG_STREAM_PROFILE_DEFINITION } from "./evlog";
|
|
9
|
+
import { METRICS_STREAM_PROFILE_DEFINITION } from "./metrics";
|
|
10
|
+
import { OTEL_TRACES_STREAM_PROFILE_DEFINITION } from "./otelTraces";
|
|
11
|
+
import {
|
|
12
|
+
buildStreamProfileResource,
|
|
13
|
+
cloneStreamProfileSpec,
|
|
14
|
+
DEFAULT_STREAM_PROFILE,
|
|
15
|
+
parseProfileUpdateEnvelopeResult,
|
|
16
|
+
readProfileKindResult,
|
|
17
|
+
type CachedStreamProfile,
|
|
18
|
+
type StoredProfileRow,
|
|
19
|
+
type StreamProfileJsonIngestCapability,
|
|
20
|
+
type StreamProfileOtlpTracesCapability,
|
|
21
|
+
type StreamProfileCorrelationCapability,
|
|
22
|
+
type StreamProfileDefinition,
|
|
23
|
+
type StreamProfileMetricsCapability,
|
|
24
|
+
type StreamProfilePersistResult,
|
|
25
|
+
type StreamProfileReadError,
|
|
26
|
+
type StreamProfileResource,
|
|
27
|
+
type StreamProfileSpec,
|
|
28
|
+
type StreamProfileMutationError,
|
|
29
|
+
type StreamTouchCapability,
|
|
30
|
+
} from "./profile";
|
|
31
|
+
import { STATE_PROTOCOL_STREAM_PROFILE_DEFINITION } from "./stateProtocol";
|
|
32
|
+
|
|
33
|
+
export * from "./profile";
|
|
34
|
+
export { EVLOG_STREAM_PROFILE_DEFINITION } from "./evlog";
|
|
35
|
+
export { GENERIC_STREAM_PROFILE_DEFINITION } from "./generic";
|
|
36
|
+
export { METRICS_STREAM_PROFILE_DEFINITION } from "./metrics";
|
|
37
|
+
export { OTEL_TRACES_STREAM_PROFILE_DEFINITION } from "./otelTraces";
|
|
38
|
+
export { STATE_PROTOCOL_STREAM_PROFILE_DEFINITION } from "./stateProtocol";
|
|
39
|
+
|
|
40
|
+
const STREAM_PROFILE_DEFINITIONS: Record<string, StreamProfileDefinition> = {
|
|
41
|
+
[EVLOG_STREAM_PROFILE_DEFINITION.kind]: EVLOG_STREAM_PROFILE_DEFINITION,
|
|
42
|
+
[GENERIC_STREAM_PROFILE_DEFINITION.kind]: GENERIC_STREAM_PROFILE_DEFINITION,
|
|
43
|
+
[METRICS_STREAM_PROFILE_DEFINITION.kind]: METRICS_STREAM_PROFILE_DEFINITION,
|
|
44
|
+
[OTEL_TRACES_STREAM_PROFILE_DEFINITION.kind]: OTEL_TRACES_STREAM_PROFILE_DEFINITION,
|
|
45
|
+
[STATE_PROTOCOL_STREAM_PROFILE_DEFINITION.kind]: STATE_PROTOCOL_STREAM_PROFILE_DEFINITION,
|
|
46
|
+
};
|
|
47
|
+
// New built-in profiles are wired here. Core runtime paths must resolve the
|
|
48
|
+
// definition and dispatch through its hooks rather than branching on profile
|
|
49
|
+
// kinds directly.
|
|
50
|
+
|
|
51
|
+
function supportedProfileKindsMessage(): string {
|
|
52
|
+
return listSupportedStreamProfileKinds().join("|");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listSupportedStreamProfileKinds(): string[] {
|
|
56
|
+
return Object.keys(STREAM_PROFILE_DEFINITIONS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveStreamProfileDefinition(kind: string | null | undefined): StreamProfileDefinition | null {
|
|
60
|
+
const normalized = typeof kind === "string" && kind !== "" ? kind : DEFAULT_STREAM_PROFILE;
|
|
61
|
+
return STREAM_PROFILE_DEFINITIONS[normalized] ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveStreamProfileDefinitionResult(
|
|
65
|
+
kind: string | null | undefined
|
|
66
|
+
): Result<StreamProfileDefinition, StreamProfileReadError> {
|
|
67
|
+
const normalized = typeof kind === "string" && kind !== "" ? kind : DEFAULT_STREAM_PROFILE;
|
|
68
|
+
const definition = resolveStreamProfileDefinition(normalized);
|
|
69
|
+
if (!definition) {
|
|
70
|
+
return Result.err({ kind: "invalid_profile", message: `unknown stream profile: ${normalized}` });
|
|
71
|
+
}
|
|
72
|
+
return Result.ok(definition);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cloneCachedProfile(cache: CachedStreamProfile | null): CachedStreamProfile | null {
|
|
76
|
+
if (!cache) return null;
|
|
77
|
+
return {
|
|
78
|
+
profile: cloneStreamProfileSpec(cache.profile),
|
|
79
|
+
updatedAtMs: cache.updatedAtMs,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseProfileUpdateResult(body: unknown): Result<StreamProfileSpec, { message: string }> {
|
|
84
|
+
const envelopeRes = parseProfileUpdateEnvelopeResult(body);
|
|
85
|
+
if (Result.isError(envelopeRes)) return envelopeRes;
|
|
86
|
+
const kindRes = readProfileKindResult(envelopeRes.value, "profile");
|
|
87
|
+
if (Result.isError(kindRes)) return kindRes;
|
|
88
|
+
const definition = resolveStreamProfileDefinition(kindRes.value);
|
|
89
|
+
if (!definition) {
|
|
90
|
+
return Result.err({ message: `profile.kind must be ${supportedProfileKindsMessage()}` });
|
|
91
|
+
}
|
|
92
|
+
return definition.validateResult(envelopeRes.value, "profile");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveTouchCapability(profile: StreamProfileSpec | null | undefined): StreamTouchCapability | null {
|
|
96
|
+
if (!profile) return null;
|
|
97
|
+
return resolveStreamProfileDefinition(profile.kind)?.touch ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveJsonIngestCapability(profile: StreamProfileSpec | null | undefined): StreamProfileJsonIngestCapability | null {
|
|
101
|
+
if (!profile) return null;
|
|
102
|
+
return resolveStreamProfileDefinition(profile.kind)?.jsonIngest ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveMetricsCapability(profile: StreamProfileSpec | null | undefined): StreamProfileMetricsCapability | null {
|
|
106
|
+
if (!profile) return null;
|
|
107
|
+
return resolveStreamProfileDefinition(profile.kind)?.metrics ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveOtlpTracesCapability(profile: StreamProfileSpec | null | undefined): StreamProfileOtlpTracesCapability | null {
|
|
111
|
+
if (!profile) return null;
|
|
112
|
+
return resolveStreamProfileDefinition(profile.kind)?.otlpTraces ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveCorrelationCapability(profile: StreamProfileSpec | null | undefined): StreamProfileCorrelationCapability | null {
|
|
116
|
+
if (!profile) return null;
|
|
117
|
+
return resolveStreamProfileDefinition(profile.kind)?.correlation ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveEnabledTouchCapability(
|
|
121
|
+
profile: StreamProfileSpec | null | undefined
|
|
122
|
+
): { capability: StreamTouchCapability; touchCfg: NonNullable<ReturnType<StreamTouchCapability["getTouchConfig"]>> } | null {
|
|
123
|
+
const capability = resolveTouchCapability(profile);
|
|
124
|
+
if (!profile || !capability) return null;
|
|
125
|
+
const touchCfg = capability.getTouchConfig(profile);
|
|
126
|
+
if (!touchCfg) return null;
|
|
127
|
+
return { capability, touchCfg };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function listTouchCapableProfileKinds(): string[] {
|
|
131
|
+
return Object.values(STREAM_PROFILE_DEFINITIONS)
|
|
132
|
+
.filter((definition) => !!definition.touch)
|
|
133
|
+
.map((definition) => definition.kind);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type StreamProfileUpdateResult = {
|
|
137
|
+
resource: StreamProfileResource;
|
|
138
|
+
schemaRegistry: SchemaRegistry | null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export class StreamProfileStore {
|
|
142
|
+
private readonly store: ProfileStore & StreamReadStore;
|
|
143
|
+
private readonly touchEnabled: boolean;
|
|
144
|
+
private readonly cache: LruCache<string, CachedStreamProfile>;
|
|
145
|
+
|
|
146
|
+
constructor(store: ProfileStore & StreamReadStore, opts?: { cacheEntries?: number; touchEnabled?: boolean }) {
|
|
147
|
+
this.store = store;
|
|
148
|
+
this.touchEnabled = opts?.touchEnabled === true;
|
|
149
|
+
this.cache = new LruCache(opts?.cacheEntries ?? 1024);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async loadRow(stream: string): Promise<StoredProfileRow | null> {
|
|
153
|
+
return this.store.getStreamProfileForRead(stream);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getProfile(stream: string, streamRow?: StreamRow | null): Promise<StreamProfileSpec> {
|
|
157
|
+
const res = await this.getProfileResult(stream, streamRow);
|
|
158
|
+
if (Result.isError(res)) throw dsError(res.error.message, { code: res.error.code });
|
|
159
|
+
return res.value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getProfileResult(stream: string, streamRow?: StreamRow | null): Promise<Result<StreamProfileSpec, StreamProfileReadError>> {
|
|
163
|
+
const srow = streamRow ?? await this.store.getStreamForRead(stream);
|
|
164
|
+
if (!srow) return Result.ok(GENERIC_STREAM_PROFILE_DEFINITION.defaultProfile());
|
|
165
|
+
|
|
166
|
+
const definitionRes = resolveStreamProfileDefinitionResult(srow.profile);
|
|
167
|
+
if (Result.isError(definitionRes)) return definitionRes;
|
|
168
|
+
|
|
169
|
+
const row = definitionRes.value.usesStoredProfileRow ? await this.loadRow(stream) : null;
|
|
170
|
+
const cached = cloneCachedProfile(this.cache.get(stream) ?? null);
|
|
171
|
+
const readRes = definitionRes.value.readProfileResult({
|
|
172
|
+
row,
|
|
173
|
+
cached: cached && cached.profile.kind === definitionRes.value.kind ? cached : null,
|
|
174
|
+
});
|
|
175
|
+
if (Result.isError(readRes)) {
|
|
176
|
+
return Result.err({ kind: "invalid_profile", message: readRes.error.message });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (readRes.value.cache) this.cache.set(stream, cloneCachedProfile(readRes.value.cache)!);
|
|
180
|
+
else this.cache.delete(stream);
|
|
181
|
+
return Result.ok(cloneStreamProfileSpec(readRes.value.profile));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getProfileResource(stream: string, streamRow?: StreamRow | null): Promise<StreamProfileResource> {
|
|
185
|
+
const res = await this.getProfileResourceResult(stream, streamRow);
|
|
186
|
+
if (Result.isError(res)) throw dsError(res.error.message, { code: res.error.code });
|
|
187
|
+
return res.value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getProfileResourceResult(stream: string, streamRow?: StreamRow | null): Promise<Result<StreamProfileResource, StreamProfileReadError>> {
|
|
191
|
+
const profileRes = await this.getProfileResult(stream, streamRow);
|
|
192
|
+
if (Result.isError(profileRes)) return profileRes;
|
|
193
|
+
return Result.ok(buildStreamProfileResource(profileRes.value));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async updateProfile(stream: string, profile: StreamProfileSpec): Promise<StreamProfileResource> {
|
|
197
|
+
const res = await this.updateProfileResult(stream, profile);
|
|
198
|
+
if (Result.isError(res)) throw dsError(res.error.message, { code: res.error.code });
|
|
199
|
+
return res.value.resource;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async updateProfileResult(stream: string, profile: StreamProfileSpec): Promise<Result<StreamProfileUpdateResult, StreamProfileMutationError>> {
|
|
203
|
+
const definition = resolveStreamProfileDefinition(profile.kind);
|
|
204
|
+
if (!definition) {
|
|
205
|
+
return Result.err({ kind: "bad_request", message: `profile.kind must be ${supportedProfileKindsMessage()}` });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const commitRes = await this.store.commitProfileMetadataMutation<StreamProfilePersistResult, StreamProfileMutationError>(stream, ({ streamRow }) => {
|
|
209
|
+
if (!streamRow) return Result.err({ kind: "bad_request", message: "stream not found" });
|
|
210
|
+
const persistRes = definition.persistProfileResult({ stream, streamRow, profile });
|
|
211
|
+
if (Result.isError(persistRes)) return persistRes;
|
|
212
|
+
if (persistRes.value.touchState !== "preserve" && !this.touchEnabled) {
|
|
213
|
+
return Result.err({ kind: "bad_request", message: `${persistRes.value.profile.kind} profile requires touch capability` });
|
|
214
|
+
}
|
|
215
|
+
return Result.ok({
|
|
216
|
+
metadata: {
|
|
217
|
+
streamProfile: persistRes.value.streamProfile,
|
|
218
|
+
profileJson: persistRes.value.profileJson,
|
|
219
|
+
schemaRegistry: persistRes.value.schemaRegistry ?? null,
|
|
220
|
+
touchState: persistRes.value.touchState,
|
|
221
|
+
},
|
|
222
|
+
value: persistRes.value,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
if (Result.isError(commitRes)) return commitRes;
|
|
226
|
+
|
|
227
|
+
const persist = commitRes.value.value;
|
|
228
|
+
const cache = persist.cache ? { ...persist.cache, updatedAtMs: commitRes.value.profileUpdatedAtMs } : null;
|
|
229
|
+
|
|
230
|
+
if (cache) this.cache.set(stream, cloneCachedProfile(cache)!);
|
|
231
|
+
else this.cache.delete(stream);
|
|
232
|
+
return Result.ok({
|
|
233
|
+
resource: buildStreamProfileResource(cloneStreamProfileSpec(persist.profile)),
|
|
234
|
+
schemaRegistry: commitRes.value.schemaRegistry,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { BinaryCursor, BinaryPayloadError, BinaryWriter, readI64 } from "../../search/binary/codec";
|
|
3
|
+
import { zstdCompressSync, zstdDecompressSync } from "../../util/zstd";
|
|
4
|
+
import type { MetricsBlockRecord } from "./normalize";
|
|
5
|
+
|
|
6
|
+
export type MetricsBlockSectionInput = {
|
|
7
|
+
record_count: number;
|
|
8
|
+
min_window_start_ms?: number;
|
|
9
|
+
max_window_end_ms?: number;
|
|
10
|
+
records: MetricsBlockRecord[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type MetricsBlockFormatError = { kind: "invalid_metrics_block"; message: string };
|
|
14
|
+
const METRICS_BLOCK_COMPRESSION_NONE = 0;
|
|
15
|
+
const METRICS_BLOCK_COMPRESSION_ZSTD = 1;
|
|
16
|
+
|
|
17
|
+
function invalidMetricsBlock<T = never>(message: string): Result<T, MetricsBlockFormatError> {
|
|
18
|
+
return Result.err({ kind: "invalid_metrics_block", message });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MetricsBlockSectionView {
|
|
22
|
+
private recordsCache: MetricsBlockRecord[] | null = null;
|
|
23
|
+
private decodedPayload: Uint8Array | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly recordCount: number,
|
|
27
|
+
readonly minWindowStartMs: number | null,
|
|
28
|
+
readonly maxWindowEndMs: number | null,
|
|
29
|
+
private readonly compression: number,
|
|
30
|
+
private readonly recordsPayload: Uint8Array
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
records(): MetricsBlockRecord[] {
|
|
34
|
+
if (!this.recordsCache) {
|
|
35
|
+
this.recordsCache = JSON.parse(new TextDecoder().decode(this.payloadBytes())) as MetricsBlockRecord[];
|
|
36
|
+
}
|
|
37
|
+
return this.recordsCache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private payloadBytes(): Uint8Array {
|
|
41
|
+
if (this.decodedPayload) return this.decodedPayload;
|
|
42
|
+
if (this.compression === METRICS_BLOCK_COMPRESSION_NONE) {
|
|
43
|
+
this.decodedPayload = this.recordsPayload;
|
|
44
|
+
return this.decodedPayload;
|
|
45
|
+
}
|
|
46
|
+
if (this.compression !== METRICS_BLOCK_COMPRESSION_ZSTD) {
|
|
47
|
+
throw new BinaryPayloadError(`unsupported metrics block compression ${this.compression}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
this.decodedPayload = new Uint8Array(zstdDecompressSync(this.recordsPayload));
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
throw new BinaryPayloadError(`invalid compressed metrics block payload: ${String((error as Error)?.message ?? error)}`);
|
|
53
|
+
}
|
|
54
|
+
return this.decodedPayload;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function encodeMetricsBlockSegmentCompanion(input: MetricsBlockSectionInput): Uint8Array {
|
|
59
|
+
const jsonPayload = new TextEncoder().encode(JSON.stringify(input.records));
|
|
60
|
+
const payload = compressPayload(jsonPayload);
|
|
61
|
+
const writer = new BinaryWriter();
|
|
62
|
+
writer.writeU32(input.record_count);
|
|
63
|
+
writer.writeI64(BigInt(input.min_window_start_ms ?? -1));
|
|
64
|
+
writer.writeI64(BigInt(input.max_window_end_ms ?? -1));
|
|
65
|
+
writer.writeU8(payload.compression);
|
|
66
|
+
writer.writeU8(0);
|
|
67
|
+
writer.writeU16(0);
|
|
68
|
+
writer.writeU32(payload.bytes.byteLength);
|
|
69
|
+
writer.writeBytes(payload.bytes);
|
|
70
|
+
return writer.finish();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function decodeMetricsBlockSegmentCompanionResult(bytes: Uint8Array): Result<MetricsBlockSectionView, MetricsBlockFormatError> {
|
|
74
|
+
try {
|
|
75
|
+
const cursor = new BinaryCursor(bytes);
|
|
76
|
+
const recordCount = cursor.readU32();
|
|
77
|
+
const minWindowStartMs = Number(readI64(bytes, 4));
|
|
78
|
+
const maxWindowEndMs = Number(readI64(bytes, 12));
|
|
79
|
+
cursor.readI64();
|
|
80
|
+
cursor.readI64();
|
|
81
|
+
const compression = cursor.readU8();
|
|
82
|
+
cursor.readU8();
|
|
83
|
+
cursor.readU16();
|
|
84
|
+
const payloadLength = cursor.readU32();
|
|
85
|
+
const payload = cursor.readBytes(payloadLength);
|
|
86
|
+
return Result.ok(
|
|
87
|
+
new MetricsBlockSectionView(
|
|
88
|
+
recordCount,
|
|
89
|
+
minWindowStartMs < 0 ? null : minWindowStartMs,
|
|
90
|
+
maxWindowEndMs < 0 ? null : maxWindowEndMs,
|
|
91
|
+
compression,
|
|
92
|
+
payload
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
} catch (e: unknown) {
|
|
96
|
+
return invalidMetricsBlock(String((e as any)?.message ?? e));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function compressPayload(jsonPayload: Uint8Array): { compression: number; bytes: Uint8Array } {
|
|
101
|
+
if (jsonPayload.byteLength === 0) {
|
|
102
|
+
return { compression: METRICS_BLOCK_COMPRESSION_NONE, bytes: jsonPayload };
|
|
103
|
+
}
|
|
104
|
+
const compressed = new Uint8Array(zstdCompressSync(jsonPayload));
|
|
105
|
+
if (compressed.byteLength >= jsonPayload.byteLength) {
|
|
106
|
+
return { compression: METRICS_BLOCK_COMPRESSION_NONE, bytes: jsonPayload };
|
|
107
|
+
}
|
|
108
|
+
return { compression: METRICS_BLOCK_COMPRESSION_ZSTD, bytes: compressed };
|
|
109
|
+
}
|