sliftutils 1.4.76 → 1.4.78
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/index.d.ts +51 -0
- package/package.json +1 -1
- package/storage/BulkDatabase2/BulkDatabaseMerge.ts +3 -0
- package/storage/embeddingFormats.d.ts +24 -0
- package/storage/embeddingFormats.ts +291 -0
- package/storage/proxydatabase/Database.d.ts +6 -0
- package/storage/proxydatabase/Database.ts +17 -0
- package/storage/proxydatabase/transactionSet.d.ts +9 -0
- package/storage/proxydatabase/transactionSet.ts +104 -0
package/index.d.ts
CHANGED
|
@@ -2258,6 +2258,34 @@ declare module "sliftutils/storage/backblaze" {
|
|
|
2258
2258
|
|
|
2259
2259
|
}
|
|
2260
2260
|
|
|
2261
|
+
declare module "sliftutils/storage/embeddingFormats" {
|
|
2262
|
+
export type EmbeddingFormat = "q8g8_2048" | "q8_g16_2048" | "q8_g16_1024" | "float32";
|
|
2263
|
+
export declare const EMBEDDING_FORMATS: EmbeddingFormat[];
|
|
2264
|
+
export declare const DEFAULT_EMBEDDING_FORMAT: EmbeddingFormat;
|
|
2265
|
+
export type QuantType = "q8";
|
|
2266
|
+
export type StoredEmbedding = {
|
|
2267
|
+
kind: "float32";
|
|
2268
|
+
model: string;
|
|
2269
|
+
values: Float32Array;
|
|
2270
|
+
} | {
|
|
2271
|
+
kind: "quant";
|
|
2272
|
+
model: string;
|
|
2273
|
+
type: QuantType;
|
|
2274
|
+
groupSize: number;
|
|
2275
|
+
data: Uint8Array;
|
|
2276
|
+
scales: Uint8Array;
|
|
2277
|
+
};
|
|
2278
|
+
export declare function encodeEmbedding(config: {
|
|
2279
|
+
input: Float32Array | StoredEmbedding;
|
|
2280
|
+
format: EmbeddingFormat;
|
|
2281
|
+
model: string;
|
|
2282
|
+
}): StoredEmbedding;
|
|
2283
|
+
export declare function serializeStoredEmbedding(stored: StoredEmbedding): string;
|
|
2284
|
+
export declare function deserializeStoredEmbedding(base64: string): StoredEmbedding;
|
|
2285
|
+
export declare const getCloseness: (embedding1: Float32Array | StoredEmbedding, embedding2: Float32Array | StoredEmbedding) => number;
|
|
2286
|
+
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2261
2289
|
declare module "sliftutils/storage/fileSystemPointer" {
|
|
2262
2290
|
export type FileSystemPointer = string;
|
|
2263
2291
|
export declare function storeFileSystemPointer(config: {
|
|
@@ -2273,6 +2301,29 @@ declare module "sliftutils/storage/fileSystemPointer" {
|
|
|
2273
2301
|
|
|
2274
2302
|
}
|
|
2275
2303
|
|
|
2304
|
+
declare module "sliftutils/storage/proxydatabase/Database" {
|
|
2305
|
+
export interface Database<Root> {
|
|
2306
|
+
readData: <Value>(deref: (root: Root) => Value) => Value | undefined;
|
|
2307
|
+
writeData: <Value>(deref: (root: Root) => Value, newValue: Value) => void;
|
|
2308
|
+
deleteData: (deref: (root: Root) => unknown) => void;
|
|
2309
|
+
}
|
|
2310
|
+
export declare function namespaceDatabase<Root, Sub>(database: Database<Root>, into: (root: Root) => Sub): Database<Sub>;
|
|
2311
|
+
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
declare module "sliftutils/storage/proxydatabase/transactionSet" {
|
|
2315
|
+
import { Database } from "./Database";
|
|
2316
|
+
export type TransactionSetStore = {
|
|
2317
|
+
[fileNumber: string]: Uint8Array;
|
|
2318
|
+
};
|
|
2319
|
+
export declare function transactionRead<Value>(database: Database<TransactionSetStore>): Map<string, Value> | undefined;
|
|
2320
|
+
export declare function transactionMutate<Value>(database: Database<TransactionSetStore>, transactions: {
|
|
2321
|
+
key: string;
|
|
2322
|
+
value: Value | undefined;
|
|
2323
|
+
}[], compactAfterFiles?: number): true | undefined;
|
|
2324
|
+
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2276
2327
|
declare module "sliftutils/storage/remoteFileServer" {
|
|
2277
2328
|
export declare function generatePassword(wordCount: number): string;
|
|
2278
2329
|
export type RemoteFileServerOptions = {
|
package/package.json
CHANGED
|
@@ -112,6 +112,9 @@ export async function runPlannedMerge(config: {
|
|
|
112
112
|
} catch (e) {
|
|
113
113
|
sourceOk[si] = false;
|
|
114
114
|
log(`${magenta("skipped")} ${config.sourceNames[si]}: ${(e as Error).message}`);
|
|
115
|
+
// Stack goes straight to console (not the buffered step log) so the merge summary stays
|
|
116
|
+
// readable but we can still trace where the failure came from.
|
|
117
|
+
console.log(`${config.collectionName} skipped ${config.sourceNames[si]}:`, (e as Error).stack || e);
|
|
115
118
|
}
|
|
116
119
|
return map;
|
|
117
120
|
}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type EmbeddingFormat = "q8g8_2048" | "q8_g16_2048" | "q8_g16_1024" | "float32";
|
|
2
|
+
export declare const EMBEDDING_FORMATS: EmbeddingFormat[];
|
|
3
|
+
export declare const DEFAULT_EMBEDDING_FORMAT: EmbeddingFormat;
|
|
4
|
+
export type QuantType = "q8";
|
|
5
|
+
export type StoredEmbedding = {
|
|
6
|
+
kind: "float32";
|
|
7
|
+
model: string;
|
|
8
|
+
values: Float32Array;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "quant";
|
|
11
|
+
model: string;
|
|
12
|
+
type: QuantType;
|
|
13
|
+
groupSize: number;
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
scales: Uint8Array;
|
|
16
|
+
};
|
|
17
|
+
export declare function encodeEmbedding(config: {
|
|
18
|
+
input: Float32Array | StoredEmbedding;
|
|
19
|
+
format: EmbeddingFormat;
|
|
20
|
+
model: string;
|
|
21
|
+
}): StoredEmbedding;
|
|
22
|
+
export declare function serializeStoredEmbedding(stored: StoredEmbedding): string;
|
|
23
|
+
export declare function deserializeStoredEmbedding(base64: string): StoredEmbedding;
|
|
24
|
+
export declare const getCloseness: (embedding1: Float32Array | StoredEmbedding, embedding2: Float32Array | StoredEmbedding) => number;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
2
|
+
import { asBuffer, asFloat32 } from "socket-function/src/buffers";
|
|
3
|
+
|
|
4
|
+
// The formats an embedding can be stored & compared in. The trailing number is the Matryoshka
|
|
5
|
+
// truncation length (the count of leading dimensions kept). "q8gN" means signed int8 values with
|
|
6
|
+
// one scale shared across every N dimensions. "float32" keeps the raw (truncated) vector.
|
|
7
|
+
export type EmbeddingFormat = "q8g8_2048" | "q8_g16_2048" | "q8_g16_1024" | "float32";
|
|
8
|
+
|
|
9
|
+
export const EMBEDDING_FORMATS: EmbeddingFormat[] = ["q8g8_2048", "q8_g16_2048", "q8_g16_1024", "float32"];
|
|
10
|
+
export const DEFAULT_EMBEDDING_FORMAT: EmbeddingFormat = "q8g8_2048";
|
|
11
|
+
|
|
12
|
+
// The only element quantization we support right now (signed int8, values in [-127, 127]).
|
|
13
|
+
export type QuantType = "q8";
|
|
14
|
+
|
|
15
|
+
const INT8_MAX = 127;
|
|
16
|
+
|
|
17
|
+
type FormatConfig = {
|
|
18
|
+
// Leading dimensions kept. undefined keeps the full vector.
|
|
19
|
+
truncation?: number;
|
|
20
|
+
// int8 group quantization parameters. undefined stores the raw float32 vector.
|
|
21
|
+
quant?: { type: QuantType; groupSize: number };
|
|
22
|
+
};
|
|
23
|
+
const FORMAT_CONFIGS: { [format in EmbeddingFormat]: FormatConfig } = {
|
|
24
|
+
q8g8_2048: { truncation: 2048, quant: { type: "q8", groupSize: 8 } },
|
|
25
|
+
q8_g16_2048: { truncation: 2048, quant: { type: "q8", groupSize: 16 } },
|
|
26
|
+
q8_g16_1024: { truncation: 1024, quant: { type: "q8", groupSize: 16 } },
|
|
27
|
+
float32: {},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type StoredEmbedding = {
|
|
31
|
+
kind: "float32";
|
|
32
|
+
model: string;
|
|
33
|
+
values: Float32Array;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "quant";
|
|
36
|
+
model: string;
|
|
37
|
+
// Element quantization. "q8" is signed int8 (values in [-127, 127]).
|
|
38
|
+
type: QuantType;
|
|
39
|
+
// Number of consecutive dimensions sharing one scale.
|
|
40
|
+
groupSize: number;
|
|
41
|
+
// int8 values (one per kept dimension), reinterpreted as raw bytes.
|
|
42
|
+
data: Uint8Array;
|
|
43
|
+
// float16 scales (one per group), reinterpreted as raw bytes.
|
|
44
|
+
scales: Uint8Array;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Float16Array isn't in our TypeScript lib and isn't guaranteed at runtime, so we read it off
|
|
48
|
+
// globalThis and fall back to a manual half<->float conversion when it's missing.
|
|
49
|
+
interface Float16ArrayLike {
|
|
50
|
+
readonly length: number;
|
|
51
|
+
[index: number]: number;
|
|
52
|
+
readonly buffer: ArrayBufferLike;
|
|
53
|
+
readonly byteOffset: number;
|
|
54
|
+
readonly byteLength: number;
|
|
55
|
+
}
|
|
56
|
+
interface Float16ArrayConstructor {
|
|
57
|
+
new(length: number): Float16ArrayLike;
|
|
58
|
+
new(elements: ArrayLike<number>): Float16ArrayLike;
|
|
59
|
+
new(buffer: ArrayBufferLike, byteOffset?: number, length?: number): Float16ArrayLike;
|
|
60
|
+
}
|
|
61
|
+
const Float16ArrayCtor = (globalThis as { Float16Array?: Float16ArrayConstructor }).Float16Array;
|
|
62
|
+
|
|
63
|
+
const f32Scratch = new Float32Array(1);
|
|
64
|
+
const i32Scratch = new Int32Array(f32Scratch.buffer);
|
|
65
|
+
function floatToHalf(value: number): number {
|
|
66
|
+
f32Scratch[0] = value;
|
|
67
|
+
let x = i32Scratch[0];
|
|
68
|
+
let bits = (x >> 16) & 0x8000;
|
|
69
|
+
let m = (x >> 12) & 0x07ff;
|
|
70
|
+
let e = (x >> 23) & 0xff;
|
|
71
|
+
if (e < 103) return bits;
|
|
72
|
+
if (e > 142) {
|
|
73
|
+
bits |= 0x7c00;
|
|
74
|
+
if (e !== 255) {
|
|
75
|
+
bits |= (x & 0x007fffff);
|
|
76
|
+
}
|
|
77
|
+
return bits;
|
|
78
|
+
}
|
|
79
|
+
if (e < 113) {
|
|
80
|
+
m |= 0x0800;
|
|
81
|
+
bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
|
|
82
|
+
return bits;
|
|
83
|
+
}
|
|
84
|
+
bits |= ((e - 112) << 10) | (m >> 1);
|
|
85
|
+
bits += m & 1;
|
|
86
|
+
return bits;
|
|
87
|
+
}
|
|
88
|
+
function halfToFloat(half: number): number {
|
|
89
|
+
let sign = (half & 0x8000) >> 15;
|
|
90
|
+
let exponent = (half & 0x7c00) >> 10;
|
|
91
|
+
let fraction = half & 0x03ff;
|
|
92
|
+
let signMultiplier = sign && -1 || 1;
|
|
93
|
+
if (exponent === 0) {
|
|
94
|
+
return signMultiplier * Math.pow(2, -14) * (fraction / 1024);
|
|
95
|
+
}
|
|
96
|
+
if (exponent === 0x1f) {
|
|
97
|
+
if (fraction) return NaN;
|
|
98
|
+
return signMultiplier * Infinity;
|
|
99
|
+
}
|
|
100
|
+
return signMultiplier * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function asInt8(bytes: Uint8Array): Int8Array {
|
|
104
|
+
return new Int8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
105
|
+
}
|
|
106
|
+
function encodeFloat16(values: Float32Array): Uint8Array {
|
|
107
|
+
if (Float16ArrayCtor) {
|
|
108
|
+
let half = new Float16ArrayCtor(values);
|
|
109
|
+
return new Uint8Array(half.buffer, half.byteOffset, half.byteLength);
|
|
110
|
+
}
|
|
111
|
+
let out = new Uint8Array(values.length * 2);
|
|
112
|
+
let view = new DataView(out.buffer);
|
|
113
|
+
for (let i = 0; i < values.length; i++) {
|
|
114
|
+
view.setUint16(i * 2, floatToHalf(values[i]), true);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function asFloat16Indexable(bytes: Uint8Array): { readonly length: number;[index: number]: number } {
|
|
119
|
+
// Float16Array / Uint16Array views require 2-byte alignment; copy when the stored bytes aren't.
|
|
120
|
+
if (bytes.byteOffset % 2 !== 0) {
|
|
121
|
+
bytes = new Uint8Array(bytes);
|
|
122
|
+
}
|
|
123
|
+
let count = Math.floor(bytes.byteLength / 2);
|
|
124
|
+
if (Float16ArrayCtor) {
|
|
125
|
+
return new Float16ArrayCtor(bytes.buffer, bytes.byteOffset, count);
|
|
126
|
+
}
|
|
127
|
+
let halves = new Uint16Array(bytes.buffer, bytes.byteOffset, count);
|
|
128
|
+
let out = new Float32Array(count);
|
|
129
|
+
for (let i = 0; i < count; i++) {
|
|
130
|
+
out[i] = halfToFloat(halves[i]);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeInPlace(vector: Float32Array): Float32Array {
|
|
136
|
+
let sum = 0;
|
|
137
|
+
for (let i = 0; i < vector.length; i++) {
|
|
138
|
+
sum += vector[i] * vector[i];
|
|
139
|
+
}
|
|
140
|
+
let magnitude = Math.sqrt(sum);
|
|
141
|
+
if (!magnitude) return vector;
|
|
142
|
+
for (let i = 0; i < vector.length; i++) {
|
|
143
|
+
vector[i] /= magnitude;
|
|
144
|
+
}
|
|
145
|
+
return vector;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function embeddingLength(input: Float32Array | StoredEmbedding): number {
|
|
149
|
+
if (input instanceof Float32Array) return input.length;
|
|
150
|
+
if (input.kind === "float32") return input.values.length;
|
|
151
|
+
return input.data.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Decode any accepted input to a fresh, mutable Float32Array of exactly `length` dimensions
|
|
155
|
+
// (truncating). Always allocates, so callers can normalize it in place without touching stored data.
|
|
156
|
+
function decodeToLength(input: Float32Array | StoredEmbedding, length: number): Float32Array {
|
|
157
|
+
let out = new Float32Array(length);
|
|
158
|
+
if (input instanceof Float32Array) {
|
|
159
|
+
for (let i = 0; i < length; i++) {
|
|
160
|
+
out[i] = input[i];
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
if (input.kind === "float32") {
|
|
165
|
+
let values = input.values;
|
|
166
|
+
for (let i = 0; i < length; i++) {
|
|
167
|
+
out[i] = values[i];
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
let int8 = asInt8(input.data);
|
|
172
|
+
let scales = asFloat16Indexable(input.scales);
|
|
173
|
+
let groupSize = input.groupSize;
|
|
174
|
+
for (let i = 0; i < length; i++) {
|
|
175
|
+
out[i] = int8[i] * scales[Math.floor(i / groupSize)];
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Convert a raw float32 vector (or re-encode an existing StoredEmbedding) into the requested format.
|
|
181
|
+
export function encodeEmbedding(config: {
|
|
182
|
+
input: Float32Array | StoredEmbedding;
|
|
183
|
+
format: EmbeddingFormat;
|
|
184
|
+
model: string;
|
|
185
|
+
}): StoredEmbedding {
|
|
186
|
+
let { input, format, model } = config;
|
|
187
|
+
let formatConfig = FORMAT_CONFIGS[format];
|
|
188
|
+
let length = embeddingLength(input);
|
|
189
|
+
if (formatConfig.truncation !== undefined) {
|
|
190
|
+
length = Math.min(formatConfig.truncation, length);
|
|
191
|
+
}
|
|
192
|
+
// Truncate to a fresh array, then renormalize so the shortened vector is unit length again.
|
|
193
|
+
let vector = normalizeInPlace(decodeToLength(input, length));
|
|
194
|
+
|
|
195
|
+
if (!formatConfig.quant) {
|
|
196
|
+
return { kind: "float32", model, values: vector };
|
|
197
|
+
}
|
|
198
|
+
let { type, groupSize } = formatConfig.quant;
|
|
199
|
+
let groupCount = Math.ceil(length / groupSize);
|
|
200
|
+
let int8 = new Int8Array(length);
|
|
201
|
+
let scales = new Float32Array(groupCount);
|
|
202
|
+
for (let g = 0; g < groupCount; g++) {
|
|
203
|
+
let start = g * groupSize;
|
|
204
|
+
let end = Math.min(start + groupSize, length);
|
|
205
|
+
let maxAbs = 0;
|
|
206
|
+
for (let i = start; i < end; i++) {
|
|
207
|
+
let magnitude = Math.abs(vector[i]);
|
|
208
|
+
if (magnitude > maxAbs) maxAbs = magnitude;
|
|
209
|
+
}
|
|
210
|
+
let scale = maxAbs / INT8_MAX;
|
|
211
|
+
scales[g] = scale;
|
|
212
|
+
if (!scale) continue;
|
|
213
|
+
for (let i = start; i < end; i++) {
|
|
214
|
+
let quantized = Math.round(vector[i] / scale);
|
|
215
|
+
if (quantized > INT8_MAX) quantized = INT8_MAX;
|
|
216
|
+
if (quantized < -INT8_MAX) quantized = -INT8_MAX;
|
|
217
|
+
int8[i] = quantized;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
kind: "quant",
|
|
222
|
+
model,
|
|
223
|
+
type,
|
|
224
|
+
groupSize,
|
|
225
|
+
data: new Uint8Array(int8.buffer, int8.byteOffset, int8.byteLength),
|
|
226
|
+
scales: encodeFloat16(scales),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type SerializedHeader =
|
|
231
|
+
| { kind: "float32"; model: string }
|
|
232
|
+
| { kind: "quant"; model: string; type: QuantType; groupSize: number; dataBytes: number };
|
|
233
|
+
|
|
234
|
+
// Serialize to base64 so a StoredEmbedding can travel through the (string) API call result. Layout:
|
|
235
|
+
// [4-byte little-endian header length][JSON header][raw typed-array payload(s)].
|
|
236
|
+
export function serializeStoredEmbedding(stored: StoredEmbedding): string {
|
|
237
|
+
let header: SerializedHeader;
|
|
238
|
+
let payload: Uint8Array[];
|
|
239
|
+
if (stored.kind === "float32") {
|
|
240
|
+
header = { kind: "float32", model: stored.model };
|
|
241
|
+
payload = [new Uint8Array(stored.values.buffer, stored.values.byteOffset, stored.values.byteLength)];
|
|
242
|
+
} else {
|
|
243
|
+
header = { kind: "quant", model: stored.model, type: stored.type, groupSize: stored.groupSize, dataBytes: stored.data.byteLength };
|
|
244
|
+
payload = [stored.data, stored.scales];
|
|
245
|
+
}
|
|
246
|
+
let headerBytes = Buffer.from(JSON.stringify(header), "utf8");
|
|
247
|
+
let headerLength = Buffer.alloc(4);
|
|
248
|
+
headerLength.writeUInt32LE(headerBytes.length, 0);
|
|
249
|
+
return Buffer.concat([headerLength, headerBytes, ...payload.map(asBuffer)]).toString("base64");
|
|
250
|
+
}
|
|
251
|
+
export function deserializeStoredEmbedding(base64: string): StoredEmbedding {
|
|
252
|
+
let buffer = Buffer.from(base64, "base64");
|
|
253
|
+
if (buffer.length < 4) {
|
|
254
|
+
throw new Error(`Serialized embedding is too short (${buffer.length} bytes): ${base64.slice(0, 200)}`);
|
|
255
|
+
}
|
|
256
|
+
let headerLength = buffer.readUInt32LE(0);
|
|
257
|
+
let headerEnd = 4 + headerLength;
|
|
258
|
+
if (headerLength <= 0 || headerEnd > buffer.length) {
|
|
259
|
+
throw new Error(`Serialized embedding header length ${headerLength} is invalid for ${buffer.length} bytes: ${base64.slice(0, 200)}`);
|
|
260
|
+
}
|
|
261
|
+
let header = JSON.parse(buffer.toString("utf8", 4, headerEnd)) as SerializedHeader;
|
|
262
|
+
let payload = buffer.subarray(headerEnd);
|
|
263
|
+
if (header.kind === "float32") {
|
|
264
|
+
return { kind: "float32", model: header.model, values: asFloat32(asBuffer(Uint8Array.from(payload))) };
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
kind: "quant",
|
|
268
|
+
model: header.model,
|
|
269
|
+
type: header.type,
|
|
270
|
+
groupSize: header.groupSize,
|
|
271
|
+
data: Uint8Array.from(payload.subarray(0, header.dataBytes)),
|
|
272
|
+
scales: Uint8Array.from(payload.subarray(header.dataBytes)),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Compare two embeddings in any format / truncation. Both are decoded, truncated to their common
|
|
277
|
+
// length, and unit-normalized, then scored as 1 minus their euclidean distance.
|
|
278
|
+
export const getCloseness = measureWrap(function getCloseness(
|
|
279
|
+
embedding1: Float32Array | StoredEmbedding,
|
|
280
|
+
embedding2: Float32Array | StoredEmbedding,
|
|
281
|
+
): number {
|
|
282
|
+
let length = Math.min(embeddingLength(embedding1), embeddingLength(embedding2));
|
|
283
|
+
let vector1 = normalizeInPlace(decodeToLength(embedding1, length));
|
|
284
|
+
let vector2 = normalizeInPlace(decodeToLength(embedding2, length));
|
|
285
|
+
let sum = 0;
|
|
286
|
+
for (let i = 0; i < length; i++) {
|
|
287
|
+
let diff = vector1[i] - vector2[i];
|
|
288
|
+
sum += diff * diff;
|
|
289
|
+
}
|
|
290
|
+
return 1 - Math.sqrt(sum);
|
|
291
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface Database<Root> {
|
|
2
|
+
readData: <Value>(deref: (root: Root) => Value) => Value | undefined;
|
|
3
|
+
writeData: <Value>(deref: (root: Root) => Value, newValue: Value) => void;
|
|
4
|
+
deleteData: (deref: (root: Root) => unknown) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function namespaceDatabase<Root, Sub>(database: Database<Root>, into: (root: Root) => Sub): Database<Sub>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// undefined from a read means "not synced yet", NOT "absent" — storage initializes on its own. So read everything a stage needs in one readData and bail if the whole result is undefined; the database never returns an individual unsynced value inside an otherwise-successful read.
|
|
2
|
+
export interface Database<Root> {
|
|
3
|
+
readData: <Value>(deref: (root: Root) => Value) => Value | undefined;
|
|
4
|
+
writeData: <Value>(deref: (root: Root) => Value, newValue: Value) => void;
|
|
5
|
+
deleteData: (deref: (root: Root) => unknown) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function namespaceDatabase<Root, Sub>(
|
|
9
|
+
database: Database<Root>,
|
|
10
|
+
into: (root: Root) => Sub,
|
|
11
|
+
): Database<Sub> {
|
|
12
|
+
return {
|
|
13
|
+
readData: (deref) => database.readData(root => deref(into(root))),
|
|
14
|
+
writeData: (deref, newValue) => database.writeData(root => deref(into(root)), newValue),
|
|
15
|
+
deleteData: (deref) => database.deleteData(root => deref(into(root))),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Database } from "./Database";
|
|
2
|
+
export type TransactionSetStore = {
|
|
3
|
+
[fileNumber: string]: Uint8Array;
|
|
4
|
+
};
|
|
5
|
+
export declare function transactionRead<Value>(database: Database<TransactionSetStore>): Map<string, Value> | undefined;
|
|
6
|
+
export declare function transactionMutate<Value>(database: Database<TransactionSetStore>, transactions: {
|
|
7
|
+
key: string;
|
|
8
|
+
value: Value | undefined;
|
|
9
|
+
}[], compactAfterFiles?: number): true | undefined;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import cborx from "cbor-x";
|
|
2
|
+
import { Database } from "./Database";
|
|
3
|
+
|
|
4
|
+
// structuredClone keeps typed arrays / Maps / undefined intact through the round-trip.
|
|
5
|
+
const transactionCbor = new cborx.Encoder({ structuredClone: true });
|
|
6
|
+
|
|
7
|
+
// On the store: files keyed by a monotonic number, each a CBOR array of transactions; a transaction sets one key (value undefined = delete) and carries a global sequence number. Replaying every transaction in sequence order (last write wins) gives the current map. Mutations append a file, then fold the whole set into one once enough accumulate.
|
|
8
|
+
export type TransactionSetStore = { [fileNumber: string]: Uint8Array };
|
|
9
|
+
|
|
10
|
+
type Transaction = { sequence: number; key: string; value: unknown };
|
|
11
|
+
|
|
12
|
+
const DEFAULT_COMPACT_AFTER_FILES = 16;
|
|
13
|
+
|
|
14
|
+
function maxNumber(values: number[], fallback: number): number {
|
|
15
|
+
let result = fallback;
|
|
16
|
+
for (const value of values) {
|
|
17
|
+
if (value > result) {
|
|
18
|
+
result = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readTransactionFiles(
|
|
25
|
+
database: Database<TransactionSetStore>,
|
|
26
|
+
): { fileKeys: string[]; transactions: Transaction[] } | undefined {
|
|
27
|
+
const snapshot = database.readData(store => ({
|
|
28
|
+
fileKeys: Object.keys(store),
|
|
29
|
+
buffers: Object.values(store),
|
|
30
|
+
}));
|
|
31
|
+
if (!snapshot) return undefined;
|
|
32
|
+
const transactions: Transaction[] = [];
|
|
33
|
+
for (const buffer of snapshot.buffers) {
|
|
34
|
+
const decoded = transactionCbor.decode(buffer) as Transaction[];
|
|
35
|
+
for (const transaction of decoded) {
|
|
36
|
+
transactions.push(transaction);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { fileKeys: snapshot.fileKeys, transactions };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function replay(transactions: Transaction[]): Map<string, unknown> {
|
|
43
|
+
const ordered = transactions.slice().sort((left, right) => left.sequence - right.sequence);
|
|
44
|
+
const state = new Map<string, unknown>();
|
|
45
|
+
for (const transaction of ordered) {
|
|
46
|
+
// undefined (not falsy) is the delete marker, so 0 / "" / false stay real values.
|
|
47
|
+
if (transaction.value === undefined) {
|
|
48
|
+
state.delete(transaction.key);
|
|
49
|
+
} else {
|
|
50
|
+
state.set(transaction.key, transaction.value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Deletes old files by their ORIGINAL string key, so a stray non-numeric key still gets removed.
|
|
57
|
+
function compactTransactions(
|
|
58
|
+
database: Database<TransactionSetStore>,
|
|
59
|
+
allTransactions: Transaction[],
|
|
60
|
+
oldFileKeys: string[],
|
|
61
|
+
newFileNumber: number,
|
|
62
|
+
): void {
|
|
63
|
+
const finalState = replay(allTransactions);
|
|
64
|
+
const merged: Transaction[] = [...finalState].map(([key, value], index) => ({ sequence: index, key, value }));
|
|
65
|
+
const encoded = transactionCbor.encode(merged) as Uint8Array;
|
|
66
|
+
database.writeData(store => store[String(newFileNumber)], encoded);
|
|
67
|
+
for (const fileKey of oldFileKeys) {
|
|
68
|
+
database.deleteData(store => store[fileKey]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function transactionRead<Value>(
|
|
73
|
+
database: Database<TransactionSetStore>,
|
|
74
|
+
): Map<string, Value> | undefined {
|
|
75
|
+
const files = readTransactionFiles(database);
|
|
76
|
+
if (!files) return undefined;
|
|
77
|
+
return replay(files.transactions) as Map<string, Value>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function transactionMutate<Value>(
|
|
81
|
+
database: Database<TransactionSetStore>,
|
|
82
|
+
transactions: { key: string; value: Value | undefined }[],
|
|
83
|
+
compactAfterFiles: number = DEFAULT_COMPACT_AFTER_FILES,
|
|
84
|
+
): true | undefined {
|
|
85
|
+
const files = readTransactionFiles(database);
|
|
86
|
+
if (!files) return undefined;
|
|
87
|
+
if (!transactions.length) return true;
|
|
88
|
+
|
|
89
|
+
const baseSequence = maxNumber(files.transactions.map(transaction => transaction.sequence), -1) + 1;
|
|
90
|
+
const incoming: Transaction[] = transactions.map((transaction, index) => ({
|
|
91
|
+
sequence: baseSequence + index,
|
|
92
|
+
key: transaction.key,
|
|
93
|
+
value: transaction.value,
|
|
94
|
+
}));
|
|
95
|
+
const nextFileNumber = maxNumber(files.fileKeys.map(fileKey => Number(fileKey)), -1) + 1;
|
|
96
|
+
|
|
97
|
+
if (files.fileKeys.length + 1 >= compactAfterFiles) {
|
|
98
|
+
compactTransactions(database, [...files.transactions, ...incoming], files.fileKeys, nextFileNumber);
|
|
99
|
+
} else {
|
|
100
|
+
const encoded = transactionCbor.encode(incoming) as Uint8Array;
|
|
101
|
+
database.writeData(store => store[String(nextFileNumber)], encoded);
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|