@concavejs/core 0.0.1-alpha.6 → 0.0.1-alpha.8

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.
Files changed (69) hide show
  1. package/dist/auth/auth-context.d.ts +4 -15
  2. package/dist/auth/auth-context.js +6 -15
  3. package/dist/auth/jwt.d.ts +5 -0
  4. package/dist/auth/jwt.js +37 -5
  5. package/dist/docstore/index.d.ts +1 -0
  6. package/dist/docstore/index.js +1 -0
  7. package/dist/docstore/interface.d.ts +8 -0
  8. package/dist/docstore/search-interfaces.d.ts +29 -0
  9. package/dist/docstore/search-interfaces.js +8 -0
  10. package/dist/http/api-router.d.ts +2 -0
  11. package/dist/http/api-router.js +60 -4
  12. package/dist/http/http-handler.js +19 -4
  13. package/dist/id-codec/document-id.js +4 -3
  14. package/dist/index.d.ts +3 -2
  15. package/dist/index.js +2 -1
  16. package/dist/kernel/context-storage.d.ts +1 -0
  17. package/dist/kernel/context-storage.js +29 -11
  18. package/dist/kernel/docstore-gateway.d.ts +31 -6
  19. package/dist/kernel/docstore-gateway.js +105 -31
  20. package/dist/kernel/index.d.ts +4 -3
  21. package/dist/kernel/index.js +4 -3
  22. package/dist/kernel/missing-schema-error.d.ts +5 -0
  23. package/dist/kernel/missing-schema-error.js +25 -0
  24. package/dist/kernel/native-timers.d.ts +7 -0
  25. package/dist/kernel/native-timers.js +14 -0
  26. package/dist/kernel/schema-service.d.ts +0 -5
  27. package/dist/kernel/schema-service.js +1 -25
  28. package/dist/kernel/udf-kernel.d.ts +11 -1
  29. package/dist/kernel/udf-kernel.js +10 -10
  30. package/dist/query/query-runtime.js +6 -3
  31. package/dist/queryengine/cursor.js +3 -2
  32. package/dist/queryengine/index.d.ts +2 -2
  33. package/dist/queryengine/index.js +1 -1
  34. package/dist/ryow/uncommitted-writes.js +4 -5
  35. package/dist/scheduler/cron-executor.d.ts +6 -2
  36. package/dist/scheduler/cron-executor.js +56 -27
  37. package/dist/scheduler/scheduled-function-executor.d.ts +6 -2
  38. package/dist/scheduler/scheduled-function-executor.js +61 -42
  39. package/dist/subscriptions/subscription-manager.d.ts +22 -0
  40. package/dist/subscriptions/subscription-manager.js +131 -44
  41. package/dist/sync/protocol-handler.d.ts +30 -0
  42. package/dist/sync/protocol-handler.js +140 -1
  43. package/dist/sync/session-backpressure.js +4 -3
  44. package/dist/sync/session-heartbeat.js +3 -2
  45. package/dist/system/internal.js +6 -3
  46. package/dist/system/system-functions.js +1 -1
  47. package/dist/transactor/occ-transaction.js +9 -8
  48. package/dist/transactor/occ-validation.js +3 -3
  49. package/dist/udf/analysis/validator.js +1 -1
  50. package/dist/udf/execution-adapter.d.ts +1 -1
  51. package/dist/udf/execution-adapter.js +2 -2
  52. package/dist/udf/executor/inline.d.ts +2 -2
  53. package/dist/udf/executor/inline.js +9 -5
  54. package/dist/udf/executor/interface.d.ts +1 -1
  55. package/dist/udf/module-loader/call-context.d.ts +5 -6
  56. package/dist/udf/module-loader/call-context.js +5 -9
  57. package/dist/udf/module-loader/module-loader.js +26 -4
  58. package/dist/udf/runtime/udf-rand.js +1 -1
  59. package/dist/udf/runtime/udf-setup.d.ts +22 -1
  60. package/dist/udf/runtime/udf-setup.js +151 -55
  61. package/dist/utils/base64.d.ts +4 -0
  62. package/dist/utils/base64.js +58 -0
  63. package/dist/utils/crypto.d.ts +2 -0
  64. package/dist/utils/crypto.js +40 -0
  65. package/dist/utils/index.d.ts +1 -0
  66. package/dist/utils/index.js +1 -0
  67. package/dist/utils/utils.d.ts +6 -1
  68. package/dist/utils/utils.js +6 -1
  69. package/package.json +5 -1
@@ -1,22 +1,11 @@
1
1
  /**
2
- * Ambient Authentication Context using AsyncLocalStorage
2
+ * Ambient Authentication Context.
3
3
  *
4
- * This module provides ambient auth context that automatically propagates through
5
- * the entire UDF execution chain without explicit parameter threading.
6
- *
7
- * Benefits:
8
- * - No need to pass auth through every function call
9
- * - Works automatically with nested UDF calls
10
- * - Simplifies function signatures
11
- * - Prevents auth context from being accidentally lost
4
+ * Context storage automatically propagates through async call chains when
5
+ * AsyncLocalStorage is available (Node/Bun). In runtimes without ALS, the
6
+ * fallback behaves correctly for serialized execution.
12
7
  */
13
- import { AsyncLocalStorage } from "node:async_hooks";
14
8
  import type { UserIdentityAttributes } from "convex/server";
15
- /**
16
- * AsyncLocalStorage for authentication context.
17
- * This provides automatic context propagation through async call chains.
18
- */
19
- export declare const authContext: AsyncLocalStorage<UserIdentityAttributes | undefined>;
20
9
  /**
21
10
  * Get the current authentication context.
22
11
  * Returns undefined if no auth context is set (unauthenticated request).
@@ -1,21 +1,12 @@
1
1
  /**
2
- * Ambient Authentication Context using AsyncLocalStorage
2
+ * Ambient Authentication Context.
3
3
  *
4
- * This module provides ambient auth context that automatically propagates through
5
- * the entire UDF execution chain without explicit parameter threading.
6
- *
7
- * Benefits:
8
- * - No need to pass auth through every function call
9
- * - Works automatically with nested UDF calls
10
- * - Simplifies function signatures
11
- * - Prevents auth context from being accidentally lost
12
- */
13
- import { AsyncLocalStorage } from "node:async_hooks";
14
- /**
15
- * AsyncLocalStorage for authentication context.
16
- * This provides automatic context propagation through async call chains.
4
+ * Context storage automatically propagates through async call chains when
5
+ * AsyncLocalStorage is available (Node/Bun). In runtimes without ALS, the
6
+ * fallback behaves correctly for serialized execution.
17
7
  */
18
- export const authContext = new AsyncLocalStorage();
8
+ import { ContextStorage } from "../kernel/context-storage";
9
+ const authContext = new ContextStorage();
19
10
  /**
20
11
  * Get the current authentication context.
21
12
  * Returns undefined if no auth context is set (unauthenticated request).
@@ -36,6 +36,11 @@ export type JWTValidationConfig = {
36
36
  secret?: string;
37
37
  skipVerification?: boolean;
38
38
  clockTolerance?: number;
39
+ /**
40
+ * Optional TTL (in milliseconds) for cached remote JWKS resolvers.
41
+ * Defaults to 5 minutes when omitted.
42
+ */
43
+ jwksCacheTtlMs?: number;
39
44
  };
40
45
  export type JWTValidationErrorCode = "MISSING_CONFIG" | "INVALID_SIGNATURE" | "TOKEN_EXPIRED" | "TOKEN_NOT_ACTIVE" | "CLAIM_VALIDATION_FAILED" | "MISSING_SUBJECT" | "MISSING_ISSUER" | "INVALID_TOKEN";
41
46
  export declare class JWTValidationError extends Error {
package/dist/auth/jwt.js CHANGED
@@ -123,12 +123,17 @@ export const WELL_KNOWN_JWKS_URLS = {
123
123
  firebase: () => "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
124
124
  };
125
125
  const DEFAULT_CLOCK_TOLERANCE_SECONDS = 60;
126
+ const DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
127
+ const MAX_JWKS_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
126
128
  const JWKS_CACHE = new Map();
127
129
  let defaultValidationConfig;
128
130
  let adminAuthConfig;
129
131
  let systemAuthConfig;
130
132
  export function setJwtValidationConfig(config) {
131
133
  defaultValidationConfig = config;
134
+ // Config updates can change issuer/audience/JWKS source semantics.
135
+ // Clearing resolver cache avoids stale long-lived entries across reconfiguration.
136
+ JWKS_CACHE.clear();
132
137
  }
133
138
  export function getJwtValidationConfig() {
134
139
  return defaultValidationConfig;
@@ -268,7 +273,15 @@ export function resolveJwtValidationConfigFromEnv(env) {
268
273
  parseBoolean(getEnvValue("CONCAVE_JWT_SKIP_VERIFICATION", env));
269
274
  const clockTolerance = parseNumber(getEnvValue("AUTH_CLOCK_TOLERANCE", env)) ??
270
275
  parseNumber(getEnvValue("CONCAVE_JWT_CLOCK_TOLERANCE", env));
271
- if (!jwksUrl && !issuer && !audience && !secret && skipVerification === undefined && clockTolerance === undefined) {
276
+ const jwksCacheTtlMs = parseNumber(getEnvValue("AUTH_JWKS_CACHE_TTL_MS", env)) ??
277
+ parseNumber(getEnvValue("CONCAVE_JWT_JWKS_CACHE_TTL_MS", env));
278
+ if (!jwksUrl &&
279
+ !issuer &&
280
+ !audience &&
281
+ !secret &&
282
+ skipVerification === undefined &&
283
+ clockTolerance === undefined &&
284
+ jwksCacheTtlMs === undefined) {
272
285
  return undefined;
273
286
  }
274
287
  return {
@@ -278,6 +291,7 @@ export function resolveJwtValidationConfigFromEnv(env) {
278
291
  secret,
279
292
  skipVerification,
280
293
  clockTolerance,
294
+ jwksCacheTtlMs,
281
295
  };
282
296
  }
283
297
  function normalizeList(value) {
@@ -324,15 +338,33 @@ function validateClaims(claims, config) {
324
338
  throw new JWTValidationError("CLAIM_VALIDATION_FAILED", "JWT claim validation failed: aud");
325
339
  }
326
340
  }
327
- function getRemoteJwks(jwksUrl) {
341
+ function getRemoteJwks(jwksUrl, config) {
342
+ const now = Date.now();
328
343
  const cached = JWKS_CACHE.get(jwksUrl);
344
+ if (cached && cached.expiresAtMs > now) {
345
+ return cached.resolver;
346
+ }
329
347
  if (cached) {
330
- return cached;
348
+ JWKS_CACHE.delete(jwksUrl);
331
349
  }
332
350
  const jwks = createRemoteJWKSet(new URL(jwksUrl));
333
- JWKS_CACHE.set(jwksUrl, jwks);
351
+ const configuredTtl = config?.jwksCacheTtlMs ?? defaultValidationConfig?.jwksCacheTtlMs;
352
+ const ttlMs = resolveJwksCacheTtlMs(configuredTtl);
353
+ JWKS_CACHE.set(jwksUrl, {
354
+ resolver: jwks,
355
+ expiresAtMs: now + ttlMs,
356
+ });
334
357
  return jwks;
335
358
  }
359
+ function resolveJwksCacheTtlMs(configuredTtl) {
360
+ if (configuredTtl === undefined) {
361
+ return DEFAULT_JWKS_CACHE_TTL_MS;
362
+ }
363
+ if (!Number.isFinite(configuredTtl)) {
364
+ return DEFAULT_JWKS_CACHE_TTL_MS;
365
+ }
366
+ return Math.max(0, Math.min(MAX_JWKS_CACHE_TTL_MS, Math.floor(configuredTtl)));
367
+ }
336
368
  export function decodeJwtUnsafe(token) {
337
369
  if (!token)
338
370
  return null;
@@ -366,7 +398,7 @@ export async function verifyJwt(token, config) {
366
398
  ({ payload } = await jwtVerify(token, key, options));
367
399
  }
368
400
  else {
369
- ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl), options));
401
+ ({ payload } = await jwtVerify(token, getRemoteJwks(effectiveConfig.jwksUrl, effectiveConfig), options));
370
402
  }
371
403
  const claims = payload;
372
404
  validateClaims(claims, effectiveConfig);
@@ -4,6 +4,7 @@
4
4
  * Platform-agnostic document storage interface
5
5
  */
6
6
  export * from "./interface";
7
+ export * from "./search-interfaces";
7
8
  export * from "./sql/schema";
8
9
  export * from "./search-index-registration";
9
10
  export * from "./vector-index-registration";
@@ -4,6 +4,7 @@
4
4
  * Platform-agnostic document storage interface
5
5
  */
6
6
  export * from "./interface";
7
+ export * from "./search-interfaces";
7
8
  export * from "./sql/schema";
8
9
  export * from "./search-index-registration";
9
10
  export * from "./vector-index-registration";
@@ -129,12 +129,20 @@ export interface DocStore {
129
129
  nextCursor: string | null;
130
130
  hasMore: boolean;
131
131
  }>;
132
+ /**
133
+ * @deprecated Prefer the `SearchCapable` interface from `./search-interfaces`.
134
+ * This method will be removed from `DocStore` in a future version.
135
+ */
132
136
  search(indexId: ArrayBufferHex, searchQuery: string, filters: Map<string, unknown>, options?: {
133
137
  limit?: number;
134
138
  }): Promise<{
135
139
  doc: LatestDocument;
136
140
  score: number;
137
141
  }[]>;
142
+ /**
143
+ * @deprecated Prefer the `VectorSearchCapable` interface from `./search-interfaces`.
144
+ * This method will be removed from `DocStore` in a future version.
145
+ */
138
146
  vectorSearch(indexId: ArrayBufferHex, vector: number[], limit: number, filters: Map<string, string>): Promise<{
139
147
  doc: LatestDocument;
140
148
  score: number;
@@ -0,0 +1,29 @@
1
+ import type { ArrayBufferHex, LatestDocument, DocStore } from "./interface";
2
+ /**
3
+ * Interface for DocStore implementations that support full-text search.
4
+ * Prefer checking for this capability via `isSearchCapable()` type guard
5
+ * rather than assuming all DocStore implementations support search.
6
+ */
7
+ export interface SearchCapable {
8
+ search(indexId: ArrayBufferHex, searchQuery: string, filters: Map<string, unknown>, options?: {
9
+ limit?: number;
10
+ }): Promise<{
11
+ doc: LatestDocument;
12
+ score: number;
13
+ }[]>;
14
+ }
15
+ /**
16
+ * Interface for DocStore implementations that support vector similarity search.
17
+ * Prefer checking for this capability via `isVectorSearchCapable()` type guard
18
+ * rather than assuming all DocStore implementations support vector search.
19
+ */
20
+ export interface VectorSearchCapable {
21
+ vectorSearch(indexId: ArrayBufferHex, vector: number[], limit: number, filters: Map<string, string>): Promise<{
22
+ doc: LatestDocument;
23
+ score: number;
24
+ }[]>;
25
+ }
26
+ /** Type guard for DocStore implementations that support full-text search */
27
+ export declare function isSearchCapable(store: DocStore): store is DocStore & SearchCapable;
28
+ /** Type guard for DocStore implementations that support vector similarity search */
29
+ export declare function isVectorSearchCapable(store: DocStore): store is DocStore & VectorSearchCapable;
@@ -0,0 +1,8 @@
1
+ /** Type guard for DocStore implementations that support full-text search */
2
+ export function isSearchCapable(store) {
3
+ return typeof store.search === "function";
4
+ }
5
+ /** Type guard for DocStore implementations that support vector similarity search */
6
+ export function isVectorSearchCapable(store) {
7
+ return typeof store.vectorSearch === "function";
8
+ }
@@ -7,6 +7,7 @@ export interface FunctionExecutionParams {
7
7
  args: Record<string, any>;
8
8
  auth?: AuthContext | UserIdentityAttributes;
9
9
  componentPath?: string;
10
+ snapshotTimestamp?: bigint;
10
11
  request: Request;
11
12
  }
12
13
  export interface FunctionExecutionResult {
@@ -36,6 +37,7 @@ export interface CoreHttpApiOptions {
36
37
  notifyWrites?: (writtenRanges?: SerializedKeyRange[], writtenTables?: string[], commitTimestamp?: bigint) => Promise<void> | void;
37
38
  storage?: StorageAdapter;
38
39
  corsHeaders?: Record<string, string>;
40
+ getSnapshotTimestamp?: (request: Request) => Promise<bigint> | bigint;
39
41
  }
40
42
  export interface CoreHttpApiResult {
41
43
  handled: boolean;
@@ -81,6 +81,40 @@ export async function resolveAuthContext(bodyAuth, headerToken, headerIdentity)
81
81
  }
82
82
  return bodyAuth;
83
83
  }
84
+ function parseTimestampInput(value) {
85
+ if (value === undefined || value === null) {
86
+ return undefined;
87
+ }
88
+ if (typeof value === "bigint") {
89
+ return value >= 0n ? value : undefined;
90
+ }
91
+ if (typeof value === "number") {
92
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
93
+ return undefined;
94
+ }
95
+ return BigInt(value);
96
+ }
97
+ if (typeof value === "string") {
98
+ const trimmed = value.trim();
99
+ if (!/^\d+$/.test(trimmed)) {
100
+ return undefined;
101
+ }
102
+ try {
103
+ return BigInt(trimmed);
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ async function resolveSnapshotTimestamp(options, request) {
112
+ const fromCallback = options.getSnapshotTimestamp ? await options.getSnapshotTimestamp(request) : undefined;
113
+ if (typeof fromCallback === "bigint") {
114
+ return fromCallback;
115
+ }
116
+ return BigInt(Date.now());
117
+ }
84
118
  export async function handleCoreHttpApiRequest(request, options) {
85
119
  const url = new URL(request.url);
86
120
  const segments = url.pathname.split("/").filter(Boolean);
@@ -124,6 +158,19 @@ export async function handleCoreHttpApiRequest(request, options) {
124
158
  throw error;
125
159
  }
126
160
  const route = routeSegments[0];
161
+ if (route === "query_ts") {
162
+ if (request.method !== "POST") {
163
+ return {
164
+ handled: true,
165
+ response: apply(Response.json({ error: "Method not allowed" }, { status: 405 })),
166
+ };
167
+ }
168
+ const snapshotTimestamp = await resolveSnapshotTimestamp(options, request);
169
+ return {
170
+ handled: true,
171
+ response: apply(Response.json({ ts: snapshotTimestamp.toString() })),
172
+ };
173
+ }
127
174
  if (route === "storage") {
128
175
  if (!options.storage) {
129
176
  return {
@@ -183,7 +230,7 @@ export async function handleCoreHttpApiRequest(request, options) {
183
230
  }
184
231
  }
185
232
  }
186
- if (route === "query" || route === "mutation" || route === "action") {
233
+ if (route === "query" || route === "mutation" || route === "action" || route === "query_at_ts") {
187
234
  if (request.method !== "POST") {
188
235
  return {
189
236
  handled: true,
@@ -207,7 +254,7 @@ export async function handleCoreHttpApiRequest(request, options) {
207
254
  response: apply(Response.json({ error: "Invalid request body" }, { status: 400 })),
208
255
  };
209
256
  }
210
- const { path, args, format, auth: bodyAuth, componentPath } = body;
257
+ const { path, args, format, auth: bodyAuth, componentPath, ts } = body;
211
258
  if (!path || typeof path !== "string") {
212
259
  return {
213
260
  handled: true,
@@ -236,6 +283,14 @@ export async function handleCoreHttpApiRequest(request, options) {
236
283
  };
237
284
  }
238
285
  const jsonArgs = rawArgs ?? {};
286
+ const executionType = route === "query_at_ts" ? "query" : route;
287
+ const snapshotTimestamp = route === "query_at_ts" ? parseTimestampInput(ts) : undefined;
288
+ if (route === "query_at_ts" && snapshotTimestamp === undefined) {
289
+ return {
290
+ handled: true,
291
+ response: apply(Response.json({ error: "Invalid or missing ts" }, { status: 400 })),
292
+ };
293
+ }
239
294
  let authForExecution;
240
295
  try {
241
296
  authForExecution = await resolveAuthContext(bodyAuth, headerToken, headerIdentity);
@@ -252,11 +307,12 @@ export async function handleCoreHttpApiRequest(request, options) {
252
307
  throw error;
253
308
  }
254
309
  const executionParams = {
255
- type: route,
310
+ type: executionType,
256
311
  path,
257
312
  args: jsonArgs,
258
313
  auth: authForExecution,
259
314
  componentPath,
315
+ snapshotTimestamp,
260
316
  request,
261
317
  };
262
318
  try {
@@ -273,7 +329,7 @@ export async function handleCoreHttpApiRequest(request, options) {
273
329
  }
274
330
  const result = await options.executeFunction(executionParams);
275
331
  if (options.notifyWrites &&
276
- (route === "mutation" || route === "action") &&
332
+ (executionType === "mutation" || executionType === "action") &&
277
333
  (result.writtenRanges?.length || result.writtenTables?.length)) {
278
334
  await options.notifyWrites(result.writtenRanges, result.writtenTables ?? writtenTablesFromRanges(result.writtenRanges));
279
335
  }
@@ -7,7 +7,7 @@
7
7
  import { loadConvexModule } from "../udf";
8
8
  import { createClientAdapter } from "../udf/execution-adapter";
9
9
  import { UdfKernel } from "../queryengine";
10
- import { applyCors, computeCorsHeaders, handleCoreHttpApiRequest, resolveAuthContext } from "./index";
10
+ import { applyCors, computeCorsHeaders, handleCoreHttpApiRequest, resolveAuthContext } from "./api-router";
11
11
  import { writtenTablesFromRanges } from "../utils";
12
12
  import { AdminAuthError, identityFromToken, isAdminToken, isSystemToken, JWTValidationError, SystemAuthError, } from "../auth";
13
13
  const VERSIONED_API_PREFIX = /^\/api\/\d+\.\d+(?:\.\d+)?(?=\/|$)/;
@@ -20,6 +20,8 @@ function isReservedApiPath(pathname) {
20
20
  normalizedPath === "/api/sync" ||
21
21
  normalizedPath === "/api/reset-test-state" ||
22
22
  normalizedPath === "/api/query" ||
23
+ normalizedPath === "/api/query_ts" ||
24
+ normalizedPath === "/api/query_at_ts" ||
23
25
  normalizedPath === "/api/mutation" ||
24
26
  normalizedPath === "/api/action") {
25
27
  return true;
@@ -108,12 +110,12 @@ export class HttpHandler {
108
110
  }
109
111
  };
110
112
  const coreResult = await handleCoreHttpApiRequest(request, {
111
- executeFunction: async ({ type, path, args, auth, componentPath }) => this.adapter.executeUdf(path, args, type, auth, componentPath),
113
+ executeFunction: async ({ type, path, args, auth, componentPath, snapshotTimestamp }) => this.adapter.executeUdf(path, args, type, auth, componentPath, undefined, snapshotTimestamp),
112
114
  notifyWrites,
113
115
  storage: this.docstore && this.blobstore
114
116
  ? {
115
117
  store: async (blob) => {
116
- const convex = new UdfKernel(this.docstore, undefined, this.blobstore);
118
+ const convex = new UdfKernel({ docstore: this.docstore, storage: this.blobstore });
117
119
  const storageId = await convex.jsSyscall("storage/storeBlob", { blob });
118
120
  const writtenRanges = convex.getTrackedWriteRanges();
119
121
  return {
@@ -123,13 +125,26 @@ export class HttpHandler {
123
125
  };
124
126
  },
125
127
  get: async (storageId) => {
126
- const convex = new UdfKernel(this.docstore, undefined, this.blobstore);
128
+ const convex = new UdfKernel({ docstore: this.docstore, storage: this.blobstore });
127
129
  const blob = await convex.jsSyscall("storage/getBlob", { storageId });
128
130
  return { blob: blob ?? null };
129
131
  },
130
132
  }
131
133
  : undefined,
132
134
  corsHeaders,
135
+ getSnapshotTimestamp: () => {
136
+ const oracle = this.docstore?.timestampOracle;
137
+ const oracleTimestamp = typeof oracle?.beginSnapshot === "function"
138
+ ? oracle.beginSnapshot()
139
+ : typeof oracle?.getCurrentTimestamp === "function"
140
+ ? oracle.getCurrentTimestamp()
141
+ : undefined;
142
+ const wallClock = BigInt(Date.now());
143
+ if (typeof oracleTimestamp === "bigint" && oracleTimestamp > wallClock) {
144
+ return oracleTimestamp;
145
+ }
146
+ return wallClock;
147
+ },
133
148
  });
134
149
  if (coreResult?.handled) {
135
150
  return coreResult.response;
@@ -13,6 +13,7 @@
13
13
  import { base32Encode, base32Decode, isValidBase32 } from './base32';
14
14
  import { vintEncode, vintDecode, vintEncodedLength } from './vint';
15
15
  import { fletcher16, verifyFletcher16 } from './fletcher16';
16
+ import { fillRandomValues } from '../utils/crypto';
16
17
  /** Internal ID is always 16 bytes (128 bits) */
17
18
  export const INTERNAL_ID_LENGTH = 16;
18
19
  /** Minimum encoded length: 1 (vint) + 16 (id) + 2 (checksum) = 19 bytes = 31 base32 chars */
@@ -98,11 +99,11 @@ export function isValidDocumentId(encoded) {
98
99
  }
99
100
  }
100
101
  // NOTE: Deterministic ID generation for retries is handled by the UDF runtime
101
- // via an injected generator. This helper remains cryptographically random.
102
- const cryptoGetRandomValues = crypto.getRandomValues.bind(crypto);
102
+ // via an injected generator. This helper remains cryptographically random when
103
+ // native crypto is available, and degrades gracefully in runtimes without it.
103
104
  function randomBytes(n) {
104
105
  const buf = new Uint8Array(n);
105
- cryptoGetRandomValues(buf);
106
+ fillRandomValues(buf);
106
107
  return buf;
107
108
  }
108
109
  /**
package/dist/index.d.ts CHANGED
@@ -10,12 +10,12 @@ export { type ArrayBufferHex, type InternalDocumentId, documentIdKey, parseDocum
10
10
  export * from "./docstore/sql/schema";
11
11
  export * from "./docstore/search-index-registration";
12
12
  export * from "./transactor";
13
- export { UdfKernel } from "./kernel/udf-kernel";
13
+ export { UdfKernel, type UdfKernelConfig } from "./kernel/udf-kernel";
14
14
  export { snapshotContext, transactionContext } from "./kernel/contexts";
15
15
  export { QueryRuntime } from "./query/query-runtime";
16
16
  export { SchemaService } from "./kernel/schema-service";
17
17
  export { BlobStoreGateway } from "./kernel/blob-store-gateway";
18
- export { DocStoreGateway } from "./kernel/docstore-gateway";
18
+ export { DocStoreGateway, createDocAccess, type DocAccess } from "./kernel/docstore-gateway";
19
19
  export { SchedulerGateway } from "./kernel/scheduler-gateway";
20
20
  export { UdfInvocationManager } from "./kernel/udf-invocation-manager";
21
21
  export * from "./queryengine/cursor";
@@ -40,6 +40,7 @@ export * from "./utils/timestamp";
40
40
  export * from "./utils/keyspace";
41
41
  export * from "./utils/serialization";
42
42
  export * from "./utils/written-ranges";
43
+ export * from "./utils/base64";
43
44
  export type { TransactionHandle, CommitResult, OplogDelta, Transactor } from "./interfaces/transactor";
44
45
  export type { KeyRange as InterfaceKeyRange } from "./interfaces/transactor";
45
46
  export { type ChangeDelta, type ChangeStreamConsumer, } from "./interfaces/change-stream-consumer";
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { snapshotContext, transactionContext } from "./kernel/contexts";
15
15
  export { QueryRuntime } from "./query/query-runtime";
16
16
  export { SchemaService } from "./kernel/schema-service";
17
17
  export { BlobStoreGateway } from "./kernel/blob-store-gateway";
18
- export { DocStoreGateway } from "./kernel/docstore-gateway";
18
+ export { DocStoreGateway, createDocAccess } from "./kernel/docstore-gateway";
19
19
  export { SchedulerGateway } from "./kernel/scheduler-gateway";
20
20
  export { UdfInvocationManager } from "./kernel/udf-invocation-manager";
21
21
  export * from "./queryengine/cursor";
@@ -49,6 +49,7 @@ export * from "./utils/timestamp";
49
49
  export * from "./utils/keyspace";
50
50
  export * from "./utils/serialization";
51
51
  export * from "./utils/written-ranges";
52
+ export * from "./utils/base64";
52
53
  export * from "./interfaces/shard-router";
53
54
  export * from "./interfaces/cache-strategy";
54
55
  export * from "./interfaces/execution-context";
@@ -1,4 +1,5 @@
1
1
  type Callback<T> = () => Promise<T> | T;
2
+ export declare function hasAsyncLocalStorageSupport(): boolean;
2
3
  export declare class ContextStorage<T> {
3
4
  private readonly als?;
4
5
  private readonly stack;
@@ -1,21 +1,39 @@
1
+ function resolveFromRequire(req) {
2
+ for (const specifier of ["node:async_hooks", "async_hooks"]) {
3
+ try {
4
+ const mod = req(specifier);
5
+ if (mod?.AsyncLocalStorage) {
6
+ return mod.AsyncLocalStorage;
7
+ }
8
+ }
9
+ catch {
10
+ // Try the next candidate module specifier.
11
+ }
12
+ }
13
+ return undefined;
14
+ }
1
15
  const resolveAsyncLocalStorage = () => {
2
- if (typeof globalThis !== "undefined" && globalThis.AsyncLocalStorage) {
3
- return globalThis.AsyncLocalStorage;
16
+ const globalCtor = globalThis?.AsyncLocalStorage;
17
+ if (globalCtor) {
18
+ return globalCtor;
4
19
  }
5
- try {
6
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
7
- const req = Function("return typeof require !== 'undefined' ? require : undefined")();
8
- if (!req) {
9
- return undefined;
20
+ const runtimeRequire = typeof require === "function" ? require : undefined;
21
+ if (runtimeRequire) {
22
+ const ctor = resolveFromRequire(runtimeRequire);
23
+ if (ctor) {
24
+ return ctor;
10
25
  }
11
- const mod = req("node:async_hooks");
12
- return mod.AsyncLocalStorage;
13
26
  }
14
- catch {
15
- return undefined;
27
+ const globalRequire = globalThis?.require;
28
+ if (typeof globalRequire === "function") {
29
+ return resolveFromRequire(globalRequire);
16
30
  }
31
+ return undefined;
17
32
  };
18
33
  const AsyncLocalStorageCtor = resolveAsyncLocalStorage();
34
+ export function hasAsyncLocalStorageSupport() {
35
+ return AsyncLocalStorageCtor !== undefined;
36
+ }
19
37
  export class ContextStorage {
20
38
  als;
21
39
  stack = [];
@@ -5,15 +5,40 @@ type IndexEntries = Set<{
5
5
  ts: bigint;
6
6
  update: DatabaseIndexUpdate;
7
7
  }>;
8
- export declare class DocStoreGateway {
9
- private readonly docstore;
10
- private readonly mutationTransaction?;
11
- private context?;
12
- constructor(docstore: DocStore, mutationTransaction?: OccMutationTransaction | undefined);
8
+ /**
9
+ * Polymorphic document access interface.
10
+ *
11
+ * Two implementations replace the previous `if (this.mutationTransaction)` branching:
12
+ * - `SnapshotDocAccess` — wraps DocStore directly (queries, actions)
13
+ * - `TransactionalDocAccess` — wraps OccMutationTransaction + DocStore (mutations)
14
+ */
15
+ export interface DocAccess {
16
+ computeSnapshotTimestamp(snapshotOverride: bigint | null | undefined, inheritedSnapshot: bigint | null): bigint;
17
+ allocateTimestamp(): bigint;
18
+ fetchLatestDocument(id: InternalDocumentId, snapshotTimestamp: bigint): Promise<LatestDocument | null>;
19
+ fetchTable(tableId: string, snapshotTimestamp: bigint): Promise<LatestDocument[]>;
20
+ applyWrites(documents: DocumentLogEntry[], indexes: IndexEntries, conflictStrategy: "Error" | "Overwrite"): Promise<void>;
21
+ hasTransaction(): boolean;
22
+ getTransaction(): OccMutationTransaction | undefined;
23
+ getDocStore(): DocStore;
24
+ /** Attach the kernel context for read/write tracking (non-transactional path) */
25
+ attachContext(context: KernelContext): void;
26
+ }
27
+ /**
28
+ * Factory function that selects the appropriate DocAccess implementation
29
+ * based on whether a mutation transaction is active.
30
+ */
31
+ export declare function createDocAccess(docstore: DocStore, transaction?: OccMutationTransaction): DocAccess;
32
+ /**
33
+ * @deprecated Use `createDocAccess` factory instead.
34
+ * This class is kept as an alias for backwards compatibility.
35
+ */
36
+ export declare class DocStoreGateway implements DocAccess {
37
+ private readonly delegate;
38
+ constructor(docstore: DocStore, transaction?: OccMutationTransaction);
13
39
  attachContext(context: KernelContext): void;
14
40
  getTransaction(): OccMutationTransaction | undefined;
15
41
  hasTransaction(): boolean;
16
- private getTimestampOracle;
17
42
  computeSnapshotTimestamp(snapshotOverride: bigint | null | undefined, inheritedSnapshot: bigint | null): bigint;
18
43
  allocateTimestamp(): bigint;
19
44
  fetchLatestDocument(id: InternalDocumentId, snapshotTimestamp: bigint): Promise<LatestDocument | null>;