sliftutils 1.7.12 → 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 (53) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/index.d.ts +95 -39
  3. package/misc/dist/yaml.ts.cache +2 -2
  4. package/misc/dist/yamlBase.ts.cache +2 -2
  5. package/misc/https/hostServer.d.ts +7 -0
  6. package/misc/https/hostServer.ts +89 -20
  7. package/misc/openrouter.ts +17 -14
  8. package/package.json +2 -2
  9. package/storage/ArchivesDisk.d.ts +1 -1
  10. package/storage/ArchivesDisk.ts +2 -1
  11. package/storage/IArchives.d.ts +5 -1
  12. package/storage/IArchives.ts +5 -1
  13. package/storage/backblaze.d.ts +1 -1
  14. package/storage/backblaze.ts +9 -6
  15. package/storage/dist/embeddingFormats.ts.cache +2 -2
  16. package/storage/proxydatabase/dist/Database.ts.cache +2 -2
  17. package/storage/proxydatabase/dist/ivfEmbeddingDatabase.ts.cache +123 -61
  18. package/storage/proxydatabase/dist/transactionSet.ts.cache +16 -4
  19. package/storage/remoteStorage/ArchivesRemote.d.ts +1 -1
  20. package/storage/remoteStorage/ArchivesRemote.ts +2 -1
  21. package/storage/remoteStorage/ArchivesUrl.d.ts +1 -1
  22. package/storage/remoteStorage/ArchivesUrl.ts +13 -9
  23. package/storage/remoteStorage/blobStore.d.ts +6 -2
  24. package/storage/remoteStorage/blobStore.ts +46 -6
  25. package/storage/remoteStorage/createArchives.d.ts +26 -11
  26. package/storage/remoteStorage/createArchives.ts +104 -28
  27. package/storage/remoteStorage/deployTakeover.d.ts +24 -0
  28. package/storage/remoteStorage/deployTakeover.ts +373 -0
  29. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +16 -11
  30. package/storage/remoteStorage/dist/blobStore.ts.cache +39 -4
  31. package/storage/remoteStorage/dist/createArchives.ts.cache +91 -9
  32. package/storage/remoteStorage/dist/deployTakeover.ts.cache +371 -0
  33. package/storage/remoteStorage/dist/remoteConfig.ts.cache +9 -3
  34. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +3 -3
  35. package/storage/remoteStorage/dist/storageController.ts.cache +11 -12
  36. package/storage/remoteStorage/dist/storageServerState.ts.cache +120 -19
  37. package/storage/remoteStorage/remoteConfig.d.ts +1 -0
  38. package/storage/remoteStorage/remoteConfig.ts +6 -0
  39. package/storage/remoteStorage/storageController.d.ts +2 -0
  40. package/storage/remoteStorage/storageController.ts +9 -9
  41. package/storage/remoteStorage/storageServer.ts +14 -1
  42. package/storage/remoteStorage/storageServerState.d.ts +16 -1
  43. package/storage/remoteStorage/storageServerState.ts +118 -16
  44. package/yarn.lock +4 -4
  45. package/storage/dist/faceFramesData.ts.cache +0 -55
  46. package/storage/embeddingBench.d.ts +0 -1
  47. package/storage/embeddingBench.ts +0 -152
  48. package/storage/faceFramesData.d.ts +0 -1
  49. package/storage/faceFramesData.ts +0 -49
  50. package/storage/faceFramesServer.d.ts +0 -1
  51. package/storage/faceFramesServer.ts +0 -56
  52. package/storage/proxydatabase/ivfDbCheck.d.ts +0 -1
  53. package/storage/proxydatabase/ivfDbCheck.ts +0 -85
@@ -6,7 +6,8 @@ import { getExternalIP } from "socket-function/src/networking";
6
6
  import { RequireController } from "socket-function/require/RequireController";
7
7
  import { hostServer } from "../../misc/https/hostServer";
8
8
  import { RemoteStorageController } from "./storageController";
9
- import { setStorageServerConfig, setWritesRejectedReason } from "./storageServerState";
9
+ import { setStorageServerConfig, setWritesRejectedReason, addExtraListenPort } from "./storageServerState";
10
+ import { initDeployTakeover, registerAltPort, getMainPortAcquireDelay } from "./deployTakeover";
10
11
  import { parseStorageUrl } from "./ArchivesRemote";
11
12
  // Import browser code, so it is allowed to be required by the client
12
13
  import "./accessPage";
@@ -118,9 +119,21 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
118
119
  }, DISK_SPACE_CHECK_INTERVAL_MS);
119
120
  (interval as { unref?: () => void }).unref?.();
120
121
 
122
+ // Zero-downtime deploys: only enabled when a deploy timeline exists - otherwise a busy port is
123
+ // a real misconfiguration and must throw, not silently listen elsewhere
124
+ let hasDeployTimeline = await initDeployTakeover({ domain, mainPort: port, storageFolder: path.resolve(folder) });
125
+
121
126
  await hostServer({
122
127
  domain,
123
128
  port,
124
129
  setDNSRecord: true,
130
+ portFallback: hasDeployTimeline && {
131
+ getAcquireDelay: getMainPortAcquireDelay,
132
+ onListening: (listeningPort, isMainPort) => {
133
+ if (isMainPort) return;
134
+ addExtraListenPort(listeningPort);
135
+ void registerAltPort(listeningPort).catch((e: Error) => console.error(`Registering alternate port ${listeningPort} failed: ${e.stack ?? e}`));
136
+ },
137
+ } || undefined,
125
138
  });
126
139
  }
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
3
  import { IBucketStore } from "./blobStore";
4
- import { RemoteConfig, HostedConfig, IArchives } from "../IArchives";
4
+ import { RemoteConfig, HostedConfig, IArchives, ArchivesConfig } from "../IArchives";
5
5
  import type { IStorage } from "../IStorage";
6
6
  import type { AccessRequest, TrustRecord } from "./storageController";
7
7
  export type StorageServerConfig = {
@@ -34,10 +34,25 @@ export type LoadedBucket = {
34
34
  self: HostedConfig | undefined;
35
35
  store: IBucketStore;
36
36
  };
37
+ export declare function addExtraListenPort(port: number): void;
37
38
  export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
38
39
  export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
39
40
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
40
41
  lastModified?: number;
41
42
  }): Promise<void>;
43
+ export declare function getBucketConfig(bucket: LoadedBucket): ArchivesConfig;
44
+ export declare function rebuildAllLoadedBuckets(): Promise<void>;
45
+ export declare function rescanAllLoadedBucketDisks(): Promise<void>;
46
+ export type ServerBucketInfo = {
47
+ bucketName: string;
48
+ active: boolean;
49
+ config?: ArchivesConfig;
50
+ error?: string;
51
+ };
52
+ /** Every bucket the account has on this server, active or not, each with its configuration.
53
+ * Inactive buckets are inspected straight from disk WITHOUT loading them - loading would start
54
+ * their synchronization, and old invalid buckets must stay inert (their parse error is reported
55
+ * instead). */
56
+ export declare function listAccountBuckets(account: string): Promise<ServerBucketInfo[]>;
42
57
  export declare function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void>;
43
58
  export declare function getLocalArchives(account: string, bucketName: string): IArchives;
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import fs from "fs";
2
3
  import { getFileStorageNested2 } from "../FileFolderAPI";
3
4
  import { TransactionStorage } from "../TransactionStorage";
4
5
  import { JSONStorage } from "../JSONStorage";
@@ -10,6 +11,7 @@ import {
10
11
  WRITE_PAST_WINDOW_GRACE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
11
12
  } from "../IArchives";
12
13
  import { ROUTING_FILE, parseRoutingData, parseHostedUrl, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
14
+ import { applyDeployRemap, getFlushDeadline, onTakeoverEvent } from "./deployTakeover";
13
15
  import { createApiArchives } from "./createArchives";
14
16
  import type { IStorage } from "../IStorage";
15
17
  import type { AccessRequest, TrustRecord } from "./storageController";
@@ -156,6 +158,13 @@ function getBucketFolder(account: string, bucketName: string): string {
156
158
  return path.join(getStorageServerConfig().folder, "buckets2", account, bucketName);
157
159
  }
158
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
+
159
168
  function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
160
169
  let { domain, port } = getStorageServerConfig();
161
170
  let indexes: number[] = [];
@@ -163,7 +172,7 @@ function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: str
163
172
  let source = routing.sources[i];
164
173
  if (typeof source === "string" || source.type !== "remote") continue;
165
174
  let parsed = parseHostedUrl(source.url);
166
- 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) {
167
176
  indexes.push(i);
168
177
  }
169
178
  }
@@ -222,12 +231,17 @@ function scheduleWindowBoundaryRebuild(loaded: LoadedBucket): void {
222
231
 
223
232
  function buildBucket(account: string, bucketName: string, routing: RemoteConfig): LoadedBucket {
224
233
  let folder = getBucketFolder(account, bucketName);
225
- let selfIndexes = findSelfIndexes(routing, account, bucketName);
226
- 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);
227
241
  let self = selectEntryAt(selfEntries, Date.now());
228
242
  let selfIndex = -1;
229
243
  if (self) {
230
- selfIndex = routing.sources.indexOf(self);
244
+ selfIndex = effective.sources.indexOf(self);
231
245
  }
232
246
  let store: IBucketStore;
233
247
  if (self?.rawDisk) {
@@ -250,8 +264,8 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
250
264
  if (selfIndex === -1) {
251
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`);
252
266
  } else {
253
- for (let i = selfIndex + 1; i < routing.sources.length; i++) {
254
- let source = routing.sources[i];
267
+ for (let i = selfIndex + 1; i < effective.sources.length; i++) {
268
+ let source = effective.sources[i];
255
269
  if (typeof source === "string" || ownIndexes.has(i)) continue;
256
270
  // Disjoint-shard sources never talk to each other; a partial overlap syncs only
257
271
  // the intersection (scans ignore the rest, writes only send matching keys)
@@ -270,6 +284,7 @@ function buildBucket(account: string, bucketName: string, routing: RemoteConfig)
270
284
  void scheduleRoutingReload(account, bucketName);
271
285
  },
272
286
  readerDiskLimit: self?.readerDiskLimit,
287
+ getFlushDeadline,
273
288
  });
274
289
  }
275
290
  let loaded: LoadedBucket = { account, bucketName, routing, routingJSON: JSON.stringify(routing), selfEntries, self, store };
@@ -416,6 +431,99 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
416
431
  await loaded.store.set(filePath, data, { ...getWriteConfig(loaded, writeTime, route), lastModified: writeTime });
417
432
  }
418
433
 
434
+ export function getBucketConfig(bucket: LoadedBucket): ArchivesConfig {
435
+ let progress = bucket.store.getSyncProgress?.();
436
+ return {
437
+ supportsChangesAfter: !!bucket.store.getChangesAfter,
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),
441
+ index: progress?.index,
442
+ indexSources: progress?.sources,
443
+ readerDiskLimit: progress?.readerDiskLimit,
444
+ syncing: progress?.syncing,
445
+ };
446
+ }
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
+
478
+ export type ServerBucketInfo = {
479
+ bucketName: string;
480
+ // Loaded in memory (created or accessed since startup), so its synchronization is running
481
+ active: boolean;
482
+ config?: ArchivesConfig;
483
+ error?: string;
484
+ };
485
+
486
+ /** Every bucket the account has on this server, active or not, each with its configuration.
487
+ * Inactive buckets are inspected straight from disk WITHOUT loading them - loading would start
488
+ * their synchronization, and old invalid buckets must stay inert (their parse error is reported
489
+ * instead). */
490
+ export async function listAccountBuckets(account: string): Promise<ServerBucketInfo[]> {
491
+ let accountFolder = path.join(getStorageServerConfig().folder, "buckets2", account);
492
+ let names: string[];
493
+ try {
494
+ names = await fs.promises.readdir(accountFolder);
495
+ } catch {
496
+ return [];
497
+ }
498
+ let results: ServerBucketInfo[] = [];
499
+ for (let bucketName of names) {
500
+ let loadedPromise = buckets.get(`${account}/${bucketName}`);
501
+ if (loadedPromise) {
502
+ try {
503
+ let loaded = await loadedPromise;
504
+ if (loaded) {
505
+ results.push({ bucketName, active: true, config: getBucketConfig(loaded) });
506
+ continue;
507
+ }
508
+ } catch (e) {
509
+ results.push({ bucketName, active: true, error: String((e as Error).stack ?? e).slice(0, 500) });
510
+ continue;
511
+ }
512
+ }
513
+ try {
514
+ let data = await new ArchivesDisk(getBucketFolder(account, bucketName)).get(ROUTING_FILE);
515
+ if (!data) {
516
+ results.push({ bucketName, active: false, error: `No routing file (${ROUTING_FILE})` });
517
+ continue;
518
+ }
519
+ results.push({ bucketName, active: false, config: { remoteConfig: parseRoutingData(data) } });
520
+ } catch (e) {
521
+ results.push({ bucketName, active: false, error: String((e as Error).stack ?? e).slice(0, 500) });
522
+ }
523
+ }
524
+ return results;
525
+ }
526
+
419
527
  export async function deleteBucketFile(account: string, bucketName: string, filePath: string): Promise<void> {
420
528
  if (filePath === ROUTING_FILE) {
421
529
  throw new Error(`The routing config ${JSON.stringify(ROUTING_FILE)} cannot be deleted (overwrite it to change the bucket's configuration)`);
@@ -450,8 +558,9 @@ class ArchivesLocalBucket implements IArchives {
450
558
  if (!bucket) return undefined;
451
559
  return await bucket.store.get2(fileName, config);
452
560
  }
453
- 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> {
454
562
  await writeBucketFile(this.account, this.bucketName, fileName, data, config);
563
+ return fileName;
455
564
  }
456
565
  public async del(fileName: string): Promise<void> {
457
566
  await deleteBucketFile(this.account, this.bucketName, fileName);
@@ -498,15 +607,8 @@ class ArchivesLocalBucket implements IArchives {
498
607
  public async getConfig(): Promise<ArchivesConfig> {
499
608
  let bucket = await this.getBucket();
500
609
  // Missing buckets say true, matching what they become once created (the default store type)
501
- let progress = bucket?.store.getSyncProgress?.();
502
- return {
503
- supportsChangesAfter: !bucket || !!bucket.store.getChangesAfter,
504
- remoteConfig: bucket?.routing,
505
- index: progress?.index,
506
- indexSources: progress?.sources,
507
- readerDiskLimit: progress?.readerDiskLimit,
508
- syncing: progress?.syncing,
509
- };
610
+ if (!bucket) return { supportsChangesAfter: true };
611
+ return getBucketConfig(bucket);
510
612
  }
511
613
  public async hasWriteAccess(): Promise<boolean> {
512
614
  return true;
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.16:
1948
- version "1.2.16"
1949
- resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.2.16.tgz#6ad7fc1971202d9768582b0212f086675fba61b5"
1950
- integrity sha512-o3GATr5Fa+496StsPWJIb0yytY/XRMZjJQ4cgb1LOQIfFv7uJUJ3NZtcldMeN/kOmqI0hBaMvhRLC3VHke9awA==
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 {};