@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.
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keyspace/index.d.ts +86 -0
- package/dist/keyspace/index.d.ts.map +1 -0
- package/dist/keyspace/index.js +929 -0
- package/dist/keyspace/index.js.map +1 -0
- package/dist/keyspace/types.d.ts +377 -0
- package/dist/keyspace/types.d.ts.map +1 -0
- package/dist/keyspace/types.js +8 -0
- package/dist/keyspace/types.js.map +1 -0
- package/dist/realtime/index.d.ts +21 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/index.js +81 -0
- package/dist/realtime/index.js.map +1 -1
- package/dist/realtime/types.d.ts +21 -1
- package/dist/realtime/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|