sliftutils 1.7.13 → 1.7.14

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 (60) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/index.d.ts +64 -38
  3. package/misc/dist/getSecret.ts.cache +2 -2
  4. package/misc/dist/strings.ts.cache +2 -2
  5. package/misc/https/dist/certs.ts.cache +2 -2
  6. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  7. package/misc/https/hostServer.d.ts +7 -0
  8. package/misc/https/hostServer.ts +89 -20
  9. package/misc/openrouter.ts +17 -14
  10. package/package.json +2 -2
  11. package/storage/ArchivesDisk.d.ts +1 -1
  12. package/storage/ArchivesDisk.ts +2 -1
  13. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
  14. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
  15. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
  16. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +2 -2
  17. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
  18. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
  19. package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
  20. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
  21. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
  22. package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
  23. package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
  24. package/storage/IArchives.d.ts +5 -1
  25. package/storage/IArchives.ts +5 -1
  26. package/storage/backblaze.d.ts +1 -1
  27. package/storage/backblaze.ts +9 -6
  28. package/storage/remoteStorage/ArchivesRemote.d.ts +1 -1
  29. package/storage/remoteStorage/ArchivesRemote.ts +2 -1
  30. package/storage/remoteStorage/ArchivesUrl.d.ts +1 -1
  31. package/storage/remoteStorage/ArchivesUrl.ts +13 -9
  32. package/storage/remoteStorage/blobStore.d.ts +6 -2
  33. package/storage/remoteStorage/blobStore.ts +46 -6
  34. package/storage/remoteStorage/createArchives.d.ts +10 -11
  35. package/storage/remoteStorage/createArchives.ts +74 -26
  36. package/storage/remoteStorage/deployTakeover.d.ts +24 -0
  37. package/storage/remoteStorage/deployTakeover.ts +373 -0
  38. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +16 -11
  39. package/storage/remoteStorage/dist/blobStore.ts.cache +39 -4
  40. package/storage/remoteStorage/dist/createArchives.ts.cache +91 -9
  41. package/storage/remoteStorage/dist/deployTakeover.ts.cache +371 -0
  42. package/storage/remoteStorage/dist/remoteConfig.ts.cache +9 -3
  43. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +3 -3
  44. package/storage/remoteStorage/dist/storageController.ts.cache +11 -12
  45. package/storage/remoteStorage/dist/storageServerState.ts.cache +120 -19
  46. package/storage/remoteStorage/remoteConfig.d.ts +1 -0
  47. package/storage/remoteStorage/remoteConfig.ts +6 -0
  48. package/storage/remoteStorage/storageServer.ts +14 -1
  49. package/storage/remoteStorage/storageServerState.d.ts +3 -0
  50. package/storage/remoteStorage/storageServerState.ts +55 -8
  51. package/yarn.lock +4 -4
  52. package/storage/dist/faceFramesData.ts.cache +0 -55
  53. package/storage/embeddingBench.d.ts +0 -1
  54. package/storage/embeddingBench.ts +0 -152
  55. package/storage/faceFramesData.d.ts +0 -1
  56. package/storage/faceFramesData.ts +0 -49
  57. package/storage/faceFramesServer.d.ts +0 -1
  58. package/storage/faceFramesServer.ts +0 -56
  59. package/storage/proxydatabase/ivfDbCheck.d.ts +0 -1
  60. package/storage/proxydatabase/ivfDbCheck.ts +0 -85
@@ -11,6 +11,7 @@ import {
11
11
  WRITE_PAST_WINDOW_GRACE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
12
12
  } from "../IArchives";
13
13
  import { ROUTING_FILE, parseRoutingData, parseHostedUrl, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
14
+ import { applyDeployRemap, getFlushDeadline, onTakeoverEvent } from "./deployTakeover";
14
15
  import { createApiArchives } from "./createArchives";
15
16
  import type { IStorage } from "../IStorage";
16
17
  import type { AccessRequest, TrustRecord } from "./storageController";
@@ -157,6 +158,13 @@ function getBucketFolder(account: string, bucketName: string): string {
157
158
  return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
158
159
  }
159
160
 
161
+ // Ports we are listening on beyond the main config port - a deploy-takeover successor serves on a
162
+ // temporary alternate port, and must recognize alternate-port source URLs as itself
163
+ const extraListenPorts = new Set<number>();
164
+ export function addExtraListenPort(port: number): void {
165
+ extraListenPorts.add(port);
166
+ }
167
+
160
168
  function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
161
169
  let { domain, port } = getStorageServerConfig();
162
170
  let indexes: number[] = [];
@@ -164,7 +172,7 @@ function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: str
164
172
  let source = routing.sources[i];
165
173
  if (typeof source === "string" || source.type !== "remote") continue;
166
174
  let parsed = parseHostedUrl(source.url);
167
- if (parsed.address === domain && parsed.port === port && parsed.account === account && parsed.bucketName === bucketName) {
175
+ if (parsed.address === domain && (parsed.port === port || extraListenPorts.has(parsed.port)) && parsed.account === account && parsed.bucketName === bucketName) {
168
176
  indexes.push(i);
169
177
  }
170
178
  }
@@ -223,12 +231,17 @@ function scheduleWindowBoundaryRebuild(loaded: LoadedBucket): void {
223
231
 
224
232
  function buildBucket(account: string, bucketName: string, routing: RemoteConfig): LoadedBucket {
225
233
  let folder = getBucketFolder(account, bucketName);
226
- let selfIndexes = findSelfIndexes(routing, account, bucketName);
227
- let selfEntries = selfIndexes.map(i => routing.sources[i] as HostedConfig);
234
+ // The deploy-takeover remap is an INTERPRETATION overlay: everything behavioral (self entries,
235
+ // windows, downstream sources) uses the remapped view, while loaded.routing/routingJSON stay
236
+ // the STORED config - so version guards, change detection, and synchronization never see (or
237
+ // persist) the remap
238
+ let effective = applyDeployRemap(routing);
239
+ let selfIndexes = findSelfIndexes(effective, account, bucketName);
240
+ let selfEntries = selfIndexes.map(i => effective.sources[i] as HostedConfig);
228
241
  let self = selectEntryAt(selfEntries, Date.now());
229
242
  let selfIndex = -1;
230
243
  if (self) {
231
- selfIndex = routing.sources.indexOf(self);
244
+ selfIndex = effective.sources.indexOf(self);
232
245
  }
233
246
  let store: IBucketStore;
234
247
  if (self?.rawDisk) {
@@ -251,8 +264,8 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
251
264
  if (selfIndex === -1) {
252
265
  console.log(`This server is not in the routing config for bucket ${account}/${bucketName}; keeping its data on disk but no longer synchronizing it`);
253
266
  } else {
254
- for (let i = selfIndex + 1; i < routing.sources.length; i++) {
255
- let source = routing.sources[i];
267
+ for (let i = selfIndex + 1; i < effective.sources.length; i++) {
268
+ let source = effective.sources[i];
256
269
  if (typeof source === "string" || ownIndexes.has(i)) continue;
257
270
  // Disjoint-shard sources never talk to each other; a partial overlap syncs only
258
271
  // the intersection (scans ignore the rest, writes only send matching keys)
@@ -271,6 +284,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
271
284
  void scheduleRoutingReload(account, bucketName);
272
285
  },
273
286
  readerDiskLimit: self?.readerDiskLimit,
287
+ getFlushDeadline,
274
288
  });
275
289
  }
276
290
  let loaded: LoadedBucket = { account, bucketName, routing, routingJSON: JSON.stringify(routing), selfEntries, self, store };
@@ -421,7 +435,9 @@ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
421
435
  let progress = bucket.store.getSyncProgress?.();
422
436
  return {
423
437
  supportsChangesAfter: !!bucket.store.getChangesAfter,
424
- remoteConfig: bucket.routing,
438
+ // The remapped (interpretation) view: getConfig is how clients learn of an in-progress
439
+ // deploy takeover, since the remap is never written into the routing file itself
440
+ remoteConfig: applyDeployRemap(bucket.routing),
425
441
  index: progress?.index,
426
442
  indexSources: progress?.sources,
427
443
  readerDiskLimit: progress?.readerDiskLimit,
@@ -429,6 +445,36 @@ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
429
445
  };
430
446
  }
431
447
 
448
+ export async function rebuildAllLoadedBuckets(): Promise<void> {
449
+ for (let key of [...buckets.keys()]) {
450
+ let slash = key.indexOf("/");
451
+ await scheduleRoutingReload(key.slice(0, slash), key.slice(slash + 1), { force: true });
452
+ }
453
+ }
454
+
455
+ export async function rescanAllLoadedBucketDisks(): Promise<void> {
456
+ for (let loadedPromise of [...buckets.values()]) {
457
+ let loaded = await loadedPromise.catch(() => undefined);
458
+ if (!loaded) continue;
459
+ let store = loaded.store;
460
+ if (!(store instanceof BlobStore)) continue;
461
+ try {
462
+ await store.rescanBase();
463
+ } catch (e) {
464
+ console.error(`Deploy switchover disk rescan failed for bucket ${loaded.account}/${loaded.bucketName}: ${(e as Error).stack ?? e}`);
465
+ }
466
+ }
467
+ }
468
+
469
+ onTakeoverEvent(event => {
470
+ if (event === "remapChanged") {
471
+ void rebuildAllLoadedBuckets();
472
+ }
473
+ if (event === "diskScan") {
474
+ void rescanAllLoadedBucketDisks();
475
+ }
476
+ });
477
+
432
478
  export type ServerBucketInfo = {
433
479
  bucketName: string;
434
480
  // Loaded in memory (created or accessed since startup), so its synchronization is running
@@ -512,8 +558,9 @@ class ArchivesLocalBucket implements IArchives {
512
558
  if (!bucket) return undefined;
513
559
  return await bucket.store.get2(fileName, config);
514
560
  }
515
- public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<void> {
561
+ public async set(fileName: string, data: Buffer, config?: { lastModified?: number }): Promise<string> {
516
562
  await writeBucketFile(this.account, this.bucketName, fileName, data, config);
563
+ return fileName;
517
564
  }
518
565
  public async del(fileName: string): Promise<void> {
519
566
  await deleteBucketFile(this.account, this.bucketName, fileName);
package/yarn.lock CHANGED
@@ -1944,10 +1944,10 @@ slash@^3.0.0:
1944
1944
  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
1945
1945
  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
1946
1946
 
1947
- socket-function@^1.2.17:
1948
- version "1.2.17"
1949
- resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.17.tgz#d6f28be00253d4a0a73dab6f0981c9af8e750a49"
1950
- integrity sha512-HGQ8YmGwS5xLG03v9koa8l4PIPVIDmi177a/sdfa6KSGRkaxTWwinfakun4rddZNnlxb7Iu6NFSUl0TfTyv7/w==
1947
+ socket-function@^1.2.20:
1948
+ version "1.2.20"
1949
+ resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.20.tgz#8cd141cd84504a320583e09749ed91cee293db45"
1950
+ integrity sha512-aLHs7S9MKb6WY5pjtNG+sfISVIWIv7toeX7Zt5K6f1R/sVFLb46IBElZ0Hf81LHenVYwDNsKN0uAX/fkBBIqlw==
1951
1951
  dependencies:
1952
1952
  "@types/pako" "^2.0.3"
1953
1953
  "@types/ws" "^8.5.3"
@@ -1,55 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule && mod.default) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
6
- //exports.ensureFaceData = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- const os_1 = __importDefault(require("os"));
9
- const path_1 = __importDefault(require("path"));
10
- const http_1 = __importDefault(require("http"));
11
- // Resolves a face-embedding test file to a local path that holds at least the requested number of bytes. On
12
- // the machine that owns the data it just uses the original file. Anywhere else it downloads the missing prefix
13
- // from the faceFramesServer over a range request and caches it in the temp dir, so re-runs only fetch what
14
- // they haven't already. Point it at a different server with FACEFRAMES_URL.
15
- const SOURCE_DIR = "/root/claude-work/face-embeddings";
16
- const SERVER_BASE = process.env.FACEFRAMES_URL || "http://65.109.93.113:8799";
17
- function downloadInto(fileName, start, endExclusive, cachePath) {
18
- return new Promise((resolve, reject) => {
19
- const url = `${SERVER_BASE}/${fileName}`;
20
- const options = { headers: { Range: `bytes=${start}-${endExclusive - 1}` } };
21
- const request = http_1.default.get(url, options, response => {
22
- if (response.statusCode !== 206 && response.statusCode !== 200) {
23
- response.resume();
24
- reject(new Error(`${url} returned ${response.statusCode}`));
25
- return;
26
- }
27
- const out = fs_1.default.createWriteStream(cachePath, start ? { flags: "r+", start } : { flags: "w" });
28
- response.pipe(out);
29
- out.on("error", reject);
30
- response.on("error", reject);
31
- out.on("finish", resolve);
32
- });
33
- request.on("error", reject);
34
- });
35
- }
36
- async function ensureFaceData(fileName, byteLength) {
37
- const local = path_1.default.join(SOURCE_DIR, fileName);
38
- if (fs_1.default.existsSync(local) && fs_1.default.statSync(local).size >= byteLength) {
39
- return local;
40
- }
41
- const cachePath = path_1.default.join(os_1.default.tmpdir(), fileName);
42
- let cachedBytes = 0;
43
- if (fs_1.default.existsSync(cachePath)) {
44
- cachedBytes = fs_1.default.statSync(cachePath).size;
45
- }
46
- if (cachedBytes < byteLength) {
47
- const megabytes = ((byteLength - cachedBytes) / 1e6).toFixed(0);
48
- console.log(`downloading ${megabytes}MB of ${fileName} from ${SERVER_BASE} -> ${cachePath}`);
49
- await downloadInto(fileName, cachedBytes, byteLength, cachePath);
50
- }
51
- return cachePath;
52
- }
53
- exports.ensureFaceData = ensureFaceData;
54
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFjZUZyYW1lc0RhdGEuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJmYWNlRnJhbWVzRGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUFBQSw0Q0FBb0I7QUFDcEIsNENBQW9CO0FBQ3BCLGdEQUF3QjtBQUN4QixnREFBd0I7QUFFeEIsNEdBQTRHO0FBQzVHLCtHQUErRztBQUMvRywyR0FBMkc7QUFDM0csNEVBQTRFO0FBQzVFLE1BQU0sVUFBVSxHQUFHLG1DQUFtQyxDQUFDO0FBQ3ZELE1BQU0sV0FBVyxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxJQUFJLDJCQUEyQixDQUFDO0FBRTlFLFNBQVMsWUFBWSxDQUFDLFFBQWdCLEVBQUUsS0FBYSxFQUFFLFlBQW9CLEVBQUUsU0FBaUI7SUFDMUYsT0FBTyxJQUFJLE9BQU8sQ0FBQyxDQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUNuQyxNQUFNLEdBQUcsR0FBRyxHQUFHLFdBQVcsSUFBSSxRQUFRLEVBQUUsQ0FBQztRQUN6QyxNQUFNLE9BQU8sR0FBRyxFQUFFLE9BQU8sRUFBRSxFQUFFLEtBQUssRUFBRSxTQUFTLEtBQUssSUFBSSxZQUFZLEdBQUcsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDO1FBQzdFLE1BQU0sT0FBTyxHQUFHLGNBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLE9BQU8sRUFBRSxRQUFRLENBQUMsRUFBRTtZQUM5QyxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssR0FBRyxJQUFJLFFBQVEsQ0FBQyxVQUFVLEtBQUssR0FBRyxFQUFFLENBQUM7Z0JBQzdELFFBQVEsQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDbEIsTUFBTSxDQUFDLElBQUksS0FBSyxDQUFDLEdBQUcsR0FBRyxhQUFhLFFBQVEsQ0FBQyxVQUFVLEVBQUUsQ0FBQyxDQUFDLENBQUM7Z0JBQzVELE9BQU87WUFDWCxDQUFDO1lBQ0QsTUFBTSxHQUFHLEdBQUcsWUFBRSxDQUFDLGlCQUFpQixDQUFDLFNBQVMsRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztZQUM3RixRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1lBQ25CLEdBQUcsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQ3hCLFFBQVEsQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO1lBQzdCLEdBQUcsQ0FBQyxFQUFFLENBQUMsUUFBUSxFQUFFLE9BQU8sQ0FBQyxDQUFDO1FBQzlCLENBQUMsQ0FBQyxDQUFDO1FBQ0gsT0FBTyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDaEMsQ0FBQyxDQUFDLENBQUM7QUFDUCxDQUFDO0FBRU0sS0FBSyxVQUFVLGNBQWMsQ0FBQyxRQUFnQixFQUFFLFVBQWtCO0lBQ3JFLE1BQU0sS0FBSyxHQUFHLGNBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBQzlDLElBQUksWUFBRSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsSUFBSSxZQUFFLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQUksSUFBSSxVQUFVLEVBQUUsQ0FBQztRQUNoRSxPQUFPLEtBQUssQ0FBQztJQUNqQixDQUFDO0lBQ0QsTUFBTSxTQUFTLEdBQUcsY0FBSSxDQUFDLElBQUksQ0FBQyxZQUFFLENBQUMsTUFBTSxFQUFFLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDbkQsSUFBSSxXQUFXLEdBQUcsQ0FBQyxDQUFDO0lBQ3BCLElBQUksWUFBRSxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDO1FBQzNCLFdBQVcsR0FBRyxZQUFFLENBQUMsUUFBUSxDQUFDLFNBQVMsQ0FBQyxDQUFDLElBQUksQ0FBQztJQUM5QyxDQUFDO0lBQ0QsSUFBSSxXQUFXLEdBQUcsVUFBVSxFQUFFLENBQUM7UUFDM0IsTUFBTSxTQUFTLEdBQUcsQ0FBQyxDQUFDLFVBQVUsR0FBRyxXQUFXLENBQUMsR0FBRyxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDaEUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxlQUFlLFNBQVMsU0FBUyxRQUFRLFNBQVMsV0FBVyxPQUFPLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFDN0YsTUFBTSxZQUFZLENBQUMsUUFBUSxFQUFFLFdBQVcsRUFBRSxVQUFVLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDckUsQ0FBQztJQUNELE9BQU8sU0FBUyxDQUFDO0FBQ3JCLENBQUM7QUFoQkQsd0NBZ0JDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IGZzIGZyb20gXCJmc1wiO1xuaW1wb3J0IG9zIGZyb20gXCJvc1wiO1xuaW1wb3J0IHBhdGggZnJvbSBcInBhdGhcIjtcbmltcG9ydCBodHRwIGZyb20gXCJodHRwXCI7XG5cbi8vIFJlc29sdmVzIGEgZmFjZS1lbWJlZGRpbmcgdGVzdCBmaWxlIHRvIGEgbG9jYWwgcGF0aCB0aGF0IGhvbGRzIGF0IGxlYXN0IHRoZSByZXF1ZXN0ZWQgbnVtYmVyIG9mIGJ5dGVzLiBPblxuLy8gdGhlIG1hY2hpbmUgdGhhdCBvd25zIHRoZSBkYXRhIGl0IGp1c3QgdXNlcyB0aGUgb3JpZ2luYWwgZmlsZS4gQW55d2hlcmUgZWxzZSBpdCBkb3dubG9hZHMgdGhlIG1pc3NpbmcgcHJlZml4XG4vLyBmcm9tIHRoZSBmYWNlRnJhbWVzU2VydmVyIG92ZXIgYSByYW5nZSByZXF1ZXN0IGFuZCBjYWNoZXMgaXQgaW4gdGhlIHRlbXAgZGlyLCBzbyByZS1ydW5zIG9ubHkgZmV0Y2ggd2hhdFxuLy8gdGhleSBoYXZlbid0IGFscmVhZHkuIFBvaW50IGl0IGF0IGEgZGlmZmVyZW50IHNlcnZlciB3aXRoIEZBQ0VGUkFNRVNfVVJMLlxuY29uc3QgU09VUkNFX0RJUiA9IFwiL3Jvb3QvY2xhdWRlLXdvcmsvZmFjZS1lbWJlZGRpbmdzXCI7XG5jb25zdCBTRVJWRVJfQkFTRSA9IHByb2Nlc3MuZW52LkZBQ0VGUkFNRVNfVVJMIHx8IFwiaHR0cDovLzY1LjEwOS45My4xMTM6ODc5OVwiO1xuXG5mdW5jdGlvbiBkb3dubG9hZEludG8oZmlsZU5hbWU6IHN0cmluZywgc3RhcnQ6IG51bWJlciwgZW5kRXhjbHVzaXZlOiBudW1iZXIsIGNhY2hlUGF0aDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gICAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgY29uc3QgdXJsID0gYCR7U0VSVkVSX0JBU0V9LyR7ZmlsZU5hbWV9YDtcbiAgICAgICAgY29uc3Qgb3B0aW9ucyA9IHsgaGVhZGVyczogeyBSYW5nZTogYGJ5dGVzPSR7c3RhcnR9LSR7ZW5kRXhjbHVzaXZlIC0gMX1gIH0gfTtcbiAgICAgICAgY29uc3QgcmVxdWVzdCA9IGh0dHAuZ2V0KHVybCwgb3B0aW9ucywgcmVzcG9uc2UgPT4ge1xuICAgICAgICAgICAgaWYgKHJlc3BvbnNlLnN0YXR1c0NvZGUgIT09IDIwNiAmJiByZXNwb25zZS5zdGF0dXNDb2RlICE9PSAyMDApIHtcbiAgICAgICAgICAgICAgICByZXNwb25zZS5yZXN1bWUoKTtcbiAgICAgICAgICAgICAgICByZWplY3QobmV3IEVycm9yKGAke3VybH0gcmV0dXJuZWQgJHtyZXNwb25zZS5zdGF0dXNDb2RlfWApKTtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBjb25zdCBvdXQgPSBmcy5jcmVhdGVXcml0ZVN0cmVhbShjYWNoZVBhdGgsIHN0YXJ0ID8geyBmbGFnczogXCJyK1wiLCBzdGFydCB9IDogeyBmbGFnczogXCJ3XCIgfSk7XG4gICAgICAgICAgICByZXNwb25zZS5waXBlKG91dCk7XG4gICAgICAgICAgICBvdXQub24oXCJlcnJvclwiLCByZWplY3QpO1xuICAgICAgICAgICAgcmVzcG9uc2Uub24oXCJlcnJvclwiLCByZWplY3QpO1xuICAgICAgICAgICAgb3V0Lm9uKFwiZmluaXNoXCIsIHJlc29sdmUpO1xuICAgICAgICB9KTtcbiAgICAgICAgcmVxdWVzdC5vbihcImVycm9yXCIsIHJlamVjdCk7XG4gICAgfSk7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBlbnN1cmVGYWNlRGF0YShmaWxlTmFtZTogc3RyaW5nLCBieXRlTGVuZ3RoOiBudW1iZXIpOiBQcm9taXNlPHN0cmluZz4ge1xuICAgIGNvbnN0IGxvY2FsID0gcGF0aC5qb2luKFNPVVJDRV9ESVIsIGZpbGVOYW1lKTtcbiAgICBpZiAoZnMuZXhpc3RzU3luYyhsb2NhbCkgJiYgZnMuc3RhdFN5bmMobG9jYWwpLnNpemUgPj0gYnl0ZUxlbmd0aCkge1xuICAgICAgICByZXR1cm4gbG9jYWw7XG4gICAgfVxuICAgIGNvbnN0IGNhY2hlUGF0aCA9IHBhdGguam9pbihvcy50bXBkaXIoKSwgZmlsZU5hbWUpO1xuICAgIGxldCBjYWNoZWRCeXRlcyA9IDA7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMoY2FjaGVQYXRoKSkge1xuICAgICAgICBjYWNoZWRCeXRlcyA9IGZzLnN0YXRTeW5jKGNhY2hlUGF0aCkuc2l6ZTtcbiAgICB9XG4gICAgaWYgKGNhY2hlZEJ5dGVzIDwgYnl0ZUxlbmd0aCkge1xuICAgICAgICBjb25zdCBtZWdhYnl0ZXMgPSAoKGJ5dGVMZW5ndGggLSBjYWNoZWRCeXRlcykgLyAxZTYpLnRvRml4ZWQoMCk7XG4gICAgICAgIGNvbnNvbGUubG9nKGBkb3dubG9hZGluZyAke21lZ2FieXRlc31NQiBvZiAke2ZpbGVOYW1lfSBmcm9tICR7U0VSVkVSX0JBU0V9IC0+ICR7Y2FjaGVQYXRofWApO1xuICAgICAgICBhd2FpdCBkb3dubG9hZEludG8oZmlsZU5hbWUsIGNhY2hlZEJ5dGVzLCBieXRlTGVuZ3RoLCBjYWNoZVBhdGgpO1xuICAgIH1cbiAgICByZXR1cm4gY2FjaGVQYXRoO1xufVxuIl19
55
- /* _JS_SOURCE_HASH = "623e4d719ca42b7e83579fd05b7d111f3158958917d536b1ac30e1e3373ad1ce"; */
@@ -1 +0,0 @@
1
- export {};
@@ -1,152 +0,0 @@
1
- import fs from "fs";
2
- import { ensureFaceData } from "./faceFramesData";
3
- import { encodeEmbedding, averageEmbeddings, embeddingToFloat32, releaseFloat32, getCloseness, StoredEmbedding, EmbeddingFormat } from "./embeddingFormats";
4
-
5
- // Runs a generic k-means parameterized by an arbitrary getCloseness, so we can race the closeness
6
- // implementations (all exported from embeddingFormats) on a real clustering workload — plus a variant that
7
- // decodes every embedding to float32 once and uses plain float dots. Push + run with: typenode storage/embeddingBench.ts
8
-
9
- const DIM = 512;
10
- const FILE_NAME = "faceEntry2.f32";
11
- const MODEL = "buffalo_l";
12
- const FORMAT: EmbeddingFormat = "q8g8_2048";
13
- const CONFIG = { format: FORMAT, model: MODEL };
14
-
15
- async function loadEmbeddings(count: number): Promise<StoredEmbedding[]> {
16
- const byteLength = count * DIM * 4;
17
- const filePath = await ensureFaceData(FILE_NAME, byteLength);
18
- const buffer = Buffer.alloc(byteLength);
19
- const handle = fs.openSync(filePath, "r");
20
- fs.readSync(handle, buffer, 0, byteLength, 0);
21
- fs.closeSync(handle);
22
- const floats = new Float32Array(buffer.buffer, buffer.byteOffset, count * DIM);
23
- const out: StoredEmbedding[] = [];
24
- for (let index = 0; index < count; index++) {
25
- out.push(encodeEmbedding({ input: floats.subarray(index * DIM, (index + 1) * DIM), format: FORMAT, model: MODEL }));
26
- }
27
- return out;
28
- }
29
-
30
- // Plain k-means: assign each member to the nearest centroid via `closeness`, recompute centroids as the
31
- // (re-encoded) mean. The closeness function is the only thing that varies.
32
- function kmeans(members: StoredEmbedding[], clusterCount: number, iterations: number, closeness: (a: StoredEmbedding, b: StoredEmbedding) => number): number {
33
- let centroids: StoredEmbedding[] = [];
34
- const seedStride = members.length / clusterCount;
35
- for (let index = 0; index < clusterCount; index++) {
36
- centroids.push(members[Math.floor(index * seedStride)]);
37
- }
38
- for (let iteration = 0; iteration < iterations; iteration++) {
39
- const groups: StoredEmbedding[][] = [];
40
- for (let index = 0; index < centroids.length; index++) {
41
- groups.push([]);
42
- }
43
- for (const member of members) {
44
- let bestIndex = 0;
45
- let bestCloseness = -Infinity;
46
- for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
47
- const value = closeness(member, centroids[centroidIndex]);
48
- if (value > bestCloseness) {
49
- bestCloseness = value;
50
- bestIndex = centroidIndex;
51
- }
52
- }
53
- groups[bestIndex].push(member);
54
- }
55
- const next: StoredEmbedding[] = [];
56
- for (const group of groups) {
57
- if (group.length) {
58
- next.push(averageEmbeddings(group, CONFIG));
59
- }
60
- }
61
- centroids = next;
62
- }
63
- return centroids.length;
64
- }
65
-
66
- // The same k-means but every embedding is decoded to a float32 array ONCE up front; centroids stay float
67
- // arrays and assignment is a plain float dot. No per-comparison decode, no centroid re-encode.
68
- function kmeansFloat32Once(members: StoredEmbedding[], clusterCount: number, iterations: number): number {
69
- const memberFloats: Float32Array[] = [];
70
- for (const member of members) {
71
- memberFloats.push(embeddingToFloat32(member, true));
72
- }
73
- let centroids: Float32Array[] = [];
74
- const seedStride = members.length / clusterCount;
75
- for (let index = 0; index < clusterCount; index++) {
76
- centroids.push(memberFloats[Math.floor(index * seedStride)]);
77
- }
78
- for (let iteration = 0; iteration < iterations; iteration++) {
79
- const groups: number[][] = [];
80
- for (let index = 0; index < centroids.length; index++) {
81
- groups.push([]);
82
- }
83
- for (let memberIndex = 0; memberIndex < memberFloats.length; memberIndex++) {
84
- const memberFloat = memberFloats[memberIndex];
85
- let bestIndex = 0;
86
- let bestDot = -Infinity;
87
- for (let centroidIndex = 0; centroidIndex < centroids.length; centroidIndex++) {
88
- const centroidFloat = centroids[centroidIndex];
89
- const length = Math.min(memberFloat.length, centroidFloat.length);
90
- let dot = 0;
91
- for (let dim = 0; dim < length; dim++) {
92
- dot += memberFloat[dim] * centroidFloat[dim];
93
- }
94
- if (dot > bestDot) {
95
- bestDot = dot;
96
- bestIndex = centroidIndex;
97
- }
98
- }
99
- groups[bestIndex].push(memberIndex);
100
- }
101
- const next: Float32Array[] = [];
102
- for (const group of groups) {
103
- if (!group.length) {
104
- continue;
105
- }
106
- const length = memberFloats[group[0]].length;
107
- const sum = new Float32Array(length);
108
- for (const memberIndex of group) {
109
- const memberFloat = memberFloats[memberIndex];
110
- for (let dim = 0; dim < length; dim++) {
111
- sum[dim] += memberFloat[dim];
112
- }
113
- }
114
- let norm = 0;
115
- for (let dim = 0; dim < length; dim++) {
116
- norm += sum[dim] * sum[dim];
117
- }
118
- const magnitude = Math.sqrt(norm) || 1;
119
- for (let dim = 0; dim < length; dim++) {
120
- sum[dim] /= magnitude;
121
- }
122
- next.push(sum);
123
- }
124
- centroids = next;
125
- }
126
- for (const memberFloat of memberFloats) {
127
- releaseFloat32(memberFloat);
128
- }
129
- return centroids.length;
130
- }
131
-
132
- function time(name: string, run: () => number): number {
133
- const start = Date.now();
134
- const clusters = run();
135
- const seconds = (Date.now() - start) / 1000;
136
- console.log(` ${name.padEnd(10)} ${seconds.toFixed(3)}s (${clusters} clusters)`);
137
- return seconds;
138
- }
139
-
140
- async function main() {
141
- const members = await loadEmbeddings(5000);
142
- const clusterCount = 40;
143
- const iterations = 4;
144
- console.log(`k-means: ${members.length} members, ${clusterCount} clusters, ${iterations} iterations\n`);
145
-
146
- kmeans(members.slice(0, 500), 8, 1, getCloseness); // warm up the JIT
147
-
148
- time("getCloseness", () => kmeans(members, clusterCount, iterations, getCloseness));
149
- time("f32-once", () => kmeansFloat32Once(members, clusterCount, iterations));
150
- }
151
-
152
- main();
@@ -1 +0,0 @@
1
- export declare function ensureFaceData(fileName: string, byteLength: number): Promise<string>;
@@ -1,49 +0,0 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import http from "http";
5
-
6
- // Resolves a face-embedding test file to a local path that holds at least the requested number of bytes. On
7
- // the machine that owns the data it just uses the original file. Anywhere else it downloads the missing prefix
8
- // from the faceFramesServer over a range request and caches it in the temp dir, so re-runs only fetch what
9
- // they haven't already. Point it at a different server with FACEFRAMES_URL.
10
- const SOURCE_DIR = "/root/claude-work/face-embeddings";
11
- const SERVER_BASE = process.env.FACEFRAMES_URL || "http://65.109.93.113:8799";
12
-
13
- function downloadInto(fileName: string, start: number, endExclusive: number, cachePath: string): Promise<void> {
14
- return new Promise((resolve, reject) => {
15
- const url = `${SERVER_BASE}/${fileName}`;
16
- const options = { headers: { Range: `bytes=${start}-${endExclusive - 1}` } };
17
- const request = http.get(url, options, response => {
18
- if (response.statusCode !== 206 && response.statusCode !== 200) {
19
- response.resume();
20
- reject(new Error(`${url} returned ${response.statusCode}`));
21
- return;
22
- }
23
- const out = fs.createWriteStream(cachePath, start ? { flags: "r+", start } : { flags: "w" });
24
- response.pipe(out);
25
- out.on("error", reject);
26
- response.on("error", reject);
27
- out.on("finish", resolve);
28
- });
29
- request.on("error", reject);
30
- });
31
- }
32
-
33
- export async function ensureFaceData(fileName: string, byteLength: number): Promise<string> {
34
- const local = path.join(SOURCE_DIR, fileName);
35
- if (fs.existsSync(local) && fs.statSync(local).size >= byteLength) {
36
- return local;
37
- }
38
- const cachePath = path.join(os.tmpdir(), fileName);
39
- let cachedBytes = 0;
40
- if (fs.existsSync(cachePath)) {
41
- cachedBytes = fs.statSync(cachePath).size;
42
- }
43
- if (cachedBytes < byteLength) {
44
- const megabytes = ((byteLength - cachedBytes) / 1e6).toFixed(0);
45
- console.log(`downloading ${megabytes}MB of ${fileName} from ${SERVER_BASE} -> ${cachePath}`);
46
- await downloadInto(fileName, cachedBytes, byteLength, cachePath);
47
- }
48
- return cachePath;
49
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,56 +0,0 @@
1
- import http from "http";
2
- import fs from "fs";
3
- import path from "path";
4
-
5
- // Throwaway HTTP server that serves the face-embedding test files so the benchmarks can be run from another
6
- // machine (which downloads + caches a prefix via faceFramesData). Supports range requests, so a client only
7
- // pulls the bytes it needs out of the 7.7GB full file. Meant to be left running in the background; it dies on
8
- // reboot and that is fine. Run with: typenode storage/faceFramesServer.ts
9
- const DATA_DIR = "/root/claude-work/face-embeddings";
10
- const PORT = Number(process.env.PORT) || 8799;
11
-
12
- function resolveFile(requestPath: string): string | undefined {
13
- const name = path.basename(decodeURIComponent(requestPath));
14
- if (!name.endsWith(".f32")) {
15
- return undefined;
16
- }
17
- const full = path.join(DATA_DIR, name);
18
- if (!fs.existsSync(full)) {
19
- return undefined;
20
- }
21
- return full;
22
- }
23
-
24
- const server = http.createServer((request, response) => {
25
- const filePath = resolveFile(request.url || "");
26
- if (!filePath) {
27
- response.writeHead(404);
28
- response.end("not found");
29
- return;
30
- }
31
- const total = fs.statSync(filePath).size;
32
- const rangeHeader = request.headers.range;
33
- const rangeMatch = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
34
- if (rangeMatch) {
35
- const start = Number(rangeMatch[1]);
36
- const end = rangeMatch[2] ? Number(rangeMatch[2]) : total - 1;
37
- response.writeHead(206, {
38
- "Content-Type": "application/octet-stream",
39
- "Accept-Ranges": "bytes",
40
- "Content-Range": `bytes ${start}-${end}/${total}`,
41
- "Content-Length": end - start + 1,
42
- });
43
- fs.createReadStream(filePath, { start, end }).pipe(response);
44
- return;
45
- }
46
- response.writeHead(200, {
47
- "Content-Type": "application/octet-stream",
48
- "Accept-Ranges": "bytes",
49
- "Content-Length": total,
50
- });
51
- fs.createReadStream(filePath).pipe(response);
52
- });
53
-
54
- server.listen(PORT, () => {
55
- console.log(`serving ${DATA_DIR}/*.f32 on port ${PORT}`);
56
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,85 +0,0 @@
1
- import fs from "fs";
2
- import { InMemoryDatabase } from "./inMemoryDatabase";
3
- import { IvfEmbeddingRoot, IvfConfig, EmbeddingInput, insertEmbeddings, searchEmbeddings } from "./ivfEmbeddingDatabase";
4
- import { encodeEmbedding, EmbeddingFormat } from "../embeddingFormats";
5
- import { ensureFaceData } from "../faceFramesData";
6
-
7
- // Exercises the tiered IVF embedding database through the in-memory Database wrapper, which counts every
8
- // read/write/delete call and the bytes through each, across dataset sizes and load patterns, with timing.
9
-
10
- const DIM = 512;
11
- const FILE_NAME = "faceFrames2_full.f32";
12
- const MODEL = "buffalo_l";
13
- const FORMAT: EmbeddingFormat = "q8g8_2048";
14
-
15
- async function loadFloats(count: number): Promise<Float32Array> {
16
- const byteLength = count * DIM * 4;
17
- const filePath = await ensureFaceData(FILE_NAME, byteLength);
18
- const buffer = Buffer.alloc(byteLength);
19
- const handle = fs.openSync(filePath, "r");
20
- fs.readSync(handle, buffer, 0, byteLength, 0);
21
- fs.closeSync(handle);
22
- return new Float32Array(buffer.buffer, buffer.byteOffset, count * DIM);
23
- }
24
-
25
- async function buildInputs(count: number): Promise<EmbeddingInput[]> {
26
- const floats = await loadFloats(count);
27
- const inputs: EmbeddingInput[] = [];
28
- for (let index = 0; index < count; index++) {
29
- const slice = floats.subarray(index * DIM, (index + 1) * DIM);
30
- inputs.push({ ref: "e" + index, embedding: encodeEmbedding({ input: slice, format: FORMAT, model: MODEL }) });
31
- }
32
- return inputs;
33
- }
34
-
35
- function freshDatabase(config: IvfConfig): InMemoryDatabase<IvfEmbeddingRoot> {
36
- const root: IvfEmbeddingRoot = { config, count: 0, flat: {}, byRef: {}, steps: {}, centroids: {}, cells: {} };
37
- return new InMemoryDatabase<IvfEmbeddingRoot>(root);
38
- }
39
-
40
- function pad(value: string | number, width: number): string {
41
- return String(value).padStart(width);
42
- }
43
-
44
- async function main() {
45
- const config: IvfConfig = { model: MODEL, format: FORMAT, cellTargetSize: 128 };
46
- const runs: { size: number; batch: number }[] = [
47
- { size: 10000, batch: 1 },
48
- { size: 10000, batch: 100 },
49
- { size: 100000, batch: 1000 },
50
- ];
51
-
52
- let maxSize = 0;
53
- for (const run of runs) {
54
- if (run.size > maxSize) {
55
- maxSize = run.size;
56
- }
57
- }
58
- const loadStart = Date.now();
59
- const allInputs = await buildInputs(maxSize);
60
- console.log(`loaded + encoded ${maxSize} embeddings in ${((Date.now() - loadStart) / 1000).toFixed(1)}s\n`);
61
-
62
- console.log(`${pad("added", 7)} ${pad("batch", 5)} | ${pad("secs", 7)} ${pad("reads", 8)} ${pad("writes", 8)} ${pad("deletes", 8)} ${pad("readMB", 9)} ${pad("writeMB", 8)} | recall`);
63
- for (const run of runs) {
64
- const database = freshDatabase(config);
65
- const start = Date.now();
66
- for (let offset = 0; offset < run.size; offset += run.batch) {
67
- insertEmbeddings(database, allInputs.slice(offset, offset + run.batch));
68
- }
69
- const seconds = (Date.now() - start) / 1000;
70
-
71
- let found = 0;
72
- const sampleCount = 100;
73
- for (let index = 0; index < sampleCount; index++) {
74
- const query = allInputs[Math.floor(index * run.size / sampleCount)];
75
- const hits = searchEmbeddings(database, query.embedding, { probeBudget: 512, resultCount: 1 });
76
- if (hits && hits.length && hits[0].ref === query.ref) {
77
- found++;
78
- }
79
- }
80
- const recall = (found / sampleCount).toFixed(2);
81
- console.log(`${pad(run.size, 7)} ${pad(run.batch, 5)} | ${pad(seconds.toFixed(1), 7)} ${pad(database.readCalls, 8)} ${pad(database.writeCalls, 8)} ${pad(database.deleteCalls, 8)} ${pad((database.bytesRead / 1e6).toFixed(0), 9)} ${pad((database.bytesWritten / 1e6).toFixed(0), 8)} | ${recall}`);
82
- }
83
- }
84
-
85
- void main();