@volter/twin-turbopuffer 0.1.0

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 (52) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +145 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +27 -0
  5. package/dist/src/generated/surface.gen.json +1 -0
  6. package/dist/src/generated/ui.gen.json +1 -0
  7. package/dist/src/index.d.ts +11 -0
  8. package/dist/src/index.js +65 -0
  9. package/dist/src/key-gate.d.ts +3 -0
  10. package/dist/src/key-gate.js +39 -0
  11. package/dist/src/manifest.d.ts +2 -0
  12. package/dist/src/manifest.js +28 -0
  13. package/dist/src/screens/dashboard.d.ts +11 -0
  14. package/dist/src/screens/dashboard.js +191 -0
  15. package/dist/src/semantics/namespaces.d.ts +5 -0
  16. package/dist/src/semantics/namespaces.js +17 -0
  17. package/dist/src/turbopuffer-capabilities.d.ts +6 -0
  18. package/dist/src/turbopuffer-capabilities.js +442 -0
  19. package/dist/src/turbopuffer-conformance.d.ts +8 -0
  20. package/dist/src/turbopuffer-conformance.js +102 -0
  21. package/dist/src/turbopuffer-connector.d.ts +34 -0
  22. package/dist/src/turbopuffer-connector.js +152 -0
  23. package/dist/src/turbopuffer-filter.d.ts +57 -0
  24. package/dist/src/turbopuffer-filter.js +286 -0
  25. package/dist/src/turbopuffer-server.d.ts +23 -0
  26. package/dist/src/turbopuffer-server.js +72 -0
  27. package/dist/src/turbopuffer-stem.d.ts +1 -0
  28. package/dist/src/turbopuffer-stem.js +133 -0
  29. package/dist/src/turbopuffer-store.d.ts +80 -0
  30. package/dist/src/turbopuffer-store.js +1304 -0
  31. package/dist/src/turbopuffer-text.d.ts +44 -0
  32. package/dist/src/turbopuffer-text.js +189 -0
  33. package/dist/src/turbopuffer-twin.d.ts +25 -0
  34. package/dist/src/turbopuffer-twin.js +406 -0
  35. package/package.json +56 -0
  36. package/src/cli.ts +28 -0
  37. package/src/generated/surface.gen.json +1 -0
  38. package/src/generated/ui.gen.json +1 -0
  39. package/src/index.ts +92 -0
  40. package/src/key-gate.ts +39 -0
  41. package/src/manifest.ts +63 -0
  42. package/src/screens/dashboard.tsx +214 -0
  43. package/src/semantics/namespaces.ts +30 -0
  44. package/src/turbopuffer-capabilities.ts +455 -0
  45. package/src/turbopuffer-conformance.ts +101 -0
  46. package/src/turbopuffer-connector.ts +153 -0
  47. package/src/turbopuffer-filter.ts +277 -0
  48. package/src/turbopuffer-server.ts +81 -0
  49. package/src/turbopuffer-stem.ts +104 -0
  50. package/src/turbopuffer-store.ts +1157 -0
  51. package/src/turbopuffer-text.ts +204 -0
  52. package/src/turbopuffer-twin.ts +429 -0
@@ -0,0 +1,406 @@
1
+ // TURBOPUFFER TWIN — THE REQUEST HANDLER. Contract:
2
+ // handleTurbopufferTwinRequest({ method, path, body, headers, root, occurredAt, readOnly, token })
3
+ // -> { status, body, headers }
4
+ //
5
+ // The wire, grounded in the installed @turbopuffer/turbopuffer@2.8.0 (src/resources/namespaces.ts,
6
+ // src/client.ts — see the README's "Grounded wire" table):
7
+ //
8
+ // GET /v1/namespaces list namespaces (prefix, page_size, cursor)
9
+ // POST /v2/namespaces/{ns} write: upsert/patch/delete rows or columns, by
10
+ // id or by filter, with schema + distance_metric
11
+ // DELETE /v2/namespaces/{ns} deleteAll
12
+ // POST /v2/namespaces/{ns}/query query; a body carrying `queries` is multiQuery
13
+ // (the SDK also tags it ?stainless_overload=multiQuery)
14
+ // GET /v2/namespaces/{ns}/metadata metadata
15
+ // GET /v1/namespaces/{ns}/schema schema
16
+ // POST /v1/namespaces/{ns}/schema updateSchema
17
+ // GET /v1/namespaces/{ns}/hint_cache_warm hintCacheWarm
18
+ //
19
+ // A leading region segment (`/aws-us-east-1/v2/...`) is accepted and ignored: it is how a world
20
+ // points an unmodified client here through the SDK's own `TURBOPUFFER_BASE_URL`, whose `{region}`
21
+ // placeholder the client fills in (client.ts:273-281) — see the pack descriptor's `endpointEnv`.
22
+ //
23
+ // Errors wear the `{ "status": "error", "error": "<message>" }` envelope. The message STRINGS are
24
+ // twin-authored (this build had no account to probe the real service); the statuses follow the
25
+ // SDK's error classes (400 BadRequest, 401 Authentication, 404 NotFound).
26
+ import { createHash } from 'node:crypto';
27
+ import { applyTwinWriteAtomic, ownFields, twinResources } from '@volter/world-core';
28
+ import { applyClone, applyWrite, approxLogicalBytes, assertNamespaceName, badRequest, checkConsistency, documentFields, documentSubjectId, loadNamespaces, namespaceFields, queryBilling, queryPerformance, runQuery, schemaWire, SERVICE, TurbopufferError, TURBOPUFFER_RESOURCE_TYPES, vectorEncodingRefusal, } from "./turbopuffer-store.js";
29
+ const JSON_HEADERS = { 'content-type': 'application/json' };
30
+ /** Real Turbopuffer routes this twin does not serve yet, each with the manifest gap that tracks it. */
31
+ const UNMODELED_ROUTES = [
32
+ { method: 'PATCH', re: /^\/v1\/namespaces\/[^/]+\/metadata$/, gap: 'turbopuffer.namespaces.update_metadata' },
33
+ { method: 'POST', re: /^\/v2\/namespaces\/[^/]+\/explain_query$/, gap: 'turbopuffer.query.explain' },
34
+ { method: 'POST', re: /^\/v1\/namespaces\/[^/]+\/_debug\/recall$/, gap: 'turbopuffer.query.recall' },
35
+ { method: 'POST', re: /^\/v1\/namespaces\/[^/]+(\/query)?$/, gap: 'turbopuffer.legacy.v1_api' },
36
+ { method: 'GET', re: /^\/v1\/namespaces\/[^/]+$/, gap: 'turbopuffer.legacy.v1_api' },
37
+ { method: 'DELETE', re: /^\/v1\/namespaces\/[^/]+$/, gap: 'turbopuffer.legacy.v1_api' },
38
+ ];
39
+ function fail(status, error) {
40
+ return { status, body: { status: 'error', error }, headers: { ...JSON_HEADERS } };
41
+ }
42
+ function ok(body) {
43
+ return { status: 200, body, headers: { ...JSON_HEADERS } };
44
+ }
45
+ function lowerHeaders(h) {
46
+ const out = {};
47
+ for (const [k, v] of Object.entries(h ?? {}))
48
+ out[k.toLowerCase()] = v;
49
+ return out;
50
+ }
51
+ /** `Authorization: Bearer <key>` (client.ts `authHeaders`). An empty key is no key. */
52
+ export function extractTurbopufferApiKey(headers) {
53
+ const raw = lowerHeaders(headers).authorization;
54
+ if (raw === undefined)
55
+ return null;
56
+ const m = /^bearer(?:\s+(.*))?$/i.exec(raw.trim());
57
+ if (!m)
58
+ return null;
59
+ const key = (m[1] ?? '').trim();
60
+ return key === '' ? null : key;
61
+ }
62
+ /** Collapse duplicate slashes and drop a leading region segment (see the header). */
63
+ export function normalizeTurbopufferPath(rawPath) {
64
+ let p = `/${rawPath.replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
65
+ p = p.replace(/^\/(?:aws|gcp|azure)-[a-z0-9-]+(?=\/v\d+\/)/, '');
66
+ return p.length > 1 ? p.replace(/\/+$/, '') : p;
67
+ }
68
+ function decodeNs(seg) {
69
+ let name;
70
+ try {
71
+ name = decodeURIComponent(seg);
72
+ }
73
+ catch {
74
+ name = seg;
75
+ }
76
+ assertNamespaceName(name);
77
+ return name;
78
+ }
79
+ function parseBody(raw) {
80
+ if (raw === undefined || raw.trim() === '')
81
+ return {};
82
+ try {
83
+ return JSON.parse(raw);
84
+ }
85
+ catch {
86
+ throw badRequest('invalid request: body is not valid JSON');
87
+ }
88
+ }
89
+ function notFound(ns) {
90
+ return new TurbopufferError(404, `namespace '${ns}' was not found`);
91
+ }
92
+ function readSpace(ns, root) {
93
+ const space = loadNamespaces(twinResources(SERVICE, root)).get(ns);
94
+ if (space === undefined)
95
+ throw notFound(ns);
96
+ return space;
97
+ }
98
+ export async function handleTurbopufferTwinRequest(req) {
99
+ const method = req.method.toUpperCase();
100
+ const [rawPath, rawQuery] = req.path.split('?');
101
+ const query = new URLSearchParams(rawQuery ?? '');
102
+ const path = normalizeTurbopufferPath(rawPath ?? '/');
103
+ const at = req.occurredAt ?? new Date().toISOString();
104
+ const key = extractTurbopufferApiKey(req.headers);
105
+ if (key === null)
106
+ return fail(401, 'missing API key: send Authorization: Bearer <TURBOPUFFER_API_KEY>');
107
+ if (req.token !== undefined && key !== req.token)
108
+ return fail(401, 'invalid API key');
109
+ try {
110
+ if (path === '/v1/namespaces') {
111
+ if (method !== 'GET')
112
+ return fail(405, `method ${method} not allowed on ${path}`);
113
+ return ok(listNamespaces(req.root, query));
114
+ }
115
+ let m = /^\/v2\/namespaces\/([^/]+)$/.exec(path);
116
+ if (m) {
117
+ const ns = decodeNs(m[1]);
118
+ if (method === 'POST') {
119
+ const body = parseBody(req.body);
120
+ return asyncRequested(req, body) ? await asyncCopy(req, ns, body, at) : await write(req, ns, body, at);
121
+ }
122
+ return method === 'DELETE' ? await deleteAll(req, ns, at) : unrouted(method, path);
123
+ }
124
+ m = /^\/v2\/namespaces\/([^/]+)\/query$/.exec(path);
125
+ if (m) {
126
+ return method === 'POST' ? ok(queryNamespace(decodeNs(m[1]), parseBody(req.body), req.root)) : unrouted(method, path);
127
+ }
128
+ m = /^\/v1\/namespaces\/([^/]+)\/operations\/([^/]+)$/.exec(path);
129
+ if (m && method === 'GET')
130
+ return pollOperation(req, decodeNs(m[1]), decodeURIComponent(m[2]), at);
131
+ // GET /v1/namespaces/:namespace/metadata is the docs' path (https://turbopuffer.com/docs/metadata); the client sends /v2
132
+ m = /^\/v[12]\/namespaces\/([^/]+)\/metadata$/.exec(path);
133
+ if (m && method === 'GET')
134
+ return ok(metadata(readSpace(decodeNs(m[1]), req.root)));
135
+ m = /^\/v1\/namespaces\/([^/]+)\/schema$/.exec(path);
136
+ if (m)
137
+ return await schemaRoute(req, decodeNs(m[1]), method, path, at);
138
+ m = /^\/v1\/namespaces\/([^/]+)\/hint_cache_warm$/.exec(path);
139
+ return m && method === 'GET' ? hintCacheWarm(decodeNs(m[1]), req.root) : unrouted(method, path);
140
+ }
141
+ catch (e) {
142
+ return e instanceof TurbopufferError ? fail(e.status, e.message) : internalFault(method, path, e);
143
+ }
144
+ }
145
+ /** A request this handler has no route for: a method a path does not take (405), a route of turbopuffer's the twin does
146
+ * not model (404 naming its gap), or none (404). The derived dispatch hands this handler only the operations it serves,
147
+ * so only the protocol 2 harness (turbopuffer-conformance.ts's router census) asks these. */
148
+ /** The twin has no cache to warm: every namespace is always "hot". Accepting the hint is its answer, 202 as the spec's
149
+ * operation answers it. */
150
+ function hintCacheWarm(ns, root) {
151
+ readSpace(ns, root);
152
+ return { status: 202, body: { status: 'ACCEPTED', message: 'cache warm hint accepted' }, headers: { ...JSON_HEADERS } };
153
+ }
154
+ function unrouted(method, path) {
155
+ const r = UNMODELED_ROUTES.find((x) => x.method === method && x.re.test(path));
156
+ if (r)
157
+ return fail(404, `turbopuffer twin: ${method} ${path} is real Turbopuffer surface this twin does not model yet (${r.gap} is the filed gap)`);
158
+ return /^(\/v2\/namespaces\/[^/]+(\/query)?|\/v1\/namespaces\/[^/]+\/schema)$/.test(path) ? fail(405, `method ${method} not allowed on ${path}`) : fail(404, `not found: ${method} ${path}`);
159
+ }
160
+ /** The engine's own bug guard: an error that is not a refusal of turbopuffer's. */
161
+ function internalFault(method, path, e) {
162
+ return fail(500, `turbopuffer twin: internal fault handling ${method} ${path} — ${e instanceof Error ? e.message : String(e)}`);
163
+ }
164
+ /** GET and POST /v1/namespaces/{namespace}/schema, the spec's "Get namespace schema" and "Update namespace schema",
165
+ * which the client sends (`schema()`, `updateSchema()`); turbopuffer's pages now show a namespace's schema in its
166
+ * metadata and change it with a write carrying only `{"schema": …}`. */
167
+ async function schemaRoute(req, ns, method, path, at) {
168
+ if (method === 'GET')
169
+ return ok(schemaWire(readSpace(ns, req.root)));
170
+ return method === 'POST' ? await updateSchema(req, ns, parseBody(req.body), at) : unrouted(method, path);
171
+ }
172
+ // ── routes ──────────────────────────────────────────────────────────────────────────────────
173
+ function listNamespaces(root, query) {
174
+ const prefix = query.get('prefix') ?? '';
175
+ const sizeRaw = query.get('page_size');
176
+ // "page_size … default: 100 … (max of 1000)" (https://turbopuffer.com/docs/namespaces)
177
+ const pageSize = sizeRaw === null ? 100 : Number(sizeRaw);
178
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1000)
179
+ throw badRequest('invalid page_size: must be an integer between 1 and 1000');
180
+ const cursor = query.get('cursor') ?? '';
181
+ const names = [...loadNamespaces(twinResources(SERVICE, root)).keys()]
182
+ .filter((n) => n.startsWith(prefix) && (cursor === '' || n > cursor))
183
+ .sort();
184
+ const page = names.slice(0, pageSize);
185
+ return {
186
+ namespaces: page.map((id) => ({ id })),
187
+ ...(names.length > pageSize ? { next_cursor: page[page.length - 1] } : {}),
188
+ };
189
+ }
190
+ async function commit(req, decide) {
191
+ if (req.readOnly)
192
+ return fail(405, 'read_only: this twin was started read-only; writes are refused');
193
+ const { value } = await applyTwinWriteAtomic(SERVICE, (resources) => {
194
+ try {
195
+ const { response, write } = decide(loadNamespaces(resources));
196
+ return { kind: 'write', value: { ok: true, response }, write };
197
+ }
198
+ catch (e) {
199
+ return refusedDecision(e);
200
+ }
201
+ }, req.root);
202
+ return value.ok ? ok(value.response) : fail(value.error.status, value.error.message);
203
+ }
204
+ /** A write the store refused (a TurbopufferError) is skipped and answered; any other error is the engine's. */
205
+ function refusedDecision(e) {
206
+ if (e instanceof TurbopufferError)
207
+ return { kind: 'skip', value: { ok: false, error: e } };
208
+ throw e;
209
+ }
210
+ function write(req, ns, body, at) {
211
+ return commit(req, (spaces) => {
212
+ const b = body !== null && typeof body === 'object' && !Array.isArray(body) ? body : {};
213
+ const out = b.copy_from_namespace !== undefined || b.branch_from_namespace !== undefined ? applyClone(spaces, ns, body, at) : applyWrite(spaces.get(ns), ns, body, at);
214
+ return {
215
+ response: out.response,
216
+ write: {
217
+ operation: 'namespace.write',
218
+ subjectType: 'namespace',
219
+ subjectId: ns,
220
+ fields: namespaceFields(out.next),
221
+ input: body,
222
+ projection: {
223
+ updates: out.upserts.map((d) => ({ type: 'document', id: documentSubjectId(ns, d.id), fields: documentFields(ns, d) })),
224
+ deletes: out.deletes.map((k) => ({ type: 'document', id: `${ns}/${k}` })),
225
+ },
226
+ occurredAt: at,
227
+ actor: { kind: 'agent' },
228
+ },
229
+ };
230
+ });
231
+ }
232
+ // ── asynchronous requests ───────────────────────────────────────────────────────────────────
233
+ // https://turbopuffer.com/docs/api-overview, Asynchronous requests: "Currently supported operations: copy_from_namespace,
234
+ // recall evaluation. Send the `Prefer: respond-async` header to allow the server to start the operation in the
235
+ // background. The server returns `202 Accepted` with a `Location` header pointing to the operation. Poll that location
236
+ // to check on progress: The response is `{"status": "running"}` until the operation finishes, then carries the result.
237
+ // The result is retained for one hour after the operation finishes, polling after will return `404`." Its example
238
+ // answers `Preference-Applied: respond-async`, `Location: /v1/namespaces/<ns>/operations/tpuf-abc123` and
239
+ // `{"token": "tpuf-abc123"}`; a finished poll `{"status": "finished", "result": {"success": <the write's answer>}}` or
240
+ // `{"status": "finished", "result": {"error": {"status_code": 400, "detail": {"status": "error", "error": …}}}}`.
241
+ // Where the docs stop and the twin decides: the twin copies at once, so an operation is finished when it is started and
242
+ // no poll answers "running"; recall evaluation is not served, so a copy is the only asynchronous operation; the token is
243
+ // `tpuf-` and 12 hex digits derived from the namespace, the moment and the count of operations.
244
+ const RETAINED_MS = 60 * 60 * 1000;
245
+ function asyncRequested(req, body) {
246
+ const prefer = lowerHeaders(req.headers).prefer ?? '';
247
+ const wants = prefer.split(',').some((p) => p.trim().toLowerCase() === 'respond-async');
248
+ return wants && body !== null && typeof body === 'object' && !Array.isArray(body) && body.copy_from_namespace !== undefined;
249
+ }
250
+ async function asyncCopy(req, ns, body, at) {
251
+ if (req.readOnly)
252
+ return fail(405, 'read_only: this twin was started read-only; writes are refused');
253
+ const { value: token } = await applyTwinWriteAtomic(SERVICE, (resources) => {
254
+ const n = resources.filter((r) => r.type === '_operation').length + 1;
255
+ const token = `tpuf-${createHash('sha256').update(`${ns}:${at}:${n}`).digest('hex').slice(0, 12)}`;
256
+ const record = (result) => ({ type: '_operation', id: `${ns}/${token}`, fields: { namespace: ns, token, finished_at: at, result } });
257
+ try {
258
+ const out = applyClone(loadNamespaces(resources), ns, body, at);
259
+ return { kind: 'write', value: token, write: {
260
+ operation: 'namespace.write', subjectType: 'namespace', subjectId: ns, fields: namespaceFields(out.next), input: body,
261
+ projection: { updates: [...out.upserts.map((d) => ({ type: 'document', id: documentSubjectId(ns, d.id), fields: documentFields(ns, d) })), record({ success: out.response })] },
262
+ occurredAt: at, actor: { kind: 'agent' },
263
+ } };
264
+ }
265
+ catch (e) {
266
+ if (!(e instanceof TurbopufferError))
267
+ throw e;
268
+ const op = record({ error: { status_code: e.status, detail: { status: 'error', error: e.message } } });
269
+ return { kind: 'write', value: token, write: { operation: 'operation.record', subjectType: op.type, subjectId: op.id, fields: op.fields, occurredAt: at, actor: { kind: 'system' } } };
270
+ }
271
+ }, req.root);
272
+ return { status: 202, body: { token }, headers: { ...JSON_HEADERS, 'preference-applied': 'respond-async', location: `/v1/namespaces/${encodeURIComponent(ns)}/operations/${token}` } };
273
+ }
274
+ function pollOperation(req, ns, token, at) {
275
+ const op = twinResources(SERVICE, req.root).find((r) => r.type === '_operation' && r.id === `${ns}/${token}`);
276
+ const f = op ? ownFields(op) : undefined;
277
+ if (!f || Date.parse(at) - Date.parse(String(f.finished_at)) > RETAINED_MS)
278
+ return fail(404, `operation '${token}' was not found`);
279
+ return ok({ status: 'finished', result: f.result });
280
+ }
281
+ function deleteAll(req, ns, at) {
282
+ return commit(req, (spaces) => {
283
+ const space = spaces.get(ns);
284
+ if (space === undefined)
285
+ throw notFound(ns);
286
+ return {
287
+ response: { status: 'OK' },
288
+ write: {
289
+ operation: 'namespace.delete_all',
290
+ subjectType: 'namespace',
291
+ subjectId: ns,
292
+ fields: { ...namespaceFields(space), schema: {}, distance_metric: null, vector_dims: null, updated_at: at, gone: true },
293
+ projection: { deletes: [...space.docs.keys()].map((k) => ({ type: 'document', id: `${ns}/${k}` })) },
294
+ occurredAt: at,
295
+ actor: { kind: 'agent' },
296
+ },
297
+ };
298
+ });
299
+ }
300
+ function updateSchema(req, ns, body, at) {
301
+ return commit(req, (spaces) => {
302
+ const space = spaces.get(ns);
303
+ if (space === undefined)
304
+ throw notFound(ns);
305
+ const out = applyWrite(space, ns, { schema: body }, at);
306
+ return {
307
+ response: schemaWire(out.next),
308
+ write: {
309
+ operation: 'namespace.update_schema',
310
+ subjectType: 'namespace',
311
+ subjectId: ns,
312
+ fields: namespaceFields(out.next),
313
+ input: { schema: body },
314
+ occurredAt: at,
315
+ actor: { kind: 'agent' },
316
+ },
317
+ };
318
+ });
319
+ }
320
+ function queryNamespace(ns, body, root) {
321
+ if (body === null || typeof body !== 'object' || Array.isArray(body))
322
+ throw badRequest('invalid query: expected a JSON object');
323
+ const space = readSpace(ns, root);
324
+ const b = body;
325
+ if (b.queries !== undefined) {
326
+ for (const k of Object.keys(b)) {
327
+ if (k !== 'queries' && k !== 'rerank_by' && k !== 'consistency' && k !== 'vector_encoding')
328
+ throw badRequest(`invalid multi-query: unknown field "${k}"`);
329
+ }
330
+ if (!Array.isArray(b.queries) || b.queries.length === 0)
331
+ throw badRequest('invalid multi-query: queries must be a non-empty array');
332
+ checkConsistency(b.consistency);
333
+ if (b.vector_encoding !== undefined && b.vector_encoding !== 'float')
334
+ throw vectorEncodingRefusal(b.vector_encoding);
335
+ const results = b.queries.map((q) => runQuery(space, q, { multi: true }));
336
+ const answered = b.rerank_by === undefined ? results : [rerankRrf(b.rerank_by, b.queries, results)];
337
+ return { results: answered, billing: queryBilling(space, answered), performance: queryPerformance(space) };
338
+ }
339
+ const result = runQuery(space, b);
340
+ return { ...result, billing: queryBilling(space, result), performance: queryPerformance(space) };
341
+ }
342
+ /** `rerank_by: ["RRF", {rank_constant?, weights?}]` (https://turbopuffer.com/docs/query, Reciprocal rank fusion): "The
343
+ * results contain a single list combining all the subquery results, sorted by descending RRF score. The RRF score for
344
+ * each row is reported in the `$dist` field. It is calculated as the sum of `weight / (rank_constant + rank)` across all
345
+ * subquery results", rank 1-based, `rank_constant` "defaults to 60 and can be set to any integer greater than zero",
346
+ * each weight "defaults to 1 and can be set to any number greater than zero. Exactly one weight must be provided for
347
+ * each subquery", and "RRF reranking requires at least two subqueries and is not supported for aggregations".
348
+ * Where the docs stop and the twin decides: a row carries the attributes its first subquery returned; rows of equal
349
+ * score order by id; every fused row is answered (the docs' "up to limit.total" names no limit on a multi-query). */
350
+ function rerankRrf(spec, queries, results) {
351
+ if (!Array.isArray(spec) || spec[0] !== 'RRF' || spec.length > 2)
352
+ throw badRequest('invalid rerank_by: the supported reranking function is ["RRF"] or ["RRF", {rank_constant, weights}]');
353
+ const cfg = (spec[1] ?? {});
354
+ if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg) || Object.keys(cfg).some((k) => k !== 'rank_constant' && k !== 'weights'))
355
+ throw badRequest('invalid rerank_by: RRF takes { rank_constant, weights }');
356
+ const k = cfg.rank_constant ?? 60;
357
+ if (typeof k !== 'number' || !Number.isInteger(k) || k <= 0)
358
+ throw badRequest('invalid rerank_by: rank_constant must be an integer greater than zero');
359
+ const weights = cfg.weights ?? queries.map(() => 1);
360
+ if (!Array.isArray(weights) || weights.length !== queries.length || !weights.every((w) => typeof w === 'number' && w > 0))
361
+ throw badRequest('invalid rerank_by: exactly one weight greater than zero must be provided for each subquery');
362
+ if (queries.length < 2)
363
+ throw badRequest('invalid rerank_by: RRF reranking requires at least two subqueries');
364
+ if (results.some((r) => r.rows === undefined))
365
+ throw badRequest('invalid rerank_by: RRF reranking is not supported for aggregations');
366
+ const fused = new Map();
367
+ results.forEach((r, i) => r.rows.forEach((row, at) => {
368
+ const id = JSON.stringify(row.id);
369
+ const hit = fused.get(id) ?? { row, score: 0 };
370
+ hit.score += weights[i] / (k + at + 1);
371
+ fused.set(id, hit);
372
+ }));
373
+ const rows = [...fused.values()].sort((a, b) => b.score - a.score || JSON.stringify(a.row.id).localeCompare(JSON.stringify(b.row.id)))
374
+ .map(({ row, score }) => { const { $dist: _d, ...rest } = row; return { ...rest, $dist: score }; });
375
+ return { rows };
376
+ }
377
+ function metadata(space) {
378
+ return {
379
+ approx_logical_bytes: approxLogicalBytes(space),
380
+ approx_row_count: space.docs.size,
381
+ created_at: space.created_at,
382
+ last_write_at: space.last_write_at,
383
+ updated_at: space.updated_at,
384
+ encryption: { mode: 'default' },
385
+ index: { status: 'up-to-date' },
386
+ schema: schemaWire(space),
387
+ // "branching … Only present for branched namespaces. … `parent` (string): The namespace this was branched from"
388
+ ...(space.parent !== undefined ? { branching: { parent: space.parent } } : {}),
389
+ };
390
+ }
391
+ // ── the conformance snapshot ────────────────────────────────────────────────────────────────
392
+ export function turbopufferTwinSnapshot() {
393
+ return {
394
+ resourceTypes: TURBOPUFFER_RESOURCE_TYPES,
395
+ implementedEndpoints: [
396
+ 'GET /v1/namespaces',
397
+ 'POST /v2/namespaces/{ns}',
398
+ 'DELETE /v2/namespaces/{ns}',
399
+ 'POST /v2/namespaces/{ns}/query',
400
+ 'GET /v2/namespaces/{ns}/metadata',
401
+ 'GET /v1/namespaces/{ns}/schema',
402
+ 'POST /v1/namespaces/{ns}/schema',
403
+ 'GET /v1/namespaces/{ns}/hint_cache_warm',
404
+ ],
405
+ };
406
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@volter/twin-turbopuffer",
3
+ "version": "0.1.0",
4
+ "description": "Local Turbopuffer twin: v2 namespaces (write upserts/patches/deletes by id or filter with schema and distance_metric; query with filters, deterministic BM25 incl. last_as_prefix, attribute order, exact ANN and Count/Sum aggregates; multiQuery; deleteAll; metadata; schema; namespace listing) served to the unmodified @turbopuffer/turbopuffer client. Built on @volter/world-core.",
5
+ "author": "Volter (https://github.com/volter-ai)",
6
+ "license": "Apache-2.0",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE",
11
+ "!**/*.test.ts",
12
+ "!**/*.test.tsx",
13
+ "dist"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/volter-ai/twin.git",
18
+ "directory": "packages/twin/turbopuffer"
19
+ },
20
+ "homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/turbopuffer#readme",
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "default": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "bin": {
29
+ "world-turbopuffer": "dist/src/cli.js"
30
+ },
31
+ "scripts": {
32
+ "test": "bun test src/*.test.ts",
33
+ "typecheck": "tsc --noEmit",
34
+ "build": "node ../../../scripts/publish/build.mjs",
35
+ "prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
36
+ "postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
37
+ },
38
+ "dependencies": {
39
+ "@volter/world-ui": "0.1.0",
40
+ "react": "^19.2.7",
41
+ "react-dom": "^19.2.7"
42
+ },
43
+ "peerDependencies": {
44
+ "@volter/world-core": "2.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.2.20",
48
+ "@types/node": "^24.0.0",
49
+ "@volter/world-core": "2.0.0",
50
+ "@volter/world-tooling": "0.1.0",
51
+ "typescript": "^5.9.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=22.3"
55
+ }
56
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ // world-turbopuffer CLI: serve the KERNEL-BACKED Turbopuffer twin, or run conformance. State lives
4
+ // in the @volter/world-core log under --root. Conformance is dev-only + lazy-imported.
5
+ import { hasFlag, optionValue } from '@volter/world-core/args';
6
+ import { createTurbopufferTwinServer } from './turbopuffer-server.ts';
7
+
8
+ const [cmd, ...rest] = process.argv.slice(2);
9
+ const port = Number(optionValue(rest, '--port', String(process.env.PORT ?? '0'))) || undefined;
10
+ const root = optionValue(rest, '--root') || process.env.VOLTER_STATE_DIR || undefined;
11
+ const token = optionValue(rest, '--token') || undefined;
12
+ const readOnly = hasFlag(rest, '--read-only');
13
+
14
+ if (cmd === 'serve' || cmd === undefined) {
15
+ const s = await createTurbopufferTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(token ? { token } : {}) });
16
+ process.stdout.write(
17
+ `turbopuffer twin (v2 namespaces: write, query, multiQuery, deleteAll, metadata, schema)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`
18
+ + ` point the SDK at it with TURBOPUFFER_BASE_URL=http://127.0.0.1:${s.port}/{region} and any non-empty TURBOPUFFER_API_KEY\n`,
19
+ );
20
+ await keepProcessAlive();
21
+ } else if (cmd === 'conformance') {
22
+ const { checkTurbopufferConformance } = await import('./turbopuffer-conformance.ts');
23
+ const report = await checkTurbopufferConformance(root ? { root } : {});
24
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
25
+ if (!report.ok) process.exitCode = 1;
26
+ } else {
27
+ process.stdout.write('Usage: world-turbopuffer serve|conformance [--port N] [--root DIR] [--token KEY] [--read-only]\n');
28
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"openapi.yaml.gz","format":"openapi","version":"0.0.1","resources":[{"name":"NamespaceSummary","schema":"NamespaceSummary","fields":[{"name":"id","type":"string","nullable":false,"required":true}],"stateCandidates":[],"expandable":[]}],"operations":[{"id":"get_v1_namespaces","method":"get","path":"/v1/namespaces","pathParams":[],"query":[{"name":"cursor","type":"string","required":false},{"name":"page_size","type":"integer","required":false},{"name":"prefix","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"NamespaceSummary","list":true,"key":"namespaces"},"class":"list","resource":"NamespaceSummary"},{"id":"post_v1_namespaces_namespace_debug_recall","method":"post","path":"/v1/namespaces/{namespace}/_debug/recall","pathParams":["namespace"],"query":[],"body":[{"name":"filters","type":"any","required":false},{"name":"include_ground_truth","type":"boolean","required":false},{"name":"num","type":"integer","required":false},{"name":"rank_by","type":"any","required":false},{"name":"top_k","type":"integer","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v1_namespaces_namespace_hint_cache_warm","method":"get","path":"/v1/namespaces/{namespace}/hint_cache_warm","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"none","successStatus":202,"class":"computed"},{"id":"get_v1_namespaces_namespace_metadata","method":"get","path":"/v1/namespaces/{namespace}/metadata","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"patch_v1_namespaces_namespace_metadata","method":"patch","path":"/v1/namespaces/{namespace}/metadata","pathParams":["namespace"],"query":[],"body":[{"name":"pinning","type":"union","required":false}],"bodyEncoding":"json","successStatus":200,"class":"update"},{"id":"get_v1_namespaces_namespace_operations_token","method":"get","path":"/v1/namespaces/{namespace}/operations/{token}","pathParams":["namespace","token"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"get_v1_namespaces_namespace_schema","method":"get","path":"/v1/namespaces/{namespace}/schema","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"post_v1_namespaces_namespace_schema","method":"post","path":"/v1/namespaces/{namespace}/schema","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"post_v2_namespaces_namespace","method":"post","path":"/v2/namespaces/{namespace}","pathParams":["namespace"],"query":[],"body":[{"name":"branch_from_namespace","type":"BranchFromNamespaceParams","required":false},{"name":"copy_from_namespace","type":"CopyFromNamespaceParams","required":false},{"name":"delete_by_filter","type":"any","required":false},{"name":"delete_by_filter_allow_partial","type":"boolean","required":false},{"name":"delete_condition","type":"any","required":false},{"name":"deletes","type":"array","required":false},{"name":"disable_backpressure","type":"boolean","required":false},{"name":"distance_metric","type":"DistanceMetric","required":false},{"name":"encryption","type":"Encryption","required":false},{"name":"patch_by_filter","type":"PatchByFilter","required":false},{"name":"patch_by_filter_allow_partial","type":"boolean","required":false},{"name":"patch_columns","type":"Columns","required":false},{"name":"patch_condition","type":"any","required":false},{"name":"patch_rows","type":"array","required":false},{"name":"return_affected_ids","type":"boolean","required":false},{"name":"schema","type":"object","required":false},{"name":"sharding","type":"ShardingConfig","required":false},{"name":"upsert_columns","type":"Columns","required":false},{"name":"upsert_condition","type":"any","required":false},{"name":"upsert_rows","type":"array","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"delete_v2_namespaces_namespace","method":"delete","path":"/v2/namespaces/{namespace}","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"post_v2_namespaces_namespace_explain_query","method":"post","path":"/v2/namespaces/{namespace}/explain_query","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_namespaces_namespace_metadata","method":"get","path":"/v2/namespaces/{namespace}/metadata","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"post_v2_namespaces_namespace_query","method":"post","path":"/v2/namespaces/{namespace}/query","pathParams":["namespace"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"}]}
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{}}
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ // @volter/twin-turbopuffer — the Turbopuffer API twin (v2 namespaces), built on @volter/world-core.
2
+ // Namespaces and their documents are kernel state; writes (upsert/patch/delete by id or filter,
3
+ // with schema and distance metric) land as one kernel action per request; queries filter, BM25-rank
4
+ // (deterministic, see turbopuffer-text.ts), order by attribute, rank by exact vector distance and
5
+ // count. See README ## Coverage for what is modelled and what fails loudly.
6
+ import { registerPack, type TwinPack } from '@volter/world-core';
7
+ import { performTurbopufferAction, syncTurbopufferFromRemote } from './turbopuffer-connector.ts';
8
+
9
+ export {
10
+ handleTurbopufferTwinRequest,
11
+ extractTurbopufferApiKey,
12
+ normalizeTurbopufferPath,
13
+ turbopufferTwinSnapshot,
14
+ } from './turbopuffer-twin.ts';
15
+ export type { TurbopufferTwinRequest, TurbopufferTwinResponse } from './turbopuffer-twin.ts';
16
+ export { createTurbopufferTwinFetch, createTurbopufferTwinServer } from './turbopuffer-server.ts';
17
+ export type { TurbopufferTwinFetchOptions } from './turbopuffer-server.ts';
18
+ export { performTurbopufferAction, syncTurbopufferFromReal, syncTurbopufferFromRemote, mapSchemaAttr } from './turbopuffer-connector.ts';
19
+ export {
20
+ SERVICE,
21
+ TURBOPUFFER_RESOURCE_TYPES,
22
+ TurbopufferError,
23
+ applyWrite,
24
+ runQuery,
25
+ loadNamespaces,
26
+ schemaWire,
27
+ cosineDistance,
28
+ euclideanSquared,
29
+ } from './turbopuffer-store.ts';
30
+ export type { NamespaceState, AttrConfig, Doc } from './turbopuffer-store.ts';
31
+ export { compileFilter } from './turbopuffer-filter.ts';
32
+ export { tokenize, queryTerms, bm25Score, buildCorpus, DEFAULT_FTS } from './turbopuffer-text.ts';
33
+
34
+ // Registry descriptor (TwinPack) — the SINGLE HOME for this vendor's world-facing facts.
35
+ // `bun scripts/pack-facts.ts` compiles `adoption` / `hosts` / `endpointEnv` into
36
+ // packages/world-core/generated/pack-facts.json; re-run it after ANY edit below.
37
+ export const pack: TwinPack = {
38
+ vendor: 'turbopuffer',
39
+ transport: 'rest',
40
+ protocol: '2',
41
+ archetype: 'crud',
42
+ bin: 'world-turbopuffer',
43
+ resources: ['namespace', 'document'],
44
+ specSource:
45
+ 'the OpenAPI document @turbopuffer/turbopuffer@2.8.0 was generated from (spec/openapi.yaml.gz, provenance in spec/SOURCE.md), with its generator pseudo-paths folded into the operations turbopuffer documents (spec/patches.json); the calls dubinc/dub makes (apps/web/lib/api/partners/search/providers/turbopuffer.ts).',
46
+ description:
47
+ 'Turbopuffer v2 namespaces twin — write (upsert/patch/delete rows or columns, delete/patch by filter, schema, distance_metric), query (filters, BM25 with last_as_prefix and Sum/Max/Product, attribute order, exact ANN, Count/Sum aggregates), multiQuery, deleteAll, metadata, schema, list namespaces. Deterministic BM25 (textbook Okapi over a word_v2 approximation — not byte-equal to the vendor). Kernel-backed, no mirror; unmodelled surface answers 400/404 naming its todo.',
48
+
49
+ refresh: { every: '15m', onDemand: { atMost: '30s' } },
50
+ stateSystem: { perform: performTurbopufferAction, refresh: syncTurbopufferFromRemote },
51
+ // An upsert of a caller-chosen id: the vendor's own idempotence, so the branch re-send lands cleanly.
52
+ roundTrip: {
53
+ method: 'POST',
54
+ path: '/v2/namespaces/round-trip',
55
+ body: { upsert_rows: [{ id: 'round-trip', title: 'round trip' }] },
56
+ headers: { authorization: 'Bearer round-trip' },
57
+ },
58
+
59
+ // ADOPTION — the official Node client (`@turbopuffer/turbopuffer`, the scope is Turbopuffer's
60
+ // alone), the official Python client (`turbopuffer` on PyPI), and the TURBOPUFFER_ env stem
61
+ // (TURBOPUFFER_API_KEY / _REGION / _BASE_URL, all read by the SDK in client.ts).
62
+ adoption: {
63
+ sdks: ['@turbopuffer/turbopuffer'],
64
+ pypi: ['turbopuffer'],
65
+ scopes: ['@turbopuffer/'],
66
+ envStems: ['TURBOPUFFER'],
67
+ },
68
+
69
+ // INTERCEPTION — the SDK's default base URL is `https://{region}.turbopuffer.com`
70
+ // (client.ts:273), the region filled from the `region` option or TURBOPUFFER_REGION: regional
71
+ // hosts such as aws-us-east-1.turbopuffer.com and gcp-us-central1.turbopuffer.com.
72
+ hosts: [{ hostPattern: '^(?:aws|gcp|azure)-[a-z0-9-]+\\.turbopuffer\\.com$' },
73
+ // the dashboard's sign-in and API keys pages and the World's sign-up door (screens/dashboard.tsx); the rest of
74
+ // turbopuffer.com (its site and docs) is left alone
75
+ { host: 'turbopuffer.com', pathPattern: '^/(login|dashboard|_twin/users)(/|$)' },
76
+ ],
77
+
78
+ // WORLD WIRING — under Node the SDK sends through undici's `Agent.request`
79
+ // (internal/custom/fetch-node.ts, selected by the package's `#fetch` import map for the `node`
80
+ // condition), which neither the injector's http/https/fetch patches nor HTTPS_PROXY reach. The
81
+ // SDK's own `TURBOPUFFER_BASE_URL` (client.ts:253) is the interception. It must carry the
82
+ // `{region}` placeholder: a client that passes `region` (Dub passes "aws-us-east-1") throws
83
+ // "region is set, but would be ignored" against a base URL without one (client.ts:282-285). The
84
+ // twin accepts and ignores that leading region segment.
85
+ endpointEnv: {
86
+ name: 'TURBOPUFFER_TWIN_URL',
87
+ templates: { TURBOPUFFER_BASE_URL: '${url}/{region}' },
88
+ note: 'grounded in @turbopuffer/turbopuffer@2.8.0 src/client.ts:253-288 (TURBOPUFFER_BASE_URL, {region} placeholder substitution) and internal/custom/fetch-node.ts (undici Agent.request under node, outside the injector); the injector also claims the regional *.turbopuffer.com hosts for fetch-based runtimes.',
89
+ },
90
+ };
91
+
92
+ registerPack(pack);
@@ -0,0 +1,39 @@
1
+ // The API key gate, in front of the derived dispatch. "The HTTP API expects the API key to be formatted as a standard
2
+ // Bearer token and passed in the Authorization header", and keys are made and expired on the dashboard
3
+ // (https://turbopuffer.com/docs/auth; screens/dashboard.tsx).
4
+ //
5
+ // What the gate refuses, and where turbopuffer's documentation stops:
6
+ // - a key the dashboard made and then expired, and a key of turbopuffer's form (`tpuf_…`) the dashboard never made:
7
+ // 401, the twin's reading. turbopuffer's pages print only the error body (https://turbopuffer.com/docs/api-overview)
8
+ // and name no status for a bad key; 401 is the status turbopuffer's own client reads as a failed authentication
9
+ // (`if (status === 401) return new AuthenticationError(...)`, https://raw.githubusercontent.com/turbopuffer/turbopuffer-typescript/v2.8.0/src/core/error.ts).
10
+ // - a key without the permission an operation needs, only where turbopuffer says which: listing namespaces "is available
11
+ // to API keys with list or admin permissions" (https://turbopuffer.com/docs/namespaces). 403, the twin's reading, by
12
+ // the same client line's neighbour (`if (status === 403) return new PermissionDeniedError(...)`); no page prints the
13
+ // answer for a key that lacks a permission. The messages are the twin's.
14
+ // Every other operation admits any live key: the docs name read/write permissions without saying which endpoint needs
15
+ // which, so the twin enforces none of them (manifest.ts records the gap).
16
+ // A key that is not of turbopuffer's form is the World's own (a World pointing an application at the twin with a key
17
+ // of its choosing) and is not checked here; the request handler still refuses a request with no key (401).
18
+ import { createHash } from 'node:crypto';
19
+ import type { SemanticsContext } from '@volter/world-core';
20
+
21
+ const refuse = (status: number, error: string): Response => Response.json({ status: 'error', error }, { status });
22
+
23
+ /** Operations whose permission turbopuffer documents, and the permissions that admit a key to each. */
24
+ const NEEDS: Record<string, readonly string[]> = { get_v1_namespaces: ['list', 'admin'] };
25
+
26
+ /** The gate's answer for a request to `operation`, or undefined when it may pass. */
27
+ export function keyGate(ctx: SemanticsContext, request: Request, operation: string): Response | undefined {
28
+ const m = /^bearer\s+(\S+)\s*$/i.exec(request.headers.get('authorization') ?? '');
29
+ const key = m?.[1];
30
+ if (!key || !key.startsWith('tpuf_')) return undefined;
31
+ const sha = createHash('sha256').update(key).digest('hex');
32
+ const held = ctx.rowsRaw('_api_key').find((k) => k.key_sha256 === sha);
33
+ if (!held) return refuse(401, 'unauthorized: the API key is not valid');
34
+ if (held.expired_at) return refuse(401, 'unauthorized: the API key has expired');
35
+ const needs = NEEDS[operation];
36
+ const grants = Array.isArray(held.grants) ? (held.grants as string[]) : [];
37
+ if (needs && !needs.some((g) => grants.includes(g))) return refuse(403, `forbidden: this API key needs the ${needs.join(' or ')} permission`);
38
+ return undefined;
39
+ }