@concavejs/runtime-cf-base 0.0.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cf-websocket-adapter.d.ts +38 -0
- package/dist/adapters/cf-websocket-adapter.js +83 -0
- 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 +169 -0
- package/dist/durable-objects/concave-do-base.js +466 -0
- 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 +53 -0
- package/dist/durable-objects/sync-notifier.d.ts +16 -0
- package/dist/durable-objects/sync-notifier.js +38 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.js +6 -0
- package/dist/http/dx-http.d.ts +43 -0
- package/dist/http/dx-http.js +327 -0
- package/dist/http/http-api.d.ts +38 -0
- package/dist/http/http-api.js +399 -0
- package/dist/http/index.d.ts +7 -0
- package/dist/http/index.js +7 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +27 -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/routing/sync-topology.d.ts +40 -0
- package/dist/routing/sync-topology.js +669 -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 +73 -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 +46 -0
- package/dist/sync/concave-do-udf-executor.js +75 -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 +58 -0
- package/dist/udf/executor/index.d.ts +8 -0
- package/dist/udf/executor/index.js +8 -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 +79 -0
- package/dist/worker/create-concave-worker.js +196 -0
- package/dist/worker/index.d.ts +6 -0
- package/dist/worker/index.js +6 -0
- package/dist/worker/udf-worker.d.ts +25 -0
- package/dist/worker/udf-worker.js +63 -0
- package/package.json +99 -0
|
@@ -0,0 +1,466 @@
|
|
|
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 { resolveAuthContext } from "@concavejs/core/http";
|
|
16
|
+
import { createTenantAuthResolverFromEnv, AuthConfigService } 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";
|
|
21
|
+
const VERSIONED_API_PREFIX = /^\/api\/\d+\.\d+(?:\.\d+)?(?=\/|$)/;
|
|
22
|
+
function stripApiVersionPrefix(pathname) {
|
|
23
|
+
return pathname.replace(VERSIONED_API_PREFIX, "/api");
|
|
24
|
+
}
|
|
25
|
+
function isReservedApiPath(pathname) {
|
|
26
|
+
const normalizedPath = stripApiVersionPrefix(pathname);
|
|
27
|
+
if (normalizedPath === "/api/execute" ||
|
|
28
|
+
normalizedPath === "/api/sync" ||
|
|
29
|
+
normalizedPath === "/api/reset-test-state" ||
|
|
30
|
+
normalizedPath === "/api/query" ||
|
|
31
|
+
normalizedPath === "/api/query_ts" ||
|
|
32
|
+
normalizedPath === "/api/query_at_ts" ||
|
|
33
|
+
normalizedPath === "/api/mutation" ||
|
|
34
|
+
normalizedPath === "/api/action") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (normalizedPath === "/api/storage" || normalizedPath.startsWith("/api/storage/")) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function shouldHandleAsHttpRoute(pathname) {
|
|
43
|
+
if (pathname.startsWith("/api/http")) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (!pathname.startsWith("/api/")) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return !isReservedApiPath(pathname);
|
|
50
|
+
}
|
|
51
|
+
function parseSnapshotTimestamp(value) {
|
|
52
|
+
if (value === undefined || value === null) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "bigint") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "number") {
|
|
59
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
60
|
+
throw new Error("Invalid snapshotTimestamp");
|
|
61
|
+
}
|
|
62
|
+
return BigInt(value);
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
67
|
+
throw new Error("Invalid snapshotTimestamp");
|
|
68
|
+
}
|
|
69
|
+
return BigInt(trimmed);
|
|
70
|
+
}
|
|
71
|
+
throw new Error("Invalid snapshotTimestamp");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Base class for Concave Durable Objects
|
|
75
|
+
*
|
|
76
|
+
* Provides:
|
|
77
|
+
* - Request handling (fetch, syscall, HTTP)
|
|
78
|
+
* - Scheduled function execution
|
|
79
|
+
* - Cron job execution
|
|
80
|
+
* - SyncDO notification
|
|
81
|
+
*
|
|
82
|
+
* Subclasses/configurers provide:
|
|
83
|
+
* - createUdfExecutor(): Platform-specific UDF executor creation
|
|
84
|
+
*/
|
|
85
|
+
export class ConcaveDOBase extends DurableObject {
|
|
86
|
+
_docstore;
|
|
87
|
+
_blobstore;
|
|
88
|
+
udfExecutor;
|
|
89
|
+
doState;
|
|
90
|
+
env;
|
|
91
|
+
scheduler;
|
|
92
|
+
cronExecutor;
|
|
93
|
+
docStoreRpc;
|
|
94
|
+
blobStoreRpc;
|
|
95
|
+
syncNotifier;
|
|
96
|
+
schedulerManager;
|
|
97
|
+
/** Cached search/vector index definitions for reuse (e.g. after reset-test-state) */
|
|
98
|
+
cachedSearchIndexes = [];
|
|
99
|
+
cachedVectorIndexes = [];
|
|
100
|
+
/** Cached JWT providers from auth.config.ts */
|
|
101
|
+
cachedJwtProviders = [];
|
|
102
|
+
constructor(state, env, config) {
|
|
103
|
+
super(state, env);
|
|
104
|
+
this.doState = state;
|
|
105
|
+
this.env = env;
|
|
106
|
+
const instanceId = state.id.name ?? state.id.toString();
|
|
107
|
+
const adapterContext = {
|
|
108
|
+
state,
|
|
109
|
+
env,
|
|
110
|
+
instance: instanceId,
|
|
111
|
+
};
|
|
112
|
+
// Create DocStore (allow override for testing or alternative implementations)
|
|
113
|
+
this._docstore = config.createDocstore ? config.createDocstore(adapterContext) : new DODocStore(state);
|
|
114
|
+
// Create BlobStore (allow override for testing or alternative implementations)
|
|
115
|
+
this._blobstore = config.createBlobstore?.(adapterContext);
|
|
116
|
+
this.docStoreRpc = new DocStoreRpc(this._docstore);
|
|
117
|
+
this.blobStoreRpc = this._blobstore ? new BlobStoreRpc(this._blobstore) : null;
|
|
118
|
+
this.syncNotifier = new SyncNotifier(state, env);
|
|
119
|
+
// Create UDF executor from resolved runtime services
|
|
120
|
+
this.udfExecutor = config.createUdfExecutor({
|
|
121
|
+
...adapterContext,
|
|
122
|
+
docstore: this._docstore,
|
|
123
|
+
blobstore: this._blobstore,
|
|
124
|
+
});
|
|
125
|
+
// Set up scheduler and cron executor
|
|
126
|
+
this.initializeSchedulers();
|
|
127
|
+
// Initialize schema and cron specs
|
|
128
|
+
this.doState.blockConcurrencyWhile(async () => {
|
|
129
|
+
let searchIndexes = [];
|
|
130
|
+
let vectorIndexes = [];
|
|
131
|
+
// Try loading schema from registered module loaders (works in self-hosted runtimes)
|
|
132
|
+
try {
|
|
133
|
+
const schemaService = new SchemaService();
|
|
134
|
+
searchIndexes = await getSearchIndexesFromSchema(schemaService);
|
|
135
|
+
vectorIndexes = await getVectorIndexesFromSchema(schemaService);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Schema module not available in this context (e.g. cloud runtime with isolated workers)
|
|
139
|
+
}
|
|
140
|
+
// Fallback: ask the UDF executor for schema info (e.g. via isolated worker shim)
|
|
141
|
+
if (searchIndexes.length === 0 && vectorIndexes.length === 0) {
|
|
142
|
+
try {
|
|
143
|
+
if (typeof this.udfExecutor.getSchemaInfo === "function") {
|
|
144
|
+
const info = await this.udfExecutor.getSchemaInfo();
|
|
145
|
+
if (info) {
|
|
146
|
+
searchIndexes = info.searchIndexes ?? [];
|
|
147
|
+
vectorIndexes = info.vectorIndexes ?? [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.warn("[ConcaveDO] Failed to resolve schema from executor:", error?.message ?? error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Cache for reuse (e.g. after reset-test-state clears storage)
|
|
156
|
+
this.cachedSearchIndexes = searchIndexes;
|
|
157
|
+
this.cachedVectorIndexes = vectorIndexes;
|
|
158
|
+
await this._docstore.setupSchema({ searchIndexes, vectorIndexes });
|
|
159
|
+
// Load auth.config.ts providers (mirrors SchemaService pattern)
|
|
160
|
+
try {
|
|
161
|
+
const authConfigService = new AuthConfigService();
|
|
162
|
+
this.cachedJwtProviders = await authConfigService.getProviders();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// auth.config.ts not available — silent
|
|
166
|
+
}
|
|
167
|
+
// Sync cron specs: use pre-computed if provided, otherwise auto-discover
|
|
168
|
+
await this.initializeCronSpecs(config);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Initialize scheduler and cron executor
|
|
173
|
+
*/
|
|
174
|
+
initializeSchedulers() {
|
|
175
|
+
const notifyWrites = async (ranges, tables, commitTimestamp) => {
|
|
176
|
+
await this.notifySyncDo(ranges, tables, commitTimestamp);
|
|
177
|
+
};
|
|
178
|
+
const allocateTimestamp = () => {
|
|
179
|
+
const oracle = this._docstore?.timestampOracle;
|
|
180
|
+
return oracle?.allocateTimestamp ? oracle.allocateTimestamp() : BigInt(Date.now());
|
|
181
|
+
};
|
|
182
|
+
this.scheduler = new ScheduledFunctionExecutor({
|
|
183
|
+
docstore: this._docstore,
|
|
184
|
+
udfExecutor: this.udfExecutor,
|
|
185
|
+
logger: console,
|
|
186
|
+
notifyWrites,
|
|
187
|
+
allocateTimestamp,
|
|
188
|
+
});
|
|
189
|
+
this.cronExecutor = new CronExecutor({
|
|
190
|
+
docstore: this._docstore,
|
|
191
|
+
udfExecutor: this.udfExecutor,
|
|
192
|
+
logger: console,
|
|
193
|
+
notifyWrites,
|
|
194
|
+
allocateTimestamp,
|
|
195
|
+
});
|
|
196
|
+
this.schedulerManager = new SchedulerManager(this.scheduler, this.cronExecutor, this.doState);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Discover and sync cron specs during DO initialization.
|
|
200
|
+
* Supports both pre-computed specs (from deploy pipelines) and
|
|
201
|
+
* runtime auto-discovery from the global module registry.
|
|
202
|
+
*/
|
|
203
|
+
async initializeCronSpecs(config) {
|
|
204
|
+
try {
|
|
205
|
+
let cronSpecs = null;
|
|
206
|
+
if (config.cronSpecs) {
|
|
207
|
+
// Pre-computed specs provided (e.g. extracted at build/deploy time)
|
|
208
|
+
cronSpecs = config.cronSpecs;
|
|
209
|
+
}
|
|
210
|
+
else if (config.discoverCrons !== false) {
|
|
211
|
+
// Auto-discover from registered modules
|
|
212
|
+
const { discoverCronSpecs } = await import("@concavejs/core/system");
|
|
213
|
+
cronSpecs = await discoverCronSpecs();
|
|
214
|
+
}
|
|
215
|
+
if (cronSpecs && Object.keys(cronSpecs).length > 0) {
|
|
216
|
+
await this.cronExecutor.syncCronSpecs(cronSpecs);
|
|
217
|
+
await this.reschedule();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
console.warn("[ConcaveDO] Failed to initialize cron specs:", error?.message ?? error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
currentSnapshotTimestamp() {
|
|
225
|
+
const oracle = this._docstore?.timestampOracle;
|
|
226
|
+
const oracleTimestamp = typeof oracle?.beginSnapshot === "function"
|
|
227
|
+
? oracle.beginSnapshot()
|
|
228
|
+
: typeof oracle?.getCurrentTimestamp === "function"
|
|
229
|
+
? oracle.getCurrentTimestamp()
|
|
230
|
+
: undefined;
|
|
231
|
+
const wallClock = BigInt(Date.now());
|
|
232
|
+
if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
|
|
233
|
+
return oracleTimestamp;
|
|
234
|
+
}
|
|
235
|
+
return wallClock;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Main request handler
|
|
239
|
+
*/
|
|
240
|
+
async fetch(request) {
|
|
241
|
+
const url = new URL(request.url);
|
|
242
|
+
if (url.pathname === "/reschedule" && request.method === "POST") {
|
|
243
|
+
try {
|
|
244
|
+
await this.reschedule();
|
|
245
|
+
return new Response("OK", { status: 200 });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error("[ConcaveDO] /reschedule failed:", error?.message ?? error);
|
|
249
|
+
return new Response("Reschedule failed", { status: 500 });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (url.pathname === "/query_ts") {
|
|
253
|
+
if (request.method !== "POST") {
|
|
254
|
+
return new Response("Method not allowed", { status: 405 });
|
|
255
|
+
}
|
|
256
|
+
return Response.json({ ts: this.currentSnapshotTimestamp().toString() }, { headers: this.corsHeaders(request) });
|
|
257
|
+
}
|
|
258
|
+
if (shouldHandleAsHttpRoute(url.pathname)) {
|
|
259
|
+
return this.handleHttp(request);
|
|
260
|
+
}
|
|
261
|
+
if (url.pathname === "/health") {
|
|
262
|
+
return new Response("OK", { status: 200 });
|
|
263
|
+
}
|
|
264
|
+
return this.handleUdfRequest(request);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Handle UDF execution request
|
|
268
|
+
*/
|
|
269
|
+
async handleUdfRequest(request) {
|
|
270
|
+
try {
|
|
271
|
+
const { path, args, type, auth, componentPath, caller, snapshotTimestamp } = await request.json();
|
|
272
|
+
const convexArgs = jsonToConvex(args);
|
|
273
|
+
const parsedSnapshotTimestamp = parseSnapshotTimestamp(snapshotTimestamp);
|
|
274
|
+
const requestId = crypto.randomUUID();
|
|
275
|
+
const exec = () => this.execute(path, convexArgs, type, auth, componentPath, requestId, parsedSnapshotTimestamp);
|
|
276
|
+
const result = caller === "server" ? await runAsServerCall(exec, path) : await runAsClientCall(exec);
|
|
277
|
+
if (type === "mutation" || type === "action") {
|
|
278
|
+
this.doState.waitUntil(this.reschedule().catch((error) => {
|
|
279
|
+
console.error("[ConcaveDO] Failed to reschedule alarm", error?.message ?? error);
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
const writtenTables = writtenTablesFromRanges(result.writtenRanges) ?? [];
|
|
283
|
+
const responseBody = {
|
|
284
|
+
result: result.result,
|
|
285
|
+
readRanges: result.readRanges,
|
|
286
|
+
writtenRanges: result.writtenRanges,
|
|
287
|
+
writtenTables,
|
|
288
|
+
logLines: result.logLines,
|
|
289
|
+
trace: result.trace,
|
|
290
|
+
snapshotTimestamp: result.snapshotTimestamp ? result.snapshotTimestamp.toString() : undefined,
|
|
291
|
+
commitTimestamp: result.commitTimestamp ? result.commitTimestamp.toString() : undefined,
|
|
292
|
+
};
|
|
293
|
+
return new Response(JSON.stringify(responseBody), {
|
|
294
|
+
headers: this.corsHeaders(request),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
console.error(e);
|
|
299
|
+
const errorMessage = e?.message ?? "Internal Server Error";
|
|
300
|
+
return new Response(JSON.stringify({ error: errorMessage }), {
|
|
301
|
+
headers: { "Content-Type": "application/json", ...this.corsHeaders(request) },
|
|
302
|
+
status: 500,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Handle HTTP action requests
|
|
308
|
+
*/
|
|
309
|
+
async handleHttp(request) {
|
|
310
|
+
try {
|
|
311
|
+
const url = new URL(request.url);
|
|
312
|
+
url.pathname = url.pathname.replace(/^\/api\/http/, "");
|
|
313
|
+
const req = new Request(url.toString(), request);
|
|
314
|
+
const authHeader = req.headers.get("Authorization");
|
|
315
|
+
const headerToken = authHeader?.replace(/^Bearer\s+/i, "").trim() || undefined;
|
|
316
|
+
const projectId = req.headers.get("X-Concave-Project-Id") ?? undefined;
|
|
317
|
+
const authResolver = createTenantAuthResolverFromEnv(this.env, projectId);
|
|
318
|
+
if (this.cachedJwtProviders.length > 0 && "updateJwtProviders" in authResolver) {
|
|
319
|
+
authResolver.updateJwtProviders(this.cachedJwtProviders);
|
|
320
|
+
}
|
|
321
|
+
const headerIdentity = undefined;
|
|
322
|
+
const auth = (await resolveAuthContext(undefined, headerToken, headerIdentity, authResolver));
|
|
323
|
+
const requestId = crypto.randomUUID();
|
|
324
|
+
return this.udfExecutor.executeHttp(req, auth, requestId);
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
console.error(e);
|
|
328
|
+
return new Response("Internal Server Error", { status: 500, headers: this.corsHeaders(request) });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Execute a UDF
|
|
333
|
+
*/
|
|
334
|
+
async execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp) {
|
|
335
|
+
return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle scheduled function alarms
|
|
339
|
+
*/
|
|
340
|
+
async alarm() {
|
|
341
|
+
return this.schedulerManager.handleAlarm();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Reschedule alarms
|
|
345
|
+
*/
|
|
346
|
+
async reschedule() {
|
|
347
|
+
return this.schedulerManager.reschedule();
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Sync cron specs
|
|
351
|
+
*/
|
|
352
|
+
async syncCronSpecs(cronSpecs) {
|
|
353
|
+
return this.schedulerManager.syncCronSpecs(cronSpecs);
|
|
354
|
+
}
|
|
355
|
+
// =============================================================================
|
|
356
|
+
// DocStore RPC Methods - Direct delegation to _docstore
|
|
357
|
+
// =============================================================================
|
|
358
|
+
async setupSchema(options) {
|
|
359
|
+
return this.docStoreRpc.setupSchema(options);
|
|
360
|
+
}
|
|
361
|
+
async write(documents, indexes, conflictStrategy) {
|
|
362
|
+
return this.docStoreRpc.write(documents, indexes, conflictStrategy);
|
|
363
|
+
}
|
|
364
|
+
async get(id, readTimestamp) {
|
|
365
|
+
return this.docStoreRpc.get(id, readTimestamp);
|
|
366
|
+
}
|
|
367
|
+
async scan(table, readTimestamp) {
|
|
368
|
+
return this.docStoreRpc.scan(table, readTimestamp);
|
|
369
|
+
}
|
|
370
|
+
async scanPaginated(table, cursor, limit, order, readTimestamp) {
|
|
371
|
+
return this.docStoreRpc.scanPaginated(table, cursor, limit, order, readTimestamp);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
375
|
+
*/
|
|
376
|
+
async index_scan(indexId, tabletId, readTimestamp, interval, order) {
|
|
377
|
+
return this.docStoreRpc.index_scan(indexId, tabletId, readTimestamp, interval, order);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Generators return arrays over RPC (cannot stream async generators)
|
|
381
|
+
*/
|
|
382
|
+
async load_documents(range, order) {
|
|
383
|
+
return this.docStoreRpc.load_documents(range, order);
|
|
384
|
+
}
|
|
385
|
+
async count(table) {
|
|
386
|
+
return this.docStoreRpc.count(table);
|
|
387
|
+
}
|
|
388
|
+
async search(indexId, searchQuery, filters, options) {
|
|
389
|
+
return this.docStoreRpc.search(indexId, searchQuery, filters, options);
|
|
390
|
+
}
|
|
391
|
+
async vectorSearch(indexId, vector, limit, filters) {
|
|
392
|
+
return this.docStoreRpc.vectorSearch(indexId, vector, limit, filters);
|
|
393
|
+
}
|
|
394
|
+
async getGlobal(key) {
|
|
395
|
+
return this.docStoreRpc.getGlobal(key);
|
|
396
|
+
}
|
|
397
|
+
async writeGlobal(key, value) {
|
|
398
|
+
return this.docStoreRpc.writeGlobal(key, value);
|
|
399
|
+
}
|
|
400
|
+
async previous_revisions(queries) {
|
|
401
|
+
return this.docStoreRpc.previous_revisions(queries);
|
|
402
|
+
}
|
|
403
|
+
async previous_revisions_of_documents(queries) {
|
|
404
|
+
return this.docStoreRpc.previous_revisions_of_documents(queries);
|
|
405
|
+
}
|
|
406
|
+
// =============================================================================
|
|
407
|
+
// Blobstore RPC Methods - Prefixed to avoid collision with other methods
|
|
408
|
+
// =============================================================================
|
|
409
|
+
async blobstoreStore(buffer, options) {
|
|
410
|
+
if (!this.blobStoreRpc) {
|
|
411
|
+
throw new Error("Blobstore not configured");
|
|
412
|
+
}
|
|
413
|
+
return this.blobStoreRpc.store(buffer, options);
|
|
414
|
+
}
|
|
415
|
+
async blobstoreGet(storageId) {
|
|
416
|
+
if (!this.blobStoreRpc) {
|
|
417
|
+
throw new Error("Blobstore not configured");
|
|
418
|
+
}
|
|
419
|
+
return this.blobStoreRpc.get(storageId);
|
|
420
|
+
}
|
|
421
|
+
async blobstoreDelete(storageId) {
|
|
422
|
+
if (!this.blobStoreRpc) {
|
|
423
|
+
throw new Error("Blobstore not configured");
|
|
424
|
+
}
|
|
425
|
+
return this.blobStoreRpc.delete(storageId);
|
|
426
|
+
}
|
|
427
|
+
async blobstoreGetUrl(storageId) {
|
|
428
|
+
if (!this.blobStoreRpc) {
|
|
429
|
+
throw new Error("Blobstore not configured");
|
|
430
|
+
}
|
|
431
|
+
return this.blobStoreRpc.getUrl(storageId);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Notify SyncDO of writes for subscription invalidation
|
|
435
|
+
*/
|
|
436
|
+
async notifySyncDo(writtenRanges, writtenTables, commitTimestamp) {
|
|
437
|
+
return this.syncNotifier.notify(writtenRanges, writtenTables, commitTimestamp);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get CORS headers for responses
|
|
441
|
+
*/
|
|
442
|
+
corsHeaders(request) {
|
|
443
|
+
const origin = request?.headers.get("Origin") ?? null;
|
|
444
|
+
const requestedHeaders = request?.headers.get("Access-Control-Request-Headers") ?? null;
|
|
445
|
+
const headers = {
|
|
446
|
+
"Content-Type": "application/json",
|
|
447
|
+
"Access-Control-Allow-Origin": origin ?? "*",
|
|
448
|
+
"Access-Control-Allow-Headers": requestedHeaders ?? "*",
|
|
449
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
450
|
+
"Access-Control-Max-Age": "86400",
|
|
451
|
+
};
|
|
452
|
+
if (origin) {
|
|
453
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
454
|
+
}
|
|
455
|
+
if (origin && requestedHeaders) {
|
|
456
|
+
headers.Vary = "Origin, Access-Control-Request-Headers";
|
|
457
|
+
}
|
|
458
|
+
else if (origin) {
|
|
459
|
+
headers.Vary = "Origin";
|
|
460
|
+
}
|
|
461
|
+
else if (requestedHeaders) {
|
|
462
|
+
headers.Vary = "Access-Control-Request-Headers";
|
|
463
|
+
}
|
|
464
|
+
return headers;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
@@ -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,53 @@
|
|
|
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
|
+
const alarmTime = Math.min(...nextTimes);
|
|
24
|
+
await this.doState.storage.setAlarm(alarmTime);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error("[SchedulerManager] Alarm handler failed:", error?.message ?? error);
|
|
29
|
+
try {
|
|
30
|
+
await this.reschedule();
|
|
31
|
+
}
|
|
32
|
+
catch (rescheduleError) {
|
|
33
|
+
console.error("[SchedulerManager] Reschedule after alarm failure also failed:", rescheduleError?.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async reschedule() {
|
|
38
|
+
const scheduledTime = await this.scheduler.getNextScheduledTime();
|
|
39
|
+
const cronTime = await this.cronExecutor.getNextScheduledTime();
|
|
40
|
+
const nextTimes = [scheduledTime, cronTime].filter((t) => t !== null);
|
|
41
|
+
if (nextTimes.length === 0) {
|
|
42
|
+
await this.doState.storage.deleteAlarm();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const alarmTime = Math.min(...nextTimes);
|
|
46
|
+
await this.doState.storage.setAlarm(alarmTime);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async syncCronSpecs(cronSpecs) {
|
|
50
|
+
await this.cronExecutor.syncCronSpecs(cronSpecs);
|
|
51
|
+
await this.reschedule();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|