sliftutils 1.4.48 → 1.4.50

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.
@@ -0,0 +1,273 @@
1
+ import { LoadedIndex } from "./LoadedIndex";
2
+ import { DELETED, WriteOverlay } from "./WriteOverlay";
3
+ import type { ReactiveDeps } from "./BulkDatabaseBase";
4
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
5
+ import { blue, red } from "socket-function/src/formatting/logColors";
6
+
7
+ const NULL = String.fromCharCode(0);
8
+ const LOAD_SIGNAL = NULL + "load";
9
+ const OVERLAY_SIGNAL = NULL + "overlay";
10
+ const TRIGGER_THROTTLE_FIRST_STEP_MS = 16;
11
+
12
+ function nullJoin(a: string, b: string): string {
13
+ return a + NULL + b;
14
+ }
15
+
16
+ export type ReaderConfig = {
17
+ name: string;
18
+ deps: ReactiveDeps;
19
+ // See BulkDatabase2Config.maxTriggerThrottleMs.
20
+ maxTriggerThrottleMs?: number;
21
+ };
22
+
23
+ // Owns the current LoadedIndex and WriteOverlay and serves every read (async + sync reactive). Apply
24
+ // writes/deletes through applyWrite/applyDelete so the right signals fire. The host swaps in a fresh
25
+ // LoadedIndex atomically via setIndex once the new one has fully built — no lazy rebuild on the next
26
+ // read, no synchronous lag spike.
27
+ export class BulkDatabaseReader<T extends { key: string }> {
28
+ constructor(private readonly cfg: ReaderConfig) { }
29
+
30
+ public index: LoadedIndex<T> | undefined;
31
+ public readonly overlay = new WriteOverlay();
32
+
33
+ private dataGen = 0;
34
+ private columnCache = new Map<string, { key: string; value: unknown; time: number }[]>();
35
+
36
+ private pendingSignals = new Set<string>();
37
+ private triggerTimer: ReturnType<typeof setTimeout> | undefined;
38
+ private currentTriggerDelay = 0;
39
+ private lastTriggerTime = 0;
40
+
41
+ get name(): string { return this.cfg.name; }
42
+ get deps(): ReactiveDeps { return this.cfg.deps; }
43
+ get dataGeneration(): number { return this.dataGen; }
44
+
45
+ setIndex(newIndex: LoadedIndex<T>, options: { dropStaleFallback?: boolean } = {}): void {
46
+ const prev = this.index;
47
+ if (prev && !options.dropStaleFallback) newIndex.inheritStaleFrom(prev);
48
+ this.cfg.deps.batch(() => {
49
+ this.index = newIndex;
50
+ this.overlay.sweepCovered(key => Math.max(
51
+ newIndex.reader.keyTimes.get(key) ?? -Infinity,
52
+ newIndex.reader.deleteTimes.get(key) ?? -Infinity,
53
+ ));
54
+ this.dataGen++;
55
+ this.columnCache.clear();
56
+ this.invalidateSignal(LOAD_SIGNAL);
57
+ this.invalidateSignal(OVERLAY_SIGNAL);
58
+ });
59
+ }
60
+
61
+ applyWrite(key: string, row: Record<string, unknown>, time: number): void {
62
+ const wasLive = this.isLiveNow(key);
63
+ const { invalidatedColumns } = this.overlay.writeRow(key, row, time, wasLive);
64
+ this.notifyOverlayMutation(key, invalidatedColumns);
65
+ }
66
+
67
+ applyDelete(key: string, time: number): void {
68
+ const wasLive = this.isLiveNow(key);
69
+ const { invalidatedColumns } = this.overlay.deleteKey(key, time, wasLive);
70
+ this.notifyOverlayMutation(key, invalidatedColumns);
71
+ }
72
+
73
+ isKeyWatched(key: string): boolean {
74
+ return this.cfg.deps.isObserved?.(key) ?? true;
75
+ }
76
+
77
+ isLiveNow(key: string): boolean {
78
+ const e = this.overlay.get(key);
79
+ if (e) return e.value !== DELETED;
80
+ return this.index?.isLive(key) ?? false;
81
+ }
82
+
83
+ localTime(key: string): number {
84
+ const e = this.overlay.get(key);
85
+ if (e) return e.time;
86
+ const st = this.index?.streamTimes.get(key);
87
+ if (st !== undefined) return st;
88
+ return -Infinity;
89
+ }
90
+
91
+ private notifyOverlayMutation(key: string, columns: Iterable<string> | "all"): void {
92
+ this.dataGen++;
93
+ if (columns === "all") this.columnCache.clear();
94
+ else for (const c of columns) this.columnCache.delete(c);
95
+ if (this.cfg.deps.isObserved?.(key) ?? true) this.invalidateSignal(key);
96
+ this.invalidateSignal(OVERLAY_SIGNAL);
97
+ }
98
+
99
+ // ── async reads ──────────────────────────────────────────────────────────────────────────────────
100
+ async getKeys(): Promise<string[]> {
101
+ const index = await this.requireIndex();
102
+ if (this.overlay.size === 0) return [...index.keys];
103
+ const set = new Set(index.keys);
104
+ for (const [key, entry] of this.overlay) {
105
+ if (entry.value === DELETED) set.delete(key);
106
+ else set.add(key);
107
+ }
108
+ return [...set];
109
+ }
110
+
111
+ async getColumn<C extends keyof T>(column: C): Promise<{ key: string; value: T[C]; time: number }[]> {
112
+ const col = String(column);
113
+ const cached = this.columnCache.get(col);
114
+ if (cached) return cached as { key: string; value: T[C]; time: number }[];
115
+ const gen = this.dataGen;
116
+ const start = Date.now();
117
+ const index = await this.requireIndex();
118
+ let base = await index.getColumn(col);
119
+ let result = this.overlay.patchColumn(base, col) as { key: string; value: T[C]; time: number }[];
120
+ const elapsed = Date.now() - start;
121
+ if (elapsed > 50) {
122
+ console.log(`${blue(`${this.cfg.name}.getColumn(${JSON.stringify(column)})`)} took ${red(formatTime(elapsed))} ${this.formatInfo(index)}`);
123
+ }
124
+ Object.freeze(result);
125
+ if (this.dataGen === gen) this.columnCache.set(col, result);
126
+ return result;
127
+ }
128
+
129
+ async getSingleField<C extends keyof T>(key: string, column: C): Promise<T[C] | undefined> {
130
+ return (await this.getSingleFieldObj(key, column))?.value;
131
+ }
132
+
133
+ async getSingleFieldObj<C extends keyof T>(key: string, column: C): Promise<{ key: string; value: T[C]; time: number } | undefined> {
134
+ const col = String(column);
135
+ const entry = this.overlay.get(key);
136
+ if (entry !== undefined) {
137
+ if (entry.value === DELETED) return undefined;
138
+ if (col in entry.value) return { key, value: entry.value[col] as T[C], time: entry.time };
139
+ }
140
+ const start = Date.now();
141
+ const index = await this.requireIndex();
142
+ const r = await index.getSingleField(key, col);
143
+ const elapsed = Date.now() - start;
144
+ if (elapsed > 50) {
145
+ console.log(`${blue(`${this.cfg.name}.getSingleFieldObj(${JSON.stringify(key)}, ${JSON.stringify(column)})`)} took ${red(formatTime(elapsed))} ${this.formatInfo(index)}`);
146
+ }
147
+ if (r === undefined) {
148
+ if (entry !== undefined && entry.value !== DELETED) return { key, value: undefined as T[C], time: entry.time };
149
+ return undefined;
150
+ }
151
+ return { key, value: r.value as T[C], time: r.time };
152
+ }
153
+
154
+ // ── sync (reactive) reads ────────────────────────────────────────────────────────────────────────
155
+ getSingleFieldSync<C extends keyof T>(key: string, column: C): T[C] | undefined {
156
+ return this.getSingleFieldObjSync(key, column)?.value;
157
+ }
158
+
159
+ getSingleFieldObjSync<C extends keyof T>(key: string, column: C): { key: string; value: T[C]; time: number } | undefined {
160
+ this.cfg.deps.observe(LOAD_SIGNAL);
161
+ this.cfg.deps.observe(key);
162
+ const col = String(column);
163
+ const entry = this.overlay.get(key);
164
+ if (entry !== undefined) {
165
+ if (entry.value === DELETED) return undefined;
166
+ if (col in entry.value) {
167
+ this.index?.ensureBaseField(key, col, () => this.invalidateSignal(LOAD_SIGNAL));
168
+ return { key, value: entry.value[col] as T[C], time: entry.time };
169
+ }
170
+ }
171
+ const index = this.index;
172
+ if (!index) {
173
+ if (entry !== undefined && entry.value !== DELETED) return { key, value: undefined as T[C], time: entry.time };
174
+ return undefined;
175
+ }
176
+ const base = index.getBaseField(key, col);
177
+ // Trigger load if not loaded OR loaded only from stale fallback (refresh needed).
178
+ if (!base.loaded || !base.fresh) index.ensureBaseField(key, col, () => this.invalidateSignal(LOAD_SIGNAL));
179
+ if (!base.loaded || base.value === undefined) {
180
+ if (entry !== undefined && entry.value !== DELETED) return { key, value: undefined as T[C], time: entry.time };
181
+ return undefined;
182
+ }
183
+ return { key, value: base.value.value as T[C], time: base.value.time };
184
+ }
185
+
186
+ getColumnSync<C extends keyof T>(column: C): { key: string; value: T[C]; time: number }[] | undefined {
187
+ this.cfg.deps.observe(LOAD_SIGNAL);
188
+ this.cfg.deps.observe(OVERLAY_SIGNAL);
189
+ const col = String(column);
190
+ const cached = this.columnCache.get(col);
191
+ if (cached) return cached as { key: string; value: T[C]; time: number }[];
192
+ const index = this.index;
193
+ if (!index) return undefined;
194
+ const base = index.getBaseColumn(col);
195
+ // Trigger load if not fresh (genuine first load OR a stale fallback awaiting refresh).
196
+ if (!base || !base.fresh) index.ensureBaseColumn(col, () => this.invalidateSignal(LOAD_SIGNAL));
197
+ if (!base) return undefined;
198
+ const result = this.overlay.patchColumn(base.entries, col) as { key: string; value: T[C]; time: number }[];
199
+ if (base.fresh) {
200
+ Object.freeze(result);
201
+ this.columnCache.set(col, result);
202
+ }
203
+ return result;
204
+ }
205
+
206
+ isFieldLoadedSync<C extends keyof T>(key: string, column: C): boolean {
207
+ this.cfg.deps.observe(LOAD_SIGNAL);
208
+ this.cfg.deps.observe(key);
209
+ const entry = this.overlay.get(key);
210
+ if (entry !== undefined) {
211
+ if (entry.value === DELETED) return true;
212
+ if (String(column) in entry.value) return true;
213
+ }
214
+ const index = this.index;
215
+ if (!index) return false;
216
+ if (index.isBaseFieldLoaded(key, String(column))) return true;
217
+ index.ensureBaseField(key, String(column), () => this.invalidateSignal(LOAD_SIGNAL));
218
+ return false;
219
+ }
220
+
221
+ isColumnLoadedSync<C extends keyof T>(column: C): boolean {
222
+ this.cfg.deps.observe(LOAD_SIGNAL);
223
+ this.cfg.deps.observe(OVERLAY_SIGNAL);
224
+ const col = String(column);
225
+ if (this.columnCache.has(col)) return true;
226
+ const index = this.index;
227
+ if (!index) return false;
228
+ if (index.isBaseColumnLoaded(col)) return true;
229
+ index.ensureBaseColumn(col, () => this.invalidateSignal(LOAD_SIGNAL));
230
+ return false;
231
+ }
232
+
233
+ // ── helpers ──────────────────────────────────────────────────────────────────────────────────────
234
+ // Both async reads await this. The host wires an `ensureIndex` that triggers the initial build if
235
+ // no index is loaded yet — reads that race the first load all wait on the same promise.
236
+ setEnsureIndex(fn: () => Promise<LoadedIndex<T>>): void { this.ensureIndexFn = fn; }
237
+ private ensureIndexFn: (() => Promise<LoadedIndex<T>>) | undefined;
238
+ private async requireIndex(): Promise<LoadedIndex<T>> {
239
+ if (this.index) return this.index;
240
+ if (!this.ensureIndexFn) throw new Error(`${this.cfg.name}: index not set — call setIndex(...) before reading`);
241
+ return this.ensureIndexFn();
242
+ }
243
+
244
+ private formatInfo(index: LoadedIndex<T>): string {
245
+ return `(collection has ${blue(formatNumber(index.reader.rowCount))} rows, ${blue(formatNumber(index.reader.totalBytes))}B)`;
246
+ }
247
+
248
+ // Notifications are rampingly delayed under sustained changes (so a high-rate writer can't re-run
249
+ // watchers per change). Underlying data is always current — only the observable notification is
250
+ // batched/delayed.
251
+ private invalidateSignal(signal: string): void {
252
+ const maxMs = this.cfg.maxTriggerThrottleMs ?? 0;
253
+ if (maxMs <= 0) { this.cfg.deps.invalidate(signal); return; }
254
+ this.pendingSignals.add(signal);
255
+ const now = Date.now();
256
+ const lull = now - this.lastTriggerTime > maxMs;
257
+ this.lastTriggerTime = now;
258
+ if (this.triggerTimer !== undefined) return;
259
+ this.currentTriggerDelay = lull ? 0 : Math.min(maxMs, Math.max(TRIGGER_THROTTLE_FIRST_STEP_MS, this.currentTriggerDelay * 2));
260
+ this.triggerTimer = setTimeout(() => { this.triggerTimer = undefined; this.flushSignals(); }, this.currentTriggerDelay);
261
+ (this.triggerTimer as { unref?: () => void }).unref?.();
262
+ }
263
+
264
+ private flushSignals(): void {
265
+ if (this.pendingSignals.size === 0) return;
266
+ const signals = [...this.pendingSignals];
267
+ this.pendingSignals.clear();
268
+ this.cfg.deps.batch(() => { for (const s of signals) this.cfg.deps.invalidate(s); });
269
+ }
270
+ }
271
+
272
+ export const READER_SIGNALS = { LOAD: LOAD_SIGNAL, OVERLAY: OVERLAY_SIGNAL };
273
+ export { nullJoin };
@@ -0,0 +1,133 @@
1
+ import type { FileStorage } from "../FileFolderAPI";
2
+ import { BaseBulkDatabaseReader } from "./BulkDatabaseFormat";
3
+ import { GetRange } from "./blockCache";
4
+ import { StreamEntry } from "./streamLog";
5
+ export type BulkFileInfo = {
6
+ fileName: string;
7
+ level: number;
8
+ timestamp: number;
9
+ };
10
+ export type StreamFileInfo = {
11
+ fileName: string;
12
+ timestamp: number;
13
+ };
14
+ export type StreamReaderCacheEntry = {
15
+ readSize: number;
16
+ parsedPos: number;
17
+ entries: StreamEntry[];
18
+ };
19
+ export declare class MissingFileError extends Error {
20
+ }
21
+ export type ResolvedReader = {
22
+ rowCount: number;
23
+ totalBytes: number;
24
+ keys: string[];
25
+ rawKeyCount: number;
26
+ readerCount: number;
27
+ columns: {
28
+ column: string;
29
+ byteSize: number;
30
+ }[];
31
+ keyTimes: Map<string, number>;
32
+ deleteTimes: Map<string, number>;
33
+ getColumn: (column: string) => Promise<{
34
+ key: string;
35
+ value: unknown;
36
+ time: number;
37
+ }[]>;
38
+ getSingleField: (key: string, column: string) => Promise<{
39
+ value: unknown;
40
+ time: number;
41
+ } | undefined>;
42
+ };
43
+ export type SubReaderCaches = {
44
+ bulk: Map<string, BaseBulkDatabaseReader>;
45
+ stream: Map<string, StreamReaderCacheEntry>;
46
+ };
47
+ export declare class LoadedIndex<T extends {
48
+ key: string;
49
+ }> {
50
+ readonly name: string;
51
+ readonly storage: FileStorage;
52
+ readonly bulkFiles: BulkFileInfo[];
53
+ readonly streamFiles: StreamFileInfo[];
54
+ readonly reader: ResolvedReader;
55
+ readonly streamTimes: Map<string, number>;
56
+ readonly streamSizes: Map<string, number>;
57
+ readonly streamRowsOnDisk: number;
58
+ readonly streamBytesOnDisk: number;
59
+ readonly subCaches: SubReaderCaches;
60
+ private constructor();
61
+ readonly keys: Set<string>;
62
+ readonly fileSet: Set<string>;
63
+ private baseColumns;
64
+ private baseColumnsLoading;
65
+ private baseFields;
66
+ private baseFieldsLoading;
67
+ private staleBaseColumns;
68
+ private staleBaseFields;
69
+ get totalBytes(): number;
70
+ get rowCount(): number;
71
+ isLive(key: string): boolean;
72
+ static build<T extends {
73
+ key: string;
74
+ }>(config: {
75
+ name: string;
76
+ storage: FileStorage;
77
+ bulkFiles: BulkFileInfo[];
78
+ streamFiles: StreamFileInfo[];
79
+ subCaches: SubReaderCaches;
80
+ onUnreadableFile?: (file: BulkFileInfo, message: string) => Promise<void>;
81
+ }): Promise<LoadedIndex<T>>;
82
+ inheritStaleFrom(prev: LoadedIndex<T>): void;
83
+ getColumn(column: string): Promise<{
84
+ key: string;
85
+ value: unknown;
86
+ time: number;
87
+ }[]>;
88
+ getSingleField(key: string, column: string): Promise<{
89
+ value: unknown;
90
+ time: number;
91
+ } | undefined>;
92
+ ensureBaseColumn(column: string, onLoaded: () => void): void;
93
+ ensureBaseField(key: string, column: string, onLoaded: () => void): void;
94
+ getBaseColumn(column: string): {
95
+ entries: {
96
+ key: string;
97
+ value: unknown;
98
+ time: number;
99
+ }[];
100
+ fresh: boolean;
101
+ } | undefined;
102
+ getBaseField(key: string, column: string): {
103
+ value: {
104
+ value: unknown;
105
+ time: number;
106
+ } | undefined;
107
+ fresh: boolean;
108
+ loaded: boolean;
109
+ };
110
+ isBaseColumnLoaded(column: string): boolean;
111
+ isBaseFieldLoaded(key: string, column: string): boolean;
112
+ dropLoadedValues(): void;
113
+ }
114
+ export declare function makeRawGetRange(storage: FileStorage, fileName: string): Promise<{
115
+ rawGetRange: GetRange;
116
+ size: number;
117
+ }>;
118
+ export declare function loadFileReader(name: string, storage: FileStorage, f: BulkFileInfo, cache: Map<string, BaseBulkDatabaseReader>): Promise<BaseBulkDatabaseReader>;
119
+ export declare function loadStreamEntries(name: string, storage: FileStorage, streamFiles: StreamFileInfo[], cache: Map<string, StreamReaderCacheEntry>): Promise<{
120
+ entries: {
121
+ time: number;
122
+ fileName: string;
123
+ entry: StreamEntry;
124
+ }[];
125
+ totalBytes: number;
126
+ missing: boolean;
127
+ sizes: Map<string, number>;
128
+ }>;
129
+ export declare function orderStreamEntries(entries: {
130
+ time: number;
131
+ fileName: string;
132
+ entry: StreamEntry;
133
+ }[]): StreamEntry[];