@concavejs/core 0.0.1-alpha.7 → 0.0.1-alpha.9

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/docstore/index.d.ts +1 -0
  4. package/dist/docstore/index.js +1 -0
  5. package/dist/docstore/interface.d.ts +8 -0
  6. package/dist/docstore/interface.js +6 -0
  7. package/dist/docstore/search-interfaces.d.ts +29 -0
  8. package/dist/docstore/search-interfaces.js +8 -0
  9. package/dist/http/http-handler.js +9 -3
  10. package/dist/id-codec/document-id.js +4 -3
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +2 -1
  13. package/dist/kernel/blob-store-gateway.js +4 -3
  14. package/dist/kernel/context-storage.d.ts +1 -0
  15. package/dist/kernel/context-storage.js +29 -11
  16. package/dist/kernel/docstore-gateway.d.ts +31 -6
  17. package/dist/kernel/docstore-gateway.js +105 -31
  18. package/dist/kernel/index.d.ts +4 -3
  19. package/dist/kernel/index.js +4 -3
  20. package/dist/kernel/kernel-context.js +5 -5
  21. package/dist/kernel/missing-schema-error.d.ts +5 -0
  22. package/dist/kernel/missing-schema-error.js +25 -0
  23. package/dist/kernel/native-timers.d.ts +7 -0
  24. package/dist/kernel/native-timers.js +14 -0
  25. package/dist/kernel/scheduler-gateway.js +5 -4
  26. package/dist/kernel/schema-service.d.ts +0 -5
  27. package/dist/kernel/schema-service.js +1 -25
  28. package/dist/kernel/syscalls/database-syscalls.js +14 -11
  29. package/dist/kernel/udf-kernel.d.ts +11 -1
  30. package/dist/kernel/udf-kernel.js +10 -10
  31. package/dist/query/query-runtime.d.ts +1 -1
  32. package/dist/query/query-runtime.js +36 -14
  33. package/dist/queryengine/cursor.js +3 -2
  34. package/dist/queryengine/index-query.d.ts +2 -2
  35. package/dist/queryengine/index-query.js +2 -1
  36. package/dist/queryengine/index.d.ts +2 -2
  37. package/dist/queryengine/index.js +1 -1
  38. package/dist/ryow/uncommitted-writes.js +4 -5
  39. package/dist/scheduler/cron-executor.js +2 -1
  40. package/dist/scheduler/scheduled-function-executor.js +3 -6
  41. package/dist/subscriptions/subscription-manager.d.ts +22 -0
  42. package/dist/subscriptions/subscription-manager.js +131 -44
  43. package/dist/sync/protocol-handler.d.ts +13 -1
  44. package/dist/sync/protocol-handler.js +55 -8
  45. package/dist/sync/session-backpressure.js +4 -3
  46. package/dist/sync/session-heartbeat.js +3 -2
  47. package/dist/sync/sync-handler.js +2 -0
  48. package/dist/system/internal.js +6 -3
  49. package/dist/system/system-functions.js +1 -1
  50. package/dist/transactor/occ-transaction.js +9 -8
  51. package/dist/transactor/occ-validation.js +16 -6
  52. package/dist/udf/analysis/validator.js +1 -1
  53. package/dist/udf/executor/inline.d.ts +1 -1
  54. package/dist/udf/executor/inline.js +15 -4
  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 +21 -0
  60. package/dist/udf/runtime/udf-setup.js +150 -57
  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).
@@ -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;
@@ -18,6 +18,12 @@ export function parseDocumentIdKey(key) {
18
18
  const internalId = key.substring(colonIndex + 1);
19
19
  if (!table || !internalId)
20
20
  return null;
21
+ if (table.startsWith("#")) {
22
+ const tableNumber = Number.parseInt(table.slice(1), 10);
23
+ if (Number.isInteger(tableNumber) && tableNumber > 0) {
24
+ return { table, internalId, tableNumber };
25
+ }
26
+ }
21
27
  return { table, internalId };
22
28
  }
23
29
  /**
@@ -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,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+)?(?=\/|$)/;
@@ -115,7 +115,7 @@ export class HttpHandler {
115
115
  storage: this.docstore && this.blobstore
116
116
  ? {
117
117
  store: async (blob) => {
118
- const convex = new UdfKernel(this.docstore, undefined, this.blobstore);
118
+ const convex = new UdfKernel({ docstore: this.docstore, storage: this.blobstore });
119
119
  const storageId = await convex.jsSyscall("storage/storeBlob", { blob });
120
120
  const writtenRanges = convex.getTrackedWriteRanges();
121
121
  return {
@@ -125,7 +125,7 @@ export class HttpHandler {
125
125
  };
126
126
  },
127
127
  get: async (storageId) => {
128
- const convex = new UdfKernel(this.docstore, undefined, this.blobstore);
128
+ const convex = new UdfKernel({ docstore: this.docstore, storage: this.blobstore });
129
129
  const blob = await convex.jsSyscall("storage/getBlob", { storageId });
130
130
  return { blob: blob ?? null };
131
131
  },
@@ -296,6 +296,12 @@ export class HttpHandler {
296
296
  if (url.pathname === "/health") {
297
297
  return apply(Response.json({ status: "ok", runtime: this.runtimeName }));
298
298
  }
299
+ // Fallback: forward any unmatched, non-reserved path to the HTTP router.
300
+ // In Convex, HTTP actions are accessible at any path on the deployment URL
301
+ // (e.g. path: "/" matches requests to the root), not just under /api/http/*.
302
+ if (!isReservedApiPath(url.pathname)) {
303
+ return forwardHttpRequest(request, "HTTP action");
304
+ }
299
305
  return apply(Response.json({ error: "Not found" }, { status: 404 }));
300
306
  }
301
307
  }
@@ -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";
@@ -93,11 +93,12 @@ export class BlobStoreGateway {
93
93
  if (!latest || latest.ts > this.context.snapshotTimestamp) {
94
94
  return;
95
95
  }
96
- await storage.delete(docId.internalId);
96
+ const canonicalDocId = latest.value.id;
97
+ await storage.delete(canonicalDocId.internalId);
97
98
  const timestamp = this.docStore.allocateTimestamp();
98
- const entry = { ts: timestamp, id: docId, value: null, prev_ts: latest.ts };
99
+ const entry = { ts: timestamp, id: canonicalDocId, value: null, prev_ts: latest.ts };
99
100
  await this.docStore.applyWrites([entry], new Set(), "Error");
100
- this.context.recordLocalWrite(storageId, "_storage", null, docId);
101
+ this.context.recordLocalWrite(storageId, "_storage", null, canonicalDocId);
101
102
  }
102
103
  requireStorage() {
103
104
  if (!this.storage) {
@@ -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>;
@@ -1,63 +1,137 @@
1
- export class DocStoreGateway {
1
+ // ---------------------------------------------------------------------------
2
+ // Snapshot (non-transactional) path — queries, actions
3
+ // ---------------------------------------------------------------------------
4
+ class SnapshotDocAccess {
2
5
  docstore;
3
- mutationTransaction;
4
6
  context;
5
- constructor(docstore, mutationTransaction) {
7
+ oracle;
8
+ constructor(docstore) {
6
9
  this.docstore = docstore;
7
- this.mutationTransaction = mutationTransaction;
10
+ this.oracle = docstore.timestampOracle;
8
11
  }
9
12
  attachContext(context) {
10
13
  this.context = context;
11
14
  }
12
- getTransaction() {
13
- return this.mutationTransaction;
14
- }
15
- hasTransaction() {
16
- return !!this.mutationTransaction;
17
- }
18
- getTimestampOracle() {
19
- return this.docstore.timestampOracle;
20
- }
21
15
  computeSnapshotTimestamp(snapshotOverride, inheritedSnapshot) {
22
- if (this.mutationTransaction) {
23
- return this.mutationTransaction.getSnapshotTimestamp();
24
- }
25
- const oracle = this.getTimestampOracle();
26
- const docstoreTs = oracle?.getCurrentTimestamp() ?? 0n;
16
+ const docstoreTs = this.oracle?.getCurrentTimestamp() ?? 0n;
27
17
  const wallClockTs = BigInt(Date.now());
28
18
  const maxTs = docstoreTs > wallClockTs ? docstoreTs : wallClockTs;
29
19
  return snapshotOverride ?? inheritedSnapshot ?? maxTs;
30
20
  }
31
21
  allocateTimestamp() {
32
- const oracle = this.getTimestampOracle();
33
- if (oracle) {
34
- return oracle.allocateTimestamp();
22
+ if (this.oracle) {
23
+ return this.oracle.allocateTimestamp();
35
24
  }
36
25
  return BigInt(Date.now());
37
26
  }
38
27
  async fetchLatestDocument(id, snapshotTimestamp) {
39
- if (this.mutationTransaction) {
40
- return this.mutationTransaction.getLatest(id);
41
- }
42
28
  return this.docstore.get(id, snapshotTimestamp);
43
29
  }
44
30
  async fetchTable(tableId, snapshotTimestamp) {
45
- if (this.mutationTransaction) {
46
- return this.mutationTransaction.scanTable(tableId);
47
- }
48
31
  return this.docstore.scan(tableId, snapshotTimestamp);
49
32
  }
50
33
  async applyWrites(documents, indexes, conflictStrategy) {
51
- if (this.mutationTransaction) {
52
- this.mutationTransaction.stageWrite(documents, indexes, conflictStrategy);
53
- return;
54
- }
55
34
  for (const { update } of indexes) {
56
35
  this.context?.recordIndexWrite(update.index_id, update.key);
57
36
  }
58
37
  await this.docstore.write(documents, indexes, conflictStrategy);
59
38
  }
39
+ hasTransaction() {
40
+ return false;
41
+ }
42
+ getTransaction() {
43
+ return undefined;
44
+ }
60
45
  getDocStore() {
61
46
  return this.docstore;
62
47
  }
63
48
  }
49
+ // ---------------------------------------------------------------------------
50
+ // Transactional path — mutations
51
+ // ---------------------------------------------------------------------------
52
+ class TransactionalDocAccess {
53
+ docstore;
54
+ transaction;
55
+ oracle;
56
+ constructor(docstore, transaction) {
57
+ this.docstore = docstore;
58
+ this.transaction = transaction;
59
+ this.oracle = docstore.timestampOracle;
60
+ }
61
+ attachContext(_context) {
62
+ // Transaction path: context read/write tracking is handled by OccMutationTransaction
63
+ }
64
+ computeSnapshotTimestamp(_snapshotOverride, _inheritedSnapshot) {
65
+ return this.transaction.getSnapshotTimestamp();
66
+ }
67
+ allocateTimestamp() {
68
+ if (this.oracle) {
69
+ return this.oracle.allocateTimestamp();
70
+ }
71
+ return BigInt(Date.now());
72
+ }
73
+ async fetchLatestDocument(id, _snapshotTimestamp) {
74
+ return this.transaction.getLatest(id);
75
+ }
76
+ async fetchTable(tableId, _snapshotTimestamp) {
77
+ return this.transaction.scanTable(tableId);
78
+ }
79
+ async applyWrites(documents, indexes, conflictStrategy) {
80
+ this.transaction.stageWrite(documents, indexes, conflictStrategy);
81
+ }
82
+ hasTransaction() {
83
+ return true;
84
+ }
85
+ getTransaction() {
86
+ return this.transaction;
87
+ }
88
+ getDocStore() {
89
+ return this.docstore;
90
+ }
91
+ }
92
+ /**
93
+ * Factory function that selects the appropriate DocAccess implementation
94
+ * based on whether a mutation transaction is active.
95
+ */
96
+ export function createDocAccess(docstore, transaction) {
97
+ return transaction
98
+ ? new TransactionalDocAccess(docstore, transaction)
99
+ : new SnapshotDocAccess(docstore);
100
+ }
101
+ /**
102
+ * @deprecated Use `createDocAccess` factory instead.
103
+ * This class is kept as an alias for backwards compatibility.
104
+ */
105
+ export class DocStoreGateway {
106
+ delegate;
107
+ constructor(docstore, transaction) {
108
+ this.delegate = createDocAccess(docstore, transaction);
109
+ }
110
+ attachContext(context) {
111
+ this.delegate.attachContext(context);
112
+ }
113
+ getTransaction() {
114
+ return this.delegate.getTransaction();
115
+ }
116
+ hasTransaction() {
117
+ return this.delegate.hasTransaction();
118
+ }
119
+ computeSnapshotTimestamp(snapshotOverride, inheritedSnapshot) {
120
+ return this.delegate.computeSnapshotTimestamp(snapshotOverride, inheritedSnapshot);
121
+ }
122
+ allocateTimestamp() {
123
+ return this.delegate.allocateTimestamp();
124
+ }
125
+ async fetchLatestDocument(id, snapshotTimestamp) {
126
+ return this.delegate.fetchLatestDocument(id, snapshotTimestamp);
127
+ }
128
+ async fetchTable(tableId, snapshotTimestamp) {
129
+ return this.delegate.fetchTable(tableId, snapshotTimestamp);
130
+ }
131
+ async applyWrites(documents, indexes, conflictStrategy) {
132
+ return this.delegate.applyWrites(documents, indexes, conflictStrategy);
133
+ }
134
+ getDocStore() {
135
+ return this.delegate.getDocStore();
136
+ }
137
+ }
@@ -7,13 +7,14 @@
7
7
  */
8
8
  export { UdfKernel } from "./udf-kernel";
9
9
  export { KernelContext } from "./kernel-context";
10
- export { ContextStorage } from "./context-storage";
10
+ export { ContextStorage, hasAsyncLocalStorageSupport } from "./context-storage";
11
11
  export { snapshotContext, transactionContext } from "./contexts";
12
12
  export { AccessLog } from "./access-log";
13
13
  export { BlobStoreGateway } from "./blob-store-gateway";
14
- export { DocStoreGateway } from "./docstore-gateway";
14
+ export { DocStoreGateway, createDocAccess, type DocAccess } from "./docstore-gateway";
15
15
  export { SchedulerGateway } from "./scheduler-gateway";
16
- export { SchemaService, isMissingSchemaModuleError } from "./schema-service";
16
+ export { SchemaService } from "./schema-service";
17
+ export { isMissingSchemaModuleError } from "./missing-schema-error";
17
18
  export type { DbIndexConfig, SearchIndexConfig, VectorIndexConfig } from "./schema-service";
18
19
  export { KernelSyscalls, SyscallRouter } from "./syscalls";
19
20
  export type { KernelAuthContext, KernelResources, LocalWrite } from "./types";
@@ -7,12 +7,13 @@
7
7
  */
8
8
  export { UdfKernel } from "./udf-kernel";
9
9
  export { KernelContext } from "./kernel-context";
10
- export { ContextStorage } from "./context-storage";
10
+ export { ContextStorage, hasAsyncLocalStorageSupport } from "./context-storage";
11
11
  export { snapshotContext, transactionContext } from "./contexts";
12
12
  export { AccessLog } from "./access-log";
13
13
  export { BlobStoreGateway } from "./blob-store-gateway";
14
- export { DocStoreGateway } from "./docstore-gateway";
14
+ export { DocStoreGateway, createDocAccess } from "./docstore-gateway";
15
15
  export { SchedulerGateway } from "./scheduler-gateway";
16
- export { SchemaService, isMissingSchemaModuleError } from "./schema-service";
16
+ export { SchemaService } from "./schema-service";
17
+ export { isMissingSchemaModuleError } from "./missing-schema-error";
17
18
  export { KernelSyscalls, SyscallRouter } from "./syscalls";
18
19
  export { UdfInvocationManager } from "./udf-invocation-manager";
@@ -1,7 +1,6 @@
1
1
  import { serializeKeyRange } from "../queryengine/indexing/read-write-set";
2
2
  import { AccessLog } from "./access-log";
3
- import { stringToHex } from "../utils/utils";
4
- import { decodeIndexId, indexKeyspaceId } from "../utils/keyspace";
3
+ import { decodeIndexId } from "../utils/keyspace";
5
4
  import { parseDeveloperId } from "../queryengine/developer-id";
6
5
  import { getGlobalTableRegistry } from "../tables/memory-table-registry";
7
6
  /**
@@ -46,7 +45,8 @@ export class KernelContext {
46
45
  }
47
46
  recordTableRead(tableName) {
48
47
  if (this.mutationTransaction) {
49
- this.mutationTransaction.recordTableScan(stringToHex(tableName), []);
48
+ // Transactional reads must be recorded with concrete documents by the
49
+ // transaction-aware read path (for example scanTable/index query handlers).
50
50
  return;
51
51
  }
52
52
  this.readLog.addTableScan(tableName);
@@ -59,8 +59,8 @@ export class KernelContext {
59
59
  }
60
60
  recordIndexRange(tableName, indexDescriptor, startKey, endKey) {
61
61
  if (this.mutationTransaction) {
62
- const indexId = indexKeyspaceId(tableName, indexDescriptor);
63
- this.mutationTransaction.recordIndexRangeScan(indexId, startKey, endKey, []);
62
+ // Transactional reads must be recorded with concrete documents by the
63
+ // transaction-aware query runtime path.
64
64
  return;
65
65
  }
66
66
  this.readLog.addIndexRange(tableName, indexDescriptor, startKey, endKey);
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Check whether an error indicates a missing convex/schema module.
3
+ * Schema is optional in Convex, so this is a normal condition — not an error.
4
+ */
5
+ export declare function isMissingSchemaModuleError(error: unknown): boolean;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Check whether an error indicates a missing convex/schema module.
3
+ * Schema is optional in Convex, so this is a normal condition — not an error.
4
+ */
5
+ export function isMissingSchemaModuleError(error) {
6
+ if (!error)
7
+ return false;
8
+ // Convert error to string and check for known messages
9
+ const errorString = String(error);
10
+ const isMissing = errorString.includes('Unable to resolve module "schema"') ||
11
+ errorString.includes("Module not found: schema") ||
12
+ errorString.includes('Unable to resolve module "convex/schema"');
13
+ if (isMissing)
14
+ return true;
15
+ // Recursively check causes if present (common in ModuleRegistry errors)
16
+ const causes = error.causes;
17
+ if (Array.isArray(causes)) {
18
+ return causes.some(isMissingSchemaModuleError);
19
+ }
20
+ const cause = error.cause;
21
+ if (cause) {
22
+ return isMissingSchemaModuleError(cause);
23
+ }
24
+ return false;
25
+ }