@thewhateverapp/platform 0.8.0 → 0.9.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.
@@ -0,0 +1,929 @@
1
+ /**
2
+ * Keyspace API
3
+ *
4
+ * Redis-inspired shared state service providing counters, KV, queues,
5
+ * sets, leaderboards, hashes, rate limiting, and distributed locks.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { getCloudflareContext } from '@opennextjs/cloudflare';
10
+ * import { configurePlatformEnv, getKeyspace } from '@thewhateverapp/platform';
11
+ *
12
+ * export async function POST(req: NextRequest) {
13
+ * configurePlatformEnv(() => getCloudflareContext().env);
14
+ *
15
+ * const keyspace = await getKeyspace();
16
+ *
17
+ * // Increment counter
18
+ * const count = await keyspace.counter('visitors').increment();
19
+ *
20
+ * // Store value with TTL
21
+ * await keyspace.kv.set('session', { user: 'john' }, 3600);
22
+ *
23
+ * // Add to leaderboard
24
+ * await keyspace.leaderboard('scores').add('player1', 100);
25
+ *
26
+ * return NextResponse.json({ count });
27
+ * }
28
+ * ```
29
+ */
30
+ import { getPlatformEnv, isPlatformEnvConfigured } from '../env';
31
+ /**
32
+ * Default base URL for HTTP-based keyspace access
33
+ */
34
+ const DEFAULT_KEYSPACE_URL = 'https://keyspace.thewhatever.app';
35
+ /**
36
+ * Internal helper to execute operations against the DO
37
+ */
38
+ async function executeOp(stub, operation) {
39
+ const response = await stub.fetch('http://internal/op', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify(operation),
43
+ });
44
+ if (!response.ok) {
45
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
46
+ throw new Error(`Keyspace operation failed: ${error.error}`);
47
+ }
48
+ return (await response.json());
49
+ }
50
+ /**
51
+ * Cloudflare Durable Object-backed Keyspace Provider
52
+ */
53
+ class CloudflareKeyspace {
54
+ stub;
55
+ constructor(stub) {
56
+ this.stub = stub;
57
+ }
58
+ counter(name, scope) {
59
+ const stub = this.stub;
60
+ return {
61
+ async increment(by = 1) {
62
+ const result = await executeOp(stub, {
63
+ type: 'counter',
64
+ action: 'increment',
65
+ name,
66
+ scope,
67
+ value: by,
68
+ });
69
+ return result.data ?? 0;
70
+ },
71
+ async decrement(by = 1) {
72
+ const result = await executeOp(stub, {
73
+ type: 'counter',
74
+ action: 'decrement',
75
+ name,
76
+ scope,
77
+ value: by,
78
+ });
79
+ return result.data ?? 0;
80
+ },
81
+ async get() {
82
+ const result = await executeOp(stub, {
83
+ type: 'counter',
84
+ action: 'get',
85
+ name,
86
+ scope,
87
+ });
88
+ return result.data ?? 0;
89
+ },
90
+ async set(value) {
91
+ const result = await executeOp(stub, {
92
+ type: 'counter',
93
+ action: 'set',
94
+ name,
95
+ scope,
96
+ value,
97
+ });
98
+ return result.data ?? value;
99
+ },
100
+ async reset() {
101
+ await executeOp(stub, {
102
+ type: 'counter',
103
+ action: 'reset',
104
+ name,
105
+ scope,
106
+ });
107
+ return 0;
108
+ },
109
+ };
110
+ }
111
+ get kv() {
112
+ const stub = this.stub;
113
+ return {
114
+ async get(key) {
115
+ const result = await executeOp(stub, {
116
+ type: 'kv',
117
+ action: 'get',
118
+ key,
119
+ });
120
+ return result.data ?? null;
121
+ },
122
+ async set(key, value, ttlSeconds) {
123
+ await executeOp(stub, {
124
+ type: 'kv',
125
+ action: 'set',
126
+ key,
127
+ value,
128
+ ttlSeconds,
129
+ });
130
+ },
131
+ async delete(key) {
132
+ await executeOp(stub, {
133
+ type: 'kv',
134
+ action: 'delete',
135
+ key,
136
+ });
137
+ },
138
+ async has(key) {
139
+ const result = await executeOp(stub, {
140
+ type: 'kv',
141
+ action: 'has',
142
+ key,
143
+ });
144
+ return result.data ?? false;
145
+ },
146
+ };
147
+ }
148
+ queue(name) {
149
+ const stub = this.stub;
150
+ return {
151
+ async enqueue(value) {
152
+ const result = await executeOp(stub, {
153
+ type: 'queue',
154
+ action: 'enqueue',
155
+ name,
156
+ value,
157
+ });
158
+ return result.data ?? 0;
159
+ },
160
+ async dequeue() {
161
+ const result = await executeOp(stub, {
162
+ type: 'queue',
163
+ action: 'dequeue',
164
+ name,
165
+ });
166
+ return result.data ?? null;
167
+ },
168
+ async readRange(start = 0, stop = 9) {
169
+ const result = await executeOp(stub, {
170
+ type: 'queue',
171
+ action: 'readRange',
172
+ name,
173
+ start,
174
+ stop,
175
+ });
176
+ return result.data ?? [];
177
+ },
178
+ async length() {
179
+ const result = await executeOp(stub, {
180
+ type: 'queue',
181
+ action: 'length',
182
+ name,
183
+ });
184
+ return result.data ?? 0;
185
+ },
186
+ async clear() {
187
+ await executeOp(stub, {
188
+ type: 'queue',
189
+ action: 'clear',
190
+ name,
191
+ });
192
+ },
193
+ };
194
+ }
195
+ set(name) {
196
+ const stub = this.stub;
197
+ return {
198
+ async add(value) {
199
+ const result = await executeOp(stub, {
200
+ type: 'set',
201
+ action: 'add',
202
+ name,
203
+ value,
204
+ });
205
+ return result.data ?? 0;
206
+ },
207
+ async remove(value) {
208
+ const result = await executeOp(stub, {
209
+ type: 'set',
210
+ action: 'remove',
211
+ name,
212
+ value,
213
+ });
214
+ return result.data ?? 0;
215
+ },
216
+ async has(value) {
217
+ const result = await executeOp(stub, {
218
+ type: 'set',
219
+ action: 'has',
220
+ name,
221
+ value,
222
+ });
223
+ return result.data ?? false;
224
+ },
225
+ async members() {
226
+ const result = await executeOp(stub, {
227
+ type: 'set',
228
+ action: 'members',
229
+ name,
230
+ });
231
+ return result.data ?? [];
232
+ },
233
+ async size() {
234
+ const result = await executeOp(stub, {
235
+ type: 'set',
236
+ action: 'size',
237
+ name,
238
+ });
239
+ return result.data ?? 0;
240
+ },
241
+ };
242
+ }
243
+ leaderboard(name) {
244
+ const stub = this.stub;
245
+ return {
246
+ async add(member, score) {
247
+ const result = await executeOp(stub, {
248
+ type: 'leaderboard',
249
+ action: 'add',
250
+ name,
251
+ member,
252
+ score,
253
+ });
254
+ return result.data ?? 0;
255
+ },
256
+ async increment(member, by = 1) {
257
+ const result = await executeOp(stub, {
258
+ type: 'leaderboard',
259
+ action: 'increment',
260
+ name,
261
+ member,
262
+ score: by,
263
+ });
264
+ return result.data ?? 0;
265
+ },
266
+ async getRange(start = 0, stop = 9) {
267
+ const result = await executeOp(stub, {
268
+ type: 'leaderboard',
269
+ action: 'getRange',
270
+ name,
271
+ start,
272
+ stop,
273
+ });
274
+ return result.data ?? [];
275
+ },
276
+ async getRank(member) {
277
+ const result = await executeOp(stub, {
278
+ type: 'leaderboard',
279
+ action: 'getRank',
280
+ name,
281
+ member,
282
+ });
283
+ return result.data ?? -1;
284
+ },
285
+ async getScore(member) {
286
+ const result = await executeOp(stub, {
287
+ type: 'leaderboard',
288
+ action: 'getScore',
289
+ name,
290
+ member,
291
+ });
292
+ return result.data ?? null;
293
+ },
294
+ async remove(member) {
295
+ await executeOp(stub, {
296
+ type: 'leaderboard',
297
+ action: 'remove',
298
+ name,
299
+ member,
300
+ });
301
+ },
302
+ };
303
+ }
304
+ hash(key) {
305
+ const stub = this.stub;
306
+ return {
307
+ async set(field, value) {
308
+ await executeOp(stub, {
309
+ type: 'hash',
310
+ action: 'set',
311
+ key,
312
+ field,
313
+ value,
314
+ });
315
+ },
316
+ async get(field) {
317
+ const result = await executeOp(stub, {
318
+ type: 'hash',
319
+ action: 'get',
320
+ key,
321
+ field,
322
+ });
323
+ return result.data ?? null;
324
+ },
325
+ async getAll() {
326
+ const result = await executeOp(stub, {
327
+ type: 'hash',
328
+ action: 'getAll',
329
+ key,
330
+ });
331
+ return result.data ?? {};
332
+ },
333
+ async delete(field) {
334
+ await executeOp(stub, {
335
+ type: 'hash',
336
+ action: 'delete',
337
+ key,
338
+ field,
339
+ });
340
+ },
341
+ async increment(field, by = 1) {
342
+ const result = await executeOp(stub, {
343
+ type: 'hash',
344
+ action: 'increment',
345
+ key,
346
+ field,
347
+ value: by,
348
+ });
349
+ return result.data ?? 0;
350
+ },
351
+ };
352
+ }
353
+ rateLimit(key) {
354
+ const stub = this.stub;
355
+ return {
356
+ async check(limit, windowSeconds) {
357
+ const result = await executeOp(stub, {
358
+ type: 'rateLimit',
359
+ action: 'check',
360
+ key,
361
+ limit,
362
+ windowSeconds,
363
+ });
364
+ return (result.data ?? {
365
+ allowed: false,
366
+ remaining: 0,
367
+ resetAt: Date.now(),
368
+ limit,
369
+ });
370
+ },
371
+ };
372
+ }
373
+ lock(name) {
374
+ const stub = this.stub;
375
+ return {
376
+ async acquire(ttlSeconds = 30, owner) {
377
+ const result = await executeOp(stub, {
378
+ type: 'lock',
379
+ action: 'acquire',
380
+ name,
381
+ ttlSeconds,
382
+ owner,
383
+ });
384
+ return (result.data ?? {
385
+ acquired: false,
386
+ owner: '',
387
+ expiresAt: 0,
388
+ });
389
+ },
390
+ async release(owner) {
391
+ const result = await executeOp(stub, {
392
+ type: 'lock',
393
+ action: 'release',
394
+ name,
395
+ owner,
396
+ });
397
+ return result.data ?? { released: false };
398
+ },
399
+ async check() {
400
+ const result = await executeOp(stub, {
401
+ type: 'lock',
402
+ action: 'check',
403
+ name,
404
+ });
405
+ return result.data ?? { locked: false };
406
+ },
407
+ };
408
+ }
409
+ async batch(_operations) {
410
+ // For now, execute sequentially. Can optimize with /batch endpoint later
411
+ const results = [];
412
+ for (const op of _operations) {
413
+ results.push(await op());
414
+ }
415
+ return results;
416
+ }
417
+ async isHealthy() {
418
+ try {
419
+ const response = await this.stub.fetch('http://internal/health');
420
+ return response.ok;
421
+ }
422
+ catch {
423
+ return false;
424
+ }
425
+ }
426
+ }
427
+ /**
428
+ * Internal helper to execute operations via HTTP
429
+ */
430
+ async function executeHttpOp(baseUrl, apiKey, operation, fetchFn = globalThis.fetch) {
431
+ const response = await fetchFn(`${baseUrl}/op`, {
432
+ method: 'POST',
433
+ headers: {
434
+ 'Content-Type': 'application/json',
435
+ Authorization: `Bearer ${apiKey}`,
436
+ },
437
+ body: JSON.stringify(operation),
438
+ });
439
+ if (!response.ok) {
440
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
441
+ throw new Error(`Keyspace operation failed: ${error.error}`);
442
+ }
443
+ return (await response.json());
444
+ }
445
+ /**
446
+ * HTTP-based Keyspace Provider
447
+ *
448
+ * Used when service bindings are not available (e.g., external services, testing).
449
+ * Authenticates via Platform API key (JWT).
450
+ */
451
+ class HttpKeyspace {
452
+ baseUrl;
453
+ apiKey;
454
+ fetchFn;
455
+ constructor(config) {
456
+ this.baseUrl = config.baseUrl || DEFAULT_KEYSPACE_URL;
457
+ this.apiKey = config.apiKey;
458
+ this.fetchFn = config.fetch || globalThis.fetch;
459
+ }
460
+ counter(name, scope) {
461
+ const { baseUrl, apiKey, fetchFn } = this;
462
+ return {
463
+ async increment(by = 1) {
464
+ const result = await executeHttpOp(baseUrl, apiKey, {
465
+ type: 'counter',
466
+ action: 'increment',
467
+ name,
468
+ scope,
469
+ value: by,
470
+ }, fetchFn);
471
+ return result.data ?? 0;
472
+ },
473
+ async decrement(by = 1) {
474
+ const result = await executeHttpOp(baseUrl, apiKey, {
475
+ type: 'counter',
476
+ action: 'decrement',
477
+ name,
478
+ scope,
479
+ value: by,
480
+ }, fetchFn);
481
+ return result.data ?? 0;
482
+ },
483
+ async get() {
484
+ const result = await executeHttpOp(baseUrl, apiKey, {
485
+ type: 'counter',
486
+ action: 'get',
487
+ name,
488
+ scope,
489
+ }, fetchFn);
490
+ return result.data ?? 0;
491
+ },
492
+ async set(value) {
493
+ const result = await executeHttpOp(baseUrl, apiKey, {
494
+ type: 'counter',
495
+ action: 'set',
496
+ name,
497
+ scope,
498
+ value,
499
+ }, fetchFn);
500
+ return result.data ?? value;
501
+ },
502
+ async reset() {
503
+ await executeHttpOp(baseUrl, apiKey, {
504
+ type: 'counter',
505
+ action: 'reset',
506
+ name,
507
+ scope,
508
+ }, fetchFn);
509
+ return 0;
510
+ },
511
+ };
512
+ }
513
+ get kv() {
514
+ const { baseUrl, apiKey, fetchFn } = this;
515
+ return {
516
+ async get(key) {
517
+ const result = await executeHttpOp(baseUrl, apiKey, {
518
+ type: 'kv',
519
+ action: 'get',
520
+ key,
521
+ }, fetchFn);
522
+ return result.data ?? null;
523
+ },
524
+ async set(key, value, ttlSeconds) {
525
+ await executeHttpOp(baseUrl, apiKey, {
526
+ type: 'kv',
527
+ action: 'set',
528
+ key,
529
+ value,
530
+ ttlSeconds,
531
+ }, fetchFn);
532
+ },
533
+ async delete(key) {
534
+ await executeHttpOp(baseUrl, apiKey, {
535
+ type: 'kv',
536
+ action: 'delete',
537
+ key,
538
+ }, fetchFn);
539
+ },
540
+ async has(key) {
541
+ const result = await executeHttpOp(baseUrl, apiKey, {
542
+ type: 'kv',
543
+ action: 'has',
544
+ key,
545
+ }, fetchFn);
546
+ return result.data ?? false;
547
+ },
548
+ };
549
+ }
550
+ queue(name) {
551
+ const { baseUrl, apiKey, fetchFn } = this;
552
+ return {
553
+ async enqueue(value) {
554
+ const result = await executeHttpOp(baseUrl, apiKey, {
555
+ type: 'queue',
556
+ action: 'enqueue',
557
+ name,
558
+ value,
559
+ }, fetchFn);
560
+ return result.data ?? 0;
561
+ },
562
+ async dequeue() {
563
+ const result = await executeHttpOp(baseUrl, apiKey, {
564
+ type: 'queue',
565
+ action: 'dequeue',
566
+ name,
567
+ }, fetchFn);
568
+ return result.data ?? null;
569
+ },
570
+ async readRange(start = 0, stop = 9) {
571
+ const result = await executeHttpOp(baseUrl, apiKey, {
572
+ type: 'queue',
573
+ action: 'readRange',
574
+ name,
575
+ start,
576
+ stop,
577
+ }, fetchFn);
578
+ return result.data ?? [];
579
+ },
580
+ async length() {
581
+ const result = await executeHttpOp(baseUrl, apiKey, {
582
+ type: 'queue',
583
+ action: 'length',
584
+ name,
585
+ }, fetchFn);
586
+ return result.data ?? 0;
587
+ },
588
+ async clear() {
589
+ await executeHttpOp(baseUrl, apiKey, {
590
+ type: 'queue',
591
+ action: 'clear',
592
+ name,
593
+ }, fetchFn);
594
+ },
595
+ };
596
+ }
597
+ set(name) {
598
+ const { baseUrl, apiKey, fetchFn } = this;
599
+ return {
600
+ async add(value) {
601
+ const result = await executeHttpOp(baseUrl, apiKey, {
602
+ type: 'set',
603
+ action: 'add',
604
+ name,
605
+ value,
606
+ }, fetchFn);
607
+ return result.data ?? 0;
608
+ },
609
+ async remove(value) {
610
+ const result = await executeHttpOp(baseUrl, apiKey, {
611
+ type: 'set',
612
+ action: 'remove',
613
+ name,
614
+ value,
615
+ }, fetchFn);
616
+ return result.data ?? 0;
617
+ },
618
+ async has(value) {
619
+ const result = await executeHttpOp(baseUrl, apiKey, {
620
+ type: 'set',
621
+ action: 'has',
622
+ name,
623
+ value,
624
+ }, fetchFn);
625
+ return result.data ?? false;
626
+ },
627
+ async members() {
628
+ const result = await executeHttpOp(baseUrl, apiKey, {
629
+ type: 'set',
630
+ action: 'members',
631
+ name,
632
+ }, fetchFn);
633
+ return result.data ?? [];
634
+ },
635
+ async size() {
636
+ const result = await executeHttpOp(baseUrl, apiKey, {
637
+ type: 'set',
638
+ action: 'size',
639
+ name,
640
+ }, fetchFn);
641
+ return result.data ?? 0;
642
+ },
643
+ };
644
+ }
645
+ leaderboard(name) {
646
+ const { baseUrl, apiKey, fetchFn } = this;
647
+ return {
648
+ async add(member, score) {
649
+ const result = await executeHttpOp(baseUrl, apiKey, {
650
+ type: 'leaderboard',
651
+ action: 'add',
652
+ name,
653
+ member,
654
+ score,
655
+ }, fetchFn);
656
+ return result.data ?? 0;
657
+ },
658
+ async increment(member, by = 1) {
659
+ const result = await executeHttpOp(baseUrl, apiKey, {
660
+ type: 'leaderboard',
661
+ action: 'increment',
662
+ name,
663
+ member,
664
+ score: by,
665
+ }, fetchFn);
666
+ return result.data ?? 0;
667
+ },
668
+ async getRange(start = 0, stop = 9) {
669
+ const result = await executeHttpOp(baseUrl, apiKey, {
670
+ type: 'leaderboard',
671
+ action: 'getRange',
672
+ name,
673
+ start,
674
+ stop,
675
+ }, fetchFn);
676
+ return result.data ?? [];
677
+ },
678
+ async getRank(member) {
679
+ const result = await executeHttpOp(baseUrl, apiKey, {
680
+ type: 'leaderboard',
681
+ action: 'getRank',
682
+ name,
683
+ member,
684
+ }, fetchFn);
685
+ return result.data ?? -1;
686
+ },
687
+ async getScore(member) {
688
+ const result = await executeHttpOp(baseUrl, apiKey, {
689
+ type: 'leaderboard',
690
+ action: 'getScore',
691
+ name,
692
+ member,
693
+ }, fetchFn);
694
+ return result.data ?? null;
695
+ },
696
+ async remove(member) {
697
+ await executeHttpOp(baseUrl, apiKey, {
698
+ type: 'leaderboard',
699
+ action: 'remove',
700
+ name,
701
+ member,
702
+ }, fetchFn);
703
+ },
704
+ };
705
+ }
706
+ hash(key) {
707
+ const { baseUrl, apiKey, fetchFn } = this;
708
+ return {
709
+ async set(field, value) {
710
+ await executeHttpOp(baseUrl, apiKey, {
711
+ type: 'hash',
712
+ action: 'set',
713
+ key,
714
+ field,
715
+ value,
716
+ }, fetchFn);
717
+ },
718
+ async get(field) {
719
+ const result = await executeHttpOp(baseUrl, apiKey, {
720
+ type: 'hash',
721
+ action: 'get',
722
+ key,
723
+ field,
724
+ }, fetchFn);
725
+ return result.data ?? null;
726
+ },
727
+ async getAll() {
728
+ const result = await executeHttpOp(baseUrl, apiKey, {
729
+ type: 'hash',
730
+ action: 'getAll',
731
+ key,
732
+ }, fetchFn);
733
+ return result.data ?? {};
734
+ },
735
+ async delete(field) {
736
+ await executeHttpOp(baseUrl, apiKey, {
737
+ type: 'hash',
738
+ action: 'delete',
739
+ key,
740
+ field,
741
+ }, fetchFn);
742
+ },
743
+ async increment(field, by = 1) {
744
+ const result = await executeHttpOp(baseUrl, apiKey, {
745
+ type: 'hash',
746
+ action: 'increment',
747
+ key,
748
+ field,
749
+ value: by,
750
+ }, fetchFn);
751
+ return result.data ?? 0;
752
+ },
753
+ };
754
+ }
755
+ rateLimit(key) {
756
+ const { baseUrl, apiKey, fetchFn } = this;
757
+ return {
758
+ async check(limit, windowSeconds) {
759
+ const result = await executeHttpOp(baseUrl, apiKey, {
760
+ type: 'rateLimit',
761
+ action: 'check',
762
+ key,
763
+ limit,
764
+ windowSeconds,
765
+ }, fetchFn);
766
+ return (result.data ?? {
767
+ allowed: false,
768
+ remaining: 0,
769
+ resetAt: Date.now(),
770
+ limit,
771
+ });
772
+ },
773
+ };
774
+ }
775
+ lock(name) {
776
+ const { baseUrl, apiKey, fetchFn } = this;
777
+ return {
778
+ async acquire(ttlSeconds = 30, owner) {
779
+ const result = await executeHttpOp(baseUrl, apiKey, {
780
+ type: 'lock',
781
+ action: 'acquire',
782
+ name,
783
+ ttlSeconds,
784
+ owner,
785
+ }, fetchFn);
786
+ return (result.data ?? {
787
+ acquired: false,
788
+ owner: '',
789
+ expiresAt: 0,
790
+ });
791
+ },
792
+ async release(owner) {
793
+ const result = await executeHttpOp(baseUrl, apiKey, {
794
+ type: 'lock',
795
+ action: 'release',
796
+ name,
797
+ owner,
798
+ }, fetchFn);
799
+ return result.data ?? { released: false };
800
+ },
801
+ async check() {
802
+ const result = await executeHttpOp(baseUrl, apiKey, {
803
+ type: 'lock',
804
+ action: 'check',
805
+ name,
806
+ }, fetchFn);
807
+ return result.data ?? { locked: false };
808
+ },
809
+ };
810
+ }
811
+ async batch(_operations) {
812
+ // For now, execute sequentially. Can optimize with /batch endpoint later
813
+ const results = [];
814
+ for (const op of _operations) {
815
+ results.push(await op());
816
+ }
817
+ return results;
818
+ }
819
+ async isHealthy() {
820
+ try {
821
+ const response = await this.fetchFn(`${this.baseUrl}/health`);
822
+ return response.ok;
823
+ }
824
+ catch {
825
+ return false;
826
+ }
827
+ }
828
+ }
829
+ /**
830
+ * Create a Keyspace instance using HTTP with API key authentication
831
+ *
832
+ * Use this when service bindings are not available (e.g., external services, testing).
833
+ * The API key must have 'keyspace' or '*' permission.
834
+ *
835
+ * @param config - HTTP configuration with API key
836
+ * @returns Keyspace provider instance
837
+ *
838
+ * @example
839
+ * ```typescript
840
+ * const keyspace = createKeyspaceHttp({
841
+ * apiKey: 'wtvr_jwt_eyJ...',
842
+ * baseUrl: 'https://keyspace.thewhatever.app', // optional
843
+ * });
844
+ *
845
+ * const count = await keyspace.counter('visitors').increment();
846
+ * ```
847
+ */
848
+ export function createKeyspaceHttp(config) {
849
+ if (!config.apiKey) {
850
+ throw new Error('API key is required for HTTP-based keyspace access');
851
+ }
852
+ return new HttpKeyspace(config);
853
+ }
854
+ /**
855
+ * Get a Keyspace instance for the current request
856
+ *
857
+ * @param reqOrEnv - Optional: object with env property, or env object directly.
858
+ * If not provided, uses the env configured via configurePlatformEnv().
859
+ * @returns Keyspace provider instance
860
+ *
861
+ * @example
862
+ * ```typescript
863
+ * // Using configured env (recommended)
864
+ * configurePlatformEnv(() => getCloudflareContext().env);
865
+ * const keyspace = await getKeyspace();
866
+ *
867
+ * // Or pass env directly
868
+ * const keyspace = await getKeyspace({ env });
869
+ * const keyspace = await getKeyspace(env);
870
+ * ```
871
+ */
872
+ export async function getKeyspace(reqOrEnv) {
873
+ let env;
874
+ // If no argument provided, use the configured env provider
875
+ if (!reqOrEnv) {
876
+ if (!isPlatformEnvConfigured()) {
877
+ throw new Error('getKeyspace() called without env and configurePlatformEnv() was not called. ' +
878
+ 'Either pass env directly: getKeyspace({ env }) or call configurePlatformEnv(() => getCloudflareContext().env) first.');
879
+ }
880
+ env = getPlatformEnv();
881
+ }
882
+ else if ('env' in reqOrEnv) {
883
+ env = reqOrEnv.env;
884
+ }
885
+ else if ('KEYSPACE' in reqOrEnv) {
886
+ // Direct env object
887
+ env = reqOrEnv;
888
+ }
889
+ else {
890
+ env = reqOrEnv.env;
891
+ }
892
+ if (!env) {
893
+ throw new Error('No environment found. Ensure you are running in edge runtime.');
894
+ }
895
+ // Check for KEYSPACE Durable Object binding
896
+ if (!env.KEYSPACE) {
897
+ throw new Error('No KEYSPACE Durable Object binding found. ' +
898
+ 'Add a service binding to the keyspace-do worker in your wrangler.jsonc:\n' +
899
+ ' "services": [{ "binding": "KEYSPACE", "service": "keyspace-do", "entrypoint": "Keyspace" }]');
900
+ }
901
+ // Get APP_ID for isolation (one DO per app)
902
+ const appId = env.APP_ID || 'default';
903
+ // Get Durable Object ID from app ID (deterministic)
904
+ const id = env.KEYSPACE.idFromName(appId);
905
+ const stub = env.KEYSPACE.get(id);
906
+ return new CloudflareKeyspace(stub);
907
+ }
908
+ /**
909
+ * Create a Keyspace instance directly (for advanced usage)
910
+ *
911
+ * @param env - Environment with KEYSPACE binding
912
+ * @param appId - Optional app ID for isolation (defaults to env.APP_ID or 'default')
913
+ *
914
+ * @example
915
+ * ```typescript
916
+ * const keyspace = createKeyspace({ KEYSPACE: env.KEYSPACE }, 'my-app-id');
917
+ * await keyspace.counter('visitors').increment();
918
+ * ```
919
+ */
920
+ export function createKeyspace(env, appId) {
921
+ if (!env.KEYSPACE) {
922
+ throw new Error('No KEYSPACE Durable Object binding found.');
923
+ }
924
+ const resolvedAppId = appId || env.APP_ID || 'default';
925
+ const id = env.KEYSPACE.idFromName(resolvedAppId);
926
+ const stub = env.KEYSPACE.get(id);
927
+ return new CloudflareKeyspace(stub);
928
+ }
929
+ //# sourceMappingURL=index.js.map