@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.
- package/dist/auth/auth-context.d.ts +4 -15
- package/dist/auth/auth-context.js +6 -15
- package/dist/auth/jwt.d.ts +5 -0
- package/dist/auth/jwt.js +37 -5
- package/dist/docstore/index.d.ts +1 -0
- package/dist/docstore/index.js +1 -0
- package/dist/docstore/interface.d.ts +8 -0
- package/dist/docstore/search-interfaces.d.ts +29 -0
- package/dist/docstore/search-interfaces.js +8 -0
- package/dist/http/api-router.d.ts +2 -0
- package/dist/http/api-router.js +60 -4
- package/dist/http/http-handler.js +19 -4
- package/dist/id-codec/document-id.js +4 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/kernel/context-storage.d.ts +1 -0
- package/dist/kernel/context-storage.js +29 -11
- package/dist/kernel/docstore-gateway.d.ts +31 -6
- package/dist/kernel/docstore-gateway.js +105 -31
- package/dist/kernel/index.d.ts +4 -3
- package/dist/kernel/index.js +4 -3
- package/dist/kernel/missing-schema-error.d.ts +5 -0
- package/dist/kernel/missing-schema-error.js +25 -0
- package/dist/kernel/native-timers.d.ts +7 -0
- package/dist/kernel/native-timers.js +14 -0
- package/dist/kernel/schema-service.d.ts +0 -5
- package/dist/kernel/schema-service.js +1 -25
- package/dist/kernel/udf-kernel.d.ts +11 -1
- package/dist/kernel/udf-kernel.js +10 -10
- package/dist/query/query-runtime.js +6 -3
- package/dist/queryengine/cursor.js +3 -2
- package/dist/queryengine/index.d.ts +2 -2
- package/dist/queryengine/index.js +1 -1
- package/dist/ryow/uncommitted-writes.js +4 -5
- package/dist/scheduler/cron-executor.d.ts +6 -2
- package/dist/scheduler/cron-executor.js +56 -27
- package/dist/scheduler/scheduled-function-executor.d.ts +6 -2
- package/dist/scheduler/scheduled-function-executor.js +61 -42
- package/dist/subscriptions/subscription-manager.d.ts +22 -0
- package/dist/subscriptions/subscription-manager.js +131 -44
- package/dist/sync/protocol-handler.d.ts +30 -0
- package/dist/sync/protocol-handler.js +140 -1
- package/dist/sync/session-backpressure.js +4 -3
- package/dist/sync/session-heartbeat.js +3 -2
- package/dist/system/internal.js +6 -3
- package/dist/system/system-functions.js +1 -1
- package/dist/transactor/occ-transaction.js +9 -8
- package/dist/transactor/occ-validation.js +3 -3
- package/dist/udf/analysis/validator.js +1 -1
- package/dist/udf/execution-adapter.d.ts +1 -1
- package/dist/udf/execution-adapter.js +2 -2
- package/dist/udf/executor/inline.d.ts +2 -2
- package/dist/udf/executor/inline.js +9 -5
- package/dist/udf/executor/interface.d.ts +1 -1
- package/dist/udf/module-loader/call-context.d.ts +5 -6
- package/dist/udf/module-loader/call-context.js +5 -9
- package/dist/udf/module-loader/module-loader.js +26 -4
- package/dist/udf/runtime/udf-rand.js +1 -1
- package/dist/udf/runtime/udf-setup.d.ts +22 -1
- package/dist/udf/runtime/udf-setup.js +151 -55
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/base64.js +58 -0
- package/dist/utils/crypto.d.ts +2 -0
- package/dist/utils/crypto.js +40 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/utils.d.ts +6 -1
- package/dist/utils/utils.js +6 -1
- package/package.json +5 -1
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ambient Authentication Context
|
|
2
|
+
* Ambient Authentication Context.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
2
|
+
* Ambient Authentication Context.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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).
|
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
348
|
+
JWKS_CACHE.delete(jwksUrl);
|
|
331
349
|
}
|
|
332
350
|
const jwks = createRemoteJWKSet(new URL(jwksUrl));
|
|
333
|
-
|
|
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);
|
package/dist/docstore/index.d.ts
CHANGED
package/dist/docstore/index.js
CHANGED
|
@@ -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;
|
package/dist/http/api-router.js
CHANGED
|
@@ -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:
|
|
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
|
-
(
|
|
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 "./
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
3
|
-
|
|
16
|
+
const globalCtor = globalThis?.AsyncLocalStorage;
|
|
17
|
+
if (globalCtor) {
|
|
18
|
+
return globalCtor;
|
|
4
19
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
return
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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>;
|