@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
@@ -1,14 +1,16 @@
1
1
  import { ValidationError } from "../../core/errors/validation-error.js";
2
+ import { ContractCacheRequiredError } from "../errors/contract-cache-required-error.js";
3
+ import { HistoryWalkRequiredError } from "../errors/history-walk-required-error.js";
2
4
  import { QueryCapabilityError } from "../errors/query-capability-error.js";
3
5
  import { InMemoryQueryEvaluator } from "../canonical/in-memory-query-evaluator.js";
4
6
  import { createQueryDataset } from "../canonical/query-dataset.js";
5
7
  import { normalizeAggregate, normalizeCount, normalizeFindMany, normalizeFindUnique, normalizeGroupBy } from "../canonical/query-normalizer.js";
6
8
  import { queryRelationEdges, queryRelations } from "../canonical/query-schema.js";
7
9
  import { QuerySource } from "../query-source.js";
10
+ import { canonicalPublicNumericIdentityParts } from "../canonical/public-identity.js";
8
11
  import { contractTypeMetadataFromCreations, createGrpcQueryDataset, mapGrpcQueryRelationFragment, packageMetadataFromEvents, referencedGrpcPackageIds } from "./grpc-relation-mapper.js";
9
12
  import { GrpcPackageRelationReader } from "./grpc-package-relation-reader.js";
10
13
  import { GrpcQuerySnapshotReader } from "./grpc-query-snapshot-reader.js";
11
- import { validDottedNameString } from "./grpc-query-value-mapper.js";
12
14
  export class GrpcQueryClient {
13
15
  options;
14
16
  source = QuerySource.grpc;
@@ -38,6 +40,9 @@ export class GrpcQueryClient {
38
40
  async invalidateContractsCache(args) {
39
41
  await this.options.contractCache?.invalidateContractsCache(args);
40
42
  }
43
+ async inspectContractsCache(args) {
44
+ return this.options.contractCache?.inspectContractsCacheAsync(args);
45
+ }
41
46
  delegate(relation) {
42
47
  return {
43
48
  findMany: async (args = {}) => this.execute(normalizeFindMany(relation, args)),
@@ -61,6 +66,7 @@ class DefaultGrpcQueryDataProvider {
61
66
  options;
62
67
  snapshots;
63
68
  packages;
69
+ warnedReplayRelations = new Set();
64
70
  constructor(options) {
65
71
  this.options = options;
66
72
  this.snapshots = new GrpcQuerySnapshotReader(options.stateService, options.updateService, { incrementalHistory: options.incrementalHistory });
@@ -73,19 +79,27 @@ class DefaultGrpcQueryDataProvider {
73
79
  const cached = activeOnly && this.options.contractCache !== undefined
74
80
  ? await this.options.contractCache.readSnapshotAsync({ parties: partiesFor(query) })
75
81
  : undefined;
76
- if (cached !== undefined && !needsHistory && !requiresPackageMetadata(closure)) {
77
- return cachedContractsDataset(cached.contracts, cached.activeAtOffset, this.options.endpointScope ?? "ledger");
82
+ // The snapshot stores per-contract creation metadata, so contractTypes joins are served from cache
83
+ // too; only exercises/exerciseTypes/packages (whose rows the snapshot cannot answer) force a fetch.
84
+ if (cached !== undefined && !needsHistory && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages")) {
85
+ return cachedContractsDataset(cached, this.options.endpointScope ?? "ledger");
78
86
  }
79
87
  else if (needsHistory) {
80
- // With a warm incremental window only the new offsets are fetched, which is no longer worth a warning.
81
- if (!(this.options.incrementalHistory === true && this.snapshots.hasHistoryCache)) {
82
- console.warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
88
+ if (this.options.walkHistory !== true) {
89
+ throw new HistoryWalkRequiredError(query.relation);
90
+ }
91
+ // With a warm incremental window only the new offsets are fetched, which is no longer worth a
92
+ // warning; otherwise warn once per relation per client, not once per query.
93
+ if (!(this.options.incrementalHistory === true && this.snapshots.hasHistoryCache) && !this.warnedReplayRelations.has(query.relation)) {
94
+ this.warnedReplayRelations.add(query.relation);
95
+ (this.options.logger ?? console).warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
83
96
  + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
84
97
  + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
85
98
  + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed"
86
99
  + (this.options.incrementalHistory === true
87
100
  ? "; incrementalHistory is enabled, so later history queries will fetch only new offsets."
88
- : ", or enable the incrementalHistory option to fetch only new offsets on repeat history queries."));
101
+ : ", or enable the incrementalHistory option to fetch only new offsets on repeat history queries.")
102
+ + ` This warning is logged once per relation; further "${query.relation}" replays stay silent.`);
89
103
  }
90
104
  const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
91
105
  const history = await this.snapshots.readHistoryAsync(endInclusive);
@@ -119,33 +133,68 @@ class DefaultGrpcQueryDataProvider {
119
133
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
120
134
  : createGrpcQueryDataset(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
121
135
  }
122
- const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
123
136
  if (query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes") {
124
137
  // A proven-active where/include on "contracts" (see predicateRequiresHistory/includesRequireHistory)
125
- // can put "contracts" in the closure here without needsHistory being true, so the ACS still has to
126
- // be readotherwise that where/include would silently resolve against an empty row set.
127
- const contractsFragment = closure.has("contracts")
128
- ? mapGrpcQueryRelationFragment([], (await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query))).activeContracts)
129
- : mapGrpcQueryRelationFragment([]);
138
+ // can put "contracts" in the closure here without needsHistory being true. Those rows come from
139
+ // the warmed cache the ACS is never downloaded implicitly.
140
+ const contractsSnapshot = closure.has("contracts") ? await this.requireCachedSnapshotAsync(query) : undefined;
141
+ const endInclusive = contractsSnapshot?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
142
+ const contractsFragment = contractsSnapshot === undefined
143
+ ? mapGrpcQueryRelationFragment([])
144
+ : fragmentFromCachedSnapshot(contractsSnapshot);
130
145
  return createGrpcQueryDataset(contractsFragment, await this.packages.readAllAsync(), endInclusive, this.options.endpointScope ?? "ledger");
131
146
  }
132
147
  else if (query.relation === "watermark") {
148
+ const endInclusive = (await this.options.stateService.getLedgerEndAsync({})).offset;
133
149
  return createGrpcQueryDataset(mapGrpcQueryRelationFragment([]), [], endInclusive, this.options.endpointScope ?? "ledger");
134
150
  }
135
- const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query), pushdownTemplateRefsFor(query));
136
- const fragment = mapGrpcQueryRelationFragment([], active.activeContracts);
137
- if (!requiresPackageMetadata(closure)) {
138
- return fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger", false);
151
+ // Only an active-only "contracts" query can reach here (anything else forces history above), and the
152
+ // cache gate has already failed — the ACS is never downloaded per query, so this is a hard error.
153
+ throw new ContractCacheRequiredError(partiesFor(query));
154
+ }
155
+ async requireCachedSnapshotAsync(query) {
156
+ const snapshot = this.options.contractCache === undefined
157
+ ? undefined
158
+ : await this.options.contractCache.readSnapshotAsync({ parties: partiesFor(query) });
159
+ if (snapshot === undefined) {
160
+ throw new ContractCacheRequiredError(partiesFor(query));
139
161
  }
140
- // Reaching here guarantees query.relation === "contracts" with needsHistory === false, which (per
141
- // predicateRequiresHistory/includesRequireHistory above) is only possible when the closure never
142
- // touches "exercises"/"exerciseTypes"/"packages"/"transactions"/"events" — those unconditionally force
143
- // history. So requiresPackageMetadata(closure) here can only be "contractTypes", and every contract in
144
- // this ACS-only fragment already carries its own packageName — no Package Service call needed.
145
- const packageMetadata = contractTypeMetadataFromCreations(fragment.creationIdentities);
146
- return createGrpcQueryDataset(fragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
162
+ return snapshot;
147
163
  }
148
164
  }
165
+ /** Rebuilds an ACS-shaped relation fragment from a cached snapshot, with no transport involved. */
166
+ function fragmentFromCachedSnapshot(snapshot) {
167
+ const metadataByContractId = new Map(snapshot.creationMetadata.map((entry) => [entry.contractId, entry]));
168
+ const creationIdentities = snapshot.contracts.map((row) => {
169
+ const metadata = metadataByContractId.get(row.contractId);
170
+ if (metadata === undefined) {
171
+ throw new ValidationError(`Cached contract snapshot is missing creation metadata for ${row.contractId}`);
172
+ }
173
+ return {
174
+ contractId: row.contractId,
175
+ offset: row.createdEventOffset,
176
+ templateId: row.templateId,
177
+ creationPackageId: row.templateId.packageId,
178
+ representativePackageId: metadata.representativePackageId,
179
+ packageName: metadata.packageName,
180
+ payload: row.payload,
181
+ witnesses: row.witnesses,
182
+ createdAt: row.createdAt ?? new Date(0),
183
+ };
184
+ });
185
+ return {
186
+ contracts: snapshot.contracts,
187
+ transactions: [],
188
+ events: [],
189
+ exercises: [],
190
+ typeIdentities: [],
191
+ packageIdentities: [],
192
+ creationIdentities,
193
+ // Only the contract ids are consumed (they mark creating transactions as legitimately absent, so the
194
+ // createdTransaction edge is flagged incomplete instead of pretending to be empty).
195
+ activeContractIdentities: snapshot.contracts.map((row) => ({ contractId: row.contractId, synchronizerId: "", reassignmentCounter: "0", activationOffset: row.createdEventOffset, activationNodeId: 0 })),
196
+ };
197
+ }
149
198
  function requiresHistory(query) {
150
199
  if (query.relation === "transactions" || query.relation === "events" || query.relation === "exercises") {
151
200
  return true;
@@ -193,110 +242,6 @@ function predicateProvesActive(predicate) {
193
242
  function requiresPackageMetadata(closure) {
194
243
  return closure.has("packages") || closure.has("contractTypes") || closure.has("exercises") || closure.has("exerciseTypes");
195
244
  }
196
- const MAX_PUSHDOWN_TEMPLATE_FILTERS = 25;
197
- /**
198
- * Extracts template filters the ACS request itself can apply, so non-matching contracts are never
199
- * downloaded or materialized. The participant scans its whole ACS either way — the win is wire volume and
200
- * client-side decode/freeze work, not node time. Correctness rule: the evaluator re-applies the complete
201
- * predicate over whatever rows come back, so a pushed filter set only has to be a SUPERSET of possible
202
- * matches — over-fetching is fine, under-fetching never happens because pins are only read from top-level
203
- * AND conjuncts (anything under or/not is ignored) and any single conjunct constrains every matching row.
204
- * Returns undefined (wildcard fetch) when no full package/module/entity pin can be proven or a pinned value
205
- * is not a syntactically valid identifier (a malformed value can never match, but pushing it would make the
206
- * node reject the request instead of returning the empty result the evaluator would produce).
207
- */
208
- function pushdownTemplateRefsFor(query) {
209
- if (query.relation !== "contracts") {
210
- return undefined;
211
- }
212
- const stringValues = (operator, value) => operator === "equals" && typeof value === "string"
213
- ? [value]
214
- : operator === "in" && Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
215
- let packageRefs;
216
- let moduleNames;
217
- let entityNames;
218
- let fqnRefs;
219
- for (const conjunct of flattenAndConjuncts(query.predicate)) {
220
- if (conjunct.kind === "scalar" && conjunct.path.length === 2 && conjunct.path[0] === "templateId") {
221
- const values = stringValues(conjunct.operator, conjunct.value);
222
- if (values === undefined) {
223
- continue;
224
- }
225
- else if (conjunct.path[1] === "packageId") {
226
- packageRefs ??= values;
227
- }
228
- else if (conjunct.path[1] === "moduleName") {
229
- moduleNames ??= values;
230
- }
231
- else if (conjunct.path[1] === "entityName") {
232
- entityNames ??= values;
233
- }
234
- }
235
- else if (conjunct.kind === "relation" && conjunct.edge === "contractType" && conjunct.quantifier === "one") {
236
- for (const inner of flattenAndConjuncts(conjunct.predicate)) {
237
- if (inner.kind !== "scalar" || inner.path.length !== 1) {
238
- continue;
239
- }
240
- const values = stringValues(inner.operator, inner.value);
241
- if (values === undefined) {
242
- continue;
243
- }
244
- else if (inner.path[0] === "packageName") {
245
- packageRefs ??= values.map((name) => `#${name}`);
246
- }
247
- else if (inner.path[0] === "moduleName") {
248
- moduleNames ??= values;
249
- }
250
- else if (inner.path[0] === "entityName") {
251
- entityNames ??= values;
252
- }
253
- else if (inner.path[0] === "templateFqn") {
254
- const triples = values.map(templateRefFromFqn);
255
- if (triples.every((triple) => triple !== undefined)) {
256
- fqnRefs ??= triples;
257
- }
258
- }
259
- }
260
- }
261
- }
262
- const refs = fqnRefs ?? (packageRefs !== undefined && moduleNames !== undefined && entityNames !== undefined
263
- ? packageRefs.flatMap((packageId) => moduleNames.flatMap((moduleName) => entityNames.map((entityName) => ({ packageId, moduleName, entityName }))))
264
- : undefined);
265
- return refs !== undefined && refs.length > 0 && refs.length <= MAX_PUSHDOWN_TEMPLATE_FILTERS && refs.every(isValidTemplateRef)
266
- ? refs
267
- : undefined;
268
- }
269
- function flattenAndConjuncts(predicate) {
270
- if (predicate === undefined) {
271
- return [];
272
- }
273
- else if (predicate.kind === "and") {
274
- return predicate.children.flatMap(flattenAndConjuncts);
275
- }
276
- return [predicate];
277
- }
278
- function templateRefFromFqn(fqn) {
279
- const parts = fqn.split(":");
280
- return parts.length === 3 && parts.every((part) => part.length > 0)
281
- ? { packageId: `#${parts[0]}`, moduleName: parts[1], entityName: parts[2] }
282
- : undefined;
283
- }
284
- function isValidTemplateRef(ref) {
285
- const validPackage = ref.packageId.startsWith("#")
286
- ? /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ref.packageId.slice(1))
287
- : /^[0-9a-f]{64}$/.test(ref.packageId);
288
- if (!validPackage) {
289
- return false;
290
- }
291
- try {
292
- validDottedNameString(ref.moduleName, "template filter module name");
293
- validDottedNameString(ref.entityName, "template filter entity name");
294
- return true;
295
- }
296
- catch {
297
- return false;
298
- }
299
- }
300
245
  function predicateRequiresHistory(relation, predicate) {
301
246
  if (predicate === undefined || predicate.kind === "scalar") {
302
247
  return false;
@@ -377,9 +322,47 @@ function addRelationPath(relation, path, relations) {
377
322
  }
378
323
  }
379
324
  }
380
- function cachedContractsDataset(contracts, offset, instanceId) {
325
+ /**
326
+ * Builds a dataset entirely from a cached snapshot: contract rows plus contractType rows derived from the
327
+ * stored per-contract creation metadata, joined through private keys. No transport calls. History-only
328
+ * edges stay incomplete so anything the snapshot cannot answer still fails loudly instead of lying.
329
+ */
330
+ function cachedContractsDataset(snapshot, instanceId) {
331
+ const offset = snapshot.activeAtOffset;
332
+ const metadataByContractId = new Map(snapshot.creationMetadata.map((entry) => [entry.contractId, entry]));
333
+ const creations = snapshot.contracts.map((row) => {
334
+ const metadata = metadataByContractId.get(row.contractId);
335
+ if (metadata === undefined) {
336
+ throw new ValidationError(`Cached contract snapshot is missing creation metadata for ${row.contractId}`);
337
+ }
338
+ return {
339
+ creationPackageId: row.templateId.packageId,
340
+ representativePackageId: metadata.representativePackageId,
341
+ packageName: metadata.packageName,
342
+ templateId: row.templateId,
343
+ };
344
+ });
345
+ const typeRowsByPk = new Map();
346
+ for (const pkg of contractTypeMetadataFromCreations(creations)) {
347
+ for (const template of pkg.templates) {
348
+ const pk = canonicalPublicNumericIdentityParts([template.payloadType, template.templateFqn]);
349
+ if (!typeRowsByPk.has(pk)) {
350
+ typeRowsByPk.set(pk, { pk, payloadType: template.payloadType, aliases: template.aliases, packageName: pkg.name, moduleName: template.moduleName, entityName: template.entityName, templateFqn: template.templateFqn });
351
+ }
352
+ }
353
+ }
354
+ const typeRows = [...typeRowsByPk.values()];
355
+ const contractTypeKeys = creations.map((creation) => [canonicalPublicNumericIdentityParts(["template", `${creation.packageName}:${creation.templateId.moduleName}:${creation.templateId.entityName}`])]);
356
+ const typeKeys = typeRows.map((row) => [row.pk]);
381
357
  const empty = [];
382
- return basicDataset({ contracts: contracts, transactions: empty, events: empty, exercises: empty }, offset, instanceId, false);
358
+ const edges = Object.fromEntries(queryRelations.map((relation) => [relation, Object.fromEntries(Object.keys(queryRelationEdges[relation] ?? {}).map((edge) => [edge, { ...cachedEdgePaths(relation, edge), complete: false }]))]));
359
+ edges.contracts.contractType = { privateKeys: { source: contractTypeKeys, target: typeKeys } };
360
+ edges.contractTypes.contracts = { privateKeys: { source: typeKeys, target: contractTypeKeys } };
361
+ return createQueryDataset({
362
+ rows: { contracts: snapshot.contracts, contractTypes: typeRows, events: empty, exercises: empty, exerciseTypes: empty, packages: empty, transactions: empty, watermark: [{ singleton: true, ix: offset, offset, instanceId }] },
363
+ uniqueKeys: { contracts: [["contractId"]], contractTypes: [["pk"]], events: [["pk"]], exercises: [["tpePk", "contractTpePk", "exerciseEventPk", "contractId"]], exerciseTypes: [["pk"]], packages: [["pk"], ["id"]], transactions: [["ix"], ["offset"]], watermark: [["singleton"]] },
364
+ edges: edges,
365
+ });
383
366
  }
384
367
  function fragmentDataset(fragment, offset, instanceId, completeHistoryEdges = true) {
385
368
  return basicDataset(fragment, offset, instanceId, completeHistoryEdges);
@@ -18,6 +18,10 @@ export interface GrpcQuerySnapshotReaderOptions {
18
18
  readonly maxHistoryUpdates?: number;
19
19
  readonly maxActiveContractPages?: number;
20
20
  readonly maxActiveContracts?: number;
21
+ /** Per-request page size for history reads; omitted means the participant's default. */
22
+ readonly historyPageSize?: number;
23
+ /** Per-request page size for ACS reads; omitted means the participant's default. */
24
+ readonly activeContractPageSize?: number;
21
25
  /**
22
26
  * Opt-in: retain the last replayed history window in memory and only fetch offsets past it on later
23
27
  * reads. Off by default because the retained window lives for this reader's lifetime — its RAM cost is
@@ -79,6 +79,7 @@ export class GrpcQuerySnapshotReader {
79
79
  endOffsetInclusive: endInclusive,
80
80
  updateFormat,
81
81
  descendingOrder: false,
82
+ maxPageSize: this.options.historyPageSize,
82
83
  pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
83
84
  };
84
85
  const response = await this.updateService.getUpdatesPageAsync(request);
@@ -146,6 +147,7 @@ export class GrpcQuerySnapshotReader {
146
147
  const request = {
147
148
  activeAtOffset,
148
149
  eventFormat,
150
+ maxPageSize: this.options.activeContractPageSize,
149
151
  pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
150
152
  };
151
153
  const response = await this.stateService.getActiveContractsPageAsync(request);
@@ -196,17 +198,17 @@ export class GrpcQuerySnapshotReader {
196
198
  }
197
199
  }
198
200
  function validateOptions(options) {
199
- const { incrementalHistory = false, ...limits } = options;
201
+ const { incrementalHistory = false, historyPageSize, activeContractPageSize, ...limits } = options;
200
202
  if (typeof incrementalHistory !== "boolean") {
201
203
  throw new ValidationError("incrementalHistory must be a boolean.");
202
204
  }
203
205
  const validated = { ...DEFAULT_LIMITS, ...limits };
204
- for (const [name, value] of Object.entries(validated)) {
206
+ for (const [name, value] of Object.entries({ ...validated, ...(historyPageSize === undefined ? {} : { historyPageSize }), ...(activeContractPageSize === undefined ? {} : { activeContractPageSize }) })) {
205
207
  if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
206
208
  throw new ValidationError(`${name} must be a finite positive integer.`);
207
209
  }
208
210
  }
209
- return Object.freeze({ ...validated, incrementalHistory });
211
+ return Object.freeze({ ...validated, historyPageSize, activeContractPageSize, incrementalHistory });
210
212
  }
211
213
  function createHistoryUpdateFormat() {
212
214
  return {
@@ -73,6 +73,8 @@ export declare function mapGrpcQueryRelationFragment(source: readonly Transactio
73
73
  export declare function createGrpcQueryDataset(fragment: GrpcQueryRelationFragment, packages: readonly GrpcPackageMetadata[], endInclusive: string, instanceId: string): QueryDataset;
74
74
  /** Package payloads required for a contract/history relation plan, excluding creation-only provenance. */
75
75
  export declare function referencedGrpcPackageIds(fragment: GrpcQueryRelationFragment): readonly string[];
76
+ /** The minimal creation facts needed to derive a contract's type metadata without decoding its package. */
77
+ export type GrpcCreationTypeSource = Pick<GrpcQueryCreationIdentity, "creationPackageId" | "representativePackageId" | "packageName" | "templateId">;
76
78
  /**
77
79
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
78
80
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
@@ -80,7 +82,7 @@ export declare function referencedGrpcPackageIds(fragment: GrpcQueryRelationFrag
80
82
  * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
81
83
  * any query that can see the "packages" relation, so it is never observed.
82
84
  */
83
- export declare function contractTypeMetadataFromCreations(creationIdentities: readonly GrpcQueryCreationIdentity[]): readonly GrpcPackageMetadata[];
85
+ export declare function contractTypeMetadataFromCreations(creationIdentities: readonly GrpcCreationTypeSource[]): readonly GrpcPackageMetadata[];
84
86
  /**
85
87
  * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
86
88
  * exercised template, so the event's own packageName names the owner's package and choice/consuming come
@@ -1,5 +1,5 @@
1
1
  import { ContractCountArgs, ContractFindManyArgs, ContractFindUniqueArgs, ContractGroupByArgs, JsonProjectionResult } from "../model-types.js";
2
- import { ContractCacheArgs, ContractCacheResult, QueryClient } from "../query-client.js";
2
+ import { ContractCacheArgs, ContractCacheInspection, ContractCacheResult, QueryClient } from "../query-client.js";
3
3
  import { QuerySource } from "../query-source.js";
4
4
  import { PqsSchemaProfileV1 } from "./pqs-schema-profile.js";
5
5
  export interface PqsQueryExecutor {
@@ -35,6 +35,7 @@ export declare class PqsQueryClient implements QueryClient {
35
35
  $queryRaw<TRow>(sql: string, values?: readonly unknown[]): Promise<readonly TRow[]>;
36
36
  cacheContracts(_args?: ContractCacheArgs): Promise<ContractCacheResult>;
37
37
  invalidateContractsCache(_args?: ContractCacheArgs): Promise<void>;
38
+ inspectContractsCache(_args?: ContractCacheArgs): Promise<ContractCacheInspection | undefined>;
38
39
  private createPhysicalDelegate;
39
40
  private readPhysicalAsync;
40
41
  private countPhysicalAsync;
@@ -55,6 +55,9 @@ export class PqsQueryClient {
55
55
  return { source: QuerySource.pqs, cached: false };
56
56
  }
57
57
  async invalidateContractsCache(_args) { }
58
+ async inspectContractsCache(_args) {
59
+ return undefined;
60
+ }
58
61
  createPhysicalDelegate(relation, hasUnique = true) {
59
62
  const queryRelation = queryRelationForPqs[relation];
60
63
  const delegate = {
@@ -18,10 +18,28 @@ export type ContractCacheResult = {
18
18
  readonly activeAtOffset: string;
19
19
  readonly contractCount: number;
20
20
  readonly expiresAt: Date;
21
+ /**
22
+ * How this prewarm was satisfied: "full" downloaded the ACS, "delta" patched the previous snapshot
23
+ * forward from the update stream, "noop" found the ledger unchanged and only re-stamped the TTL.
24
+ */
25
+ readonly refresh: "full" | "delta" | "noop";
26
+ /** Ledger-end minus the previous snapshot's offset at refresh time; undefined on a cold prewarm. */
27
+ readonly offsetGap?: string;
28
+ /** Update count applied by a "delta" refresh. */
29
+ readonly deltaUpdateCount?: number;
21
30
  } | {
22
31
  readonly source: QuerySource.pqs;
23
32
  readonly cached: false;
24
33
  };
34
+ /** Measurement of a cached ACS snapshot against the current ledger end, for deciding when to re-warm. */
35
+ export interface ContractCacheInspection {
36
+ readonly activeAtOffset: string;
37
+ readonly ledgerEndOffset: string;
38
+ /** ledgerEndOffset - activeAtOffset: how far the ledger has run past the snapshot. */
39
+ readonly offsetGap: string;
40
+ readonly contractCount: number;
41
+ readonly expiresAt: Date;
42
+ }
25
43
  export interface QueryDelegate<TRow, TWhere, TSelect, TOrderBy, TUnique, TInclude = never, TGroupBy = never, TGroupRow = never> {
26
44
  findMany<TArgs extends FindManyArgs<TWhere, TSelect, TOrderBy, TInclude>>(args?: TArgs): Promise<readonly (TRow & JsonProjectionResult<TArgs>)[]>;
27
45
  findUnique<TArgs extends {
@@ -52,6 +70,8 @@ export interface QueryClient {
52
70
  $queryRaw<TRow>(sql: string, values?: readonly unknown[]): Promise<readonly TRow[]>;
53
71
  cacheContracts(args?: ContractCacheArgs): Promise<ContractCacheResult>;
54
72
  invalidateContractsCache(args?: ContractCacheArgs): Promise<void>;
73
+ /** Measures a warmed snapshot against the current ledger end (undefined when cold or not cache-backed). */
74
+ inspectContractsCache(args?: ContractCacheArgs): Promise<ContractCacheInspection | undefined>;
55
75
  readonly contracts: {
56
76
  findMany<TArgs extends ContractFindManyArgs>(args?: TArgs): Promise<readonly (ContractResult & JsonProjectionResult<TArgs>)[]>;
57
77
  findUnique<TArgs extends ContractFindUniqueArgs>(args: TArgs): Promise<(ContractResult & JsonProjectionResult<TArgs>) | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distrohelena/canton-typescript-sdk",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",