@prisma/streams-server 0.1.2 → 0.1.3

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.
Files changed (90) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime_memory.ts +200 -0
  45. package/src/runtime_memory_sampler.ts +235 -0
  46. package/src/schema/read_json.ts +43 -0
  47. package/src/schema/registry.ts +563 -59
  48. package/src/search/agg_format.ts +638 -0
  49. package/src/search/aggregate.ts +389 -0
  50. package/src/search/binary/codec.ts +162 -0
  51. package/src/search/binary/docset.ts +67 -0
  52. package/src/search/binary/restart_strings.ts +181 -0
  53. package/src/search/binary/varint.ts +34 -0
  54. package/src/search/bitset.ts +19 -0
  55. package/src/search/col_format.ts +382 -0
  56. package/src/search/col_runtime.ts +59 -0
  57. package/src/search/column_encoding.ts +43 -0
  58. package/src/search/companion_file_cache.ts +319 -0
  59. package/src/search/companion_format.ts +313 -0
  60. package/src/search/companion_manager.ts +1086 -0
  61. package/src/search/companion_plan.ts +218 -0
  62. package/src/search/fts_format.ts +423 -0
  63. package/src/search/fts_runtime.ts +333 -0
  64. package/src/search/query.ts +875 -0
  65. package/src/search/schema.ts +245 -0
  66. package/src/segment/cache.ts +93 -2
  67. package/src/segment/cached_segment.ts +89 -0
  68. package/src/segment/format.ts +108 -36
  69. package/src/segment/segmenter.ts +79 -5
  70. package/src/segment/segmenter_worker.ts +31 -5
  71. package/src/segment/segmenter_workers.ts +40 -12
  72. package/src/server.ts +150 -36
  73. package/src/sqlite/adapter.ts +155 -8
  74. package/src/sqlite/runtime_stats.ts +163 -0
  75. package/src/stats.ts +3 -3
  76. package/src/stream_size_reconciler.ts +100 -0
  77. package/src/touch/canonical_change.ts +7 -0
  78. package/src/touch/live_metrics.ts +94 -64
  79. package/src/touch/live_templates.ts +15 -1
  80. package/src/touch/manager.ts +166 -88
  81. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +13 -13
  82. package/src/touch/spec.ts +95 -92
  83. package/src/touch/touch_journal.ts +4 -0
  84. package/src/touch/worker_pool.ts +6 -13
  85. package/src/touch/worker_protocol.ts +3 -3
  86. package/src/uploader.ts +77 -6
  87. package/src/util/bloom256.ts +2 -2
  88. package/src/util/byte_lru.ts +73 -0
  89. package/src/util/lru.ts +8 -0
  90. package/src/util/stream_paths.ts +19 -0
@@ -0,0 +1,85 @@
1
+ import { Result } from "better-result";
2
+ import type {
3
+ StreamProfileDefinition,
4
+ StreamProfilePersistResult,
5
+ StreamProfileReadResult,
6
+ } from "./profile";
7
+ import { cloneStreamProfileSpec, expectPlainObjectResult, rejectUnknownKeysResult, normalizeProfileContentType } from "./profile";
8
+ import { buildInternalMetricsRegistry, buildMetricsDefaultRegistry } from "./metrics/schema";
9
+ import { normalizeMetricsRecordResult } from "./metrics/normalize";
10
+
11
+ const INTERNAL_METRICS_STREAM = "__stream_metrics__";
12
+
13
+ export type MetricsStreamProfile = {
14
+ kind: "metrics";
15
+ };
16
+
17
+ function cloneMetricsProfile(): MetricsStreamProfile {
18
+ return { kind: "metrics" };
19
+ }
20
+
21
+ function validateMetricsProfileResult(raw: unknown, path: string): Result<MetricsStreamProfile, { message: string }> {
22
+ const objRes = expectPlainObjectResult(raw, path);
23
+ if (Result.isError(objRes)) return objRes;
24
+ if (objRes.value.kind !== "metrics") return Result.err({ message: `${path}.kind must be metrics` });
25
+ const keyCheck = rejectUnknownKeysResult(objRes.value, ["kind"], path);
26
+ if (Result.isError(keyCheck)) return keyCheck;
27
+ return Result.ok(cloneMetricsProfile());
28
+ }
29
+
30
+ export const METRICS_STREAM_PROFILE_DEFINITION: StreamProfileDefinition = {
31
+ kind: "metrics",
32
+ usesStoredProfileRow: false,
33
+
34
+ defaultProfile(): MetricsStreamProfile {
35
+ return cloneMetricsProfile();
36
+ },
37
+
38
+ validateResult(raw, path) {
39
+ return validateMetricsProfileResult(raw, path);
40
+ },
41
+
42
+ readProfileResult(): Result<StreamProfileReadResult, { message: string }> {
43
+ return Result.ok({ profile: cloneMetricsProfile(), cache: null });
44
+ },
45
+
46
+ persistProfileResult({ db, registry, stream, streamRow, profile }): Result<StreamProfilePersistResult, { kind: "bad_request"; message: string }> {
47
+ if (profile.kind !== "metrics") return Result.err({ kind: "bad_request", message: "invalid metrics profile" });
48
+ const contentType = normalizeProfileContentType(streamRow.content_type);
49
+ if (contentType !== "application/json") {
50
+ return Result.err({
51
+ kind: "bad_request",
52
+ message: "metrics profile requires application/json stream content-type",
53
+ });
54
+ }
55
+ const desiredRegistry =
56
+ stream === INTERNAL_METRICS_STREAM ? buildInternalMetricsRegistry(stream) : buildMetricsDefaultRegistry(stream);
57
+ const registryRes = registry.replaceRegistryResult(stream, desiredRegistry);
58
+ if (Result.isError(registryRes)) return Result.err({ kind: "bad_request", message: registryRes.error.message });
59
+ db.updateStreamProfile(stream, "metrics");
60
+ db.deleteStreamProfile(stream);
61
+ db.deleteStreamTouchState(stream);
62
+ return Result.ok({
63
+ profile: cloneStreamProfileSpec(cloneMetricsProfile()),
64
+ cache: null,
65
+ schemaRegistry: registryRes.value,
66
+ });
67
+ },
68
+
69
+ jsonIngest: {
70
+ prepareRecordResult({ value }) {
71
+ const normalizedRes = normalizeMetricsRecordResult(value);
72
+ if (Result.isError(normalizedRes)) return normalizedRes;
73
+ return Result.ok({
74
+ value: normalizedRes.value.value,
75
+ routingKey: normalizedRes.value.routingKey,
76
+ });
77
+ },
78
+ },
79
+
80
+ metrics: {
81
+ normalizeRecordResult({ value }) {
82
+ return normalizeMetricsRecordResult(value);
83
+ },
84
+ },
85
+ };
@@ -0,0 +1,225 @@
1
+ import { Result } from "better-result";
2
+ import type { SqliteDurableStore, StreamRow } from "../db/db";
3
+ import type { SchemaRegistry, SchemaRegistryStore } from "../schema/registry";
4
+ import type { TouchProcessorManager } from "../touch/manager";
5
+ import type { CanonicalChange } from "../touch/canonical_change";
6
+ import type { TouchConfig } from "../touch/spec";
7
+ import type { AggSummaryState } from "../search/agg_format";
8
+
9
+ export const STREAM_PROFILE_API_VERSION = "durable.streams/profile/v1" as const;
10
+ export const DEFAULT_STREAM_PROFILE = "generic" as const;
11
+
12
+ export type StreamProfileKind = string;
13
+
14
+ export type StreamProfileSpec = {
15
+ kind: StreamProfileKind;
16
+ [key: string]: unknown;
17
+ };
18
+
19
+ export type StreamProfileResource = {
20
+ apiVersion: typeof STREAM_PROFILE_API_VERSION;
21
+ profile: StreamProfileSpec;
22
+ };
23
+
24
+ export type StreamProfileMutationError = {
25
+ kind: "bad_request";
26
+ message: string;
27
+ code?: string;
28
+ };
29
+
30
+ export type StreamProfileReadError = {
31
+ kind: "invalid_profile";
32
+ message: string;
33
+ code?: string;
34
+ };
35
+
36
+ export type StreamProfileValidationError = {
37
+ message: string;
38
+ };
39
+
40
+ export type StoredProfileRow = {
41
+ stream: string;
42
+ profile_json: string;
43
+ updated_at_ms: bigint;
44
+ };
45
+
46
+ export type CachedStreamProfile = {
47
+ profile: StreamProfileSpec;
48
+ updatedAtMs: bigint;
49
+ };
50
+
51
+ export type StreamProfileReadResult = {
52
+ profile: StreamProfileSpec;
53
+ cache: CachedStreamProfile | null;
54
+ };
55
+
56
+ export type StreamProfilePersistResult = {
57
+ profile: StreamProfileSpec;
58
+ cache: CachedStreamProfile | null;
59
+ schemaRegistry?: SchemaRegistry | null;
60
+ };
61
+
62
+ export type PersistProfileArgs = {
63
+ db: SqliteDurableStore;
64
+ registry: SchemaRegistryStore;
65
+ stream: string;
66
+ streamRow: StreamRow;
67
+ profile: StreamProfileSpec;
68
+ };
69
+
70
+ export type PreparedJsonRecord = {
71
+ value: unknown;
72
+ routingKey: string | null;
73
+ };
74
+
75
+ export type MetricsCompanionRecord = {
76
+ metric: string;
77
+ unit: string;
78
+ metricKind: string;
79
+ temporality: string;
80
+ windowStartMs: number;
81
+ windowEndMs: number;
82
+ intervalMs: number;
83
+ stream: string | null;
84
+ instance: string | null;
85
+ attributes: Record<string, string>;
86
+ dimensionPairs: string[];
87
+ dimensionKey: string | null;
88
+ seriesKey: string;
89
+ summary: AggSummaryState;
90
+ };
91
+
92
+ export type NormalizedMetricsRecord = PreparedJsonRecord & {
93
+ value: Record<string, unknown>;
94
+ companion: MetricsCompanionRecord;
95
+ };
96
+
97
+ export type StreamTouchRoute =
98
+ | { kind: "meta" }
99
+ | { kind: "wait" }
100
+ | { kind: "templates_activate" };
101
+
102
+ export type StreamProfileTouchResponder = {
103
+ json(status: number, body: any, headers?: HeadersInit): Response;
104
+ badRequest(message: string): Response;
105
+ internalError(message?: string): Response;
106
+ notFound(message?: string): Response;
107
+ };
108
+
109
+ export type StreamTouchRouteArgs = {
110
+ route: StreamTouchRoute;
111
+ req: Request;
112
+ stream: string;
113
+ streamRow: StreamRow;
114
+ profile: StreamProfileSpec;
115
+ db: SqliteDurableStore;
116
+ touchManager: TouchProcessorManager;
117
+ respond: StreamProfileTouchResponder;
118
+ };
119
+
120
+ export interface StreamTouchCapability {
121
+ getTouchConfig(profile: StreamProfileSpec): TouchConfig | null;
122
+ syncState(args: { db: SqliteDurableStore; stream: string; profile: StreamProfileSpec }): void;
123
+ deriveCanonicalChanges(record: unknown, profile: StreamProfileSpec): CanonicalChange[];
124
+ handleRoute?(args: StreamTouchRouteArgs): Promise<Response>;
125
+ }
126
+
127
+ export interface StreamProfileJsonIngestCapability {
128
+ prepareRecordResult(args: { stream: string; profile: StreamProfileSpec; value: unknown }): Result<PreparedJsonRecord, StreamProfileValidationError>;
129
+ }
130
+
131
+ export interface StreamProfileMetricsCapability {
132
+ normalizeRecordResult(args: {
133
+ stream: string;
134
+ profile: StreamProfileSpec;
135
+ value: unknown;
136
+ }): Result<NormalizedMetricsRecord, StreamProfileValidationError>;
137
+ }
138
+
139
+ export interface StreamProfileDefinition {
140
+ kind: StreamProfileKind;
141
+ usesStoredProfileRow: boolean;
142
+ defaultProfile(): StreamProfileSpec;
143
+ validateResult(raw: unknown, path: string): Result<StreamProfileSpec, StreamProfileValidationError>;
144
+ readProfileResult(args: { row: StoredProfileRow | null; cached: CachedStreamProfile | null }): Result<StreamProfileReadResult, StreamProfileValidationError>;
145
+ persistProfileResult(args: PersistProfileArgs): Result<StreamProfilePersistResult, StreamProfileMutationError>;
146
+ touch?: StreamTouchCapability;
147
+ jsonIngest?: StreamProfileJsonIngestCapability;
148
+ metrics?: StreamProfileMetricsCapability;
149
+ }
150
+
151
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
152
+ return !!value && typeof value === "object" && !Array.isArray(value);
153
+ }
154
+
155
+ export function expectPlainObjectResult(
156
+ value: unknown,
157
+ path: string
158
+ ): Result<Record<string, unknown>, StreamProfileValidationError> {
159
+ if (!isPlainObject(value)) return Result.err({ message: `${path} must be an object` });
160
+ return Result.ok(value);
161
+ }
162
+
163
+ export function rejectUnknownKeysResult(
164
+ obj: Record<string, unknown>,
165
+ allowed: readonly string[],
166
+ path: string
167
+ ): Result<void, StreamProfileValidationError> {
168
+ const allowedSet = new Set(allowed);
169
+ for (const key of Object.keys(obj)) {
170
+ if (!allowedSet.has(key)) return Result.err({ message: `${path}.${key} is not supported` });
171
+ }
172
+ return Result.ok(undefined);
173
+ }
174
+
175
+ export function normalizeProfileContentType(value: string | null): string | null {
176
+ if (!value) return null;
177
+ const base = value.split(";")[0]?.trim().toLowerCase();
178
+ return base ? base : null;
179
+ }
180
+
181
+ export function parseStoredProfileJsonResult(raw: string): Result<unknown, StreamProfileValidationError> {
182
+ try {
183
+ return Result.ok(JSON.parse(raw));
184
+ } catch (e: any) {
185
+ return Result.err({ message: String(e?.message ?? e) });
186
+ }
187
+ }
188
+
189
+ export function readProfileKindResult(
190
+ raw: unknown,
191
+ path = "profile"
192
+ ): Result<StreamProfileKind, StreamProfileValidationError> {
193
+ const objRes = expectPlainObjectResult(raw, path);
194
+ if (Result.isError(objRes)) return objRes;
195
+ const kind = typeof objRes.value.kind === "string" ? objRes.value.kind.trim() : "";
196
+ if (kind !== "") return Result.ok(kind);
197
+ return Result.err({ message: `${path}.kind must be a non-empty string` });
198
+ }
199
+
200
+ export function parseProfileUpdateEnvelopeResult(body: unknown): Result<unknown, StreamProfileValidationError> {
201
+ const bodyRes = expectPlainObjectResult(body, "profile update");
202
+ if (Result.isError(bodyRes)) {
203
+ return Result.err({ message: "profile update must be a JSON object" });
204
+ }
205
+ const keyCheck = rejectUnknownKeysResult(bodyRes.value, ["apiVersion", "profile"], "profileUpdate");
206
+ if (Result.isError(keyCheck)) return keyCheck;
207
+ if (bodyRes.value.apiVersion !== undefined && bodyRes.value.apiVersion !== STREAM_PROFILE_API_VERSION) {
208
+ return Result.err({ message: "invalid profile apiVersion" });
209
+ }
210
+ if (!Object.prototype.hasOwnProperty.call(bodyRes.value, "profile")) {
211
+ return Result.err({ message: "missing profile" });
212
+ }
213
+ return Result.ok(bodyRes.value.profile);
214
+ }
215
+
216
+ export function cloneStreamProfileSpec(profile: StreamProfileSpec): StreamProfileSpec {
217
+ return structuredClone(profile);
218
+ }
219
+
220
+ export function buildStreamProfileResource(profile: StreamProfileSpec): StreamProfileResource {
221
+ return {
222
+ apiVersion: STREAM_PROFILE_API_VERSION,
223
+ profile: cloneStreamProfileSpec(profile),
224
+ };
225
+ }
@@ -1,23 +1,10 @@
1
- import type { StreamInterpreterConfig } from "./spec.ts";
1
+ import type { CanonicalChange } from "../../touch/canonical_change";
2
2
 
3
- export type CanonicalChange = {
4
- entity: string;
5
- key?: string;
6
- op: "insert" | "update" | "delete";
7
- before?: unknown;
8
- after?: unknown;
9
- };
10
-
11
- export function interpretRecordToChanges(record: any, _cfg: StreamInterpreterConfig): CanonicalChange[] {
12
- return interpretStateProtocolRecord(record);
13
- }
14
-
15
- function interpretStateProtocolRecord(record: any): CanonicalChange[] {
3
+ export function deriveStateProtocolChanges(record: unknown): CanonicalChange[] {
16
4
  if (!record || typeof record !== "object" || Array.isArray(record)) return [];
17
5
  const headers = (record as any).headers;
18
6
  if (!headers || typeof headers !== "object" || Array.isArray(headers)) return [];
19
7
 
20
- // Control messages are ignored by touch derivation.
21
8
  if (typeof (headers as any).control === "string") return [];
22
9
 
23
10
  const opRaw = (headers as any).operation;
@@ -30,11 +17,7 @@ function interpretStateProtocolRecord(record: any): CanonicalChange[] {
30
17
  if (typeof type !== "string" || type.trim() === "") return [];
31
18
  if (typeof key !== "string" || key.trim() === "") return [];
32
19
 
33
- const before = Object.prototype.hasOwnProperty.call(record, "oldValue")
34
- ? (record as any).oldValue
35
- : Object.prototype.hasOwnProperty.call(record, "old_value")
36
- ? (record as any).old_value
37
- : undefined;
20
+ const before = Object.prototype.hasOwnProperty.call(record, "oldValue") ? (record as any).oldValue : undefined;
38
21
  const after = Object.prototype.hasOwnProperty.call(record, "value") ? (record as any).value : undefined;
39
22
 
40
23
  return [{ entity: type, key, op, before, after }];