@prisma/streams-server 0.1.1 → 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.
- package/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -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 +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -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 +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -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 +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- package/src/util/stream_paths.ts +19 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
statSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, join, relative } from "node:path";
|
|
11
|
+
import { Result } from "better-result";
|
|
12
|
+
import { LruCache } from "../util/lru";
|
|
13
|
+
|
|
14
|
+
export type LexiconFileCacheError = { kind: "invalid_lexicon_cache"; message: string };
|
|
15
|
+
|
|
16
|
+
export type MappedLexiconFile = {
|
|
17
|
+
objectKey: string;
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
sizeBytes: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type FileEntry = {
|
|
24
|
+
path: string;
|
|
25
|
+
size: number;
|
|
26
|
+
mtimeMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LexiconFileCacheStats = {
|
|
30
|
+
usedBytes: number;
|
|
31
|
+
entryCount: number;
|
|
32
|
+
mappedBytes: number;
|
|
33
|
+
mappedEntryCount: number;
|
|
34
|
+
pinnedEntryCount: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function invalidLexiconCache<T = never>(message: string): Result<T, LexiconFileCacheError> {
|
|
38
|
+
return Result.err({ kind: "invalid_lexicon_cache", message });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class LexiconFileCache {
|
|
42
|
+
private readonly entries = new Map<string, FileEntry>();
|
|
43
|
+
private readonly pinnedKeys = new Set<string>();
|
|
44
|
+
private readonly mappedFiles: LruCache<string, MappedLexiconFile>;
|
|
45
|
+
private totalBytes = 0;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly rootDir: string,
|
|
49
|
+
private readonly maxBytes: number,
|
|
50
|
+
mappedEntries: number
|
|
51
|
+
) {
|
|
52
|
+
if (this.maxBytes > 0) mkdirSync(this.rootDir, { recursive: true });
|
|
53
|
+
this.mappedFiles = new LruCache(Math.max(1, mappedEntries));
|
|
54
|
+
this.loadIndex();
|
|
55
|
+
this.pruneForBudget(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clearMapped(): void {
|
|
59
|
+
this.mappedFiles.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bytesForObjectKeyPrefix(prefix: string): number {
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (const [objectKey, entry] of this.entries.entries()) {
|
|
65
|
+
if (objectKey.startsWith(prefix)) total += entry.size;
|
|
66
|
+
}
|
|
67
|
+
return total;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
storeBytesResult(objectKey: string, bytes: Uint8Array): Result<string, LexiconFileCacheError> {
|
|
71
|
+
if (this.maxBytes <= 0) return Result.ok("");
|
|
72
|
+
if (bytes.byteLength > this.maxBytes) {
|
|
73
|
+
return invalidLexiconCache(`lexicon cache object exceeds budget: ${objectKey}`);
|
|
74
|
+
}
|
|
75
|
+
this.pruneForBudget(bytes.byteLength);
|
|
76
|
+
const path = this.pathFor(objectKey);
|
|
77
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
78
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
79
|
+
try {
|
|
80
|
+
writeFileSync(tmpPath, bytes);
|
|
81
|
+
renameSync(tmpPath, path);
|
|
82
|
+
} catch (e: unknown) {
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(tmpPath);
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore temp cleanup failures
|
|
87
|
+
}
|
|
88
|
+
return invalidLexiconCache(String((e as any)?.message ?? e));
|
|
89
|
+
}
|
|
90
|
+
const existing = this.entries.get(objectKey);
|
|
91
|
+
if (existing) this.totalBytes = Math.max(0, this.totalBytes - existing.size);
|
|
92
|
+
this.entries.delete(objectKey);
|
|
93
|
+
this.entries.set(objectKey, { path, size: bytes.byteLength, mtimeMs: Date.now() });
|
|
94
|
+
this.totalBytes += bytes.byteLength;
|
|
95
|
+
return Result.ok(path);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async loadMappedFileResult(args: {
|
|
99
|
+
objectKey: string;
|
|
100
|
+
expectedSize: number;
|
|
101
|
+
loadBytes: () => Promise<Uint8Array>;
|
|
102
|
+
}): Promise<Result<MappedLexiconFile, LexiconFileCacheError>> {
|
|
103
|
+
const cached = this.mappedFiles.get(args.objectKey);
|
|
104
|
+
if (cached) {
|
|
105
|
+
this.pinnedKeys.add(args.objectKey);
|
|
106
|
+
this.touch(args.objectKey);
|
|
107
|
+
return Result.ok(cached);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const localPathRes = await this.ensureLocalFileResult(args.objectKey, args.expectedSize, args.loadBytes);
|
|
111
|
+
if (Result.isError(localPathRes)) return localPathRes;
|
|
112
|
+
|
|
113
|
+
const mappedRes = this.mapFileResult(args.objectKey, localPathRes.value, args.expectedSize);
|
|
114
|
+
if (Result.isError(mappedRes)) return mappedRes;
|
|
115
|
+
this.pinnedKeys.add(args.objectKey);
|
|
116
|
+
this.mappedFiles.set(args.objectKey, mappedRes.value);
|
|
117
|
+
this.touch(args.objectKey);
|
|
118
|
+
return mappedRes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stats(): LexiconFileCacheStats {
|
|
122
|
+
let mappedBytes = 0;
|
|
123
|
+
let mappedEntryCount = 0;
|
|
124
|
+
for (const mapped of this.mappedFiles.values()) {
|
|
125
|
+
mappedBytes += mapped.sizeBytes;
|
|
126
|
+
mappedEntryCount += 1;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
usedBytes: this.totalBytes,
|
|
130
|
+
entryCount: this.entries.size,
|
|
131
|
+
mappedBytes,
|
|
132
|
+
mappedEntryCount,
|
|
133
|
+
pinnedEntryCount: this.pinnedKeys.size,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async ensureLocalFileResult(
|
|
138
|
+
objectKey: string,
|
|
139
|
+
expectedSize: number,
|
|
140
|
+
loadBytes: () => Promise<Uint8Array>
|
|
141
|
+
): Promise<Result<string, LexiconFileCacheError>> {
|
|
142
|
+
if (this.maxBytes <= 0) return invalidLexiconCache("lexicon cache disabled");
|
|
143
|
+
const existing = this.entries.get(objectKey);
|
|
144
|
+
if (existing) {
|
|
145
|
+
const stat = this.safeStat(existing.path);
|
|
146
|
+
if (stat && stat.size === expectedSize) {
|
|
147
|
+
this.touch(objectKey);
|
|
148
|
+
return Result.ok(existing.path);
|
|
149
|
+
}
|
|
150
|
+
this.removeEntry(objectKey);
|
|
151
|
+
}
|
|
152
|
+
let bytes: Uint8Array;
|
|
153
|
+
try {
|
|
154
|
+
bytes = await loadBytes();
|
|
155
|
+
} catch (e: unknown) {
|
|
156
|
+
return invalidLexiconCache(String((e as any)?.message ?? e));
|
|
157
|
+
}
|
|
158
|
+
if (bytes.byteLength !== expectedSize) {
|
|
159
|
+
return invalidLexiconCache(`unexpected lexicon object size for ${objectKey}`);
|
|
160
|
+
}
|
|
161
|
+
return this.storeBytesResult(objectKey, bytes);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private mapFileResult(objectKey: string, path: string, expectedSize: number): Result<MappedLexiconFile, LexiconFileCacheError> {
|
|
165
|
+
const stat = this.safeStat(path);
|
|
166
|
+
if (!stat) return invalidLexiconCache(`missing cached lexicon file ${path}`);
|
|
167
|
+
if (stat.size !== expectedSize) return invalidLexiconCache(`unexpected cached lexicon size for ${objectKey}`);
|
|
168
|
+
let bytes: Uint8Array;
|
|
169
|
+
try {
|
|
170
|
+
bytes = (Bun as any).mmap(path, { shared: true }) as Uint8Array;
|
|
171
|
+
} catch (e: unknown) {
|
|
172
|
+
return invalidLexiconCache(String((e as any)?.message ?? e));
|
|
173
|
+
}
|
|
174
|
+
if (bytes.byteLength !== expectedSize) {
|
|
175
|
+
return invalidLexiconCache(`unexpected mapped lexicon size for ${objectKey}`);
|
|
176
|
+
}
|
|
177
|
+
return Result.ok({ objectKey, path, bytes, sizeBytes: expectedSize });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private pathFor(objectKey: string): string {
|
|
181
|
+
return join(this.rootDir, objectKey);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private loadIndex(): void {
|
|
185
|
+
if (!existsSync(this.rootDir)) return;
|
|
186
|
+
const files: Array<{ key: string; path: string; size: number; mtimeMs: number }> = [];
|
|
187
|
+
const walk = (dir: string) => {
|
|
188
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
189
|
+
const full = join(dir, entry.name);
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
walk(full);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!entry.isFile()) continue;
|
|
195
|
+
const stat = statSync(full);
|
|
196
|
+
files.push({
|
|
197
|
+
key: relative(this.rootDir, full),
|
|
198
|
+
path: full,
|
|
199
|
+
size: stat.size,
|
|
200
|
+
mtimeMs: stat.mtimeMs,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
walk(this.rootDir);
|
|
205
|
+
files.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
this.entries.set(file.key, { path: file.path, size: file.size, mtimeMs: file.mtimeMs });
|
|
208
|
+
this.totalBytes += file.size;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private pruneForBudget(incomingBytes: number): void {
|
|
213
|
+
if (this.maxBytes <= 0) return;
|
|
214
|
+
while (this.totalBytes + incomingBytes > this.maxBytes) {
|
|
215
|
+
const next = this.entries.keys().next();
|
|
216
|
+
if (next.done) break;
|
|
217
|
+
const objectKey = next.value as string;
|
|
218
|
+
if (this.pinnedKeys.has(objectKey)) {
|
|
219
|
+
let removed = false;
|
|
220
|
+
for (const candidateKey of this.entries.keys()) {
|
|
221
|
+
if (this.pinnedKeys.has(candidateKey)) continue;
|
|
222
|
+
this.removeEntry(candidateKey);
|
|
223
|
+
removed = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (!removed) break;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
this.removeEntry(objectKey);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private touch(objectKey: string): void {
|
|
234
|
+
const entry = this.entries.get(objectKey);
|
|
235
|
+
if (!entry) return;
|
|
236
|
+
this.entries.delete(objectKey);
|
|
237
|
+
this.entries.set(objectKey, { ...entry });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private removeEntry(objectKey: string): void {
|
|
241
|
+
if (this.pinnedKeys.has(objectKey)) return;
|
|
242
|
+
const entry = this.entries.get(objectKey);
|
|
243
|
+
if (!entry) return;
|
|
244
|
+
try {
|
|
245
|
+
unlinkSync(entry.path);
|
|
246
|
+
} catch {
|
|
247
|
+
// ignore remove failures
|
|
248
|
+
}
|
|
249
|
+
this.totalBytes = Math.max(0, this.totalBytes - entry.size);
|
|
250
|
+
this.entries.delete(objectKey);
|
|
251
|
+
this.mappedFiles.delete(objectKey);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private safeStat(path: string): { size: number } | null {
|
|
255
|
+
try {
|
|
256
|
+
return existsSync(path) ? { size: statSync(path).size } : null;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
import { RestartStringTableView, encodeRestartStringTable } from "../search/binary/restart_strings";
|
|
3
|
+
import { concatBytes, readU32BE, readU64BE, writeU32BE, writeU64BE } from "../util/endian";
|
|
4
|
+
import { dsError } from "../util/ds_error.ts";
|
|
5
|
+
|
|
6
|
+
export const LEXICON_RUN_MAGIC = "LRN1";
|
|
7
|
+
export const LEXICON_RUN_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export type LexiconRunMeta = {
|
|
10
|
+
runId: string;
|
|
11
|
+
level: number;
|
|
12
|
+
startSegment: number;
|
|
13
|
+
endSegment: number;
|
|
14
|
+
objectKey: string;
|
|
15
|
+
recordCount: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LexiconRun = {
|
|
19
|
+
meta: LexiconRunMeta;
|
|
20
|
+
payloadBytes: Uint8Array;
|
|
21
|
+
terms: RestartStringTableView;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LexiconRunFormatError = {
|
|
25
|
+
kind: "invalid_lexicon_run";
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function invalidLexiconRun<T = never>(message: string): Result<T, LexiconRunFormatError> {
|
|
30
|
+
return Result.err({ kind: "invalid_lexicon_run", message });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function encodeLexiconRun(run: LexiconRun): Uint8Array {
|
|
34
|
+
const res = encodeLexiconRunResult(run);
|
|
35
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
36
|
+
return res.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function encodeLexiconRunResult(run: LexiconRun): Result<Uint8Array, LexiconRunFormatError> {
|
|
40
|
+
const payload = run.payloadBytes;
|
|
41
|
+
const header = new Uint8Array(32);
|
|
42
|
+
header[0] = LEXICON_RUN_MAGIC.charCodeAt(0);
|
|
43
|
+
header[1] = LEXICON_RUN_MAGIC.charCodeAt(1);
|
|
44
|
+
header[2] = LEXICON_RUN_MAGIC.charCodeAt(2);
|
|
45
|
+
header[3] = LEXICON_RUN_MAGIC.charCodeAt(3);
|
|
46
|
+
header[4] = LEXICON_RUN_VERSION;
|
|
47
|
+
header[5] = run.meta.level & 0xff;
|
|
48
|
+
header[6] = 0;
|
|
49
|
+
header[7] = 0;
|
|
50
|
+
writeU64BE(header, 8, BigInt(run.meta.startSegment));
|
|
51
|
+
writeU64BE(header, 16, BigInt(run.meta.endSegment));
|
|
52
|
+
writeU32BE(header, 24, run.meta.recordCount);
|
|
53
|
+
writeU32BE(header, 28, payload.byteLength);
|
|
54
|
+
return Result.ok(concatBytes([header, payload]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function decodeLexiconRun(data: Uint8Array): LexiconRun {
|
|
58
|
+
const res = decodeLexiconRunResult(data);
|
|
59
|
+
if (Result.isError(res)) throw dsError(res.error.message);
|
|
60
|
+
return res.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decodeLexiconRunResult(data: Uint8Array): Result<LexiconRun, LexiconRunFormatError> {
|
|
64
|
+
if (data.byteLength < 32) return invalidLexiconRun("run too short");
|
|
65
|
+
const magic = String.fromCharCode(data[0], data[1], data[2], data[3]);
|
|
66
|
+
if (magic !== LEXICON_RUN_MAGIC) return invalidLexiconRun("invalid run magic");
|
|
67
|
+
if (data[4] !== LEXICON_RUN_VERSION) return invalidLexiconRun("unsupported run version");
|
|
68
|
+
const level = data[5] ?? 0;
|
|
69
|
+
const startSegment = Number(readU64BE(data, 8));
|
|
70
|
+
const endSegment = Number(readU64BE(data, 16));
|
|
71
|
+
const recordCount = readU32BE(data, 24);
|
|
72
|
+
const payloadLen = readU32BE(data, 28);
|
|
73
|
+
if (32 + payloadLen > data.byteLength) return invalidLexiconRun("run payload truncated");
|
|
74
|
+
const payloadBytes = data.subarray(32, 32 + payloadLen);
|
|
75
|
+
const terms = new RestartStringTableView(payloadBytes);
|
|
76
|
+
if (terms.count() !== recordCount) return invalidLexiconRun("record count mismatch");
|
|
77
|
+
return Result.ok({
|
|
78
|
+
meta: {
|
|
79
|
+
runId: "",
|
|
80
|
+
level,
|
|
81
|
+
startSegment,
|
|
82
|
+
endSegment,
|
|
83
|
+
objectKey: "",
|
|
84
|
+
recordCount,
|
|
85
|
+
},
|
|
86
|
+
payloadBytes,
|
|
87
|
+
terms,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildLexiconRunPayload(keys: string[]): Uint8Array {
|
|
92
|
+
return encodeRestartStringTable(keys, 16);
|
|
93
|
+
}
|