@concavejs/runtime-cf-base 0.0.1-alpha.4
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.
Potentially problematic release.
This version of @concavejs/runtime-cf-base might be problematic. Click here for more details.
- package/dist/adapters/cf-websocket-adapter.d.ts +38 -0
- package/dist/adapters/cf-websocket-adapter.js +83 -0
- package/dist/durable-objects/concave-do-base.d.ts +158 -0
- package/dist/durable-objects/concave-do-base.js +412 -0
- package/dist/http/dx-http.d.ts +28 -0
- package/dist/http/dx-http.js +306 -0
- package/dist/http/http-api.d.ts +1 -0
- package/dist/http/http-api.js +262 -0
- package/dist/http/index.d.ts +7 -0
- package/dist/http/index.js +7 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +25 -0
- package/dist/internal.d.ts +4 -0
- package/dist/internal.js +4 -0
- package/dist/routing/instance.d.ts +25 -0
- package/dist/routing/instance.js +101 -0
- package/dist/rpc/blobstore-proxy.d.ts +11 -0
- package/dist/rpc/blobstore-proxy.js +28 -0
- package/dist/rpc/docstore-proxy.d.ts +11 -0
- package/dist/rpc/docstore-proxy.js +72 -0
- package/dist/rpc/index.d.ts +2 -0
- package/dist/rpc/index.js +2 -0
- package/dist/sync/cf-websocket-adapter.d.ts +15 -0
- package/dist/sync/cf-websocket-adapter.js +22 -0
- package/dist/sync/concave-do-udf-executor.d.ts +37 -0
- package/dist/sync/concave-do-udf-executor.js +67 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +2 -0
- package/dist/udf/executor/do-client-executor.d.ts +14 -0
- package/dist/udf/executor/do-client-executor.js +42 -0
- package/dist/udf/executor/index.d.ts +9 -0
- package/dist/udf/executor/index.js +9 -0
- package/dist/udf/executor/inline-executor.d.ts +13 -0
- package/dist/udf/executor/inline-executor.js +25 -0
- package/dist/udf/executor/isolated-executor.d.ts +24 -0
- package/dist/udf/executor/isolated-executor.js +31 -0
- package/dist/udf/executor/shim-content.d.ts +1 -0
- package/dist/udf/executor/shim-content.js +3 -0
- package/dist/worker/create-concave-worker.d.ts +34 -0
- package/dist/worker/create-concave-worker.js +162 -0
- package/dist/worker/index.d.ts +6 -0
- package/dist/worker/index.js +6 -0
- package/dist/worker/udf-worker.d.ts +14 -0
- package/dist/worker/udf-worker.js +63 -0
- package/package.json +45 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare WebSocket Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts Cloudflare Workers WebSocket to the platform-agnostic WebSocketConnection interface
|
|
5
|
+
*/
|
|
6
|
+
import { type WebSocketConnection, WebSocketReadyState, type WebSocketMessageHandler, type WebSocketCloseHandler, type WebSocketErrorHandler } from "@concavejs/core/abstractions";
|
|
7
|
+
export declare class CFWebSocketAdapter implements WebSocketConnection {
|
|
8
|
+
private ws;
|
|
9
|
+
private messageHandlers;
|
|
10
|
+
private closeHandlers;
|
|
11
|
+
private errorHandlers;
|
|
12
|
+
constructor(ws: WebSocket);
|
|
13
|
+
get readyState(): WebSocketReadyState;
|
|
14
|
+
send(data: string | ArrayBuffer): void;
|
|
15
|
+
close(code?: number, reason?: string): void;
|
|
16
|
+
onMessage(handler: WebSocketMessageHandler): void;
|
|
17
|
+
onClose(handler: WebSocketCloseHandler): void;
|
|
18
|
+
onError(handler: WebSocketErrorHandler): void;
|
|
19
|
+
/**
|
|
20
|
+
* Internal method to trigger message handlers
|
|
21
|
+
* Call this from your Durable Object's webSocketMessage handler
|
|
22
|
+
*/
|
|
23
|
+
triggerMessage(data: string | ArrayBuffer): void;
|
|
24
|
+
/**
|
|
25
|
+
* Internal method to trigger close handlers
|
|
26
|
+
* Call this from your Durable Object's webSocketClose handler
|
|
27
|
+
*/
|
|
28
|
+
triggerClose(code?: number, reason?: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Internal method to trigger error handlers
|
|
31
|
+
* Call this from your Durable Object's webSocketError handler
|
|
32
|
+
*/
|
|
33
|
+
triggerError(error: Error): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get the underlying Cloudflare WebSocket
|
|
36
|
+
*/
|
|
37
|
+
get underlying(): WebSocket;
|
|
38
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare WebSocket Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts Cloudflare Workers WebSocket to the platform-agnostic WebSocketConnection interface
|
|
5
|
+
*/
|
|
6
|
+
import { WebSocketReadyState, } from "@concavejs/core/abstractions";
|
|
7
|
+
export class CFWebSocketAdapter {
|
|
8
|
+
ws;
|
|
9
|
+
messageHandlers = [];
|
|
10
|
+
closeHandlers = [];
|
|
11
|
+
errorHandlers = [];
|
|
12
|
+
constructor(ws) {
|
|
13
|
+
this.ws = ws;
|
|
14
|
+
}
|
|
15
|
+
get readyState() {
|
|
16
|
+
// Map CF WebSocket ready states to our enum
|
|
17
|
+
switch (this.ws.readyState) {
|
|
18
|
+
case WebSocket.READY_STATE_CONNECTING:
|
|
19
|
+
return WebSocketReadyState.CONNECTING;
|
|
20
|
+
case WebSocket.READY_STATE_OPEN:
|
|
21
|
+
return WebSocketReadyState.OPEN;
|
|
22
|
+
case WebSocket.READY_STATE_CLOSING:
|
|
23
|
+
return WebSocketReadyState.CLOSING;
|
|
24
|
+
case WebSocket.READY_STATE_CLOSED:
|
|
25
|
+
return WebSocketReadyState.CLOSED;
|
|
26
|
+
default:
|
|
27
|
+
return WebSocketReadyState.CLOSED;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
send(data) {
|
|
31
|
+
this.ws.send(data);
|
|
32
|
+
}
|
|
33
|
+
close(code, reason) {
|
|
34
|
+
if (code !== undefined) {
|
|
35
|
+
this.ws.close(code, reason);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.ws.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
onMessage(handler) {
|
|
42
|
+
this.messageHandlers.push(handler);
|
|
43
|
+
}
|
|
44
|
+
onClose(handler) {
|
|
45
|
+
this.closeHandlers.push(handler);
|
|
46
|
+
}
|
|
47
|
+
onError(handler) {
|
|
48
|
+
this.errorHandlers.push(handler);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Internal method to trigger message handlers
|
|
52
|
+
* Call this from your Durable Object's webSocketMessage handler
|
|
53
|
+
*/
|
|
54
|
+
triggerMessage(data) {
|
|
55
|
+
for (const handler of this.messageHandlers) {
|
|
56
|
+
handler(data);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Internal method to trigger close handlers
|
|
61
|
+
* Call this from your Durable Object's webSocketClose handler
|
|
62
|
+
*/
|
|
63
|
+
triggerClose(code, reason) {
|
|
64
|
+
for (const handler of this.closeHandlers) {
|
|
65
|
+
handler(code, reason);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Internal method to trigger error handlers
|
|
70
|
+
* Call this from your Durable Object's webSocketError handler
|
|
71
|
+
*/
|
|
72
|
+
triggerError(error) {
|
|
73
|
+
for (const handler of this.errorHandlers) {
|
|
74
|
+
handler(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the underlying Cloudflare WebSocket
|
|
79
|
+
*/
|
|
80
|
+
get underlying() {
|
|
81
|
+
return this.ws;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for ConcaveDO implementations
|
|
3
|
+
*
|
|
4
|
+
* This provides the common functionality shared between runtime-cf and runtime-cloud.
|
|
5
|
+
* Subclasses provide platform-specific wiring through constructor config.
|
|
6
|
+
*/
|
|
7
|
+
import { DurableObject } from "cloudflare:workers";
|
|
8
|
+
import type { UdfExec, UdfResult } from "@concavejs/core/udf";
|
|
9
|
+
import type { DocStore, DocumentLogEntry, DatabaseIndexUpdate, Interval, Order, IndexKeyBytes, LatestDocument, TimestampRange, InternalDocumentId, GlobalKey, DocumentPrevTsQuery, SearchIndexDefinition, VectorIndexDefinition } from "@concavejs/core/docstore";
|
|
10
|
+
import type { BlobStore, StorageOptions, StorageMetadata } from "@concavejs/core/abstractions";
|
|
11
|
+
import { ScheduledFunctionExecutor, CronExecutor } from "@concavejs/core";
|
|
12
|
+
import type { SerializedKeyRange } from "@concavejs/core/queryengine";
|
|
13
|
+
import type { JSONValue } from "convex/values";
|
|
14
|
+
/**
|
|
15
|
+
* Configuration for ConcaveDOBase
|
|
16
|
+
*/
|
|
17
|
+
export interface ConcaveDOAdapterContext<Env = any> {
|
|
18
|
+
state: DurableObjectState;
|
|
19
|
+
env: Env;
|
|
20
|
+
instance: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ConcaveDOExecutorContext<Env = any> extends ConcaveDOAdapterContext<Env> {
|
|
23
|
+
docstore: DocStore;
|
|
24
|
+
blobstore?: BlobStore;
|
|
25
|
+
}
|
|
26
|
+
export interface ConcaveDOConfig {
|
|
27
|
+
/** Override the DocStore implementation (default: DODocStore) */
|
|
28
|
+
createDocstore?: (context: ConcaveDOAdapterContext) => DocStore;
|
|
29
|
+
/** Override the BlobStore implementation */
|
|
30
|
+
createBlobstore?: (context: ConcaveDOAdapterContext) => BlobStore | undefined;
|
|
31
|
+
/** Create the UDF executor from resolved runtime services */
|
|
32
|
+
createUdfExecutor: (context: ConcaveDOExecutorContext) => UdfExec;
|
|
33
|
+
/**
|
|
34
|
+
* Pre-computed cron specs to sync on initialization.
|
|
35
|
+
* When provided, these are used directly instead of auto-discovering from modules.
|
|
36
|
+
* Useful for deploy pipelines that extract specs at build time.
|
|
37
|
+
*/
|
|
38
|
+
cronSpecs?: Record<string, any>;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to auto-discover cron specs from registered modules (default: true).
|
|
41
|
+
* Set to false to disable auto-discovery (e.g. if no modules are registered globally).
|
|
42
|
+
* Ignored when `cronSpecs` is explicitly provided.
|
|
43
|
+
*/
|
|
44
|
+
discoverCrons?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Base class for Concave Durable Objects
|
|
48
|
+
*
|
|
49
|
+
* Provides:
|
|
50
|
+
* - Request handling (fetch, syscall, HTTP)
|
|
51
|
+
* - Scheduled function execution
|
|
52
|
+
* - Cron job execution
|
|
53
|
+
* - SyncDO notification
|
|
54
|
+
*
|
|
55
|
+
* Subclasses/configurers provide:
|
|
56
|
+
* - createUdfExecutor(): Platform-specific UDF executor creation
|
|
57
|
+
*/
|
|
58
|
+
export declare class ConcaveDOBase extends DurableObject {
|
|
59
|
+
protected _docstore: DocStore;
|
|
60
|
+
protected _blobstore?: BlobStore;
|
|
61
|
+
protected udfExecutor: UdfExec;
|
|
62
|
+
protected doState: DurableObjectState;
|
|
63
|
+
env: any;
|
|
64
|
+
protected scheduler: ScheduledFunctionExecutor;
|
|
65
|
+
protected cronExecutor: CronExecutor;
|
|
66
|
+
constructor(state: DurableObjectState, env: any, config: ConcaveDOConfig);
|
|
67
|
+
/**
|
|
68
|
+
* Initialize scheduler and cron executor
|
|
69
|
+
*/
|
|
70
|
+
private initializeSchedulers;
|
|
71
|
+
/**
|
|
72
|
+
* Discover and sync cron specs during DO initialization.
|
|
73
|
+
* Supports both pre-computed specs (from deploy pipelines) and
|
|
74
|
+
* runtime auto-discovery from the global module registry.
|
|
75
|
+
*/
|
|
76
|
+
private initializeCronSpecs;
|
|
77
|
+
/**
|
|
78
|
+
* Main request handler
|
|
79
|
+
*/
|
|
80
|
+
fetch(request: Request): Promise<Response>;
|
|
81
|
+
/**
|
|
82
|
+
* Handle UDF execution request
|
|
83
|
+
*/
|
|
84
|
+
protected handleUdfRequest(request: Request): Promise<Response>;
|
|
85
|
+
/**
|
|
86
|
+
* Handle HTTP action requests
|
|
87
|
+
*/
|
|
88
|
+
protected handleHttp(request: Request): Promise<Response>;
|
|
89
|
+
/**
|
|
90
|
+
* Execute a UDF
|
|
91
|
+
*/
|
|
92
|
+
protected execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string): Promise<UdfResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Handle scheduled function alarms
|
|
95
|
+
*/
|
|
96
|
+
alarm(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Reschedule alarms
|
|
99
|
+
*/
|
|
100
|
+
protected reschedule(): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Sync cron specs
|
|
103
|
+
*/
|
|
104
|
+
syncCronSpecs(cronSpecs: Record<string, any>): Promise<void>;
|
|
105
|
+
setupSchema(options?: {
|
|
106
|
+
searchIndexes?: SearchIndexDefinition[];
|
|
107
|
+
vectorIndexes?: VectorIndexDefinition[];
|
|
108
|
+
}): Promise<void>;
|
|
109
|
+
write(documents: DocumentLogEntry[], indexes: Set<{
|
|
110
|
+
ts: bigint;
|
|
111
|
+
update: DatabaseIndexUpdate;
|
|
112
|
+
}>, conflictStrategy: "Error" | "Overwrite"): Promise<void>;
|
|
113
|
+
get(id: InternalDocumentId, readTimestamp?: bigint): Promise<LatestDocument | null>;
|
|
114
|
+
scan(table: string, readTimestamp?: bigint): Promise<LatestDocument[]>;
|
|
115
|
+
scanPaginated(table: string, cursor: string | null, limit: number, order: Order, readTimestamp?: bigint): Promise<{
|
|
116
|
+
documents: LatestDocument[];
|
|
117
|
+
nextCursor: string | null;
|
|
118
|
+
hasMore: boolean;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
122
|
+
*/
|
|
123
|
+
index_scan(indexId: string, tabletId: string, readTimestamp: bigint, interval: Interval, order: Order): Promise<[IndexKeyBytes, LatestDocument][]>;
|
|
124
|
+
/**
|
|
125
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
126
|
+
*/
|
|
127
|
+
load_documents(range: TimestampRange, order: Order): Promise<DocumentLogEntry[]>;
|
|
128
|
+
count(table: string): Promise<number>;
|
|
129
|
+
search(indexId: string, searchQuery: string, filters: Map<string, unknown>, options?: {
|
|
130
|
+
limit?: number;
|
|
131
|
+
}): Promise<{
|
|
132
|
+
doc: LatestDocument;
|
|
133
|
+
score: number;
|
|
134
|
+
}[]>;
|
|
135
|
+
vectorSearch(indexId: string, vector: number[], limit: number, filters: Map<string, string>): Promise<{
|
|
136
|
+
doc: LatestDocument;
|
|
137
|
+
score: number;
|
|
138
|
+
}[]>;
|
|
139
|
+
getGlobal(key: GlobalKey): Promise<JSONValue | null>;
|
|
140
|
+
writeGlobal(key: GlobalKey, value: JSONValue): Promise<void>;
|
|
141
|
+
previous_revisions(queries: Set<{
|
|
142
|
+
id: InternalDocumentId;
|
|
143
|
+
ts: bigint;
|
|
144
|
+
}>): Promise<[string, DocumentLogEntry][]>;
|
|
145
|
+
previous_revisions_of_documents(queries: Set<DocumentPrevTsQuery>): Promise<[string, DocumentLogEntry][]>;
|
|
146
|
+
blobstoreStore(buffer: ArrayBuffer, options?: StorageOptions): Promise<StorageMetadata>;
|
|
147
|
+
blobstoreGet(storageId: string): Promise<ArrayBuffer | null>;
|
|
148
|
+
blobstoreDelete(storageId: string): Promise<void>;
|
|
149
|
+
blobstoreGetUrl(storageId: string): Promise<string | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Notify SyncDO of writes for subscription invalidation
|
|
152
|
+
*/
|
|
153
|
+
protected notifySyncDo(writtenRanges?: SerializedKeyRange[], writtenTables?: string[] | undefined, commitTimestamp?: bigint): Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* Get CORS headers for responses
|
|
156
|
+
*/
|
|
157
|
+
protected corsHeaders(request?: Request): Record<string, string>;
|
|
158
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for ConcaveDO implementations
|
|
3
|
+
*
|
|
4
|
+
* This provides the common functionality shared between runtime-cf and runtime-cloud.
|
|
5
|
+
* Subclasses provide platform-specific wiring through constructor config.
|
|
6
|
+
*/
|
|
7
|
+
import { DurableObject } from "cloudflare:workers";
|
|
8
|
+
import { DODocStore } from "@concavejs/docstore-cf-do";
|
|
9
|
+
import { writtenTablesFromRanges } from "@concavejs/core/utils";
|
|
10
|
+
import { jsonToConvex } from "convex/values";
|
|
11
|
+
import { getSearchIndexesFromSchema, getVectorIndexesFromSchema } from "@concavejs/core/docstore";
|
|
12
|
+
import { SchemaService } from "@concavejs/core/kernel";
|
|
13
|
+
import { runAsClientCall, runAsServerCall } from "@concavejs/core/udf";
|
|
14
|
+
import { ScheduledFunctionExecutor, CronExecutor } from "@concavejs/core";
|
|
15
|
+
import { resolveAdminAuthConfigFromEnv, resolveJwtValidationConfigFromEnv, resolveSystemAuthConfigFromEnv, setAdminAuthConfig, setJwtValidationConfig, setSystemAuthConfig, } from "@concavejs/core/auth";
|
|
16
|
+
const VERSIONED_API_PREFIX = /^\/api\/\d+\.\d+(?:\.\d+)?(?=\/|$)/;
|
|
17
|
+
function stripApiVersionPrefix(pathname) {
|
|
18
|
+
return pathname.replace(VERSIONED_API_PREFIX, "/api");
|
|
19
|
+
}
|
|
20
|
+
function isReservedApiPath(pathname) {
|
|
21
|
+
const normalizedPath = stripApiVersionPrefix(pathname);
|
|
22
|
+
if (normalizedPath === "/api/execute" ||
|
|
23
|
+
normalizedPath === "/api/sync" ||
|
|
24
|
+
normalizedPath === "/api/reset-test-state" ||
|
|
25
|
+
normalizedPath === "/api/query" ||
|
|
26
|
+
normalizedPath === "/api/mutation" ||
|
|
27
|
+
normalizedPath === "/api/action") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function shouldHandleAsHttpRoute(pathname) {
|
|
36
|
+
if (pathname.startsWith("/api/http")) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (!pathname.startsWith("/api/")) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return !isReservedApiPath(pathname);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Base class for Concave Durable Objects
|
|
46
|
+
*
|
|
47
|
+
* Provides:
|
|
48
|
+
* - Request handling (fetch, syscall, HTTP)
|
|
49
|
+
* - Scheduled function execution
|
|
50
|
+
* - Cron job execution
|
|
51
|
+
* - SyncDO notification
|
|
52
|
+
*
|
|
53
|
+
* Subclasses/configurers provide:
|
|
54
|
+
* - createUdfExecutor(): Platform-specific UDF executor creation
|
|
55
|
+
*/
|
|
56
|
+
export class ConcaveDOBase extends DurableObject {
|
|
57
|
+
_docstore;
|
|
58
|
+
_blobstore;
|
|
59
|
+
udfExecutor;
|
|
60
|
+
doState;
|
|
61
|
+
env;
|
|
62
|
+
scheduler;
|
|
63
|
+
cronExecutor;
|
|
64
|
+
constructor(state, env, config) {
|
|
65
|
+
super(state, env);
|
|
66
|
+
this.doState = state;
|
|
67
|
+
this.env = env;
|
|
68
|
+
const jwtConfig = resolveJwtValidationConfigFromEnv(env);
|
|
69
|
+
if (jwtConfig) {
|
|
70
|
+
setJwtValidationConfig(jwtConfig);
|
|
71
|
+
}
|
|
72
|
+
const adminConfig = resolveAdminAuthConfigFromEnv(env);
|
|
73
|
+
const systemConfig = resolveSystemAuthConfigFromEnv(env);
|
|
74
|
+
setAdminAuthConfig(adminConfig);
|
|
75
|
+
setSystemAuthConfig(systemConfig);
|
|
76
|
+
const instanceId = state.id.name ?? state.id.toString();
|
|
77
|
+
console.log(`[ConcaveDO.constructor] instanceId=${instanceId}`);
|
|
78
|
+
const adapterContext = {
|
|
79
|
+
state,
|
|
80
|
+
env,
|
|
81
|
+
instance: instanceId,
|
|
82
|
+
};
|
|
83
|
+
// Create DocStore (allow override for testing or alternative implementations)
|
|
84
|
+
this._docstore = config.createDocstore ? config.createDocstore(adapterContext) : new DODocStore(state);
|
|
85
|
+
// Create BlobStore (allow override for testing or alternative implementations)
|
|
86
|
+
this._blobstore = config.createBlobstore?.(adapterContext);
|
|
87
|
+
// Create UDF executor from resolved runtime services
|
|
88
|
+
this.udfExecutor = config.createUdfExecutor({
|
|
89
|
+
...adapterContext,
|
|
90
|
+
docstore: this._docstore,
|
|
91
|
+
blobstore: this._blobstore,
|
|
92
|
+
});
|
|
93
|
+
// Set up scheduler and cron executor
|
|
94
|
+
this.initializeSchedulers();
|
|
95
|
+
// Initialize schema and cron specs
|
|
96
|
+
this.doState.blockConcurrencyWhile(async () => {
|
|
97
|
+
const schemaService = new SchemaService();
|
|
98
|
+
const searchIndexes = await getSearchIndexesFromSchema(schemaService);
|
|
99
|
+
const vectorIndexes = await getVectorIndexesFromSchema(schemaService);
|
|
100
|
+
await this._docstore.setupSchema({ searchIndexes, vectorIndexes });
|
|
101
|
+
// Sync cron specs: use pre-computed if provided, otherwise auto-discover
|
|
102
|
+
await this.initializeCronSpecs(config);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Initialize scheduler and cron executor
|
|
107
|
+
*/
|
|
108
|
+
initializeSchedulers() {
|
|
109
|
+
const notifyWrites = async (ranges, tables, commitTimestamp) => {
|
|
110
|
+
await this.notifySyncDo(ranges, tables, commitTimestamp);
|
|
111
|
+
};
|
|
112
|
+
const allocateTimestamp = () => {
|
|
113
|
+
const oracle = this._docstore?.timestampOracle;
|
|
114
|
+
return oracle?.allocateTimestamp ? oracle.allocateTimestamp() : BigInt(Date.now());
|
|
115
|
+
};
|
|
116
|
+
this.scheduler = new ScheduledFunctionExecutor({
|
|
117
|
+
docstore: this._docstore,
|
|
118
|
+
udfExecutor: this.udfExecutor,
|
|
119
|
+
logger: console,
|
|
120
|
+
notifyWrites,
|
|
121
|
+
allocateTimestamp,
|
|
122
|
+
runMutationInTransaction: async (operation) => this.doState.storage.transaction(async () => operation()),
|
|
123
|
+
});
|
|
124
|
+
this.cronExecutor = new CronExecutor({
|
|
125
|
+
docstore: this._docstore,
|
|
126
|
+
udfExecutor: this.udfExecutor,
|
|
127
|
+
logger: console,
|
|
128
|
+
notifyWrites,
|
|
129
|
+
allocateTimestamp,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Discover and sync cron specs during DO initialization.
|
|
134
|
+
* Supports both pre-computed specs (from deploy pipelines) and
|
|
135
|
+
* runtime auto-discovery from the global module registry.
|
|
136
|
+
*/
|
|
137
|
+
async initializeCronSpecs(config) {
|
|
138
|
+
try {
|
|
139
|
+
let cronSpecs = null;
|
|
140
|
+
if (config.cronSpecs) {
|
|
141
|
+
// Pre-computed specs provided (e.g. extracted at build/deploy time)
|
|
142
|
+
cronSpecs = config.cronSpecs;
|
|
143
|
+
}
|
|
144
|
+
else if (config.discoverCrons !== false) {
|
|
145
|
+
// Auto-discover from registered modules
|
|
146
|
+
const { discoverCronSpecs } = await import("@concavejs/core/system");
|
|
147
|
+
cronSpecs = await discoverCronSpecs();
|
|
148
|
+
}
|
|
149
|
+
if (cronSpecs && Object.keys(cronSpecs).length > 0) {
|
|
150
|
+
await this.cronExecutor.syncCronSpecs(cronSpecs);
|
|
151
|
+
await this.reschedule();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.warn("[ConcaveDO] Failed to initialize cron specs:", error?.message ?? error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Main request handler
|
|
160
|
+
*/
|
|
161
|
+
async fetch(request) {
|
|
162
|
+
const url = new URL(request.url);
|
|
163
|
+
if (shouldHandleAsHttpRoute(url.pathname)) {
|
|
164
|
+
return this.handleHttp(request);
|
|
165
|
+
}
|
|
166
|
+
if (url.pathname === "/health") {
|
|
167
|
+
return new Response("OK", { status: 200 });
|
|
168
|
+
}
|
|
169
|
+
return this.handleUdfRequest(request);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Handle UDF execution request
|
|
173
|
+
*/
|
|
174
|
+
async handleUdfRequest(request) {
|
|
175
|
+
try {
|
|
176
|
+
const { path, args, type, auth, componentPath, caller } = await request.json();
|
|
177
|
+
const convexArgs = jsonToConvex(args);
|
|
178
|
+
const requestId = crypto.randomUUID();
|
|
179
|
+
const exec = () => this.execute(path, convexArgs, type, auth, componentPath, requestId);
|
|
180
|
+
const result = caller === "server" ? await runAsServerCall(exec, path) : await runAsClientCall(exec);
|
|
181
|
+
if (type === "mutation" || type === "action") {
|
|
182
|
+
this.doState.waitUntil(this.reschedule());
|
|
183
|
+
}
|
|
184
|
+
const writtenTables = writtenTablesFromRanges(result.writtenRanges) ?? [];
|
|
185
|
+
const responseBody = {
|
|
186
|
+
result: result.result,
|
|
187
|
+
readRanges: result.readRanges,
|
|
188
|
+
writtenRanges: result.writtenRanges,
|
|
189
|
+
writtenTables,
|
|
190
|
+
logLines: result.logLines,
|
|
191
|
+
commitTimestamp: result.commitTimestamp ? result.commitTimestamp.toString() : undefined,
|
|
192
|
+
};
|
|
193
|
+
return new Response(JSON.stringify(responseBody), {
|
|
194
|
+
headers: this.corsHeaders(request),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
console.error(e);
|
|
199
|
+
return new Response(`Error in Durable Object: ${e.message}`, {
|
|
200
|
+
headers: this.corsHeaders(request),
|
|
201
|
+
status: 500,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Handle HTTP action requests
|
|
207
|
+
*/
|
|
208
|
+
async handleHttp(request) {
|
|
209
|
+
try {
|
|
210
|
+
const url = new URL(request.url);
|
|
211
|
+
url.pathname = url.pathname.replace(/^\/api\/http/, "");
|
|
212
|
+
const req = new Request(url.toString(), request);
|
|
213
|
+
const auth = undefined;
|
|
214
|
+
const requestId = crypto.randomUUID();
|
|
215
|
+
return this.udfExecutor.executeHttp(req, auth, requestId);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
console.error(e);
|
|
219
|
+
return new Response(`Error in Durable Object: ${e.message}`, { status: 500 });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Execute a UDF
|
|
224
|
+
*/
|
|
225
|
+
async execute(path, args, type, auth, componentPath, requestId) {
|
|
226
|
+
return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Handle scheduled function alarms
|
|
230
|
+
*/
|
|
231
|
+
async alarm() {
|
|
232
|
+
const scheduledResult = await this.scheduler.runDueJobs();
|
|
233
|
+
const cronResult = await this.cronExecutor.runDueJobs();
|
|
234
|
+
const nextTimes = [scheduledResult.nextScheduledTime, cronResult.nextScheduledTime].filter((t) => t !== null);
|
|
235
|
+
if (nextTimes.length === 0) {
|
|
236
|
+
await this.doState.storage.deleteAlarm();
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
await this.doState.storage.setAlarm(Math.min(...nextTimes));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Reschedule alarms
|
|
244
|
+
*/
|
|
245
|
+
async reschedule() {
|
|
246
|
+
const scheduledTime = await this.scheduler.getNextScheduledTime();
|
|
247
|
+
const cronTime = await this.cronExecutor.getNextScheduledTime();
|
|
248
|
+
const nextTimes = [scheduledTime, cronTime].filter((t) => t !== null);
|
|
249
|
+
if (nextTimes.length === 0) {
|
|
250
|
+
await this.doState.storage.deleteAlarm();
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await this.doState.storage.setAlarm(Math.min(...nextTimes));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Sync cron specs
|
|
258
|
+
*/
|
|
259
|
+
async syncCronSpecs(cronSpecs) {
|
|
260
|
+
await this.cronExecutor.syncCronSpecs(cronSpecs);
|
|
261
|
+
await this.reschedule();
|
|
262
|
+
}
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// DocStore RPC Methods - Direct delegation to _docstore
|
|
265
|
+
// =============================================================================
|
|
266
|
+
async setupSchema(options) {
|
|
267
|
+
return this._docstore.setupSchema(options);
|
|
268
|
+
}
|
|
269
|
+
async write(documents, indexes, conflictStrategy) {
|
|
270
|
+
return this._docstore.write(documents, indexes, conflictStrategy);
|
|
271
|
+
}
|
|
272
|
+
async get(id, readTimestamp) {
|
|
273
|
+
return this._docstore.get(id, readTimestamp);
|
|
274
|
+
}
|
|
275
|
+
async scan(table, readTimestamp) {
|
|
276
|
+
return this._docstore.scan(table, readTimestamp);
|
|
277
|
+
}
|
|
278
|
+
async scanPaginated(table, cursor, limit, order, readTimestamp) {
|
|
279
|
+
return this._docstore.scanPaginated(table, cursor, limit, order, readTimestamp);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
283
|
+
*/
|
|
284
|
+
async index_scan(indexId, tabletId, readTimestamp, interval, order) {
|
|
285
|
+
const results = [];
|
|
286
|
+
for await (const item of this._docstore.index_scan(indexId, tabletId, readTimestamp, interval, order)) {
|
|
287
|
+
results.push(item);
|
|
288
|
+
}
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
293
|
+
*/
|
|
294
|
+
async load_documents(range, order) {
|
|
295
|
+
const results = [];
|
|
296
|
+
for await (const item of this._docstore.load_documents(range, order)) {
|
|
297
|
+
results.push(item);
|
|
298
|
+
}
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
async count(table) {
|
|
302
|
+
return this._docstore.count(table);
|
|
303
|
+
}
|
|
304
|
+
async search(indexId, searchQuery, filters, options) {
|
|
305
|
+
return this._docstore.search(indexId, searchQuery, filters, options);
|
|
306
|
+
}
|
|
307
|
+
async vectorSearch(indexId, vector, limit, filters) {
|
|
308
|
+
return this._docstore.vectorSearch(indexId, vector, limit, filters);
|
|
309
|
+
}
|
|
310
|
+
async getGlobal(key) {
|
|
311
|
+
return this._docstore.getGlobal(key);
|
|
312
|
+
}
|
|
313
|
+
async writeGlobal(key, value) {
|
|
314
|
+
return this._docstore.writeGlobal(key, value);
|
|
315
|
+
}
|
|
316
|
+
async previous_revisions(queries) {
|
|
317
|
+
const result = await this._docstore.previous_revisions(queries);
|
|
318
|
+
return Array.from(result.entries());
|
|
319
|
+
}
|
|
320
|
+
async previous_revisions_of_documents(queries) {
|
|
321
|
+
const result = await this._docstore.previous_revisions_of_documents(queries);
|
|
322
|
+
return Array.from(result.entries());
|
|
323
|
+
}
|
|
324
|
+
// =============================================================================
|
|
325
|
+
// Blobstore RPC Methods - Prefixed to avoid collision with other methods
|
|
326
|
+
// =============================================================================
|
|
327
|
+
async blobstoreStore(buffer, options) {
|
|
328
|
+
if (!this._blobstore) {
|
|
329
|
+
throw new Error("Blobstore not configured");
|
|
330
|
+
}
|
|
331
|
+
return this._blobstore.store(buffer, options);
|
|
332
|
+
}
|
|
333
|
+
async blobstoreGet(storageId) {
|
|
334
|
+
if (!this._blobstore) {
|
|
335
|
+
throw new Error("Blobstore not configured");
|
|
336
|
+
}
|
|
337
|
+
const result = await this._blobstore.get(storageId);
|
|
338
|
+
if (result === null)
|
|
339
|
+
return null;
|
|
340
|
+
if (result instanceof Blob)
|
|
341
|
+
return result.arrayBuffer();
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
async blobstoreDelete(storageId) {
|
|
345
|
+
if (!this._blobstore) {
|
|
346
|
+
throw new Error("Blobstore not configured");
|
|
347
|
+
}
|
|
348
|
+
return this._blobstore.delete(storageId);
|
|
349
|
+
}
|
|
350
|
+
async blobstoreGetUrl(storageId) {
|
|
351
|
+
if (!this._blobstore) {
|
|
352
|
+
throw new Error("Blobstore not configured");
|
|
353
|
+
}
|
|
354
|
+
return this._blobstore.getUrl(storageId);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Notify SyncDO of writes for subscription invalidation
|
|
358
|
+
*/
|
|
359
|
+
async notifySyncDo(writtenRanges, writtenTables, commitTimestamp) {
|
|
360
|
+
if (!writtenRanges?.length && !writtenTables?.length) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const instanceName = this.doState.id.name ?? "singleton";
|
|
365
|
+
const syncNamespace = this.env?.SYNC_DO;
|
|
366
|
+
if (!syncNamespace) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const syncId = syncNamespace.idFromName(instanceName);
|
|
370
|
+
const syncStub = syncNamespace.get(syncId);
|
|
371
|
+
await syncStub.fetch("http://do/notify", {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { "Content-Type": "application/json" },
|
|
374
|
+
body: JSON.stringify({
|
|
375
|
+
writtenRanges,
|
|
376
|
+
writtenTables,
|
|
377
|
+
commitTimestamp: commitTimestamp ? commitTimestamp.toString() : undefined,
|
|
378
|
+
}),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
console.warn("Failed to notify SyncDO", error?.message ?? error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get CORS headers for responses
|
|
387
|
+
*/
|
|
388
|
+
corsHeaders(request) {
|
|
389
|
+
const origin = request?.headers.get("Origin") ?? null;
|
|
390
|
+
const requestedHeaders = request?.headers.get("Access-Control-Request-Headers") ?? null;
|
|
391
|
+
const headers = {
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
"Access-Control-Allow-Origin": origin ?? "*",
|
|
394
|
+
"Access-Control-Allow-Headers": requestedHeaders ?? "*",
|
|
395
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
396
|
+
"Access-Control-Max-Age": "86400",
|
|
397
|
+
};
|
|
398
|
+
if (origin) {
|
|
399
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
400
|
+
}
|
|
401
|
+
if (origin && requestedHeaders) {
|
|
402
|
+
headers.Vary = "Origin, Access-Control-Request-Headers";
|
|
403
|
+
}
|
|
404
|
+
else if (origin) {
|
|
405
|
+
headers.Vary = "Origin";
|
|
406
|
+
}
|
|
407
|
+
else if (requestedHeaders) {
|
|
408
|
+
headers.Vary = "Access-Control-Request-Headers";
|
|
409
|
+
}
|
|
410
|
+
return headers;
|
|
411
|
+
}
|
|
412
|
+
}
|