@concavejs/runtime-cf-base 0.0.1-alpha.6 → 0.0.1-alpha.7

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.
@@ -74,6 +74,7 @@ export declare class ConcaveDOBase extends DurableObject {
74
74
  * runtime auto-discovery from the global module registry.
75
75
  */
76
76
  private initializeCronSpecs;
77
+ private currentSnapshotTimestamp;
77
78
  /**
78
79
  * Main request handler
79
80
  */
@@ -89,7 +90,7 @@ export declare class ConcaveDOBase extends DurableObject {
89
90
  /**
90
91
  * Execute a UDF
91
92
  */
92
- protected execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string): Promise<UdfResult>;
93
+ protected execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string, snapshotTimestamp?: bigint): Promise<UdfResult>;
93
94
  /**
94
95
  * Handle scheduled function alarms
95
96
  */
@@ -12,7 +12,8 @@ import { getSearchIndexesFromSchema, getVectorIndexesFromSchema } from "@concave
12
12
  import { SchemaService } from "@concavejs/core/kernel";
13
13
  import { runAsClientCall, runAsServerCall } from "@concavejs/core/udf";
14
14
  import { ScheduledFunctionExecutor, CronExecutor } from "@concavejs/core";
15
- import { resolveAdminAuthConfigFromEnv, resolveJwtValidationConfigFromEnv, resolveSystemAuthConfigFromEnv, setAdminAuthConfig, setJwtValidationConfig, setSystemAuthConfig, } from "@concavejs/core/auth";
15
+ import { resolveAuthContext } from "@concavejs/core/http";
16
+ import { AdminAuthError, identityFromToken, isAdminToken, isSystemToken, JWTValidationError, resolveAdminAuthConfigFromEnv, resolveJwtValidationConfigFromEnv, resolveSystemAuthConfigFromEnv, setAdminAuthConfig, setJwtValidationConfig, setSystemAuthConfig, SystemAuthError, } from "@concavejs/core/auth";
16
17
  const VERSIONED_API_PREFIX = /^\/api\/\d+\.\d+(?:\.\d+)?(?=\/|$)/;
17
18
  function stripApiVersionPrefix(pathname) {
18
19
  return pathname.replace(VERSIONED_API_PREFIX, "/api");
@@ -23,6 +24,8 @@ function isReservedApiPath(pathname) {
23
24
  normalizedPath === "/api/sync" ||
24
25
  normalizedPath === "/api/reset-test-state" ||
25
26
  normalizedPath === "/api/query" ||
27
+ normalizedPath === "/api/query_ts" ||
28
+ normalizedPath === "/api/query_at_ts" ||
26
29
  normalizedPath === "/api/mutation" ||
27
30
  normalizedPath === "/api/action") {
28
31
  return true;
@@ -41,6 +44,28 @@ function shouldHandleAsHttpRoute(pathname) {
41
44
  }
42
45
  return !isReservedApiPath(pathname);
43
46
  }
47
+ function parseSnapshotTimestamp(value) {
48
+ if (value === undefined || value === null) {
49
+ return undefined;
50
+ }
51
+ if (typeof value === "bigint") {
52
+ return value;
53
+ }
54
+ if (typeof value === "number") {
55
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
56
+ throw new Error("Invalid snapshotTimestamp");
57
+ }
58
+ return BigInt(value);
59
+ }
60
+ if (typeof value === "string") {
61
+ const trimmed = value.trim();
62
+ if (!/^\d+$/.test(trimmed)) {
63
+ throw new Error("Invalid snapshotTimestamp");
64
+ }
65
+ return BigInt(trimmed);
66
+ }
67
+ throw new Error("Invalid snapshotTimestamp");
68
+ }
44
69
  /**
45
70
  * Base class for Concave Durable Objects
46
71
  *
@@ -74,7 +99,6 @@ export class ConcaveDOBase extends DurableObject {
74
99
  setAdminAuthConfig(adminConfig);
75
100
  setSystemAuthConfig(systemConfig);
76
101
  const instanceId = state.id.name ?? state.id.toString();
77
- console.log(`[ConcaveDO.constructor] instanceId=${instanceId}`);
78
102
  const adapterContext = {
79
103
  state,
80
104
  env,
@@ -155,11 +179,30 @@ export class ConcaveDOBase extends DurableObject {
155
179
  console.warn("[ConcaveDO] Failed to initialize cron specs:", error?.message ?? error);
156
180
  }
157
181
  }
182
+ currentSnapshotTimestamp() {
183
+ const oracle = this._docstore?.timestampOracle;
184
+ const oracleTimestamp = typeof oracle?.beginSnapshot === "function"
185
+ ? oracle.beginSnapshot()
186
+ : typeof oracle?.getCurrentTimestamp === "function"
187
+ ? oracle.getCurrentTimestamp()
188
+ : undefined;
189
+ const wallClock = BigInt(Date.now());
190
+ if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
191
+ return oracleTimestamp;
192
+ }
193
+ return wallClock;
194
+ }
158
195
  /**
159
196
  * Main request handler
160
197
  */
161
198
  async fetch(request) {
162
199
  const url = new URL(request.url);
200
+ if (url.pathname === "/query_ts") {
201
+ if (request.method !== "POST") {
202
+ return new Response("Method not allowed", { status: 405 });
203
+ }
204
+ return Response.json({ ts: this.currentSnapshotTimestamp().toString() }, { headers: this.corsHeaders(request) });
205
+ }
163
206
  if (shouldHandleAsHttpRoute(url.pathname)) {
164
207
  return this.handleHttp(request);
165
208
  }
@@ -173,10 +216,11 @@ export class ConcaveDOBase extends DurableObject {
173
216
  */
174
217
  async handleUdfRequest(request) {
175
218
  try {
176
- const { path, args, type, auth, componentPath, caller } = await request.json();
219
+ const { path, args, type, auth, componentPath, caller, snapshotTimestamp } = await request.json();
177
220
  const convexArgs = jsonToConvex(args);
221
+ const parsedSnapshotTimestamp = parseSnapshotTimestamp(snapshotTimestamp);
178
222
  const requestId = crypto.randomUUID();
179
- const exec = () => this.execute(path, convexArgs, type, auth, componentPath, requestId);
223
+ const exec = () => this.execute(path, convexArgs, type, auth, componentPath, requestId, parsedSnapshotTimestamp);
180
224
  const result = caller === "server" ? await runAsServerCall(exec, path) : await runAsClientCall(exec);
181
225
  if (type === "mutation" || type === "action") {
182
226
  this.doState.waitUntil(this.reschedule());
@@ -196,7 +240,7 @@ export class ConcaveDOBase extends DurableObject {
196
240
  }
197
241
  catch (e) {
198
242
  console.error(e);
199
- return new Response(`Error in Durable Object: ${e.message}`, {
243
+ return new Response("Internal Server Error", {
200
244
  headers: this.corsHeaders(request),
201
245
  status: 500,
202
246
  });
@@ -210,33 +254,60 @@ export class ConcaveDOBase extends DurableObject {
210
254
  const url = new URL(request.url);
211
255
  url.pathname = url.pathname.replace(/^\/api\/http/, "");
212
256
  const req = new Request(url.toString(), request);
213
- const auth = undefined;
257
+ const authHeader = req.headers.get("Authorization");
258
+ const headerToken = authHeader?.replace(/^Bearer\s+/i, "").trim() || undefined;
259
+ let headerIdentity;
260
+ try {
261
+ headerIdentity =
262
+ headerToken && !isAdminToken(headerToken) && !isSystemToken(headerToken)
263
+ ? await identityFromToken(headerToken)
264
+ : undefined;
265
+ }
266
+ catch (error) {
267
+ if (error instanceof JWTValidationError || error instanceof AdminAuthError || error instanceof SystemAuthError) {
268
+ return Response.json({ error: "Unauthorized" }, { status: 401, headers: this.corsHeaders(request) });
269
+ }
270
+ throw error;
271
+ }
272
+ const auth = (await resolveAuthContext(undefined, headerToken, headerIdentity));
214
273
  const requestId = crypto.randomUUID();
215
274
  return this.udfExecutor.executeHttp(req, auth, requestId);
216
275
  }
217
276
  catch (e) {
218
277
  console.error(e);
219
- return new Response(`Error in Durable Object: ${e.message}`, { status: 500 });
278
+ return new Response("Internal Server Error", { status: 500, headers: this.corsHeaders(request) });
220
279
  }
221
280
  }
222
281
  /**
223
282
  * Execute a UDF
224
283
  */
225
- async execute(path, args, type, auth, componentPath, requestId) {
226
- return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId);
284
+ async execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp) {
285
+ return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp);
227
286
  }
228
287
  /**
229
288
  * Handle scheduled function alarms
230
289
  */
231
290
  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();
291
+ try {
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
+ }
237
301
  }
238
- else {
239
- await this.doState.storage.setAlarm(Math.min(...nextTimes));
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
+ }
240
311
  }
241
312
  }
242
313
  /**
@@ -15,6 +15,8 @@ function isReservedApiPath(pathname) {
15
15
  normalizedPath === "/api/sync" ||
16
16
  normalizedPath === "/api/reset-test-state" ||
17
17
  normalizedPath === "/api/query" ||
18
+ normalizedPath === "/api/query_ts" ||
19
+ normalizedPath === "/api/query_at_ts" ||
18
20
  normalizedPath === "/api/mutation" ||
19
21
  normalizedPath === "/api/action") {
20
22
  return true;
@@ -36,6 +38,24 @@ function shouldForwardApiPath(pathname) {
36
38
  }
37
39
  return !isReservedApiPath(pathname);
38
40
  }
41
+ function arrayBufferToBase64(buffer) {
42
+ const bytes = new Uint8Array(buffer);
43
+ const chunkSize = 0x8000;
44
+ let binary = "";
45
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
46
+ const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
47
+ binary += String.fromCharCode(...chunk);
48
+ }
49
+ return btoa(binary);
50
+ }
51
+ function base64ToArrayBuffer(base64) {
52
+ const binary = atob(base64);
53
+ const bytes = new Uint8Array(binary.length);
54
+ for (let i = 0; i < binary.length; i++) {
55
+ bytes[i] = binary.charCodeAt(i);
56
+ }
57
+ return bytes.buffer;
58
+ }
39
59
  /**
40
60
  * Create a storage adapter that routes through the ConcaveDO's storage syscall handler.
41
61
  * This ensures storage operations are properly isolated within the DO.
@@ -44,7 +64,7 @@ function createStorageAdapter(concaveDO, _instance) {
44
64
  return {
45
65
  store: async (blob) => {
46
66
  const buffer = await blob.arrayBuffer();
47
- const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
67
+ const base64 = arrayBufferToBase64(buffer);
48
68
  const response = await concaveDO.fetch("http://do/storage", {
49
69
  method: "POST",
50
70
  headers: { "Content-Type": "application/json" },
@@ -81,12 +101,8 @@ function createStorageAdapter(concaveDO, _instance) {
81
101
  if (!result.result || !result.result.__arrayBuffer) {
82
102
  return { blob: null };
83
103
  }
84
- const binary = atob(result.result.__arrayBuffer);
85
- const bytes = new Uint8Array(binary.length);
86
- for (let i = 0; i < binary.length; i++) {
87
- bytes[i] = binary.charCodeAt(i);
88
- }
89
- return { blob: new Blob([bytes.buffer]) };
104
+ const buffer = base64ToArrayBuffer(result.result.__arrayBuffer);
105
+ return { blob: new Blob([buffer]) };
90
106
  },
91
107
  };
92
108
  }
@@ -142,10 +158,28 @@ export async function handleHttpApiRequest(request, env, ctx, instance = "single
142
158
  }
143
159
  // Note: Internal function access control is now handled by core executor (fail-closed)
144
160
  const coreResult = await handleCoreHttpApiRequest(request, {
145
- executeFunction: async ({ type, path, args, auth, componentPath }) => adapter.executeUdf(path, args, type, auth, componentPath),
161
+ executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
146
162
  notifyWrites,
147
163
  storage: storageAdapter,
148
164
  corsHeaders,
165
+ getSnapshotTimestamp: async () => {
166
+ try {
167
+ const response = await concave.fetch("http://do/query_ts", {
168
+ method: "POST",
169
+ });
170
+ if (!response.ok) {
171
+ throw new Error(`query_ts failed with status ${response.status}`);
172
+ }
173
+ const body = (await response.json());
174
+ if (typeof body.ts !== "string" || !/^\d+$/.test(body.ts)) {
175
+ throw new Error("Invalid query_ts response");
176
+ }
177
+ return BigInt(body.ts);
178
+ }
179
+ catch {
180
+ return BigInt(Date.now());
181
+ }
182
+ },
149
183
  });
150
184
  if (coreResult?.handled) {
151
185
  return coreResult.response;
@@ -1,3 +1,4 @@
1
+ import { TimestampOracle } from "@concavejs/core/utils";
1
2
  /**
2
3
  * Wraps a DO stub as a DocStore.
3
4
  * Generator methods are converted from arrays back to async generators.
@@ -16,6 +17,9 @@ export function createGatewayDocStoreProxy(gateway, projectId, instance) {
16
17
  * Internal helper that creates a DocStore proxy with configurable argument transformation.
17
18
  */
18
19
  function createDocStoreProxyInternal(target, transformArgs) {
20
+ // Use a real TimestampOracle to guarantee monotonic, unique timestamps.
21
+ // The DO-side oracle cannot be accessed via RPC, so each proxy gets its own.
22
+ const oracle = new TimestampOracle();
19
23
  return new Proxy({}, {
20
24
  get(_, prop) {
21
25
  // Convert array results back to async generators
@@ -48,25 +52,22 @@ function createDocStoreProxyInternal(target, transformArgs) {
48
52
  return new Map(result);
49
53
  };
50
54
  }
51
- // Provide a local timestampOracle (DO-side oracle cannot be accessed via RPC)
55
+ // Provide a local TimestampOracle (DO-side oracle cannot be accessed via RPC)
52
56
  if (prop === "timestampOracle") {
53
- return {
54
- observeTimestamp: () => { },
55
- allocateTimestamp: () => BigInt(Date.now()),
56
- getCurrentTimestamp: () => BigInt(Date.now()),
57
- beginSnapshot: () => BigInt(Date.now()),
58
- };
57
+ return oracle;
59
58
  }
60
59
  // No-op close
61
60
  if (prop === "close") {
62
61
  return async () => { };
63
62
  }
64
- // Bind methods to target with transformed args
65
- const method = target[prop];
66
- if (typeof method === "function") {
67
- return (...args) => method.call(target, ...transformArgs(...args));
63
+ // Bind methods to target with transformed args.
64
+ // Use direct invocation (target[prop](...)) instead of .call() because
65
+ // Cloudflare RPC stubs intercept property access - .call() would be
66
+ // interpreted as an RPC method name rather than Function.prototype.call.
67
+ if (typeof target[prop] === "function") {
68
+ return (...args) => target[prop](...transformArgs(...args));
68
69
  }
69
- return method;
70
+ return target[prop];
70
71
  },
71
72
  });
72
73
  }
@@ -9,6 +9,6 @@ import type { AuthContext } from "@concavejs/core/sync/protocol-handler";
9
9
  export declare class ConcaveStubExecutor implements UdfExec {
10
10
  private readonly stub;
11
11
  constructor(stub: DurableObjectStub);
12
- execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: AuthContext | UserIdentityAttributes, componentPath?: string, requestId?: string): Promise<UdfResult>;
12
+ execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: AuthContext | UserIdentityAttributes, componentPath?: string, requestId?: string, snapshotTimestamp?: bigint): Promise<UdfResult>;
13
13
  executeHttp(request: Request): Promise<Response>;
14
14
  }
@@ -8,7 +8,7 @@ export class ConcaveStubExecutor {
8
8
  constructor(stub) {
9
9
  this.stub = stub;
10
10
  }
11
- async execute(path, args, type, auth, componentPath, requestId) {
11
+ async execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp) {
12
12
  const payload = {
13
13
  path,
14
14
  args: convexToJson(args),
@@ -17,6 +17,7 @@ export class ConcaveStubExecutor {
17
17
  componentPath,
18
18
  caller: "client",
19
19
  requestId,
20
+ snapshotTimestamp: snapshotTimestamp?.toString(),
20
21
  };
21
22
  const response = await this.stub.fetch("http://do/execute", {
22
23
  method: "POST",
@@ -19,6 +19,6 @@ export declare class UdfExecIsolated implements UdfExec {
19
19
  private instance;
20
20
  private projectId;
21
21
  constructor(stubOrOptions: UdfExecutorRpc | UdfExecIsolatedOptions);
22
- execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string): Promise<UdfResult>;
22
+ execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string, snapshotTimestamp?: bigint): Promise<UdfResult>;
23
23
  executeHttp(request: Request, auth?: any, requestId?: string): Promise<Response>;
24
24
  }
@@ -21,9 +21,9 @@ export class UdfExecIsolated {
21
21
  this.projectId = "default";
22
22
  }
23
23
  }
24
- async execute(path, args, type, auth, componentPath, requestId) {
24
+ async execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp) {
25
25
  // Pass instance context for syscall routing
26
- return this.rpc.execute(path, args, type, auth, componentPath, requestId, this.instance, this.projectId);
26
+ return this.rpc.execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp, this.instance, this.projectId);
27
27
  }
28
28
  async executeHttp(request, auth, requestId) {
29
29
  return this.rpc.executeHttp(request, auth, requestId, this.instance, this.projectId);
@@ -19,7 +19,7 @@ interface Env {
19
19
  export declare class UdfExecutorRpc extends WorkerEntrypoint {
20
20
  private udfExecutor;
21
21
  constructor(ctx: ExecutionContext, env: Env);
22
- execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string, _instance?: string, _projectId?: string): Promise<import("@concavejs/core").UdfResult>;
22
+ execute(path: string, args: Record<string, any>, type: "query" | "mutation" | "action", auth?: any, componentPath?: string, requestId?: string, snapshotTimestamp?: bigint, _instance?: string, _projectId?: string): Promise<import("@concavejs/core").UdfResult>;
23
23
  executeHttp(request: Request, auth?: any, requestId?: string, _instance?: string, _projectId?: string): Promise<Response>;
24
24
  }
25
25
  export {};
@@ -51,11 +51,11 @@ export class UdfExecutorRpc extends WorkerEntrypoint {
51
51
  // Pass blobstore if available, otherwise fall back to R2Bucket for direct mode
52
52
  this.udfExecutor = new UdfExecInline(docstore, blobstore ?? env.STORAGE_BUCKET, env.R2_PUBLIC_URL);
53
53
  }
54
- async execute(path, args, type, auth, componentPath, requestId, _instance, _projectId) {
54
+ async execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp, _instance, _projectId) {
55
55
  // Note: instance and projectId are passed for per-request context but
56
56
  // currently we rely on the environment settings for syscall routing.
57
57
  // This can be enhanced to create per-request syscall clients if needed.
58
- return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId);
58
+ return this.udfExecutor.execute(path, args, type, auth, componentPath, requestId, snapshotTimestamp);
59
59
  }
60
60
  async executeHttp(request, auth, requestId, _instance, _projectId) {
61
61
  return this.udfExecutor.executeHttp(request, auth, requestId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@concavejs/runtime-cf-base",
3
- "version": "0.0.1-alpha.6",
3
+ "version": "0.0.1-alpha.7",
4
4
  "license": "FSL-1.1-Apache-2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"