@concavejs/runtime-cf-base 0.0.1-alpha.7 → 0.0.1-alpha.9
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/durable-objects/blobstore-rpc.d.ts +13 -0
- package/dist/durable-objects/blobstore-rpc.js +27 -0
- package/dist/durable-objects/concave-do-base.d.ts +4 -0
- package/dist/durable-objects/concave-do-base.js +44 -96
- package/dist/durable-objects/docstore-rpc.d.ts +46 -0
- package/dist/durable-objects/docstore-rpc.js +63 -0
- package/dist/durable-objects/scheduler-manager.d.ts +19 -0
- package/dist/durable-objects/scheduler-manager.js +52 -0
- package/dist/durable-objects/sync-notifier.d.ts +16 -0
- package/dist/durable-objects/sync-notifier.js +38 -0
- package/dist/http/http-api.d.ts +32 -1
- package/dist/http/http-api.js +95 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/routing/sync-topology.d.ts +40 -0
- package/dist/routing/sync-topology.js +669 -0
- package/dist/udf/executor/do-client-executor.js +17 -1
- package/dist/worker/create-concave-worker.d.ts +45 -0
- package/dist/worker/create-concave-worker.js +36 -5
- package/package.json +11 -7
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BlobStore, StorageOptions, StorageMetadata } from "@concavejs/core/abstractions";
|
|
2
|
+
/**
|
|
3
|
+
* BlobStore RPC surface for service-binding isolation.
|
|
4
|
+
* Provides async delegation methods for blob storage operations.
|
|
5
|
+
*/
|
|
6
|
+
export declare class BlobStoreRpc {
|
|
7
|
+
private readonly blobstore;
|
|
8
|
+
constructor(blobstore: BlobStore);
|
|
9
|
+
store(buffer: ArrayBuffer, options?: StorageOptions): Promise<StorageMetadata>;
|
|
10
|
+
get(storageId: string): Promise<ArrayBuffer | null>;
|
|
11
|
+
delete(storageId: string): Promise<void>;
|
|
12
|
+
getUrl(storageId: string): Promise<string | null>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlobStore RPC surface for service-binding isolation.
|
|
3
|
+
* Provides async delegation methods for blob storage operations.
|
|
4
|
+
*/
|
|
5
|
+
export class BlobStoreRpc {
|
|
6
|
+
blobstore;
|
|
7
|
+
constructor(blobstore) {
|
|
8
|
+
this.blobstore = blobstore;
|
|
9
|
+
}
|
|
10
|
+
async store(buffer, options) {
|
|
11
|
+
return this.blobstore.store(buffer, options);
|
|
12
|
+
}
|
|
13
|
+
async get(storageId) {
|
|
14
|
+
const result = await this.blobstore.get(storageId);
|
|
15
|
+
if (result === null)
|
|
16
|
+
return null;
|
|
17
|
+
if (result instanceof Blob)
|
|
18
|
+
return result.arrayBuffer();
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
async delete(storageId) {
|
|
22
|
+
return this.blobstore.delete(storageId);
|
|
23
|
+
}
|
|
24
|
+
async getUrl(storageId) {
|
|
25
|
+
return this.blobstore.getUrl(storageId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -63,6 +63,10 @@ export declare class ConcaveDOBase extends DurableObject {
|
|
|
63
63
|
env: any;
|
|
64
64
|
protected scheduler: ScheduledFunctionExecutor;
|
|
65
65
|
protected cronExecutor: CronExecutor;
|
|
66
|
+
private readonly docStoreRpc;
|
|
67
|
+
private readonly blobStoreRpc;
|
|
68
|
+
private readonly syncNotifier;
|
|
69
|
+
private schedulerManager;
|
|
66
70
|
constructor(state: DurableObjectState, env: any, config: ConcaveDOConfig);
|
|
67
71
|
/**
|
|
68
72
|
* Initialize scheduler and cron executor
|
|
@@ -14,6 +14,10 @@ import { runAsClientCall, runAsServerCall } from "@concavejs/core/udf";
|
|
|
14
14
|
import { ScheduledFunctionExecutor, CronExecutor } from "@concavejs/core";
|
|
15
15
|
import { resolveAuthContext } from "@concavejs/core/http";
|
|
16
16
|
import { AdminAuthError, identityFromToken, isAdminToken, isSystemToken, JWTValidationError, resolveAdminAuthConfigFromEnv, resolveJwtValidationConfigFromEnv, resolveSystemAuthConfigFromEnv, setAdminAuthConfig, setJwtValidationConfig, setSystemAuthConfig, SystemAuthError, } from "@concavejs/core/auth";
|
|
17
|
+
import { DocStoreRpc } from "./docstore-rpc";
|
|
18
|
+
import { BlobStoreRpc } from "./blobstore-rpc";
|
|
19
|
+
import { SyncNotifier } from "./sync-notifier";
|
|
20
|
+
import { SchedulerManager } from "./scheduler-manager";
|
|
17
21
|
const VERSIONED_API_PREFIX = /^\/api\/\d+\.\d+(?:\.\d+)?(?=\/|$)/;
|
|
18
22
|
function stripApiVersionPrefix(pathname) {
|
|
19
23
|
return pathname.replace(VERSIONED_API_PREFIX, "/api");
|
|
@@ -86,6 +90,10 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
86
90
|
env;
|
|
87
91
|
scheduler;
|
|
88
92
|
cronExecutor;
|
|
93
|
+
docStoreRpc;
|
|
94
|
+
blobStoreRpc;
|
|
95
|
+
syncNotifier;
|
|
96
|
+
schedulerManager;
|
|
89
97
|
constructor(state, env, config) {
|
|
90
98
|
super(state, env);
|
|
91
99
|
this.doState = state;
|
|
@@ -108,6 +116,9 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
108
116
|
this._docstore = config.createDocstore ? config.createDocstore(adapterContext) : new DODocStore(state);
|
|
109
117
|
// Create BlobStore (allow override for testing or alternative implementations)
|
|
110
118
|
this._blobstore = config.createBlobstore?.(adapterContext);
|
|
119
|
+
this.docStoreRpc = new DocStoreRpc(this._docstore);
|
|
120
|
+
this.blobStoreRpc = this._blobstore ? new BlobStoreRpc(this._blobstore) : null;
|
|
121
|
+
this.syncNotifier = new SyncNotifier(state, env);
|
|
111
122
|
// Create UDF executor from resolved runtime services
|
|
112
123
|
this.udfExecutor = config.createUdfExecutor({
|
|
113
124
|
...adapterContext,
|
|
@@ -152,6 +163,7 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
152
163
|
notifyWrites,
|
|
153
164
|
allocateTimestamp,
|
|
154
165
|
});
|
|
166
|
+
this.schedulerManager = new SchedulerManager(this.scheduler, this.cronExecutor, this.doState);
|
|
155
167
|
}
|
|
156
168
|
/**
|
|
157
169
|
* Discover and sync cron specs during DO initialization.
|
|
@@ -223,7 +235,9 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
223
235
|
const exec = () => this.execute(path, convexArgs, type, auth, componentPath, requestId, parsedSnapshotTimestamp);
|
|
224
236
|
const result = caller === "server" ? await runAsServerCall(exec, path) : await runAsClientCall(exec);
|
|
225
237
|
if (type === "mutation" || type === "action") {
|
|
226
|
-
this.doState.waitUntil(this.reschedule())
|
|
238
|
+
this.doState.waitUntil(this.reschedule().catch((error) => {
|
|
239
|
+
console.error("[ConcaveDO] Failed to reschedule alarm", error?.message ?? error);
|
|
240
|
+
}));
|
|
227
241
|
}
|
|
228
242
|
const writtenTables = writtenTablesFromRanges(result.writtenRanges) ?? [];
|
|
229
243
|
const responseBody = {
|
|
@@ -240,8 +254,9 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
240
254
|
}
|
|
241
255
|
catch (e) {
|
|
242
256
|
console.error(e);
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
const errorMessage = e?.message ?? "Internal Server Error";
|
|
258
|
+
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
259
|
+
headers: { "Content-Type": "application/json", ...this.corsHeaders(request) },
|
|
245
260
|
status: 500,
|
|
246
261
|
});
|
|
247
262
|
}
|
|
@@ -288,170 +303,103 @@ export class ConcaveDOBase extends DurableObject {
|
|
|
288
303
|
* Handle scheduled function alarms
|
|
289
304
|
*/
|
|
290
305
|
async alarm() {
|
|
291
|
-
|
|
292
|
-
const scheduledResult = await this.scheduler.runDueJobs();
|
|
293
|
-
const cronResult = await this.cronExecutor.runDueJobs();
|
|
294
|
-
const nextTimes = [scheduledResult.nextScheduledTime, cronResult.nextScheduledTime].filter((t) => t !== null);
|
|
295
|
-
if (nextTimes.length === 0) {
|
|
296
|
-
await this.doState.storage.deleteAlarm();
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
await this.doState.storage.setAlarm(Math.min(...nextTimes));
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
console.error("[ConcaveDO] Alarm handler failed:", error?.message ?? error);
|
|
304
|
-
// Re-check for pending jobs so we don't lose scheduled work
|
|
305
|
-
try {
|
|
306
|
-
await this.reschedule();
|
|
307
|
-
}
|
|
308
|
-
catch (rescheduleError) {
|
|
309
|
-
console.error("[ConcaveDO] Reschedule after alarm failure also failed:", rescheduleError?.message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
306
|
+
return this.schedulerManager.handleAlarm();
|
|
312
307
|
}
|
|
313
308
|
/**
|
|
314
309
|
* Reschedule alarms
|
|
315
310
|
*/
|
|
316
311
|
async reschedule() {
|
|
317
|
-
|
|
318
|
-
const cronTime = await this.cronExecutor.getNextScheduledTime();
|
|
319
|
-
const nextTimes = [scheduledTime, cronTime].filter((t) => t !== null);
|
|
320
|
-
if (nextTimes.length === 0) {
|
|
321
|
-
await this.doState.storage.deleteAlarm();
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
await this.doState.storage.setAlarm(Math.min(...nextTimes));
|
|
325
|
-
}
|
|
312
|
+
return this.schedulerManager.reschedule();
|
|
326
313
|
}
|
|
327
314
|
/**
|
|
328
315
|
* Sync cron specs
|
|
329
316
|
*/
|
|
330
317
|
async syncCronSpecs(cronSpecs) {
|
|
331
|
-
|
|
332
|
-
await this.reschedule();
|
|
318
|
+
return this.schedulerManager.syncCronSpecs(cronSpecs);
|
|
333
319
|
}
|
|
334
320
|
// =============================================================================
|
|
335
321
|
// DocStore RPC Methods - Direct delegation to _docstore
|
|
336
322
|
// =============================================================================
|
|
337
323
|
async setupSchema(options) {
|
|
338
|
-
return this.
|
|
324
|
+
return this.docStoreRpc.setupSchema(options);
|
|
339
325
|
}
|
|
340
326
|
async write(documents, indexes, conflictStrategy) {
|
|
341
|
-
return this.
|
|
327
|
+
return this.docStoreRpc.write(documents, indexes, conflictStrategy);
|
|
342
328
|
}
|
|
343
329
|
async get(id, readTimestamp) {
|
|
344
|
-
return this.
|
|
330
|
+
return this.docStoreRpc.get(id, readTimestamp);
|
|
345
331
|
}
|
|
346
332
|
async scan(table, readTimestamp) {
|
|
347
|
-
return this.
|
|
333
|
+
return this.docStoreRpc.scan(table, readTimestamp);
|
|
348
334
|
}
|
|
349
335
|
async scanPaginated(table, cursor, limit, order, readTimestamp) {
|
|
350
|
-
return this.
|
|
336
|
+
return this.docStoreRpc.scanPaginated(table, cursor, limit, order, readTimestamp);
|
|
351
337
|
}
|
|
352
338
|
/**
|
|
353
339
|
* Generators return arrays over RPC (cannot stream async generators)
|
|
354
340
|
*/
|
|
355
341
|
async index_scan(indexId, tabletId, readTimestamp, interval, order) {
|
|
356
|
-
|
|
357
|
-
for await (const item of this._docstore.index_scan(indexId, tabletId, readTimestamp, interval, order)) {
|
|
358
|
-
results.push(item);
|
|
359
|
-
}
|
|
360
|
-
return results;
|
|
342
|
+
return this.docStoreRpc.index_scan(indexId, tabletId, readTimestamp, interval, order);
|
|
361
343
|
}
|
|
362
344
|
/**
|
|
363
345
|
* Generators return arrays over RPC (cannot stream async generators)
|
|
364
346
|
*/
|
|
365
347
|
async load_documents(range, order) {
|
|
366
|
-
|
|
367
|
-
for await (const item of this._docstore.load_documents(range, order)) {
|
|
368
|
-
results.push(item);
|
|
369
|
-
}
|
|
370
|
-
return results;
|
|
348
|
+
return this.docStoreRpc.load_documents(range, order);
|
|
371
349
|
}
|
|
372
350
|
async count(table) {
|
|
373
|
-
return this.
|
|
351
|
+
return this.docStoreRpc.count(table);
|
|
374
352
|
}
|
|
375
353
|
async search(indexId, searchQuery, filters, options) {
|
|
376
|
-
return this.
|
|
354
|
+
return this.docStoreRpc.search(indexId, searchQuery, filters, options);
|
|
377
355
|
}
|
|
378
356
|
async vectorSearch(indexId, vector, limit, filters) {
|
|
379
|
-
return this.
|
|
357
|
+
return this.docStoreRpc.vectorSearch(indexId, vector, limit, filters);
|
|
380
358
|
}
|
|
381
359
|
async getGlobal(key) {
|
|
382
|
-
return this.
|
|
360
|
+
return this.docStoreRpc.getGlobal(key);
|
|
383
361
|
}
|
|
384
362
|
async writeGlobal(key, value) {
|
|
385
|
-
return this.
|
|
363
|
+
return this.docStoreRpc.writeGlobal(key, value);
|
|
386
364
|
}
|
|
387
365
|
async previous_revisions(queries) {
|
|
388
|
-
|
|
389
|
-
return Array.from(result.entries());
|
|
366
|
+
return this.docStoreRpc.previous_revisions(queries);
|
|
390
367
|
}
|
|
391
368
|
async previous_revisions_of_documents(queries) {
|
|
392
|
-
|
|
393
|
-
return Array.from(result.entries());
|
|
369
|
+
return this.docStoreRpc.previous_revisions_of_documents(queries);
|
|
394
370
|
}
|
|
395
371
|
// =============================================================================
|
|
396
372
|
// Blobstore RPC Methods - Prefixed to avoid collision with other methods
|
|
397
373
|
// =============================================================================
|
|
398
374
|
async blobstoreStore(buffer, options) {
|
|
399
|
-
if (!this.
|
|
375
|
+
if (!this.blobStoreRpc) {
|
|
400
376
|
throw new Error("Blobstore not configured");
|
|
401
377
|
}
|
|
402
|
-
return this.
|
|
378
|
+
return this.blobStoreRpc.store(buffer, options);
|
|
403
379
|
}
|
|
404
380
|
async blobstoreGet(storageId) {
|
|
405
|
-
if (!this.
|
|
381
|
+
if (!this.blobStoreRpc) {
|
|
406
382
|
throw new Error("Blobstore not configured");
|
|
407
383
|
}
|
|
408
|
-
|
|
409
|
-
if (result === null)
|
|
410
|
-
return null;
|
|
411
|
-
if (result instanceof Blob)
|
|
412
|
-
return result.arrayBuffer();
|
|
413
|
-
return result;
|
|
384
|
+
return this.blobStoreRpc.get(storageId);
|
|
414
385
|
}
|
|
415
386
|
async blobstoreDelete(storageId) {
|
|
416
|
-
if (!this.
|
|
387
|
+
if (!this.blobStoreRpc) {
|
|
417
388
|
throw new Error("Blobstore not configured");
|
|
418
389
|
}
|
|
419
|
-
return this.
|
|
390
|
+
return this.blobStoreRpc.delete(storageId);
|
|
420
391
|
}
|
|
421
392
|
async blobstoreGetUrl(storageId) {
|
|
422
|
-
if (!this.
|
|
393
|
+
if (!this.blobStoreRpc) {
|
|
423
394
|
throw new Error("Blobstore not configured");
|
|
424
395
|
}
|
|
425
|
-
return this.
|
|
396
|
+
return this.blobStoreRpc.getUrl(storageId);
|
|
426
397
|
}
|
|
427
398
|
/**
|
|
428
399
|
* Notify SyncDO of writes for subscription invalidation
|
|
429
400
|
*/
|
|
430
401
|
async notifySyncDo(writtenRanges, writtenTables, commitTimestamp) {
|
|
431
|
-
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
try {
|
|
435
|
-
const instanceName = this.doState.id.name ?? "singleton";
|
|
436
|
-
const syncNamespace = this.env?.SYNC_DO;
|
|
437
|
-
if (!syncNamespace) {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const syncId = syncNamespace.idFromName(instanceName);
|
|
441
|
-
const syncStub = syncNamespace.get(syncId);
|
|
442
|
-
await syncStub.fetch("http://do/notify", {
|
|
443
|
-
method: "POST",
|
|
444
|
-
headers: { "Content-Type": "application/json" },
|
|
445
|
-
body: JSON.stringify({
|
|
446
|
-
writtenRanges,
|
|
447
|
-
writtenTables,
|
|
448
|
-
commitTimestamp: commitTimestamp ? commitTimestamp.toString() : undefined,
|
|
449
|
-
}),
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
catch (error) {
|
|
453
|
-
console.warn("Failed to notify SyncDO", error?.message ?? error);
|
|
454
|
-
}
|
|
402
|
+
return this.syncNotifier.notify(writtenRanges, writtenTables, commitTimestamp);
|
|
455
403
|
}
|
|
456
404
|
/**
|
|
457
405
|
* Get CORS headers for responses
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DocStore, DocumentLogEntry, DatabaseIndexUpdate, Interval, Order, IndexKeyBytes, LatestDocument, TimestampRange, InternalDocumentId, GlobalKey, DocumentPrevTsQuery, SearchIndexDefinition, VectorIndexDefinition } from "@concavejs/core/docstore";
|
|
2
|
+
import type { JSONValue } from "convex/values";
|
|
3
|
+
/**
|
|
4
|
+
* DocStore RPC surface for service-binding isolation.
|
|
5
|
+
* Provides async delegation methods that flatten generators into arrays
|
|
6
|
+
* for RPC transport (async generators cannot be streamed over service bindings).
|
|
7
|
+
*/
|
|
8
|
+
export declare class DocStoreRpc {
|
|
9
|
+
private readonly docstore;
|
|
10
|
+
constructor(docstore: DocStore);
|
|
11
|
+
setupSchema(options?: {
|
|
12
|
+
searchIndexes?: SearchIndexDefinition[];
|
|
13
|
+
vectorIndexes?: VectorIndexDefinition[];
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
write(documents: DocumentLogEntry[], indexes: Set<{
|
|
16
|
+
ts: bigint;
|
|
17
|
+
update: DatabaseIndexUpdate;
|
|
18
|
+
}>, conflictStrategy: "Error" | "Overwrite"): Promise<void>;
|
|
19
|
+
get(id: InternalDocumentId, readTimestamp?: bigint): Promise<LatestDocument | null>;
|
|
20
|
+
scan(table: string, readTimestamp?: bigint): Promise<LatestDocument[]>;
|
|
21
|
+
scanPaginated(table: string, cursor: string | null, limit: number, order: Order, readTimestamp?: bigint): Promise<{
|
|
22
|
+
documents: LatestDocument[];
|
|
23
|
+
nextCursor: string | null;
|
|
24
|
+
hasMore: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
index_scan(indexId: string, tabletId: string, readTimestamp: bigint, interval: Interval, order: Order): Promise<[IndexKeyBytes, LatestDocument][]>;
|
|
27
|
+
load_documents(range: TimestampRange, order: Order): Promise<DocumentLogEntry[]>;
|
|
28
|
+
count(table: string): Promise<number>;
|
|
29
|
+
search(indexId: string, searchQuery: string, filters: Map<string, unknown>, options?: {
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
doc: LatestDocument;
|
|
33
|
+
score: number;
|
|
34
|
+
}[]>;
|
|
35
|
+
vectorSearch(indexId: string, vector: number[], limit: number, filters: Map<string, string>): Promise<{
|
|
36
|
+
doc: LatestDocument;
|
|
37
|
+
score: number;
|
|
38
|
+
}[]>;
|
|
39
|
+
getGlobal(key: GlobalKey): Promise<JSONValue | null>;
|
|
40
|
+
writeGlobal(key: GlobalKey, value: JSONValue): Promise<void>;
|
|
41
|
+
previous_revisions(queries: Set<{
|
|
42
|
+
id: InternalDocumentId;
|
|
43
|
+
ts: bigint;
|
|
44
|
+
}>): Promise<[string, DocumentLogEntry][]>;
|
|
45
|
+
previous_revisions_of_documents(queries: Set<DocumentPrevTsQuery>): Promise<[string, DocumentLogEntry][]>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocStore RPC surface for service-binding isolation.
|
|
3
|
+
* Provides async delegation methods that flatten generators into arrays
|
|
4
|
+
* for RPC transport (async generators cannot be streamed over service bindings).
|
|
5
|
+
*/
|
|
6
|
+
export class DocStoreRpc {
|
|
7
|
+
docstore;
|
|
8
|
+
constructor(docstore) {
|
|
9
|
+
this.docstore = docstore;
|
|
10
|
+
}
|
|
11
|
+
async setupSchema(options) {
|
|
12
|
+
return this.docstore.setupSchema(options);
|
|
13
|
+
}
|
|
14
|
+
async write(documents, indexes, conflictStrategy) {
|
|
15
|
+
return this.docstore.write(documents, indexes, conflictStrategy);
|
|
16
|
+
}
|
|
17
|
+
async get(id, readTimestamp) {
|
|
18
|
+
return this.docstore.get(id, readTimestamp);
|
|
19
|
+
}
|
|
20
|
+
async scan(table, readTimestamp) {
|
|
21
|
+
return this.docstore.scan(table, readTimestamp);
|
|
22
|
+
}
|
|
23
|
+
async scanPaginated(table, cursor, limit, order, readTimestamp) {
|
|
24
|
+
return this.docstore.scanPaginated(table, cursor, limit, order, readTimestamp);
|
|
25
|
+
}
|
|
26
|
+
async index_scan(indexId, tabletId, readTimestamp, interval, order) {
|
|
27
|
+
const results = [];
|
|
28
|
+
for await (const item of this.docstore.index_scan(indexId, tabletId, readTimestamp, interval, order)) {
|
|
29
|
+
results.push(item);
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
async load_documents(range, order) {
|
|
34
|
+
const results = [];
|
|
35
|
+
for await (const item of this.docstore.load_documents(range, order)) {
|
|
36
|
+
results.push(item);
|
|
37
|
+
}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
async count(table) {
|
|
41
|
+
return this.docstore.count(table);
|
|
42
|
+
}
|
|
43
|
+
async search(indexId, searchQuery, filters, options) {
|
|
44
|
+
return this.docstore.search(indexId, searchQuery, filters, options);
|
|
45
|
+
}
|
|
46
|
+
async vectorSearch(indexId, vector, limit, filters) {
|
|
47
|
+
return this.docstore.vectorSearch(indexId, vector, limit, filters);
|
|
48
|
+
}
|
|
49
|
+
async getGlobal(key) {
|
|
50
|
+
return this.docstore.getGlobal(key);
|
|
51
|
+
}
|
|
52
|
+
async writeGlobal(key, value) {
|
|
53
|
+
return this.docstore.writeGlobal(key, value);
|
|
54
|
+
}
|
|
55
|
+
async previous_revisions(queries) {
|
|
56
|
+
const result = await this.docstore.previous_revisions(queries);
|
|
57
|
+
return Array.from(result.entries());
|
|
58
|
+
}
|
|
59
|
+
async previous_revisions_of_documents(queries) {
|
|
60
|
+
const result = await this.docstore.previous_revisions_of_documents(queries);
|
|
61
|
+
return Array.from(result.entries());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ScheduledFunctionExecutor, CronExecutor } from "@concavejs/core";
|
|
2
|
+
/**
|
|
3
|
+
* Manages scheduled function and cron job execution.
|
|
4
|
+
* Extracted from ConcaveDOBase for single-responsibility.
|
|
5
|
+
*/
|
|
6
|
+
export declare class SchedulerManager {
|
|
7
|
+
private readonly scheduler;
|
|
8
|
+
private readonly cronExecutor;
|
|
9
|
+
private readonly doState;
|
|
10
|
+
constructor(scheduler: ScheduledFunctionExecutor, cronExecutor: CronExecutor, doState: {
|
|
11
|
+
storage: {
|
|
12
|
+
setAlarm(time: number): Promise<void>;
|
|
13
|
+
deleteAlarm(): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
handleAlarm(): Promise<void>;
|
|
17
|
+
reschedule(): Promise<void>;
|
|
18
|
+
syncCronSpecs(cronSpecs: Record<string, any>): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages scheduled function and cron job execution.
|
|
3
|
+
* Extracted from ConcaveDOBase for single-responsibility.
|
|
4
|
+
*/
|
|
5
|
+
export class SchedulerManager {
|
|
6
|
+
scheduler;
|
|
7
|
+
cronExecutor;
|
|
8
|
+
doState;
|
|
9
|
+
constructor(scheduler, cronExecutor, doState) {
|
|
10
|
+
this.scheduler = scheduler;
|
|
11
|
+
this.cronExecutor = cronExecutor;
|
|
12
|
+
this.doState = doState;
|
|
13
|
+
}
|
|
14
|
+
async handleAlarm() {
|
|
15
|
+
try {
|
|
16
|
+
const scheduledResult = await this.scheduler.runDueJobs();
|
|
17
|
+
const cronResult = await this.cronExecutor.runDueJobs();
|
|
18
|
+
const nextTimes = [scheduledResult.nextScheduledTime, cronResult.nextScheduledTime].filter((t) => t !== null);
|
|
19
|
+
if (nextTimes.length === 0) {
|
|
20
|
+
await this.doState.storage.deleteAlarm();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
await this.doState.storage.setAlarm(Math.min(...nextTimes));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error("[ConcaveDO] Alarm handler failed:", error?.message ?? error);
|
|
28
|
+
try {
|
|
29
|
+
await this.reschedule();
|
|
30
|
+
}
|
|
31
|
+
catch (rescheduleError) {
|
|
32
|
+
console.error("[ConcaveDO] Reschedule after alarm failure also failed:", rescheduleError?.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async reschedule() {
|
|
37
|
+
const scheduledTime = await this.scheduler.getNextScheduledTime();
|
|
38
|
+
const cronTime = await this.cronExecutor.getNextScheduledTime();
|
|
39
|
+
const nextTimes = [scheduledTime, cronTime].filter((t) => t !== null);
|
|
40
|
+
if (nextTimes.length === 0) {
|
|
41
|
+
await this.doState.storage.deleteAlarm();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const alarmTime = Math.min(...nextTimes);
|
|
45
|
+
await this.doState.storage.setAlarm(alarmTime);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async syncCronSpecs(cronSpecs) {
|
|
49
|
+
await this.cronExecutor.syncCronSpecs(cronSpecs);
|
|
50
|
+
await this.reschedule();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SerializedKeyRange } from "@concavejs/core/queryengine";
|
|
2
|
+
/**
|
|
3
|
+
* Notifies the SyncDO of writes for subscription invalidation.
|
|
4
|
+
* Extracted from ConcaveDOBase for single-responsibility.
|
|
5
|
+
*/
|
|
6
|
+
export declare class SyncNotifier {
|
|
7
|
+
private readonly doState;
|
|
8
|
+
private readonly env;
|
|
9
|
+
constructor(doState: {
|
|
10
|
+
id: {
|
|
11
|
+
name: string | null;
|
|
12
|
+
toString(): string;
|
|
13
|
+
};
|
|
14
|
+
}, env: any);
|
|
15
|
+
notify(writtenRanges?: SerializedKeyRange[], writtenTables?: string[], commitTimestamp?: bigint): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notifies the SyncDO of writes for subscription invalidation.
|
|
3
|
+
* Extracted from ConcaveDOBase for single-responsibility.
|
|
4
|
+
*/
|
|
5
|
+
export class SyncNotifier {
|
|
6
|
+
doState;
|
|
7
|
+
env;
|
|
8
|
+
constructor(doState, env) {
|
|
9
|
+
this.doState = doState;
|
|
10
|
+
this.env = env;
|
|
11
|
+
}
|
|
12
|
+
async notify(writtenRanges, writtenTables, commitTimestamp) {
|
|
13
|
+
if (!writtenRanges?.length && !writtenTables?.length) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const instanceName = this.doState.id.name ?? "singleton";
|
|
18
|
+
const syncNamespace = this.env?.SYNC_DO;
|
|
19
|
+
if (!syncNamespace) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const syncId = syncNamespace.idFromName(instanceName);
|
|
23
|
+
const syncStub = syncNamespace.get(syncId);
|
|
24
|
+
await syncStub.fetch("http://do/notify", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
writtenRanges,
|
|
29
|
+
writtenTables,
|
|
30
|
+
commitTimestamp: commitTimestamp ? commitTimestamp.toString() : undefined,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.warn("Failed to notify SyncDO", error?.message ?? error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/http/http-api.d.ts
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
|
+
import type { SerializedKeyRange } from "@concavejs/core/queryengine";
|
|
1
2
|
/** Minimal Env shape needed by the HTTP API handler */
|
|
2
3
|
interface Env {
|
|
3
4
|
CONCAVE_DO: DurableObjectNamespace;
|
|
4
5
|
SYNC_DO: DurableObjectNamespace;
|
|
6
|
+
SYNC_NOTIFY_QUEUE?: QueueLike;
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
type QueueLike = {
|
|
9
|
+
send(message: unknown): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export interface HttpApiRoutingTargets {
|
|
12
|
+
/** Physical ConcaveDO name for execution traffic. Defaults to the logical `instance`. */
|
|
13
|
+
concaveDoName?: string;
|
|
14
|
+
/** Alias for non-DO runtimes to describe the write execution target identifier. */
|
|
15
|
+
concaveTargetName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Physical SyncDO names to notify after writes.
|
|
18
|
+
* Defaults to `[instance]`.
|
|
19
|
+
*/
|
|
20
|
+
syncDoNames?: string[];
|
|
21
|
+
/** Alias for non-DO runtimes to describe sync invalidation targets. */
|
|
22
|
+
syncTargetNames?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Optional custom write dispatcher.
|
|
25
|
+
* When set, it is responsible for fanning out invalidations (queue, stream, etc.).
|
|
26
|
+
*/
|
|
27
|
+
dispatchSyncWrites?: (payload: SyncWriteDispatchPayload) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface SyncWriteDispatchPayload {
|
|
30
|
+
logicalInstance: string;
|
|
31
|
+
projectId?: string;
|
|
32
|
+
syncTargets: string[];
|
|
33
|
+
writtenRanges?: SerializedKeyRange[];
|
|
34
|
+
writtenTables?: string[];
|
|
35
|
+
commitTimestamp?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function handleHttpApiRequest(request: Request, env: Env, ctx: ExecutionContext, instance?: string, routingTargets?: HttpApiRoutingTargets): Promise<Response>;
|
|
7
38
|
export {};
|