@distrohelena/canton-typescript-sdk 0.1.46 → 0.1.48

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 (30) hide show
  1. package/dist/cjs/core/types/canton-logger.js +2 -0
  2. package/dist/cjs/index.js +10 -6
  3. package/dist/cjs/query/canton-manager.js +4 -1
  4. package/dist/cjs/query/errors/contract-cache-required-error.js +21 -0
  5. package/dist/cjs/query/errors/history-walk-required-error.js +20 -0
  6. package/dist/cjs/query/grpc/grpc-contract-cache.js +293 -9
  7. package/dist/cjs/query/grpc/grpc-query-client.js +113 -130
  8. package/dist/cjs/query/grpc/grpc-query-snapshot-reader.js +5 -3
  9. package/dist/cjs/query/pqs/pqs-query-client.js +3 -0
  10. package/dist/core/types/canton-logger.d.ts +4 -0
  11. package/dist/core/types/canton-logger.js +1 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +2 -0
  14. package/dist/query/canton-manager-options.d.ts +26 -0
  15. package/dist/query/canton-manager.js +4 -1
  16. package/dist/query/errors/contract-cache-required-error.d.ts +11 -0
  17. package/dist/query/errors/contract-cache-required-error.js +17 -0
  18. package/dist/query/errors/history-walk-required-error.d.ts +10 -0
  19. package/dist/query/errors/history-walk-required-error.js +16 -0
  20. package/dist/query/grpc/grpc-contract-cache.d.ts +40 -3
  21. package/dist/query/grpc/grpc-contract-cache.js +293 -9
  22. package/dist/query/grpc/grpc-query-client.d.ts +11 -1
  23. package/dist/query/grpc/grpc-query-client.js +113 -130
  24. package/dist/query/grpc/grpc-query-snapshot-reader.d.ts +4 -0
  25. package/dist/query/grpc/grpc-query-snapshot-reader.js +5 -3
  26. package/dist/query/grpc/grpc-relation-mapper.d.ts +3 -1
  27. package/dist/query/pqs/pqs-query-client.d.ts +2 -1
  28. package/dist/query/pqs/pqs-query-client.js +3 -0
  29. package/dist/query/query-client.d.ts +20 -0
  30. package/package.json +1 -1
@@ -2,16 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GrpcQueryClient = void 0;
4
4
  const validation_error_js_1 = require("../../core/errors/validation-error.js");
5
+ const contract_cache_required_error_js_1 = require("../errors/contract-cache-required-error.js");
6
+ const history_walk_required_error_js_1 = require("../errors/history-walk-required-error.js");
5
7
  const query_capability_error_js_1 = require("../errors/query-capability-error.js");
6
8
  const in_memory_query_evaluator_js_1 = require("../canonical/in-memory-query-evaluator.js");
7
9
  const query_dataset_js_1 = require("../canonical/query-dataset.js");
8
10
  const query_normalizer_js_1 = require("../canonical/query-normalizer.js");
9
11
  const query_schema_js_1 = require("../canonical/query-schema.js");
10
12
  const query_source_js_1 = require("../query-source.js");
13
+ const public_identity_js_1 = require("../canonical/public-identity.js");
11
14
  const grpc_relation_mapper_js_1 = require("./grpc-relation-mapper.js");
12
15
  const grpc_package_relation_reader_js_1 = require("./grpc-package-relation-reader.js");
13
16
  const grpc_query_snapshot_reader_js_1 = require("./grpc-query-snapshot-reader.js");
14
- const grpc_query_value_mapper_js_1 = require("./grpc-query-value-mapper.js");
15
17
  class GrpcQueryClient {
16
18
  options;
17
19
  source = query_source_js_1.QuerySource.grpc;
@@ -41,6 +43,9 @@ class GrpcQueryClient {
41
43
  async invalidateContractsCache(args) {
42
44
  await this.options.contractCache?.invalidateContractsCache(args);
43
45
  }
46
+ async inspectContractsCache(args) {
47
+ return this.options.contractCache?.inspectContractsCacheAsync(args);
48
+ }
44
49
  delegate(relation) {
45
50
  return {
46
51
  findMany: async (args = {}) => this.execute((0, query_normalizer_js_1.normalizeFindMany)(relation, args)),
@@ -65,6 +70,7 @@ class DefaultGrpcQueryDataProvider {
65
70
  options;
66
71
  snapshots;
67
72
  packages;
73
+ warnedReplayRelations = new Set();
68
74
  constructor(options) {
69
75
  this.options = options;
70
76
  this.snapshots = new grpc_query_snapshot_reader_js_1.GrpcQuerySnapshotReader(options.stateService, options.updateService, { incrementalHistory: options.incrementalHistory });
@@ -77,19 +83,27 @@ class DefaultGrpcQueryDataProvider {
77
83
  const cached = activeOnly && this.options.contractCache !== undefined
78
84
  ? await this.options.contractCache.readSnapshotAsync({ parties: partiesFor(query) })
79
85
  : undefined;
80
- if (cached !== undefined && !needsHistory && !requiresPackageMetadata(closure)) {
81
- return cachedContractsDataset(cached.contracts, cached.activeAtOffset, this.options.endpointScope ?? "ledger");
86
+ // The snapshot stores per-contract creation metadata, so contractTypes joins are served from cache
87
+ // too; only exercises/exerciseTypes/packages (whose rows the snapshot cannot answer) force a fetch.
88
+ if (cached !== undefined && !needsHistory && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages")) {
89
+ return cachedContractsDataset(cached, this.options.endpointScope ?? "ledger");
82
90
  }
83
91
  else if (needsHistory) {
84
- // With a warm incremental window only the new offsets are fetched, which is no longer worth a warning.
85
- if (!(this.options.incrementalHistory === true && this.snapshots.hasHistoryCache)) {
86
- console.warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
92
+ if (this.options.walkHistory !== true) {
93
+ throw new history_walk_required_error_js_1.HistoryWalkRequiredError(query.relation);
94
+ }
95
+ // With a warm incremental window only the new offsets are fetched, which is no longer worth a
96
+ // warning; otherwise warn once per relation per client, not once per query.
97
+ if (!(this.options.incrementalHistory === true && this.snapshots.hasHistoryCache) && !this.warnedReplayRelations.has(query.relation)) {
98
+ this.warnedReplayRelations.add(query.relation);
99
+ (this.options.logger ?? console).warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
87
100
  + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
88
101
  + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
89
102
  + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed"
90
103
  + (this.options.incrementalHistory === true
91
104
  ? "; incrementalHistory is enabled, so later history queries will fetch only new offsets."
92
- : ", or enable the incrementalHistory option to fetch only new offsets on repeat history queries."));
105
+ : ", or enable the incrementalHistory option to fetch only new offsets on repeat history queries.")
106
+ + ` This warning is logged once per relation; further "${query.relation}" replays stay silent.`);
93
107
  }
94
108
  const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
95
109
  const history = await this.snapshots.readHistoryAsync(endInclusive);
@@ -123,33 +137,68 @@ class DefaultGrpcQueryDataProvider {
123
137
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
124
138
  : (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
125
139
  }
126
- const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
127
140
  if (query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes") {
128
141
  // A proven-active where/include on "contracts" (see predicateRequiresHistory/includesRequireHistory)
129
- // can put "contracts" in the closure here without needsHistory being true, so the ACS still has to
130
- // be readotherwise that where/include would silently resolve against an empty row set.
131
- const contractsFragment = closure.has("contracts")
132
- ? (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([], (await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query))).activeContracts)
133
- : (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([]);
142
+ // can put "contracts" in the closure here without needsHistory being true. Those rows come from
143
+ // the warmed cache the ACS is never downloaded implicitly.
144
+ const contractsSnapshot = closure.has("contracts") ? await this.requireCachedSnapshotAsync(query) : undefined;
145
+ const endInclusive = contractsSnapshot?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
146
+ const contractsFragment = contractsSnapshot === undefined
147
+ ? (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([])
148
+ : fragmentFromCachedSnapshot(contractsSnapshot);
134
149
  return (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)(contractsFragment, await this.packages.readAllAsync(), endInclusive, this.options.endpointScope ?? "ledger");
135
150
  }
136
151
  else if (query.relation === "watermark") {
152
+ const endInclusive = (await this.options.stateService.getLedgerEndAsync({})).offset;
137
153
  return (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)((0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([]), [], endInclusive, this.options.endpointScope ?? "ledger");
138
154
  }
139
- const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query), pushdownTemplateRefsFor(query));
140
- const fragment = (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([], active.activeContracts);
141
- if (!requiresPackageMetadata(closure)) {
142
- return fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger", false);
155
+ // Only an active-only "contracts" query can reach here (anything else forces history above), and the
156
+ // cache gate has already failed — the ACS is never downloaded per query, so this is a hard error.
157
+ throw new contract_cache_required_error_js_1.ContractCacheRequiredError(partiesFor(query));
158
+ }
159
+ async requireCachedSnapshotAsync(query) {
160
+ const snapshot = this.options.contractCache === undefined
161
+ ? undefined
162
+ : await this.options.contractCache.readSnapshotAsync({ parties: partiesFor(query) });
163
+ if (snapshot === undefined) {
164
+ throw new contract_cache_required_error_js_1.ContractCacheRequiredError(partiesFor(query));
143
165
  }
144
- // Reaching here guarantees query.relation === "contracts" with needsHistory === false, which (per
145
- // predicateRequiresHistory/includesRequireHistory above) is only possible when the closure never
146
- // touches "exercises"/"exerciseTypes"/"packages"/"transactions"/"events" — those unconditionally force
147
- // history. So requiresPackageMetadata(closure) here can only be "contractTypes", and every contract in
148
- // this ACS-only fragment already carries its own packageName — no Package Service call needed.
149
- const packageMetadata = (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(fragment.creationIdentities);
150
- return (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)(fragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
166
+ return snapshot;
151
167
  }
152
168
  }
169
+ /** Rebuilds an ACS-shaped relation fragment from a cached snapshot, with no transport involved. */
170
+ function fragmentFromCachedSnapshot(snapshot) {
171
+ const metadataByContractId = new Map(snapshot.creationMetadata.map((entry) => [entry.contractId, entry]));
172
+ const creationIdentities = snapshot.contracts.map((row) => {
173
+ const metadata = metadataByContractId.get(row.contractId);
174
+ if (metadata === undefined) {
175
+ throw new validation_error_js_1.ValidationError(`Cached contract snapshot is missing creation metadata for ${row.contractId}`);
176
+ }
177
+ return {
178
+ contractId: row.contractId,
179
+ offset: row.createdEventOffset,
180
+ templateId: row.templateId,
181
+ creationPackageId: row.templateId.packageId,
182
+ representativePackageId: metadata.representativePackageId,
183
+ packageName: metadata.packageName,
184
+ payload: row.payload,
185
+ witnesses: row.witnesses,
186
+ createdAt: row.createdAt ?? new Date(0),
187
+ };
188
+ });
189
+ return {
190
+ contracts: snapshot.contracts,
191
+ transactions: [],
192
+ events: [],
193
+ exercises: [],
194
+ typeIdentities: [],
195
+ packageIdentities: [],
196
+ creationIdentities,
197
+ // Only the contract ids are consumed (they mark creating transactions as legitimately absent, so the
198
+ // createdTransaction edge is flagged incomplete instead of pretending to be empty).
199
+ activeContractIdentities: snapshot.contracts.map((row) => ({ contractId: row.contractId, synchronizerId: "", reassignmentCounter: "0", activationOffset: row.createdEventOffset, activationNodeId: 0 })),
200
+ };
201
+ }
153
202
  function requiresHistory(query) {
154
203
  if (query.relation === "transactions" || query.relation === "events" || query.relation === "exercises") {
155
204
  return true;
@@ -197,110 +246,6 @@ function predicateProvesActive(predicate) {
197
246
  function requiresPackageMetadata(closure) {
198
247
  return closure.has("packages") || closure.has("contractTypes") || closure.has("exercises") || closure.has("exerciseTypes");
199
248
  }
200
- const MAX_PUSHDOWN_TEMPLATE_FILTERS = 25;
201
- /**
202
- * Extracts template filters the ACS request itself can apply, so non-matching contracts are never
203
- * downloaded or materialized. The participant scans its whole ACS either way — the win is wire volume and
204
- * client-side decode/freeze work, not node time. Correctness rule: the evaluator re-applies the complete
205
- * predicate over whatever rows come back, so a pushed filter set only has to be a SUPERSET of possible
206
- * matches — over-fetching is fine, under-fetching never happens because pins are only read from top-level
207
- * AND conjuncts (anything under or/not is ignored) and any single conjunct constrains every matching row.
208
- * Returns undefined (wildcard fetch) when no full package/module/entity pin can be proven or a pinned value
209
- * is not a syntactically valid identifier (a malformed value can never match, but pushing it would make the
210
- * node reject the request instead of returning the empty result the evaluator would produce).
211
- */
212
- function pushdownTemplateRefsFor(query) {
213
- if (query.relation !== "contracts") {
214
- return undefined;
215
- }
216
- const stringValues = (operator, value) => operator === "equals" && typeof value === "string"
217
- ? [value]
218
- : operator === "in" && Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
219
- let packageRefs;
220
- let moduleNames;
221
- let entityNames;
222
- let fqnRefs;
223
- for (const conjunct of flattenAndConjuncts(query.predicate)) {
224
- if (conjunct.kind === "scalar" && conjunct.path.length === 2 && conjunct.path[0] === "templateId") {
225
- const values = stringValues(conjunct.operator, conjunct.value);
226
- if (values === undefined) {
227
- continue;
228
- }
229
- else if (conjunct.path[1] === "packageId") {
230
- packageRefs ??= values;
231
- }
232
- else if (conjunct.path[1] === "moduleName") {
233
- moduleNames ??= values;
234
- }
235
- else if (conjunct.path[1] === "entityName") {
236
- entityNames ??= values;
237
- }
238
- }
239
- else if (conjunct.kind === "relation" && conjunct.edge === "contractType" && conjunct.quantifier === "one") {
240
- for (const inner of flattenAndConjuncts(conjunct.predicate)) {
241
- if (inner.kind !== "scalar" || inner.path.length !== 1) {
242
- continue;
243
- }
244
- const values = stringValues(inner.operator, inner.value);
245
- if (values === undefined) {
246
- continue;
247
- }
248
- else if (inner.path[0] === "packageName") {
249
- packageRefs ??= values.map((name) => `#${name}`);
250
- }
251
- else if (inner.path[0] === "moduleName") {
252
- moduleNames ??= values;
253
- }
254
- else if (inner.path[0] === "entityName") {
255
- entityNames ??= values;
256
- }
257
- else if (inner.path[0] === "templateFqn") {
258
- const triples = values.map(templateRefFromFqn);
259
- if (triples.every((triple) => triple !== undefined)) {
260
- fqnRefs ??= triples;
261
- }
262
- }
263
- }
264
- }
265
- }
266
- const refs = fqnRefs ?? (packageRefs !== undefined && moduleNames !== undefined && entityNames !== undefined
267
- ? packageRefs.flatMap((packageId) => moduleNames.flatMap((moduleName) => entityNames.map((entityName) => ({ packageId, moduleName, entityName }))))
268
- : undefined);
269
- return refs !== undefined && refs.length > 0 && refs.length <= MAX_PUSHDOWN_TEMPLATE_FILTERS && refs.every(isValidTemplateRef)
270
- ? refs
271
- : undefined;
272
- }
273
- function flattenAndConjuncts(predicate) {
274
- if (predicate === undefined) {
275
- return [];
276
- }
277
- else if (predicate.kind === "and") {
278
- return predicate.children.flatMap(flattenAndConjuncts);
279
- }
280
- return [predicate];
281
- }
282
- function templateRefFromFqn(fqn) {
283
- const parts = fqn.split(":");
284
- return parts.length === 3 && parts.every((part) => part.length > 0)
285
- ? { packageId: `#${parts[0]}`, moduleName: parts[1], entityName: parts[2] }
286
- : undefined;
287
- }
288
- function isValidTemplateRef(ref) {
289
- const validPackage = ref.packageId.startsWith("#")
290
- ? /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ref.packageId.slice(1))
291
- : /^[0-9a-f]{64}$/.test(ref.packageId);
292
- if (!validPackage) {
293
- return false;
294
- }
295
- try {
296
- (0, grpc_query_value_mapper_js_1.validDottedNameString)(ref.moduleName, "template filter module name");
297
- (0, grpc_query_value_mapper_js_1.validDottedNameString)(ref.entityName, "template filter entity name");
298
- return true;
299
- }
300
- catch {
301
- return false;
302
- }
303
- }
304
249
  function predicateRequiresHistory(relation, predicate) {
305
250
  if (predicate === undefined || predicate.kind === "scalar") {
306
251
  return false;
@@ -381,9 +326,47 @@ function addRelationPath(relation, path, relations) {
381
326
  }
382
327
  }
383
328
  }
384
- function cachedContractsDataset(contracts, offset, instanceId) {
329
+ /**
330
+ * Builds a dataset entirely from a cached snapshot: contract rows plus contractType rows derived from the
331
+ * stored per-contract creation metadata, joined through private keys. No transport calls. History-only
332
+ * edges stay incomplete so anything the snapshot cannot answer still fails loudly instead of lying.
333
+ */
334
+ function cachedContractsDataset(snapshot, instanceId) {
335
+ const offset = snapshot.activeAtOffset;
336
+ const metadataByContractId = new Map(snapshot.creationMetadata.map((entry) => [entry.contractId, entry]));
337
+ const creations = snapshot.contracts.map((row) => {
338
+ const metadata = metadataByContractId.get(row.contractId);
339
+ if (metadata === undefined) {
340
+ throw new validation_error_js_1.ValidationError(`Cached contract snapshot is missing creation metadata for ${row.contractId}`);
341
+ }
342
+ return {
343
+ creationPackageId: row.templateId.packageId,
344
+ representativePackageId: metadata.representativePackageId,
345
+ packageName: metadata.packageName,
346
+ templateId: row.templateId,
347
+ };
348
+ });
349
+ const typeRowsByPk = new Map();
350
+ for (const pkg of (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(creations)) {
351
+ for (const template of pkg.templates) {
352
+ const pk = (0, public_identity_js_1.canonicalPublicNumericIdentityParts)([template.payloadType, template.templateFqn]);
353
+ if (!typeRowsByPk.has(pk)) {
354
+ typeRowsByPk.set(pk, { pk, payloadType: template.payloadType, aliases: template.aliases, packageName: pkg.name, moduleName: template.moduleName, entityName: template.entityName, templateFqn: template.templateFqn });
355
+ }
356
+ }
357
+ }
358
+ const typeRows = [...typeRowsByPk.values()];
359
+ const contractTypeKeys = creations.map((creation) => [(0, public_identity_js_1.canonicalPublicNumericIdentityParts)(["template", `${creation.packageName}:${creation.templateId.moduleName}:${creation.templateId.entityName}`])]);
360
+ const typeKeys = typeRows.map((row) => [row.pk]);
385
361
  const empty = [];
386
- return basicDataset({ contracts: contracts, transactions: empty, events: empty, exercises: empty }, offset, instanceId, false);
362
+ const edges = Object.fromEntries(query_schema_js_1.queryRelations.map((relation) => [relation, Object.fromEntries(Object.keys(query_schema_js_1.queryRelationEdges[relation] ?? {}).map((edge) => [edge, { ...cachedEdgePaths(relation, edge), complete: false }]))]));
363
+ edges.contracts.contractType = { privateKeys: { source: contractTypeKeys, target: typeKeys } };
364
+ edges.contractTypes.contracts = { privateKeys: { source: typeKeys, target: contractTypeKeys } };
365
+ return (0, query_dataset_js_1.createQueryDataset)({
366
+ rows: { contracts: snapshot.contracts, contractTypes: typeRows, events: empty, exercises: empty, exerciseTypes: empty, packages: empty, transactions: empty, watermark: [{ singleton: true, ix: offset, offset, instanceId }] },
367
+ uniqueKeys: { contracts: [["contractId"]], contractTypes: [["pk"]], events: [["pk"]], exercises: [["tpePk", "contractTpePk", "exerciseEventPk", "contractId"]], exerciseTypes: [["pk"]], packages: [["pk"], ["id"]], transactions: [["ix"], ["offset"]], watermark: [["singleton"]] },
368
+ edges: edges,
369
+ });
387
370
  }
388
371
  function fragmentDataset(fragment, offset, instanceId, completeHistoryEdges = true) {
389
372
  return basicDataset(fragment, offset, instanceId, completeHistoryEdges);
@@ -83,6 +83,7 @@ class GrpcQuerySnapshotReader {
83
83
  endOffsetInclusive: endInclusive,
84
84
  updateFormat,
85
85
  descendingOrder: false,
86
+ maxPageSize: this.options.historyPageSize,
86
87
  pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
87
88
  };
88
89
  const response = await this.updateService.getUpdatesPageAsync(request);
@@ -150,6 +151,7 @@ class GrpcQuerySnapshotReader {
150
151
  const request = {
151
152
  activeAtOffset,
152
153
  eventFormat,
154
+ maxPageSize: this.options.activeContractPageSize,
153
155
  pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
154
156
  };
155
157
  const response = await this.stateService.getActiveContractsPageAsync(request);
@@ -201,17 +203,17 @@ class GrpcQuerySnapshotReader {
201
203
  }
202
204
  exports.GrpcQuerySnapshotReader = GrpcQuerySnapshotReader;
203
205
  function validateOptions(options) {
204
- const { incrementalHistory = false, ...limits } = options;
206
+ const { incrementalHistory = false, historyPageSize, activeContractPageSize, ...limits } = options;
205
207
  if (typeof incrementalHistory !== "boolean") {
206
208
  throw new validation_error_js_1.ValidationError("incrementalHistory must be a boolean.");
207
209
  }
208
210
  const validated = { ...DEFAULT_LIMITS, ...limits };
209
- for (const [name, value] of Object.entries(validated)) {
211
+ for (const [name, value] of Object.entries({ ...validated, ...(historyPageSize === undefined ? {} : { historyPageSize }), ...(activeContractPageSize === undefined ? {} : { activeContractPageSize }) })) {
210
212
  if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
211
213
  throw new validation_error_js_1.ValidationError(`${name} must be a finite positive integer.`);
212
214
  }
213
215
  }
214
- return Object.freeze({ ...validated, incrementalHistory });
216
+ return Object.freeze({ ...validated, historyPageSize, activeContractPageSize, incrementalHistory });
215
217
  }
216
218
  function createHistoryUpdateFormat() {
217
219
  return {
@@ -58,6 +58,9 @@ class PqsQueryClient {
58
58
  return { source: query_source_js_1.QuerySource.pqs, cached: false };
59
59
  }
60
60
  async invalidateContractsCache(_args) { }
61
+ async inspectContractsCache(_args) {
62
+ return undefined;
63
+ }
61
64
  createPhysicalDelegate(relation, hasUnique = true) {
62
65
  const queryRelation = queryRelationForPqs[relation];
63
66
  const delegate = {
@@ -0,0 +1,4 @@
1
+ /** Minimal logging seam for SDK diagnostics; defaults to the global console when not provided. */
2
+ export interface CantonLogger {
3
+ warn(message: string): void;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -2,6 +2,9 @@ export { CantonClient } from "./client/canton-client.js";
2
2
  export { CantonManager } from "./query/canton-manager.js";
3
3
  export { QuerySource } from "./query/query-source.js";
4
4
  export { MemoryQueryCache } from "./query/cache/memory-query-cache.js";
5
+ export type { CantonLogger } from "./core/types/canton-logger.js";
6
+ export { ContractCacheRequiredError } from "./query/errors/contract-cache-required-error.js";
7
+ export { HistoryWalkRequiredError } from "./query/errors/history-walk-required-error.js";
5
8
  export { QueryCapabilityError } from "./query/errors/query-capability-error.js";
6
9
  export { QuerySnapshotIncompleteError } from "./query/errors/query-snapshot-incomplete-error.js";
7
10
  export type { QuerySnapshotIncompleteReason } from "./query/errors/query-snapshot-incomplete-error.js";
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ export { CantonClient } from "./client/canton-client.js";
2
2
  export { CantonManager } from "./query/canton-manager.js";
3
3
  export { QuerySource } from "./query/query-source.js";
4
4
  export { MemoryQueryCache } from "./query/cache/memory-query-cache.js";
5
+ export { ContractCacheRequiredError } from "./query/errors/contract-cache-required-error.js";
6
+ export { HistoryWalkRequiredError } from "./query/errors/history-walk-required-error.js";
5
7
  export { QueryCapabilityError } from "./query/errors/query-capability-error.js";
6
8
  export { QuerySnapshotIncompleteError } from "./query/errors/query-snapshot-incomplete-error.js";
7
9
  export { PqsQueryError } from "./query/errors/pqs-query-error.js";
@@ -1,4 +1,5 @@
1
1
  import { CantonClientOptions } from "../client/canton-client-options.js";
2
+ import { CantonLogger } from "../core/types/canton-logger.js";
2
3
  import { QueryCacheStore } from "./cache/query-cache-store.js";
3
4
  import { QuerySource } from "./query-source.js";
4
5
  export interface PqsQueryOptions {
@@ -8,10 +9,35 @@ export interface PqsQueryOptions {
8
9
  export interface QueryCacheOptions {
9
10
  readonly store: QueryCacheStore;
10
11
  readonly ttlMs: number;
12
+ /** Per-request ACS page size for cache prewarms; omitted means the participant's default. */
13
+ readonly maxPageSize?: number;
14
+ /**
15
+ * BETA: opt into delta refresh. Re-warms with a warm (or expired) snapshot then try to patch it forward
16
+ * from the update stream instead of re-downloading the ACS. Any unprovable window — reassignments or
17
+ * topology changes, pruning past the snapshot, exceeded budgets — falls back to the full download
18
+ * automatically. Off by default: every re-warm performs the full ACS download.
19
+ */
20
+ readonly betaDeltaRefresh?: boolean;
21
+ /** Delta refresh only: skip the delta attempt when the ledger has run further than this past the snapshot. */
22
+ readonly maxDeltaOffsetGap?: number;
23
+ /** Delta refresh only: abandon the delta (fall back to full download) after this many applied updates. */
24
+ readonly maxDeltaUpdates?: number;
11
25
  }
12
26
  export interface CantonManagerOptions {
13
27
  readonly grpc: CantonClientOptions;
14
28
  readonly querySource: QuerySource;
15
29
  readonly pqs?: PqsQueryOptions;
16
30
  readonly cache?: QueryCacheOptions;
31
+ /**
32
+ * Opt-in for gRPC typed queries: keep the replayed history window in memory and fetch only new offsets
33
+ * on later history queries. Costs RAM for the manager's lifetime — see GrpcQueryClientOptions.
34
+ */
35
+ readonly incrementalHistory?: boolean;
36
+ /**
37
+ * Opt-in for gRPC typed queries: permit queries that replay ledger history. Off by default — such
38
+ * queries throw HistoryWalkRequiredError so the replay cost is never paid implicitly.
39
+ */
40
+ readonly walkHistory?: boolean;
41
+ /** Receives SDK diagnostics (e.g. the once-per-relation full-replay warning); defaults to console. */
42
+ readonly logger?: CantonLogger;
17
43
  }
@@ -34,9 +34,12 @@ export class CantonManager {
34
34
  updateService: this.grpc.updateService,
35
35
  packageService: this.grpc.packageService,
36
36
  endpointScope: options.grpc.ledgerEndpoint ?? "ledger",
37
+ incrementalHistory: options.incrementalHistory,
38
+ walkHistory: options.walkHistory,
39
+ logger: options.logger,
37
40
  contractCache: options.cache === undefined
38
41
  ? undefined
39
- : new GrpcContractCache(this.grpc.stateService, options.cache.store, options.cache.ttlMs, options.grpc.ledgerEndpoint ?? "ledger"),
42
+ : new GrpcContractCache(this.grpc.stateService, options.cache.store, options.cache.ttlMs, options.grpc.ledgerEndpoint ?? "ledger", undefined, options.cache.maxPageSize, this.grpc.updateService, { enabled: options.cache.betaDeltaRefresh, maxOffsetGap: options.cache.maxDeltaOffsetGap, maxUpdates: options.cache.maxDeltaUpdates }),
40
43
  });
41
44
  }
42
45
  }
@@ -0,0 +1,11 @@
1
+ import { CantonError } from "../../core/errors/canton-error.js";
2
+ /**
3
+ * gRPC typed queries never download the ACS implicitly: reading active contracts requires an explicitly
4
+ * warmed contract cache, so the (potentially very large) ACS transfer only ever happens when the caller
5
+ * asks for it via cacheContracts(). Thrown when an ACS-backed query runs without a warm entry for its
6
+ * party scope.
7
+ */
8
+ export declare class ContractCacheRequiredError extends CantonError {
9
+ readonly parties?: readonly string[] | undefined;
10
+ constructor(parties?: readonly string[] | undefined);
11
+ }
@@ -0,0 +1,17 @@
1
+ import { CantonError } from "../../core/errors/canton-error.js";
2
+ /**
3
+ * gRPC typed queries never download the ACS implicitly: reading active contracts requires an explicitly
4
+ * warmed contract cache, so the (potentially very large) ACS transfer only ever happens when the caller
5
+ * asks for it via cacheContracts(). Thrown when an ACS-backed query runs without a warm entry for its
6
+ * party scope.
7
+ */
8
+ export class ContractCacheRequiredError extends CantonError {
9
+ parties;
10
+ constructor(parties) {
11
+ super("gRPC active-contract queries require a warmed contract cache. Configure the cache "
12
+ + "({ store, ttlMs }) and call cacheContracts("
13
+ + (parties === undefined ? "" : JSON.stringify({ parties }))
14
+ + ") before querying; the prewarm's party scope must match the query's.");
15
+ this.parties = parties;
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ import { CantonError } from "../../core/errors/canton-error.js";
2
+ /**
3
+ * gRPC typed queries never replay ledger history implicitly: transactions/events/exercises queries — and
4
+ * contracts queries whose predicates or includes reach archived state — require the walkHistory option, so
5
+ * the (potentially very expensive) replay only ever happens when the caller asked for it.
6
+ */
7
+ export declare class HistoryWalkRequiredError extends CantonError {
8
+ readonly relation: string;
9
+ constructor(relation: string);
10
+ }
@@ -0,0 +1,16 @@
1
+ import { CantonError } from "../../core/errors/canton-error.js";
2
+ /**
3
+ * gRPC typed queries never replay ledger history implicitly: transactions/events/exercises queries — and
4
+ * contracts queries whose predicates or includes reach archived state — require the walkHistory option, so
5
+ * the (potentially very expensive) replay only ever happens when the caller asked for it.
6
+ */
7
+ export class HistoryWalkRequiredError extends CantonError {
8
+ relation;
9
+ constructor(relation) {
10
+ super(`The "${relation}" query requires replaying ledger history, which is disabled by default. `
11
+ + "Enable the walkHistory option to permit it (pair it with incrementalHistory so repeat "
12
+ + "queries fetch only new offsets), or reshape the query to active-only state served by the "
13
+ + "contract cache.");
14
+ this.relation = relation;
15
+ }
16
+ }
@@ -1,12 +1,33 @@
1
1
  import { StateServiceClient } from "../../services/state/state-service-client.js";
2
+ import { UpdateServiceClient } from "../../services/update/update-service-client.js";
2
3
  import { QueryCacheStore } from "../cache/query-cache-store.js";
3
4
  import { ContractRow } from "../model-types.js";
4
- import { ContractCacheArgs, ContractCacheResult } from "../query-client.js";
5
- type ActiveContractsReader = Pick<StateServiceClient, "getActiveContractsPageAsync">;
5
+ import { ContractCacheArgs, ContractCacheInspection, ContractCacheResult } from "../query-client.js";
6
+ type ActiveContractsReader = Pick<StateServiceClient, "getActiveContractsPageAsync"> & Partial<Pick<StateServiceClient, "getLedgerEndAsync" | "getLatestPrunedOffsetsAsync">>;
7
+ type CacheUpdateReader = Pick<UpdateServiceClient, "getUpdatesPageAsync">;
8
+ /** Budgets for patching a warm snapshot forward from the update stream instead of re-downloading the ACS. */
9
+ export interface GrpcContractCacheDeltaOptions {
10
+ /**
11
+ * BETA: delta refresh is opt-in. When absent or false, every re-warm performs the full ACS download
12
+ * regardless of the other options here.
13
+ */
14
+ readonly enabled?: boolean;
15
+ /** Skip the delta attempt outright when the ledger has run further than this past the snapshot. */
16
+ readonly maxOffsetGap?: number;
17
+ /** Abandon the delta (fall back to a full download) after this many applied updates. */
18
+ readonly maxUpdates?: number;
19
+ }
20
+ /** Per-contract creation facts the contractType join needs; stored so cached reads never re-fetch the ACS. */
21
+ export interface GrpcCachedCreationMetadata {
22
+ readonly contractId: string;
23
+ readonly packageName: string;
24
+ readonly representativePackageId: string | null;
25
+ }
6
26
  /** Internal point-in-time active-contract cache lookup used by query planning. */
7
27
  export interface GrpcCachedContractSnapshot {
8
28
  readonly activeAtOffset: string;
9
29
  readonly contracts: readonly ContractRow[];
30
+ readonly creationMetadata: readonly GrpcCachedCreationMetadata[];
10
31
  }
11
32
  export declare class GrpcContractCache {
12
33
  private readonly stateService;
@@ -14,13 +35,29 @@ export declare class GrpcContractCache {
14
35
  private readonly ttlMs;
15
36
  private readonly endpointScope;
16
37
  private readonly now;
38
+ private readonly maxPageSize?;
39
+ private readonly updateService?;
40
+ private readonly delta;
17
41
  private readonly inflight;
18
- constructor(stateService: ActiveContractsReader, store: QueryCacheStore, ttlMs: number, endpointScope: string, now?: () => number);
42
+ constructor(stateService: ActiveContractsReader, store: QueryCacheStore, ttlMs: number, endpointScope: string, now?: () => number, maxPageSize?: number | undefined, updateService?: CacheUpdateReader | undefined, delta?: GrpcContractCacheDeltaOptions);
19
43
  cacheContracts(args?: ContractCacheArgs): Promise<ContractCacheResult>;
20
44
  readContractsAsync(args?: ContractCacheArgs): Promise<readonly ContractRow[] | undefined>;
21
45
  readSnapshotAsync(args?: ContractCacheArgs): Promise<GrpcCachedContractSnapshot | undefined>;
22
46
  invalidateContractsCache(args?: ContractCacheArgs): Promise<void>;
47
+ /** Measures the cached snapshot against the current ledger end, without changing anything. */
48
+ inspectContractsCacheAsync(args?: ContractCacheArgs): Promise<ContractCacheInspection | undefined>;
23
49
  private populateAsync;
50
+ /**
51
+ * Patches the base snapshot forward from the update stream instead of re-downloading the ACS. Returns
52
+ * undefined — meaning "do the full download instead" — whenever correctness cannot be proven cheaply:
53
+ * pruning past the base offset, an offset gap or update count beyond the configured budgets, any
54
+ * reassignment or topology change in the window (multi-synchronizer moves cannot be patched safely), an
55
+ * exercised event where ACS_DELTA promises none, or an archive/create inconsistent with the base rows.
56
+ */
57
+ private tryDeltaRefreshAsync;
58
+ private readDeltaWindowAsync;
59
+ private writeSnapshotAsync;
60
+ private fullRefreshAsync;
24
61
  private clearInflight;
25
62
  }
26
63
  export declare function normalizeParties(args?: ContractCacheArgs): readonly string[] | undefined;