@distrohelena/canton-typescript-sdk 0.1.43 → 0.1.45

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.
@@ -65,12 +65,19 @@ class GrpcContractCache {
65
65
  async populateAsync(parties, key) {
66
66
  const activeContracts = [];
67
67
  const seenPageTokens = new Set();
68
+ // Every continuation request must be identical to the first page's request apart from the page
69
+ // token: the participant validates each token against the request it was minted for, so adopting
70
+ // the response's now-explicit activeAtOffset into page 2+ makes it reject the token
71
+ // (INVALID_ACS_PAGE_TOKEN) — the token was prepared for a request with the field absent. The token
72
+ // itself pins the snapshot offset; the echoed offset is only tracked to validate it stays constant.
73
+ const baseRequest = (0, contracts_mapper_js_1.mapGrpcQueryContractsRequest)(parties === undefined ? { allParties: true } : { parties });
68
74
  let activeAtOffset;
69
75
  let pageToken;
70
76
  do {
71
- const page = materializeActiveContractsPage(await this.stateService.getActiveContractsPageAsync((0, contracts_mapper_js_1.mapGrpcQueryContractsRequest)(parties === undefined
72
- ? { allParties: true, activeAtOffset, pageToken }
73
- : { parties, activeAtOffset, pageToken })));
77
+ const page = materializeActiveContractsPage(await this.stateService.getActiveContractsPageAsync({
78
+ ...baseRequest,
79
+ pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
80
+ }));
74
81
  if (activeAtOffset === undefined) {
75
82
  activeAtOffset = page.activeAtOffset;
76
83
  }
@@ -11,6 +11,7 @@ const query_source_js_1 = require("../query-source.js");
11
11
  const grpc_relation_mapper_js_1 = require("./grpc-relation-mapper.js");
12
12
  const grpc_package_relation_reader_js_1 = require("./grpc-package-relation-reader.js");
13
13
  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");
14
15
  class GrpcQueryClient {
15
16
  options;
16
17
  source = query_source_js_1.QuerySource.grpc;
@@ -66,7 +67,7 @@ class DefaultGrpcQueryDataProvider {
66
67
  packages;
67
68
  constructor(options) {
68
69
  this.options = options;
69
- this.snapshots = new grpc_query_snapshot_reader_js_1.GrpcQuerySnapshotReader(options.stateService, options.updateService);
70
+ this.snapshots = new grpc_query_snapshot_reader_js_1.GrpcQuerySnapshotReader(options.stateService, options.updateService, { incrementalHistory: options.incrementalHistory });
70
71
  this.packages = new grpc_package_relation_reader_js_1.GrpcPackageRelationReader(options.packageService);
71
72
  }
72
73
  async readDatasetAsync(query) {
@@ -80,29 +81,44 @@ class DefaultGrpcQueryDataProvider {
80
81
  return cachedContractsDataset(cached.contracts, cached.activeAtOffset, this.options.endpointScope ?? "ledger");
81
82
  }
82
83
  else if (needsHistory) {
83
- console.warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
84
- + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
85
- + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
86
- + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed.");
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. `
87
+ + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
88
+ + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
89
+ + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed"
90
+ + (this.options.incrementalHistory === true
91
+ ? "; 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."));
93
+ }
87
94
  const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
88
95
  const history = await this.snapshots.readHistoryAsync(endInclusive);
89
96
  const transactions = history.updates.flatMap((response) => response.update.oneofKind === "transaction" ? [response.update.transaction] : []);
90
97
  const fragment = (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)(transactions);
91
- const useCreationsOnlyMetadata = requiresPackageMetadata(closure)
92
- && query.relation !== "packages" && query.relation !== "contractTypes" && query.relation !== "exerciseTypes"
93
- && derivableFromCreationsOnly(closure);
98
+ // Catalog queries rooted at packages/contractTypes/exerciseTypes need every known package's
99
+ // full type inventory, which only the decoded archives provide.
100
+ const catalogRelation = query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes";
101
+ const closureNeedsExercises = closure.has("exercises") || closure.has("exerciseTypes");
102
+ // Non-catalog metadata can usually come straight from the fetched events: creations always name
103
+ // their own package, and direct exercises name their choice owner's. packageMetadataFromEvents
104
+ // returns undefined when the window holds an interface-exercised choice (owner package name is
105
+ // not on the event), and "packages" rows (version) have no event equivalent at all — both fall
106
+ // back to the archive decode.
107
+ const derivedMetadata = requiresPackageMetadata(closure) && !catalogRelation && !closure.has("packages")
108
+ ? closureNeedsExercises
109
+ ? (0, grpc_relation_mapper_js_1.packageMetadataFromEvents)(fragment)
110
+ : (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(fragment.creationIdentities)
111
+ : undefined;
94
112
  const packageMetadata = !requiresPackageMetadata(closure)
95
113
  ? []
96
- : query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes"
114
+ : catalogRelation
97
115
  ? await this.packages.readAllAsync()
98
- : useCreationsOnlyMetadata
99
- ? (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(fragment.creationIdentities)
100
- : await this.packages.readPackagesAsync((0, grpc_relation_mapper_js_1.referencedGrpcPackageIds)(fragment));
101
- // The closure guarantees this query never reads "exercises" here, but the replayed window can still
102
- // contain unrelated exercises (createGrpcQueryDataset always resolves package metadata for every
103
- // exercise in the fragment). contractTypeMetadataFromCreations only knows about templates, so drop
104
- // the exercises this query doesn't need instead of paying for their canonical keys.
105
- const datasetFragment = useCreationsOnlyMetadata ? { ...fragment, exercises: [] } : fragment;
116
+ : derivedMetadata ?? await this.packages.readPackagesAsync((0, grpc_relation_mapper_js_1.referencedGrpcPackageIds)(fragment));
117
+ // When the closure never reads exercises, the replayed window's unrelated exercises are dropped:
118
+ // createGrpcQueryDataset resolves canonical keys for every exercise present, and creations-only
119
+ // metadata knows nothing about choices. When the closure does read exercises, the event-derived
120
+ // metadata covers them and they stay.
121
+ const datasetFragment = derivedMetadata !== undefined && !closureNeedsExercises ? { ...fragment, exercises: [] } : fragment;
106
122
  return packageMetadata.length === 0
107
123
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
108
124
  : (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
@@ -120,7 +136,7 @@ class DefaultGrpcQueryDataProvider {
120
136
  else if (query.relation === "watermark") {
121
137
  return (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)((0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([]), [], endInclusive, this.options.endpointScope ?? "ledger");
122
138
  }
123
- const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query));
139
+ const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query), pushdownTemplateRefsFor(query));
124
140
  const fragment = (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)([], active.activeContracts);
125
141
  if (!requiresPackageMetadata(closure)) {
126
142
  return fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger", false);
@@ -181,23 +197,109 @@ function predicateProvesActive(predicate) {
181
197
  function requiresPackageMetadata(closure) {
182
198
  return closure.has("packages") || closure.has("contractTypes") || closure.has("exercises") || closure.has("exerciseTypes");
183
199
  }
200
+ const MAX_PUSHDOWN_TEMPLATE_FILTERS = 25;
184
201
  /**
185
- * True when contractType metadata can be derived from the fetched creation events alone (see
186
- * contractTypeMetadataFromCreations), instead of decoding LF packages. Every CreatedEvent already carries its
187
- * own packageName, so this always holds whenever the query's closure needs only "contractTypes" (plus
188
- * "contracts"/"transactions"/"events", which carry no package linkage of their own). It does NOT extend to
189
- * "exercises"/"exerciseTypes": ExercisedEvent.packageName documents "the package name of the contract" i.e.
190
- * the template side so it is not a reliable source for the choice owner's package name when a choice was
191
- * exercised via an interface. "packages" is excluded because its own rows (e.g. version) have no event
192
- * equivalent at all.
193
- *
194
- * The caller is responsible for dropping any exercises the fetched fragment happens to contain before handing
195
- * it to createGrpcQueryDataset: a full-history fragment reflects the *entire* replayed window, not just what
196
- * this query's closure touches, and createGrpcQueryDataset unconditionally resolves package metadata for
197
- * every exercise present — this function only knows about templates, not choices.
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).
198
211
  */
199
- function derivableFromCreationsOnly(closure) {
200
- return closure.has("contractTypes") && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages");
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
+ }
201
303
  }
202
304
  function predicateRequiresHistory(relation, predicate) {
203
305
  if (predicate === undefined || predicate.kind === "scalar") {
@@ -9,7 +9,7 @@ const transaction_filter_js_1 = require("../../transports/grpc/generated/canton/
9
9
  const contracts_mapper_js_1 = require("../../transports/grpc/mappers/contracts-mapper.js");
10
10
  const query_dataset_js_1 = require("../canonical/query-dataset.js");
11
11
  const query_snapshot_incomplete_error_js_1 = require("../errors/query-snapshot-incomplete-error.js");
12
- const DEFAULT_OPTIONS = {
12
+ const DEFAULT_LIMITS = {
13
13
  maxHistoryPages: 10_000,
14
14
  maxHistoryUpdates: 1_000_000,
15
15
  maxActiveContractPages: 10_000,
@@ -20,11 +20,16 @@ class GrpcQuerySnapshotReader {
20
20
  stateService;
21
21
  updateService;
22
22
  options;
23
+ historyCache;
23
24
  constructor(stateService, updateService, options = {}) {
24
25
  this.stateService = stateService;
25
26
  this.updateService = updateService;
26
27
  this.options = validateOptions(options);
27
28
  }
29
+ /** Whether an incremental history window is already held, meaning the next read only fetches new offsets. */
30
+ get hasHistoryCache() {
31
+ return this.historyCache !== undefined;
32
+ }
28
33
  async readCurrentHistoryAsync() {
29
34
  const ledgerEnd = await this.stateService.getLedgerEndAsync({});
30
35
  return this.readHistoryAsync(ledgerEnd.offset);
@@ -32,26 +37,49 @@ class GrpcQuerySnapshotReader {
32
37
  async readHistoryAsync(endInclusive) {
33
38
  const end = parseOffset(endInclusive);
34
39
  if (end === undefined) {
35
- throw this.historyError(endInclusive, "invalid-offset");
40
+ throw this.historyError(LEDGER_BEGIN, endInclusive, "invalid-offset");
41
+ }
42
+ const cached = this.options.incrementalHistory ? this.historyCache : undefined;
43
+ // History is append-only, so a cached window ending at or past the requested offset already contains
44
+ // the complete answer: an exact hit is returned as-is, a shorter request is a prefix of the window.
45
+ if (cached !== undefined && cached.end >= end) {
46
+ const updates = cached.end === end
47
+ ? cached.updates
48
+ : cached.updates.filter((update) => {
49
+ const offset = parseOffset(extractUpdateOffset(update));
50
+ return offset !== undefined && offset <= end;
51
+ });
52
+ return freezeSnapshot({ endInclusive, updates: Object.freeze([...updates]) });
53
+ }
54
+ const beginExclusive = cached?.end ?? 0n;
55
+ const snapshot = await this.readHistoryRangeAsync(beginExclusive, end, endInclusive, cached?.updates ?? []);
56
+ if (this.options.incrementalHistory && (this.historyCache === undefined || this.historyCache.end < end)) {
57
+ this.historyCache = { end, updates: snapshot.updates };
36
58
  }
59
+ return snapshot;
60
+ }
61
+ async readHistoryRangeAsync(beginExclusive, end, endInclusive, seed) {
62
+ const begin = beginExclusive.toString();
37
63
  const pruned = await this.stateService.getLatestPrunedOffsetsAsync({});
38
64
  const prunedUpTo = parseOffset(pruned.participantPrunedUpToInclusive);
39
- if (prunedUpTo === undefined || prunedUpTo !== 0n) {
40
- throw this.historyError(endInclusive, "participant-pruned");
65
+ // Offsets at or below beginExclusive are already held (or not requested), so pruning only breaks the
66
+ // read when it reaches past the range start.
67
+ if (prunedUpTo === undefined || prunedUpTo > beginExclusive) {
68
+ throw this.historyError(begin, endInclusive, "participant-pruned");
41
69
  }
42
70
  const updateFormat = freezeDeep(createHistoryUpdateFormat());
43
71
  const updates = [];
44
72
  const observedPageTokens = new Set();
45
- let expectedLowestExclusive = 0n;
73
+ let expectedLowestExclusive = beginExclusive;
46
74
  let pageToken;
47
75
  let pagesRead = 0;
48
76
  let previousUpdateOffset;
49
77
  while (true) {
50
78
  if (pagesRead >= this.options.maxHistoryPages) {
51
- throw this.historyError(endInclusive, "max-pages-exceeded");
79
+ throw this.historyError(begin, endInclusive, "max-pages-exceeded");
52
80
  }
53
81
  const request = {
54
- beginOffsetExclusive: LEDGER_BEGIN,
82
+ beginOffsetExclusive: begin,
55
83
  endOffsetInclusive: endInclusive,
56
84
  updateFormat,
57
85
  descendingOrder: false,
@@ -62,18 +90,18 @@ class GrpcQuerySnapshotReader {
62
90
  const lowest = parseOffset(response.lowestPageOffsetExclusive);
63
91
  const highest = parseOffset(response.highestPageOffsetInclusive);
64
92
  if (lowest === undefined || highest === undefined) {
65
- throw this.historyError(endInclusive, "missing-boundary");
93
+ throw this.historyError(begin, endInclusive, "missing-boundary");
66
94
  }
67
95
  else if (lowest !== expectedLowestExclusive || highest < lowest || highest > end) {
68
- throw this.historyError(endInclusive, "page-boundary-mismatch");
96
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
69
97
  }
70
- if (response.updates.length > this.options.maxHistoryUpdates - updates.length) {
71
- throw this.historyError(endInclusive, "max-updates-exceeded");
98
+ if (response.updates.length > this.options.maxHistoryUpdates - seed.length - updates.length) {
99
+ throw this.historyError(begin, endInclusive, "max-updates-exceeded");
72
100
  }
73
101
  for (const update of response.updates) {
74
102
  const updateOffset = parseOffset(extractUpdateOffset(update));
75
103
  if (updateOffset === undefined || updateOffset <= lowest || updateOffset > highest || (previousUpdateOffset !== undefined && updateOffset <= previousUpdateOffset)) {
76
- throw this.historyError(endInclusive, "page-boundary-mismatch");
104
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
77
105
  }
78
106
  previousUpdateOffset = updateOffset;
79
107
  }
@@ -81,33 +109,36 @@ class GrpcQuerySnapshotReader {
81
109
  const nextPageToken = response.nextPageToken;
82
110
  if (nextPageToken === undefined || nextPageToken.length === 0) {
83
111
  if (highest !== end) {
84
- throw this.historyError(endInclusive, "nonterminal-page-without-token");
112
+ throw this.historyError(begin, endInclusive, "nonterminal-page-without-token");
85
113
  }
86
114
  return freezeSnapshot({
87
115
  endInclusive,
88
- updates: Object.freeze(updates),
116
+ updates: Object.freeze([...seed, ...updates]),
89
117
  });
90
118
  }
91
119
  else if (highest >= end) {
92
- throw this.historyError(endInclusive, "nonterminal-page-reaches-end");
120
+ throw this.historyError(begin, endInclusive, "nonterminal-page-reaches-end");
93
121
  }
94
122
  else if (highest <= lowest) {
95
- throw this.historyError(endInclusive, "page-boundary-mismatch");
123
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
96
124
  }
97
125
  const tokenKey = tokenKeyFor(nextPageToken);
98
126
  if (observedPageTokens.has(tokenKey)) {
99
- throw this.historyError(endInclusive, "repeated-page-token");
127
+ throw this.historyError(begin, endInclusive, "repeated-page-token");
100
128
  }
101
129
  observedPageTokens.add(tokenKey);
102
130
  expectedLowestExclusive = highest;
103
131
  pageToken = Uint8Array.from(nextPageToken);
104
132
  }
105
133
  }
106
- async readActiveContractsAsync(activeAtOffset, parties) {
134
+ async readActiveContractsAsync(activeAtOffset, parties, templateRefs) {
107
135
  if (parseOffset(activeAtOffset) === undefined) {
108
136
  throw this.activeError(activeAtOffset, "invalid-offset");
109
137
  }
110
- const eventFormat = freezeDeep(parties === undefined ? createAllPartiesEventFormat() : (0, contracts_mapper_js_1.mapGrpcQueryContractsRequest)({ parties }).eventFormat);
138
+ const eventFormat = freezeDeep((0, contracts_mapper_js_1.mapGrpcQueryContractsRequest)({
139
+ ...(parties === undefined ? { allParties: true } : { parties }),
140
+ ...(templateRefs === undefined || templateRefs.length === 0 ? {} : { templateRefs: [...templateRefs] }),
141
+ }).eventFormat);
111
142
  const activeContracts = [];
112
143
  const observedPageTokens = new Set();
113
144
  let pageToken;
@@ -152,9 +183,9 @@ class GrpcQuerySnapshotReader {
152
183
  pageToken = Uint8Array.from(nextPageToken);
153
184
  }
154
185
  }
155
- historyError(endInclusive, reason) {
186
+ historyError(beginExclusive, endInclusive, reason) {
156
187
  return new query_snapshot_incomplete_error_js_1.QuerySnapshotIncompleteError({
157
- beginExclusive: LEDGER_BEGIN,
188
+ beginExclusive,
158
189
  endInclusive,
159
190
  reason,
160
191
  });
@@ -170,13 +201,17 @@ class GrpcQuerySnapshotReader {
170
201
  }
171
202
  exports.GrpcQuerySnapshotReader = GrpcQuerySnapshotReader;
172
203
  function validateOptions(options) {
173
- const validated = { ...DEFAULT_OPTIONS, ...options };
204
+ const { incrementalHistory = false, ...limits } = options;
205
+ if (typeof incrementalHistory !== "boolean") {
206
+ throw new validation_error_js_1.ValidationError("incrementalHistory must be a boolean.");
207
+ }
208
+ const validated = { ...DEFAULT_LIMITS, ...limits };
174
209
  for (const [name, value] of Object.entries(validated)) {
175
210
  if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
176
211
  throw new validation_error_js_1.ValidationError(`${name} must be a finite positive integer.`);
177
212
  }
178
213
  }
179
- return Object.freeze(validated);
214
+ return Object.freeze({ ...validated, incrementalHistory });
180
215
  }
181
216
  function createHistoryUpdateFormat() {
182
217
  return {
@@ -4,6 +4,7 @@ exports.mapGrpcQueryRelationFragment = mapGrpcQueryRelationFragment;
4
4
  exports.createGrpcQueryDataset = createGrpcQueryDataset;
5
5
  exports.referencedGrpcPackageIds = referencedGrpcPackageIds;
6
6
  exports.contractTypeMetadataFromCreations = contractTypeMetadataFromCreations;
7
+ exports.packageMetadataFromEvents = packageMetadataFromEvents;
7
8
  const validation_error_js_1 = require("../../core/errors/validation-error.js");
8
9
  const query_dataset_js_1 = require("../canonical/query-dataset.js");
9
10
  const public_identity_js_1 = require("../canonical/public-identity.js");
@@ -248,40 +249,92 @@ function referencedGrpcPackageIds(fragment) {
248
249
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
249
250
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
250
251
  * {contracts, contractTypes} — every contract's own creation event already carries packageName directly,
251
- * so no archive decode is needed. "version" is a placeholder: this path is unreachable from any query that
252
- * can see the "packages" relation, so it is never observed.
252
+ * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
253
+ * any query that can see the "packages" relation, so it is never observed.
253
254
  */
254
255
  function contractTypeMetadataFromCreations(creationIdentities) {
255
256
  const packages = new Map();
256
- for (const creation of creationIdentities) {
257
- const packageId = creation.representativePackageId ?? creation.creationPackageId;
258
- const { moduleName, entityName } = creation.templateId;
259
- let pkg = packages.get(packageId);
260
- if (pkg === undefined) {
261
- pkg = { name: creation.packageName, templates: new Map() };
262
- packages.set(packageId, pkg);
263
- }
264
- else if (pkg.name !== creation.packageName) {
265
- throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
266
- }
267
- const identity = `${moduleName}${entityName}`;
268
- if (!pkg.templates.has(identity)) {
269
- const templateFqn = `${pkg.name}:${moduleName}:${entityName}`;
270
- pkg.templates.set(identity, Object.freeze({
271
- moduleName,
272
- entityName,
273
- payloadType: "template",
274
- aliases: Object.freeze([templateFqn, `${moduleName}:${entityName}`, entityName]),
275
- templateFqn,
276
- choices: Object.freeze([]),
277
- }));
257
+ addDerivedCreationEntries(packages, creationIdentities);
258
+ return finalizeDerivedPackages(packages);
259
+ }
260
+ /**
261
+ * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
262
+ * exercised template, so the event's own packageName names the owner's package and choice/consuming come
263
+ * straight off the event. Returns undefined when the window contains an interface-exercised choice — its
264
+ * owner is the interface, whose package name the event does not carry, so only the decoded archive can
265
+ * produce its canonical choiceFqn. Unobserved choices are simply absent, which is complete for query plans
266
+ * that reach exerciseTypes only through observed exercises (never for exerciseTypes catalog queries).
267
+ */
268
+ function packageMetadataFromEvents(fragment) {
269
+ if (fragment.typeIdentities.some((identity) => identity.choice !== undefined && identity.packageName === undefined)) {
270
+ return undefined;
271
+ }
272
+ const packages = new Map();
273
+ addDerivedCreationEntries(packages, fragment.creationIdentities);
274
+ for (const identity of fragment.typeIdentities) {
275
+ if (identity.packageName === undefined) {
276
+ throw new validation_error_js_1.ValidationError("gRPC query contract type identity is missing its package name");
277
+ }
278
+ const template = derivedTemplateFor(derivedPackageFor(packages, identity.packageId, identity.packageName), identity.templateId.moduleName, identity.templateId.entityName);
279
+ if (identity.choice !== undefined) {
280
+ const consuming = template.choices.get(identity.choice);
281
+ if (consuming !== undefined && consuming !== identity.consuming) {
282
+ throw new validation_error_js_1.ValidationError(`gRPC query choice ${identity.choice} reports conflicting consuming flags`);
283
+ }
284
+ template.choices.set(identity.choice, identity.consuming === true);
278
285
  }
279
286
  }
287
+ return finalizeDerivedPackages(packages);
288
+ }
289
+ function addDerivedCreationEntries(packages, creationIdentities) {
290
+ for (const creation of creationIdentities) {
291
+ derivedTemplateFor(derivedPackageFor(packages, creation.representativePackageId ?? creation.creationPackageId, creation.packageName), creation.templateId.moduleName, creation.templateId.entityName);
292
+ }
293
+ }
294
+ function derivedPackageFor(packages, packageId, packageName) {
295
+ const existing = packages.get(packageId);
296
+ if (existing === undefined) {
297
+ const created = { name: packageName, templates: new Map() };
298
+ packages.set(packageId, created);
299
+ return created;
300
+ }
301
+ else if (existing.name !== packageName) {
302
+ throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
303
+ }
304
+ return existing;
305
+ }
306
+ function derivedTemplateFor(pkg, moduleName, entityName) {
307
+ const identity = `${moduleName} ${entityName}`;
308
+ let template = pkg.templates.get(identity);
309
+ if (template === undefined) {
310
+ template = { moduleName, entityName, choices: new Map() };
311
+ pkg.templates.set(identity, template);
312
+ }
313
+ return template;
314
+ }
315
+ function finalizeDerivedPackages(packages) {
280
316
  return Object.freeze([...packages.entries()].map(([id, pkg]) => Object.freeze({
281
317
  id,
282
318
  name: pkg.name,
283
- version: "unresolved",
284
- templates: Object.freeze([...pkg.templates.values()]),
319
+ // Unique per package: two versions of the same package name can both appear in one window, and
320
+ // duplicate name+version pairs are rejected during dataset normalization.
321
+ version: `unresolved-${id}`,
322
+ templates: Object.freeze([...pkg.templates.values()].map((template) => {
323
+ const templateFqn = `${pkg.name}:${template.moduleName}:${template.entityName}`;
324
+ return Object.freeze({
325
+ moduleName: template.moduleName,
326
+ entityName: template.entityName,
327
+ payloadType: "template",
328
+ aliases: Object.freeze([templateFqn, `${template.moduleName}:${template.entityName}`, template.entityName]),
329
+ templateFqn,
330
+ choices: Object.freeze([...template.choices.entries()].map(([choice, consuming]) => Object.freeze({
331
+ choice,
332
+ consuming,
333
+ aliases: Object.freeze([`${templateFqn}:${choice}`, `${template.moduleName}:${template.entityName}:${choice}`, `${template.entityName}:${choice}`, choice]),
334
+ choiceFqn: `${templateFqn}:${choice}`,
335
+ })).sort((left, right) => left.choice.localeCompare(right.choice))),
336
+ });
337
+ })),
285
338
  })));
286
339
  }
287
340
  function compareTemplateMetadata(left, right) {
@@ -304,7 +357,6 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
304
357
  }
305
358
  const packageValues = Array.from(packages);
306
359
  const packageIds = new Set();
307
- const packageNameVersions = new Set();
308
360
  const normalized = [];
309
361
  for (const pkg of packageValues) {
310
362
  if (pkg === null || typeof pkg !== "object") {
@@ -319,11 +371,10 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
319
371
  throw new validation_error_js_1.ValidationError(`gRPC query has duplicate package metadata ${packageId}`);
320
372
  }
321
373
  packageIds.add(packageId);
322
- const nameVersion = `${packageName}\u0000${packageVersion}`;
323
- if (packageNameVersions.has(nameVersion)) {
324
- throw new validation_error_js_1.ValidationError(`gRPC query has duplicate package metadata ${packageName}@${packageVersion}`);
325
- }
326
- packageNameVersions.add(nameVersion);
374
+ // Same name+version under two different ids is NOT rejected: package ids are content-addressed, so a
375
+ // recompiled or re-uploaded DAR legitimately reappears with a new id and an unchanged manifest. The
376
+ // name-derived canonical rows go through deduplicateCanonicalRows, which throws on any genuine
377
+ // metadata conflict for one canonical key — the only conflict that would actually matter.
327
378
  const templatesValue = value.templates;
328
379
  if (!Array.isArray(templatesValue)) {
329
380
  throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} templates are invalid`);
@@ -569,12 +620,19 @@ function identitiesFor(event) {
569
620
  }
570
621
  function typeIdentityRows(event, registry) {
571
622
  const template = requiredTemplate(event.templateId, "event template");
572
- const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId };
623
+ const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId, packageName: event.packageName };
573
624
  if (!isExercised(event)) {
574
625
  return [contract];
575
626
  }
576
627
  const owner = exerciseOwner(event);
577
- return [contract, { pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)), templateId: copyTemplate(owner), packageId: owner.packageId, choice: event.choice, consuming: event.consuming }];
628
+ return [contract, {
629
+ pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)),
630
+ templateId: copyTemplate(owner),
631
+ packageId: owner.packageId,
632
+ ...(event.interfaceId === undefined ? { packageName: event.packageName } : {}),
633
+ choice: event.choice,
634
+ consuming: event.consuming,
635
+ }];
578
636
  }
579
637
  function activeContractEntries(responses) {
580
638
  const entries = responses.map((response) => {
@@ -56,6 +56,17 @@ function createFilters(request) {
56
56
  },
57
57
  });
58
58
  }
59
+ for (const ref of request.templateRefs ?? []) {
60
+ cumulative.push({
61
+ identifierFilter: {
62
+ oneofKind: "templateFilter",
63
+ templateFilter: {
64
+ templateId: { packageId: ref.packageId, moduleName: ref.moduleName, entityName: ref.entityName },
65
+ includeCreatedEventBlob,
66
+ },
67
+ },
68
+ });
69
+ }
59
70
  if (request.interfaceId) {
60
71
  cumulative.push({
61
72
  identifierFilter: {
@@ -61,12 +61,19 @@ export class GrpcContractCache {
61
61
  async populateAsync(parties, key) {
62
62
  const activeContracts = [];
63
63
  const seenPageTokens = new Set();
64
+ // Every continuation request must be identical to the first page's request apart from the page
65
+ // token: the participant validates each token against the request it was minted for, so adopting
66
+ // the response's now-explicit activeAtOffset into page 2+ makes it reject the token
67
+ // (INVALID_ACS_PAGE_TOKEN) — the token was prepared for a request with the field absent. The token
68
+ // itself pins the snapshot offset; the echoed offset is only tracked to validate it stays constant.
69
+ const baseRequest = mapGrpcQueryContractsRequest(parties === undefined ? { allParties: true } : { parties });
64
70
  let activeAtOffset;
65
71
  let pageToken;
66
72
  do {
67
- const page = materializeActiveContractsPage(await this.stateService.getActiveContractsPageAsync(mapGrpcQueryContractsRequest(parties === undefined
68
- ? { allParties: true, activeAtOffset, pageToken }
69
- : { parties, activeAtOffset, pageToken })));
73
+ const page = materializeActiveContractsPage(await this.stateService.getActiveContractsPageAsync({
74
+ ...baseRequest,
75
+ pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
76
+ }));
70
77
  if (activeAtOffset === undefined) {
71
78
  activeAtOffset = page.activeAtOffset;
72
79
  }