jotdb 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,17 +3,64 @@ import { Hono } from 'hono';
3
3
  import { cors } from 'hono/cors';
4
4
  import { prettyJSON } from 'hono/pretty-json';
5
5
  import { DurableObject } from "cloudflare:workers";
6
+ import { JotStore, SQLiteStoreAdapter } from './store';
7
+ function isFieldDescriptor(v) {
8
+ return typeof v === 'object' && v !== null && 'type' in v;
9
+ }
10
+ function fieldType(v) {
11
+ return isFieldDescriptor(v) ? v.type : v;
12
+ }
13
+ function fieldDefault(v) {
14
+ if (isFieldDescriptor(v) && 'default' in v)
15
+ return { has: true, value: v.default };
16
+ return { has: false, value: undefined };
17
+ }
18
+ function fieldOptional(v) {
19
+ return isFieldDescriptor(v) && v.optional === true;
20
+ }
21
+ function isZodError(e) {
22
+ return e && typeof e === 'object' && Array.isArray(e.issues);
23
+ }
24
+ function handleZod(fn) {
25
+ try {
26
+ return fn();
27
+ }
28
+ catch (e) {
29
+ if (isZodError(e)) {
30
+ throw new Error('Validation failed: ' + e.issues.map(issue => issue.message).join('; '));
31
+ }
32
+ throw e;
33
+ }
34
+ }
35
+ function isArraySchema(schema) {
36
+ return '__arrayType' in schema;
37
+ }
38
+ function isObjectSchema(schema) {
39
+ return !('__arrayType' in schema);
40
+ }
41
+ function inferPrimitiveType(v) {
42
+ if (typeof v === "string")
43
+ return v.includes("@") ? "email" : "string";
44
+ if (typeof v === "number")
45
+ return "number";
46
+ if (typeof v === "boolean")
47
+ return "boolean";
48
+ return "any";
49
+ }
6
50
  export class JotDB extends DurableObject {
51
+ data = {};
52
+ rawSchema = {};
53
+ zodSchema = null;
54
+ options = {
55
+ autoStrip: false,
56
+ readOnly: false,
57
+ };
58
+ auditLog = [];
59
+ store = null;
7
60
  constructor(state, env) {
8
61
  super(state, env);
9
- this.data = {};
10
- this.rawSchema = {};
11
- this.zodSchema = null;
12
- this.options = {
13
- autoStrip: false,
14
- readOnly: false,
15
- };
16
- this.auditLog = [];
62
+ if (state.storage?.sql)
63
+ this.store = new JotStore(new SQLiteStoreAdapter(state.storage.sql));
17
64
  }
18
65
  async load() {
19
66
  if (this.data == null || (typeof this.data === 'object' && Object.keys(this.data).length === 0)) {
@@ -36,19 +83,94 @@ export class JotDB extends DurableObject {
36
83
  isArrayMode() {
37
84
  return Array.isArray(this.data);
38
85
  }
86
+ inferSchemaFromValue(value) {
87
+ if (Array.isArray(value)) {
88
+ if (value.length === 0)
89
+ return { __arrayType: "any" };
90
+ const first = value[0];
91
+ if (typeof first === "object" && first !== null && !Array.isArray(first)) {
92
+ // Array of objects
93
+ return { __arrayType: this.inferSchemaFromValue(first) };
94
+ }
95
+ // Array of primitives
96
+ return { __arrayType: inferPrimitiveType(first) };
97
+ }
98
+ if (typeof value === "object" && value !== null) {
99
+ const schema = {};
100
+ for (const [k, v] of Object.entries(value)) {
101
+ if (Array.isArray(v)) {
102
+ schema[k] = "array";
103
+ }
104
+ else if (typeof v === "object" && v !== null) {
105
+ schema[k] = "object";
106
+ }
107
+ else {
108
+ schema[k] = inferPrimitiveType(v);
109
+ }
110
+ }
111
+ return schema;
112
+ }
113
+ // Top-level primitive (shouldn't happen for objects, but fallback)
114
+ return {};
115
+ }
39
116
  async push(item) {
40
117
  await this.load();
41
118
  if (!Array.isArray(this.data)) {
42
119
  this.data = [];
43
120
  }
44
- this.data.push(item);
121
+ if (!this.zodSchema) {
122
+ const schema = this.inferSchemaFromValue([item]);
123
+ await this.setSchema(schema);
124
+ console.info('[JotDB] Auto-inferred and set schema from first push:', schema);
125
+ }
126
+ let toPush = item;
127
+ if (isArraySchema(this.rawSchema)) {
128
+ const at = this.rawSchema.__arrayType;
129
+ if (typeof at === 'object' && at !== null && toPush && typeof toPush === 'object' && !Array.isArray(toPush)) {
130
+ toPush = this.applyDefaultsForObjectSchema(at, toPush);
131
+ }
132
+ toPush = handleZod(() => this.zodSchema.parse([toPush]))[0];
133
+ }
134
+ else if (this.zodSchema && isObjectSchema(this.rawSchema)) {
135
+ toPush = handleZod(() => this.zodSchema.parse(toPush));
136
+ }
137
+ this.data.push(toPush);
45
138
  await this.save();
46
139
  await this.logAudit("push", []);
47
140
  }
141
+ applyDefaultsForObjectSchema(schema, target) {
142
+ const out = { ...target };
143
+ for (const [key, spec] of Object.entries(schema)) {
144
+ if (out[key] === undefined) {
145
+ const def = fieldDefault(spec);
146
+ if (def.has)
147
+ out[key] = def.value;
148
+ }
149
+ }
150
+ return out;
151
+ }
48
152
  async setAll(objOrArr) {
49
153
  await this.load();
50
- if (!Array.isArray(objOrArr) && this.zodSchema) {
51
- objOrArr = this.zodSchema.parse(objOrArr);
154
+ if (!this.zodSchema) {
155
+ const schema = this.inferSchemaFromValue(objOrArr);
156
+ await this.setSchema(schema);
157
+ console.info('[JotDB] Auto-inferred and set schema from first setAll:', schema);
158
+ }
159
+ if (Array.isArray(objOrArr) && isArraySchema(this.rawSchema)) {
160
+ const at = this.rawSchema.__arrayType;
161
+ if (typeof at === 'object' && at !== null) {
162
+ objOrArr = objOrArr.map(item => item && typeof item === 'object' && !Array.isArray(item)
163
+ ? this.applyDefaultsForObjectSchema(at, item)
164
+ : item);
165
+ }
166
+ objOrArr = handleZod(() => this.zodSchema.parse(objOrArr));
167
+ }
168
+ else if (!Array.isArray(objOrArr) && this.zodSchema && isObjectSchema(this.rawSchema)) {
169
+ objOrArr = this.applyObjectDefaults(objOrArr);
170
+ objOrArr = handleZod(() => this.zodSchema.parse(objOrArr));
171
+ }
172
+ else if (Array.isArray(objOrArr) && this.zodSchema && isObjectSchema(this.rawSchema)) {
173
+ objOrArr.forEach(item => handleZod(() => this.zodSchema.parse(item)));
52
174
  }
53
175
  this.data = objOrArr;
54
176
  await this.save();
@@ -107,11 +229,17 @@ export class JotDB extends DurableObject {
107
229
  }
108
230
  warnSchemaDiff(newSchema) {
109
231
  const current = this.rawSchema;
232
+ if (isArraySchema(newSchema) || isArraySchema(current))
233
+ return;
110
234
  for (const key in newSchema) {
111
235
  if (!(key in current))
112
236
  console.warn(`[JotDB] New key added: ${key}`);
113
- else if (newSchema[key] !== current[key]) {
114
- console.warn(`[JotDB] Type changed for "${key}": ${current[key]} → ${newSchema[key]}`);
237
+ else {
238
+ const nt = fieldType(newSchema[key]);
239
+ const ct = fieldType(current[key]);
240
+ if (nt !== ct) {
241
+ console.warn(`[JotDB] Type changed for "${key}": ${ct} → ${nt}`);
242
+ }
115
243
  }
116
244
  }
117
245
  for (const key in current) {
@@ -129,6 +257,113 @@ export class JotDB extends DurableObject {
129
257
  this.zodSchema = this.buildZodSchema(schemaObj);
130
258
  await this.ctx.storage.put("__schema__", schemaObj);
131
259
  }
260
+ /**
261
+ * Non-destructively merge additional fields into the current object schema.
262
+ * Useful for additive schema evolution without a full migration.
263
+ * Throws if the current schema is an array schema.
264
+ * New fields with `default` are backfilled into existing stored data.
265
+ * New fields without `default` and without `optional: true` will cause
266
+ * existing stored objects to fail validation on next write — prefer adding
267
+ * `default` or `optional: true` for safe rollouts.
268
+ */
269
+ async extendSchema(partial) {
270
+ await this.load();
271
+ if (Object.keys(this.rawSchema).length > 0 && isArraySchema(this.rawSchema)) {
272
+ throw new Error("extendSchema is only supported for object schemas");
273
+ }
274
+ const merged = { ...this.rawSchema, ...partial };
275
+ this.warnSchemaDiff(merged);
276
+ this.rawSchema = merged;
277
+ this.zodSchema = this.buildZodSchema(merged);
278
+ await this.ctx.storage.put("__schema__", merged);
279
+ // Backfill defaults into existing stored data, if applicable.
280
+ let mutated = false;
281
+ if (!Array.isArray(this.data)) {
282
+ const next = this.applyObjectDefaults(this.data);
283
+ if (JSON.stringify(next) !== JSON.stringify(this.data)) {
284
+ this.data = next;
285
+ mutated = true;
286
+ }
287
+ }
288
+ if (mutated)
289
+ await this.save();
290
+ await this.logAudit("extendSchema", Object.keys(partial));
291
+ }
292
+ /**
293
+ * Validate arbitrary data (or current stored data if omitted) against the
294
+ * current schema without throwing. Returns `{ ok, errors, data }` where
295
+ * `data` is the (possibly default-applied / coerced) parsed result on
296
+ * success. If no schema is set, returns `{ ok: true }`.
297
+ */
298
+ async validate(data) {
299
+ await this.load();
300
+ if (!this.zodSchema)
301
+ return { ok: true, data: data ?? this.data };
302
+ const target = data === undefined ? this.data : data;
303
+ try {
304
+ const parsed = this.zodSchema.parse(target);
305
+ return { ok: true, data: parsed };
306
+ }
307
+ catch (e) {
308
+ if (isZodError(e)) {
309
+ return { ok: false, errors: e.issues.map(i => `${i.path.join('.') || '<root>'}: ${i.message}`) };
310
+ }
311
+ return { ok: false, errors: [e instanceof Error ? e.message : String(e)] };
312
+ }
313
+ }
314
+ /**
315
+ * Apply a transform to the stored data. For object mode, `fn` is called once
316
+ * with the entire object and must return the new object. For array mode, `fn`
317
+ * is called once per item and must return the replacement item (return
318
+ * undefined to drop the item).
319
+ * If a schema is set, the resulting data is validated before being saved;
320
+ * on validation failure the original data is left untouched and the function
321
+ * throws. Records an audit entry on success.
322
+ */
323
+ async migrate(fn) {
324
+ await this.load();
325
+ if (this.options.readOnly)
326
+ throw new Error("Database is in read-only mode");
327
+ let next;
328
+ if (Array.isArray(this.data)) {
329
+ const out = [];
330
+ for (let i = 0; i < this.data.length; i++) {
331
+ const r = fn(this.data[i], i);
332
+ if (r !== undefined)
333
+ out.push(r);
334
+ }
335
+ next = out;
336
+ }
337
+ else {
338
+ const r = fn({ ...this.data });
339
+ if (r === null || typeof r !== 'object' || Array.isArray(r)) {
340
+ throw new Error("migrate(fn) for object mode must return a plain object");
341
+ }
342
+ next = r;
343
+ }
344
+ if (this.zodSchema) {
345
+ try {
346
+ if (Array.isArray(next) && isArraySchema(this.rawSchema)) {
347
+ next = this.zodSchema.parse(next);
348
+ }
349
+ else if (!Array.isArray(next) && isObjectSchema(this.rawSchema)) {
350
+ next = this.zodSchema.parse(next);
351
+ }
352
+ else if (Array.isArray(next) && isObjectSchema(this.rawSchema)) {
353
+ next.forEach(item => this.zodSchema.parse(item));
354
+ }
355
+ }
356
+ catch (e) {
357
+ if (isZodError(e)) {
358
+ throw new Error('migrate validation failed: ' + e.issues.map(i => `${i.path.join('.') || '<root>'}: ${i.message}`).join('; '));
359
+ }
360
+ throw e;
361
+ }
362
+ }
363
+ this.data = next;
364
+ await this.save();
365
+ await this.logAudit("migrate", Array.isArray(next) ? [] : Object.keys(next));
366
+ }
132
367
  async setOptions(opts) {
133
368
  await this.load();
134
369
  Object.assign(this.options, opts);
@@ -147,45 +382,110 @@ export class JotDB extends DurableObject {
147
382
  await this.ctx.storage.put("__audit__", []);
148
383
  }
149
384
  buildZodSchema(schema) {
385
+ if (isArraySchema(schema)) {
386
+ const t = schema.__arrayType;
387
+ if (typeof t === "string") {
388
+ switch (t) {
389
+ case "string": return z.string().array();
390
+ case "number": return z.number().array();
391
+ case "boolean": return z.boolean().array();
392
+ case "email": return z.string().email().array();
393
+ default: return z.any().array();
394
+ }
395
+ }
396
+ else {
397
+ // Array of objects
398
+ return this.buildZodSchema(t).array();
399
+ }
400
+ }
401
+ // Object schema
150
402
  const shape = {};
151
- for (const [key, type] of Object.entries(schema)) {
152
- switch (type) {
403
+ for (const [key, spec] of Object.entries(schema)) {
404
+ const t = fieldType(spec);
405
+ let zt;
406
+ switch (t) {
153
407
  case "string":
154
- shape[key] = z.string();
408
+ zt = z.string();
155
409
  break;
156
410
  case "number":
157
- shape[key] = z.number();
411
+ zt = z.number();
158
412
  break;
159
413
  case "boolean":
160
- shape[key] = z.boolean();
414
+ zt = z.boolean();
161
415
  break;
162
416
  case "email":
163
- shape[key] = z.string().email();
417
+ zt = z.string().email();
164
418
  break;
165
419
  case "array":
166
- shape[key] = z.array(z.any());
420
+ zt = z.array(z.any());
167
421
  break;
168
422
  case "object":
169
- shape[key] = z.record(z.any());
423
+ zt = z.record(z.string(), z.any());
170
424
  break;
171
- default:
172
- shape[key] = z.any();
425
+ default: zt = z.any();
426
+ }
427
+ const def = fieldDefault(spec);
428
+ if (def.has) {
429
+ zt = zt.default(def.value);
173
430
  }
431
+ else if (fieldOptional(spec)) {
432
+ zt = zt.optional();
433
+ }
434
+ shape[key] = zt;
174
435
  }
175
436
  return z.object(shape);
176
437
  }
438
+ applyObjectDefaults(target) {
439
+ if (isArraySchema(this.rawSchema))
440
+ return target;
441
+ const out = { ...target };
442
+ for (const [key, spec] of Object.entries(this.rawSchema)) {
443
+ if (out[key] === undefined) {
444
+ const def = fieldDefault(spec);
445
+ if (def.has)
446
+ out[key] = def.value;
447
+ }
448
+ }
449
+ return out;
450
+ }
177
451
  async fetch(request) {
178
452
  return new Response("Hello, World!");
179
453
  }
454
+ async scan(prefix = '', options) {
455
+ if (!this.store)
456
+ throw new Error('SQLite-backed store is not enabled');
457
+ return this.store.scan(prefix, options);
458
+ }
459
+ async append(stream, value) {
460
+ if (!this.store)
461
+ throw new Error('SQLite-backed store is not enabled');
462
+ return this.store.append(stream, value);
463
+ }
464
+ async appendCapped(stream, value, max) {
465
+ if (!this.store)
466
+ throw new Error('SQLite-backed store is not enabled');
467
+ return this.store.appendCapped(stream, value, max);
468
+ }
469
+ async retention(prefix, maxAgeMs) {
470
+ if (!this.store)
471
+ throw new Error('SQLite-backed store is not enabled');
472
+ return this.store.retention(prefix, maxAgeMs);
473
+ }
180
474
  async set(key, value) {
181
475
  await this.load();
182
476
  if (this.options.readOnly) {
183
477
  throw new Error("Database is in read-only mode");
184
478
  }
185
479
  if (!Array.isArray(this.data)) {
480
+ // Auto-infer schema if not set
481
+ if (!this.zodSchema) {
482
+ const schema = this.inferSchemaFromValue({ [key]: value });
483
+ await this.setSchema(schema);
484
+ console.info('[JotDB] Auto-inferred and set schema from first set:', schema);
485
+ }
186
486
  this.data[key] = value;
187
487
  if (this.zodSchema) {
188
- this.zodSchema.parse(this.data);
488
+ handleZod(() => this.zodSchema.parse(this.data));
189
489
  }
190
490
  await this.save();
191
491
  await this.logAudit("set", key);
@@ -196,12 +496,32 @@ export class JotDB extends DurableObject {
196
496
  }
197
497
  }
198
498
  const app = new Hono();
499
+ // Benchmarks are intentionally gated before any Durable Object access. Set this
500
+ // as a Worker secret before exposing a route, or put Cloudflare Access in front
501
+ // of the route. This avoids turning benchmark endpoints into public cost sinks.
502
+ app.use('/bench', async (c, next) => {
503
+ const token = c.env.BENCH_TOKEN;
504
+ if (!token || c.req.header('Authorization') !== `Bearer ${token}`) {
505
+ return c.json({ error: 'Unauthorized' }, 401);
506
+ }
507
+ await next();
508
+ });
509
+ // Refuse all HTTP traffic unless an explicit deploy-time gate is configured.
510
+ // This keeps local library use working while making accidental public Worker
511
+ // deployment fail closed.
512
+ app.use('*', async (c, next) => {
513
+ if (!c.env.HTTP_ENABLED) {
514
+ return c.json({ error: 'HTTP surface disabled' }, 403);
515
+ }
516
+ await next();
517
+ });
199
518
  // Middleware
200
519
  app.use('*', cors());
201
520
  app.use('*', prettyJSON());
202
521
  // Test endpoint
203
522
  app.get('/test', async (c) => {
204
- const id = c.env.JOTDB.idFromName("test-db");
523
+ const JOB_ID = Date.now().toString();
524
+ const id = c.env.JOTDB.idFromName(JOB_ID);
205
525
  const db = c.env.JOTDB.get(id);
206
526
  const results = {
207
527
  timestamp: Date.now(),
@@ -265,17 +585,218 @@ app.get('/test', async (c) => {
265
585
  passed: !("extra" in stripped),
266
586
  value: stripped
267
587
  });
588
+ // Test 5: Array mode - setAll and getAll
589
+ const arrayId = c.env.JOTDB.idFromName(JOB_ID + "-test-array");
590
+ const arrayDb = c.env.JOTDB.get(arrayId);
591
+ await arrayDb.setAll([1, 2, 3]);
592
+ const arr = await arrayDb.getAll();
593
+ results.tests.push({
594
+ name: "Array mode setAll/getAll",
595
+ passed: Array.isArray(arr) && arr.length === 3 && arr[0] === 1 && arr[2] === 3,
596
+ value: arr
597
+ });
598
+ // Test 6: Array mode - push
599
+ await arrayDb.push(4);
600
+ const arr2 = await arrayDb.getAll();
601
+ results.tests.push({
602
+ name: "Array mode push",
603
+ passed: Array.isArray(arr2) && arr2.length === 4 && arr2[3] === 4,
604
+ value: arr2
605
+ });
606
+ // --- Schema inference tests-- -
607
+ // Object mode
608
+ const objId = c.env.JOTDB.idFromName("schema-obj");
609
+ const objDb = c.env.JOTDB.get(objId);
610
+ await objDb.setAll({ foo: "bar", count: 1 });
611
+ const objSchema = await objDb.getSchema();
612
+ let objPassed = false;
613
+ if (!('__arrayType' in objSchema)) {
614
+ objPassed = objSchema.foo === "string" && objSchema.count === "number";
615
+ }
616
+ results.tests.push({
617
+ name: "Object mode: inferred schema",
618
+ passed: objPassed,
619
+ value: objSchema
620
+ });
621
+ let objError = null;
622
+ try {
623
+ await objDb.setAll({ foo: 123, count: "not a number" });
624
+ }
625
+ catch (e) {
626
+ objError = e instanceof Error ? e.message : String(e);
627
+ }
628
+ results.tests.push({
629
+ name: "Object mode: invalid shape fails",
630
+ passed: !!objError,
631
+ error: objError
632
+ });
633
+ // Array mode
634
+ const arrId = c.env.JOTDB.idFromName("schema-arr");
635
+ const arrDb = c.env.JOTDB.get(arrId);
636
+ await arrDb.setAll([{ foo: "bar", count: 1 }]);
637
+ const arrSchema = await arrDb.getSchema();
638
+ let arrPassed = false;
639
+ if ('__arrayType' in arrSchema && typeof arrSchema.__arrayType === 'object') {
640
+ const itemSchema = arrSchema.__arrayType;
641
+ arrPassed = itemSchema.foo === "string" && itemSchema.count === "number";
642
+ }
643
+ results.tests.push({
644
+ name: "Array mode: inferred schema",
645
+ passed: arrPassed,
646
+ value: arrSchema
647
+ });
648
+ let arrError = null;
649
+ try {
650
+ await arrDb.push({ foo: 123, count: "not a number" });
651
+ }
652
+ catch (e) {
653
+ arrError = e instanceof Error ? e.message : String(e);
654
+ }
655
+ results.tests.push({
656
+ name: "Array mode: invalid item fails",
657
+ passed: !!arrError,
658
+ error: arrError
659
+ });
660
+ // Test 0: Clear database, set options to read-only: false
661
+ await db.clear();
662
+ results.tests.push({
663
+ name: "Clear database",
664
+ passed: true,
665
+ value: await db.getAll()
666
+ });
268
667
  // Get audit log
269
668
  results.auditLog = await db.getAuditLog();
270
- return c.json(results);
669
+ // HTML output
670
+ let html = `<!DOCTYPE html><html><head><title>JotDB Test Results</title>
671
+ <style>
672
+ body { font-family: sans-serif; margin: 2em; }
673
+ .pass { color: green; }
674
+ .fail { color: red; }
675
+ .test { margin-bottom: 1em; }
676
+ pre { background: #f4f4f4; padding: 0.5em; }
677
+ </style>
678
+ </head><body>
679
+ <h1>JotDB Test Results</h1>
680
+ <p><b>Timestamp:</b> ${new Date(results.timestamp).toLocaleString()}</p>
681
+ <div>
682
+ ${results.tests.map(test => `
683
+ <div class="test">
684
+ <b>${test.name}:</b> <span class="${test.passed ? 'pass' : 'fail'}">${test.passed ? 'PASS' : 'FAIL'}</span><br/>
685
+ <pre>${JSON.stringify(test.value ?? test.error, null, 2)}</pre>
686
+ </div>
687
+ `).join('')}
688
+ </div>
689
+ <h2>Audit Log</h2>
690
+ <pre>${JSON.stringify(results.auditLog, null, 2)}</pre>
691
+ </body></html>`;
692
+ return new Response(html, { headers: { 'Content-Type': 'text/html' } });
271
693
  }
272
694
  catch (error) {
273
- return c.json({
274
- error: error instanceof Error ? error.message : String(error),
275
- tests: results.tests,
276
- auditLog: results.auditLog
277
- }, 500);
695
+ console.error(error);
696
+ let errorMessage = error instanceof Error ? error.message : String(error);
697
+ if (isZodError(error)) {
698
+ console.error(error.issues);
699
+ errorMessage = error.issues.map(issue => issue.message).join('\n');
700
+ }
701
+ let html = `<!DOCTYPE html><html><head><title>JotDB Test Error</title></head><body>` +
702
+ `<h1 style="color:red">Error</h1>` +
703
+ `<pre>${errorMessage}</pre>` +
704
+ `<h2>Partial Results</h2>` +
705
+ `<pre>${JSON.stringify(results.tests, null, 2)}</pre>` +
706
+ `<h2>Audit Log</h2>` +
707
+ `<pre>${JSON.stringify(results.auditLog, null, 2)}</pre>` +
708
+ `</body></html>`;
709
+ return new Response(html, { headers: { 'Content-Type': 'text/html' } });
710
+ }
711
+ });
712
+ function percentile(values, p) {
713
+ if (values.length === 0)
714
+ return 0;
715
+ const sorted = [...values].sort((a, b) => a - b);
716
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
717
+ }
718
+ async function measure(fn, timings) {
719
+ const start = performance.now();
720
+ const result = await fn();
721
+ timings.push(performance.now() - start);
722
+ return result;
723
+ }
724
+ app.get('/bench', async (c) => {
725
+ const mode = c.req.query('mode') ?? 'user-prefs';
726
+ const count = Math.min(Number(c.req.query('count') ?? 100), 1000);
727
+ const timings = [];
728
+ const started = performance.now();
729
+ let operations = 0;
730
+ if (mode === 'user-prefs') {
731
+ const db = c.env.JOTDB.getByName(`bench:prefs:${Date.now()}`);
732
+ await db.setSchema({ theme: 'string', notifications: 'boolean', locale: 'string' });
733
+ for (let i = 0; i < count; i++) {
734
+ await measure(() => db.setAll({ theme: i % 2 ? 'dark' : 'light', notifications: true, locale: 'en' }), timings);
735
+ operations++;
736
+ }
737
+ }
738
+ else if (mode === 'feature-flags') {
739
+ const db = c.env.JOTDB.getByName(`bench:flags:${Date.now()}`);
740
+ await db.setSchema({ darkMode: 'boolean', betaEditor: 'boolean', aiSearch: 'boolean' });
741
+ await db.setAll({ darkMode: true, betaEditor: false, aiSearch: true });
742
+ for (let i = 0; i < count; i++) {
743
+ await measure(() => db.getAll(), timings);
744
+ operations++;
745
+ }
746
+ }
747
+ else if (mode === 'chat-append') {
748
+ const db = c.env.JOTDB.getByName(`bench:chat:${Date.now()}`);
749
+ await db.setAll([]);
750
+ for (let i = 0; i < count; i++) {
751
+ await measure(() => db.push({ id: i, body: `message ${i}` }), timings);
752
+ operations++;
753
+ }
754
+ }
755
+ else if (mode === 'hot-key') {
756
+ const db = c.env.JOTDB.getByName(`bench:hot:${Date.now()}`);
757
+ await db.setSchema({ counter: 'number' });
758
+ await db.setAll({ counter: 0 });
759
+ await Promise.all(Array.from({ length: count }, async (_, i) => { await measure(() => db.setAll({ counter: i }), timings); }));
760
+ operations = count;
761
+ }
762
+ else if (mode === 'multi-instance') {
763
+ await Promise.all(Array.from({ length: count }, async (_, i) => {
764
+ const db = c.env.JOTDB.getByName(`bench:user:${Date.now()}:${i}`);
765
+ await measure(() => db.set('id', i), timings);
766
+ }));
767
+ operations = count;
768
+ }
769
+ else if (mode === 'cold-warm') {
770
+ const db = c.env.JOTDB.getByName(`bench:cold:${Date.now()}`);
771
+ await measure(() => db.set('first', true), timings);
772
+ operations++;
773
+ for (let i = 0; i < count; i++) {
774
+ await measure(() => db.get('first'), timings);
775
+ operations++;
776
+ }
777
+ }
778
+ else if (mode === 'schema-validation') {
779
+ const db = c.env.JOTDB.getByName(`bench:schema:${Date.now()}`);
780
+ await db.setSchema({ name: 'string', age: 'number', email: 'email' });
781
+ for (let i = 0; i < count; i++) {
782
+ await measure(() => db.setAll({ name: `User ${i}`, age: i, email: `u${i}@example.com` }), timings);
783
+ operations++;
784
+ }
785
+ }
786
+ else {
787
+ return c.json({ error: `Unknown benchmark mode: ${mode}` }, 400);
278
788
  }
789
+ const durationMs = performance.now() - started;
790
+ return c.json({
791
+ mode,
792
+ operations,
793
+ durationMs,
794
+ opsPerSecond: operations / (durationMs / 1000),
795
+ p50Ms: percentile(timings, 0.5),
796
+ p95Ms: percentile(timings, 0.95),
797
+ p99Ms: percentile(timings, 0.99),
798
+ errors: 0,
799
+ });
279
800
  });
280
801
  // Health check endpoint
281
802
  app.get('/', (c) => c.text('JotDB Durable Object'));
@@ -0,0 +1,7 @@
1
+ export { JotDB } from './index';
2
+ declare const _default: {
3
+ fetch(request: Request, env: {
4
+ ASSETS: Fetcher;
5
+ }): Promise<Response>;
6
+ };
7
+ export default _default;