@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.
@@ -17,6 +17,12 @@ export interface GrpcQueryClientOptions {
17
17
  readonly packageService: Pick<PackageServiceClient, "listPackagesAsync" | "getPackageAsync">;
18
18
  readonly contractCache?: GrpcContractCache;
19
19
  readonly endpointScope?: string;
20
+ /**
21
+ * Opt-in: after the first history replay, keep the materialized window in memory and only fetch offsets
22
+ * past it on later history queries — turning repeat full replays into delta reads. Off by default because
23
+ * the retained window lives for this client's lifetime and its RAM cost is the full replayed history.
24
+ */
25
+ readonly incrementalHistory?: boolean;
20
26
  }
21
27
  export declare class GrpcQueryClient implements QueryClient {
22
28
  private readonly options;
@@ -5,9 +5,10 @@ import { createQueryDataset } from "../canonical/query-dataset.js";
5
5
  import { normalizeAggregate, normalizeCount, normalizeFindMany, normalizeFindUnique, normalizeGroupBy } from "../canonical/query-normalizer.js";
6
6
  import { queryRelationEdges, queryRelations } from "../canonical/query-schema.js";
7
7
  import { QuerySource } from "../query-source.js";
8
- import { contractTypeMetadataFromCreations, createGrpcQueryDataset, mapGrpcQueryRelationFragment, referencedGrpcPackageIds } from "./grpc-relation-mapper.js";
8
+ import { contractTypeMetadataFromCreations, createGrpcQueryDataset, mapGrpcQueryRelationFragment, packageMetadataFromEvents, referencedGrpcPackageIds } from "./grpc-relation-mapper.js";
9
9
  import { GrpcPackageRelationReader } from "./grpc-package-relation-reader.js";
10
10
  import { GrpcQuerySnapshotReader } from "./grpc-query-snapshot-reader.js";
11
+ import { validDottedNameString } from "./grpc-query-value-mapper.js";
11
12
  export class GrpcQueryClient {
12
13
  options;
13
14
  source = QuerySource.grpc;
@@ -62,7 +63,7 @@ class DefaultGrpcQueryDataProvider {
62
63
  packages;
63
64
  constructor(options) {
64
65
  this.options = options;
65
- this.snapshots = new GrpcQuerySnapshotReader(options.stateService, options.updateService);
66
+ this.snapshots = new GrpcQuerySnapshotReader(options.stateService, options.updateService, { incrementalHistory: options.incrementalHistory });
66
67
  this.packages = new GrpcPackageRelationReader(options.packageService);
67
68
  }
68
69
  async readDatasetAsync(query) {
@@ -76,29 +77,44 @@ class DefaultGrpcQueryDataProvider {
76
77
  return cachedContractsDataset(cached.contracts, cached.activeAtOffset, this.options.endpointScope ?? "ledger");
77
78
  }
78
79
  else if (needsHistory) {
79
- console.warn(`[GrpcQueryClient] Falling back to a full ledger replay from offset 0 for a "${query.relation}" query. `
80
- + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
81
- + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
82
- + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed.");
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. `
83
+ + "This is expensive and should be an extreme edge case. It is usually triggered by a \"contracts\" query "
84
+ + "that does not explicitly prove `active: true` (so archived contracts may be in scope), or by querying "
85
+ + "\"transactions\"/\"events\"/\"exercises\" directly. Add an explicit active:true filter if only current state is needed"
86
+ + (this.options.incrementalHistory === true
87
+ ? "; 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."));
89
+ }
83
90
  const endInclusive = cached?.activeAtOffset ?? (await this.options.stateService.getLedgerEndAsync({})).offset;
84
91
  const history = await this.snapshots.readHistoryAsync(endInclusive);
85
92
  const transactions = history.updates.flatMap((response) => response.update.oneofKind === "transaction" ? [response.update.transaction] : []);
86
93
  const fragment = mapGrpcQueryRelationFragment(transactions);
87
- const useCreationsOnlyMetadata = requiresPackageMetadata(closure)
88
- && query.relation !== "packages" && query.relation !== "contractTypes" && query.relation !== "exerciseTypes"
89
- && derivableFromCreationsOnly(closure);
94
+ // Catalog queries rooted at packages/contractTypes/exerciseTypes need every known package's
95
+ // full type inventory, which only the decoded archives provide.
96
+ const catalogRelation = query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes";
97
+ const closureNeedsExercises = closure.has("exercises") || closure.has("exerciseTypes");
98
+ // Non-catalog metadata can usually come straight from the fetched events: creations always name
99
+ // their own package, and direct exercises name their choice owner's. packageMetadataFromEvents
100
+ // returns undefined when the window holds an interface-exercised choice (owner package name is
101
+ // not on the event), and "packages" rows (version) have no event equivalent at all — both fall
102
+ // back to the archive decode.
103
+ const derivedMetadata = requiresPackageMetadata(closure) && !catalogRelation && !closure.has("packages")
104
+ ? closureNeedsExercises
105
+ ? packageMetadataFromEvents(fragment)
106
+ : contractTypeMetadataFromCreations(fragment.creationIdentities)
107
+ : undefined;
90
108
  const packageMetadata = !requiresPackageMetadata(closure)
91
109
  ? []
92
- : query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes"
110
+ : catalogRelation
93
111
  ? await this.packages.readAllAsync()
94
- : useCreationsOnlyMetadata
95
- ? contractTypeMetadataFromCreations(fragment.creationIdentities)
96
- : await this.packages.readPackagesAsync(referencedGrpcPackageIds(fragment));
97
- // The closure guarantees this query never reads "exercises" here, but the replayed window can still
98
- // contain unrelated exercises (createGrpcQueryDataset always resolves package metadata for every
99
- // exercise in the fragment). contractTypeMetadataFromCreations only knows about templates, so drop
100
- // the exercises this query doesn't need instead of paying for their canonical keys.
101
- const datasetFragment = useCreationsOnlyMetadata ? { ...fragment, exercises: [] } : fragment;
112
+ : derivedMetadata ?? await this.packages.readPackagesAsync(referencedGrpcPackageIds(fragment));
113
+ // When the closure never reads exercises, the replayed window's unrelated exercises are dropped:
114
+ // createGrpcQueryDataset resolves canonical keys for every exercise present, and creations-only
115
+ // metadata knows nothing about choices. When the closure does read exercises, the event-derived
116
+ // metadata covers them and they stay.
117
+ const datasetFragment = derivedMetadata !== undefined && !closureNeedsExercises ? { ...fragment, exercises: [] } : fragment;
102
118
  return packageMetadata.length === 0
103
119
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
104
120
  : createGrpcQueryDataset(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
@@ -116,7 +132,7 @@ class DefaultGrpcQueryDataProvider {
116
132
  else if (query.relation === "watermark") {
117
133
  return createGrpcQueryDataset(mapGrpcQueryRelationFragment([]), [], endInclusive, this.options.endpointScope ?? "ledger");
118
134
  }
119
- const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query));
135
+ const active = await this.snapshots.readActiveContractsAsync(endInclusive, partiesFor(query), pushdownTemplateRefsFor(query));
120
136
  const fragment = mapGrpcQueryRelationFragment([], active.activeContracts);
121
137
  if (!requiresPackageMetadata(closure)) {
122
138
  return fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger", false);
@@ -177,23 +193,109 @@ function predicateProvesActive(predicate) {
177
193
  function requiresPackageMetadata(closure) {
178
194
  return closure.has("packages") || closure.has("contractTypes") || closure.has("exercises") || closure.has("exerciseTypes");
179
195
  }
196
+ const MAX_PUSHDOWN_TEMPLATE_FILTERS = 25;
180
197
  /**
181
- * True when contractType metadata can be derived from the fetched creation events alone (see
182
- * contractTypeMetadataFromCreations), instead of decoding LF packages. Every CreatedEvent already carries its
183
- * own packageName, so this always holds whenever the query's closure needs only "contractTypes" (plus
184
- * "contracts"/"transactions"/"events", which carry no package linkage of their own). It does NOT extend to
185
- * "exercises"/"exerciseTypes": ExercisedEvent.packageName documents "the package name of the contract" i.e.
186
- * the template side so it is not a reliable source for the choice owner's package name when a choice was
187
- * exercised via an interface. "packages" is excluded because its own rows (e.g. version) have no event
188
- * equivalent at all.
189
- *
190
- * The caller is responsible for dropping any exercises the fetched fragment happens to contain before handing
191
- * it to createGrpcQueryDataset: a full-history fragment reflects the *entire* replayed window, not just what
192
- * this query's closure touches, and createGrpcQueryDataset unconditionally resolves package metadata for
193
- * every exercise present — this function only knows about templates, not choices.
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).
194
207
  */
195
- function derivableFromCreationsOnly(closure) {
196
- return closure.has("contractTypes") && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages");
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
+ }
197
299
  }
198
300
  function predicateRequiresHistory(relation, predicate) {
199
301
  if (predicate === undefined || predicate.kind === "scalar") {
@@ -2,6 +2,7 @@ import { StateServiceClient } from "../../services/state/state-service-client.js
2
2
  import { UpdateServiceClient } from "../../services/update/update-service-client.js";
3
3
  import { type GetActiveContractsResponse as GetActiveContractsResponseType } from "../../transports/grpc/generated/canton/com/daml/ledger/api/v2/state_service.js";
4
4
  import { type GetUpdateResponse as GetUpdateResponseType } from "../../transports/grpc/generated/canton/com/daml/ledger/api/v2/update_service.js";
5
+ import { type GrpcQueryTemplateRef } from "../../transports/grpc/mappers/contracts-mapper.js";
5
6
  type StateSnapshotReader = Pick<StateServiceClient, "getLedgerEndAsync" | "getLatestPrunedOffsetsAsync" | "getActiveContractsPageAsync">;
6
7
  type UpdateSnapshotReader = Pick<UpdateServiceClient, "getUpdatesPageAsync">;
7
8
  export interface GrpcHistorySnapshot {
@@ -17,15 +18,25 @@ export interface GrpcQuerySnapshotReaderOptions {
17
18
  readonly maxHistoryUpdates?: number;
18
19
  readonly maxActiveContractPages?: number;
19
20
  readonly maxActiveContracts?: number;
21
+ /**
22
+ * Opt-in: retain the last replayed history window in memory and only fetch offsets past it on later
23
+ * reads. Off by default because the retained window lives for this reader's lifetime — its RAM cost is
24
+ * the full materialized history, bounded only by maxHistoryUpdates.
25
+ */
26
+ readonly incrementalHistory?: boolean;
20
27
  }
21
28
  export declare class GrpcQuerySnapshotReader {
22
29
  private readonly stateService;
23
30
  private readonly updateService;
24
31
  private readonly options;
32
+ private historyCache;
25
33
  constructor(stateService: StateSnapshotReader, updateService: UpdateSnapshotReader, options?: GrpcQuerySnapshotReaderOptions);
34
+ /** Whether an incremental history window is already held, meaning the next read only fetches new offsets. */
35
+ get hasHistoryCache(): boolean;
26
36
  readCurrentHistoryAsync(): Promise<GrpcHistorySnapshot>;
27
37
  readHistoryAsync(endInclusive: string): Promise<GrpcHistorySnapshot>;
28
- readActiveContractsAsync(activeAtOffset: string, parties?: readonly string[]): Promise<GrpcActiveContractSnapshot>;
38
+ private readHistoryRangeAsync;
39
+ readActiveContractsAsync(activeAtOffset: string, parties?: readonly string[], templateRefs?: readonly GrpcQueryTemplateRef[]): Promise<GrpcActiveContractSnapshot>;
29
40
  private historyError;
30
41
  private activeError;
31
42
  }
@@ -5,7 +5,7 @@ import { TransactionShape } from "../../transports/grpc/generated/canton/com/dam
5
5
  import { mapGrpcQueryContractsRequest } from "../../transports/grpc/mappers/contracts-mapper.js";
6
6
  import { immutableQueryValue } from "../canonical/query-dataset.js";
7
7
  import { QuerySnapshotIncompleteError } from "../errors/query-snapshot-incomplete-error.js";
8
- const DEFAULT_OPTIONS = {
8
+ const DEFAULT_LIMITS = {
9
9
  maxHistoryPages: 10_000,
10
10
  maxHistoryUpdates: 1_000_000,
11
11
  maxActiveContractPages: 10_000,
@@ -16,11 +16,16 @@ export class GrpcQuerySnapshotReader {
16
16
  stateService;
17
17
  updateService;
18
18
  options;
19
+ historyCache;
19
20
  constructor(stateService, updateService, options = {}) {
20
21
  this.stateService = stateService;
21
22
  this.updateService = updateService;
22
23
  this.options = validateOptions(options);
23
24
  }
25
+ /** Whether an incremental history window is already held, meaning the next read only fetches new offsets. */
26
+ get hasHistoryCache() {
27
+ return this.historyCache !== undefined;
28
+ }
24
29
  async readCurrentHistoryAsync() {
25
30
  const ledgerEnd = await this.stateService.getLedgerEndAsync({});
26
31
  return this.readHistoryAsync(ledgerEnd.offset);
@@ -28,26 +33,49 @@ export class GrpcQuerySnapshotReader {
28
33
  async readHistoryAsync(endInclusive) {
29
34
  const end = parseOffset(endInclusive);
30
35
  if (end === undefined) {
31
- throw this.historyError(endInclusive, "invalid-offset");
36
+ throw this.historyError(LEDGER_BEGIN, endInclusive, "invalid-offset");
37
+ }
38
+ const cached = this.options.incrementalHistory ? this.historyCache : undefined;
39
+ // History is append-only, so a cached window ending at or past the requested offset already contains
40
+ // the complete answer: an exact hit is returned as-is, a shorter request is a prefix of the window.
41
+ if (cached !== undefined && cached.end >= end) {
42
+ const updates = cached.end === end
43
+ ? cached.updates
44
+ : cached.updates.filter((update) => {
45
+ const offset = parseOffset(extractUpdateOffset(update));
46
+ return offset !== undefined && offset <= end;
47
+ });
48
+ return freezeSnapshot({ endInclusive, updates: Object.freeze([...updates]) });
49
+ }
50
+ const beginExclusive = cached?.end ?? 0n;
51
+ const snapshot = await this.readHistoryRangeAsync(beginExclusive, end, endInclusive, cached?.updates ?? []);
52
+ if (this.options.incrementalHistory && (this.historyCache === undefined || this.historyCache.end < end)) {
53
+ this.historyCache = { end, updates: snapshot.updates };
32
54
  }
55
+ return snapshot;
56
+ }
57
+ async readHistoryRangeAsync(beginExclusive, end, endInclusive, seed) {
58
+ const begin = beginExclusive.toString();
33
59
  const pruned = await this.stateService.getLatestPrunedOffsetsAsync({});
34
60
  const prunedUpTo = parseOffset(pruned.participantPrunedUpToInclusive);
35
- if (prunedUpTo === undefined || prunedUpTo !== 0n) {
36
- throw this.historyError(endInclusive, "participant-pruned");
61
+ // Offsets at or below beginExclusive are already held (or not requested), so pruning only breaks the
62
+ // read when it reaches past the range start.
63
+ if (prunedUpTo === undefined || prunedUpTo > beginExclusive) {
64
+ throw this.historyError(begin, endInclusive, "participant-pruned");
37
65
  }
38
66
  const updateFormat = freezeDeep(createHistoryUpdateFormat());
39
67
  const updates = [];
40
68
  const observedPageTokens = new Set();
41
- let expectedLowestExclusive = 0n;
69
+ let expectedLowestExclusive = beginExclusive;
42
70
  let pageToken;
43
71
  let pagesRead = 0;
44
72
  let previousUpdateOffset;
45
73
  while (true) {
46
74
  if (pagesRead >= this.options.maxHistoryPages) {
47
- throw this.historyError(endInclusive, "max-pages-exceeded");
75
+ throw this.historyError(begin, endInclusive, "max-pages-exceeded");
48
76
  }
49
77
  const request = {
50
- beginOffsetExclusive: LEDGER_BEGIN,
78
+ beginOffsetExclusive: begin,
51
79
  endOffsetInclusive: endInclusive,
52
80
  updateFormat,
53
81
  descendingOrder: false,
@@ -58,18 +86,18 @@ export class GrpcQuerySnapshotReader {
58
86
  const lowest = parseOffset(response.lowestPageOffsetExclusive);
59
87
  const highest = parseOffset(response.highestPageOffsetInclusive);
60
88
  if (lowest === undefined || highest === undefined) {
61
- throw this.historyError(endInclusive, "missing-boundary");
89
+ throw this.historyError(begin, endInclusive, "missing-boundary");
62
90
  }
63
91
  else if (lowest !== expectedLowestExclusive || highest < lowest || highest > end) {
64
- throw this.historyError(endInclusive, "page-boundary-mismatch");
92
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
65
93
  }
66
- if (response.updates.length > this.options.maxHistoryUpdates - updates.length) {
67
- throw this.historyError(endInclusive, "max-updates-exceeded");
94
+ if (response.updates.length > this.options.maxHistoryUpdates - seed.length - updates.length) {
95
+ throw this.historyError(begin, endInclusive, "max-updates-exceeded");
68
96
  }
69
97
  for (const update of response.updates) {
70
98
  const updateOffset = parseOffset(extractUpdateOffset(update));
71
99
  if (updateOffset === undefined || updateOffset <= lowest || updateOffset > highest || (previousUpdateOffset !== undefined && updateOffset <= previousUpdateOffset)) {
72
- throw this.historyError(endInclusive, "page-boundary-mismatch");
100
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
73
101
  }
74
102
  previousUpdateOffset = updateOffset;
75
103
  }
@@ -77,33 +105,36 @@ export class GrpcQuerySnapshotReader {
77
105
  const nextPageToken = response.nextPageToken;
78
106
  if (nextPageToken === undefined || nextPageToken.length === 0) {
79
107
  if (highest !== end) {
80
- throw this.historyError(endInclusive, "nonterminal-page-without-token");
108
+ throw this.historyError(begin, endInclusive, "nonterminal-page-without-token");
81
109
  }
82
110
  return freezeSnapshot({
83
111
  endInclusive,
84
- updates: Object.freeze(updates),
112
+ updates: Object.freeze([...seed, ...updates]),
85
113
  });
86
114
  }
87
115
  else if (highest >= end) {
88
- throw this.historyError(endInclusive, "nonterminal-page-reaches-end");
116
+ throw this.historyError(begin, endInclusive, "nonterminal-page-reaches-end");
89
117
  }
90
118
  else if (highest <= lowest) {
91
- throw this.historyError(endInclusive, "page-boundary-mismatch");
119
+ throw this.historyError(begin, endInclusive, "page-boundary-mismatch");
92
120
  }
93
121
  const tokenKey = tokenKeyFor(nextPageToken);
94
122
  if (observedPageTokens.has(tokenKey)) {
95
- throw this.historyError(endInclusive, "repeated-page-token");
123
+ throw this.historyError(begin, endInclusive, "repeated-page-token");
96
124
  }
97
125
  observedPageTokens.add(tokenKey);
98
126
  expectedLowestExclusive = highest;
99
127
  pageToken = Uint8Array.from(nextPageToken);
100
128
  }
101
129
  }
102
- async readActiveContractsAsync(activeAtOffset, parties) {
130
+ async readActiveContractsAsync(activeAtOffset, parties, templateRefs) {
103
131
  if (parseOffset(activeAtOffset) === undefined) {
104
132
  throw this.activeError(activeAtOffset, "invalid-offset");
105
133
  }
106
- const eventFormat = freezeDeep(parties === undefined ? createAllPartiesEventFormat() : mapGrpcQueryContractsRequest({ parties }).eventFormat);
134
+ const eventFormat = freezeDeep(mapGrpcQueryContractsRequest({
135
+ ...(parties === undefined ? { allParties: true } : { parties }),
136
+ ...(templateRefs === undefined || templateRefs.length === 0 ? {} : { templateRefs: [...templateRefs] }),
137
+ }).eventFormat);
107
138
  const activeContracts = [];
108
139
  const observedPageTokens = new Set();
109
140
  let pageToken;
@@ -148,9 +179,9 @@ export class GrpcQuerySnapshotReader {
148
179
  pageToken = Uint8Array.from(nextPageToken);
149
180
  }
150
181
  }
151
- historyError(endInclusive, reason) {
182
+ historyError(beginExclusive, endInclusive, reason) {
152
183
  return new QuerySnapshotIncompleteError({
153
- beginExclusive: LEDGER_BEGIN,
184
+ beginExclusive,
154
185
  endInclusive,
155
186
  reason,
156
187
  });
@@ -165,13 +196,17 @@ export class GrpcQuerySnapshotReader {
165
196
  }
166
197
  }
167
198
  function validateOptions(options) {
168
- const validated = { ...DEFAULT_OPTIONS, ...options };
199
+ const { incrementalHistory = false, ...limits } = options;
200
+ if (typeof incrementalHistory !== "boolean") {
201
+ throw new ValidationError("incrementalHistory must be a boolean.");
202
+ }
203
+ const validated = { ...DEFAULT_LIMITS, ...limits };
169
204
  for (const [name, value] of Object.entries(validated)) {
170
205
  if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
171
206
  throw new ValidationError(`${name} must be a finite positive integer.`);
172
207
  }
173
208
  }
174
- return Object.freeze(validated);
209
+ return Object.freeze({ ...validated, incrementalHistory });
175
210
  }
176
211
  function createHistoryUpdateFormat() {
177
212
  return {
@@ -11,6 +11,13 @@ export interface GrpcQueryTypeIdentity {
11
11
  entityName: string;
12
12
  }>;
13
13
  readonly packageId: string;
14
+ /**
15
+ * The owning package's name when the event states it reliably: always for contract types
16
+ * (CreatedEvent/ExercisedEvent packageName is the contract's package), and for choice types only on
17
+ * direct exercises — an interface-exercised choice is owned by the interface, whose package name the
18
+ * event does not carry.
19
+ */
20
+ readonly packageName?: string;
14
21
  readonly choice?: string;
15
22
  readonly consuming?: boolean;
16
23
  }
@@ -70,7 +77,16 @@ export declare function referencedGrpcPackageIds(fragment: GrpcQueryRelationFrag
70
77
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
71
78
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
72
79
  * {contracts, contractTypes} — every contract's own creation event already carries packageName directly,
73
- * so no archive decode is needed. "version" is a placeholder: this path is unreachable from any query that
74
- * can see the "packages" relation, so it is never observed.
80
+ * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
81
+ * any query that can see the "packages" relation, so it is never observed.
75
82
  */
76
83
  export declare function contractTypeMetadataFromCreations(creationIdentities: readonly GrpcQueryCreationIdentity[]): readonly GrpcPackageMetadata[];
84
+ /**
85
+ * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
86
+ * exercised template, so the event's own packageName names the owner's package and choice/consuming come
87
+ * straight off the event. Returns undefined when the window contains an interface-exercised choice — its
88
+ * owner is the interface, whose package name the event does not carry, so only the decoded archive can
89
+ * produce its canonical choiceFqn. Unobserved choices are simply absent, which is complete for query plans
90
+ * that reach exerciseTypes only through observed exercises (never for exerciseTypes catalog queries).
91
+ */
92
+ export declare function packageMetadataFromEvents(fragment: Pick<GrpcQueryRelationFragment, "creationIdentities" | "typeIdentities">): readonly GrpcPackageMetadata[] | undefined;