@distrohelena/canton-typescript-sdk 0.1.46 → 0.1.47
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.
- package/dist/cjs/core/types/canton-logger.js +2 -0
- package/dist/cjs/index.js +10 -6
- package/dist/cjs/query/canton-manager.js +4 -1
- package/dist/cjs/query/errors/contract-cache-required-error.js +21 -0
- package/dist/cjs/query/errors/history-walk-required-error.js +20 -0
- package/dist/cjs/query/grpc/grpc-contract-cache.js +293 -9
- package/dist/cjs/query/grpc/grpc-query-client.js +113 -130
- package/dist/cjs/query/grpc/grpc-query-snapshot-reader.js +5 -3
- package/dist/cjs/query/pqs/pqs-query-client.js +3 -0
- package/dist/core/types/canton-logger.d.ts +4 -0
- package/dist/core/types/canton-logger.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/query/canton-manager-options.d.ts +26 -0
- package/dist/query/canton-manager.js +4 -1
- package/dist/query/errors/contract-cache-required-error.d.ts +11 -0
- package/dist/query/errors/contract-cache-required-error.js +17 -0
- package/dist/query/errors/history-walk-required-error.d.ts +10 -0
- package/dist/query/errors/history-walk-required-error.js +16 -0
- package/dist/query/grpc/grpc-contract-cache.d.ts +40 -3
- package/dist/query/grpc/grpc-contract-cache.js +293 -9
- package/dist/query/grpc/grpc-query-client.d.ts +11 -1
- package/dist/query/grpc/grpc-query-client.js +113 -130
- package/dist/query/grpc/grpc-query-snapshot-reader.d.ts +4 -0
- package/dist/query/grpc/grpc-query-snapshot-reader.js +5 -3
- package/dist/query/grpc/grpc-relation-mapper.d.ts +3 -1
- package/dist/query/pqs/pqs-query-client.d.ts +2 -1
- package/dist/query/pqs/pqs-query-client.js +3 -0
- package/dist/query/query-client.d.ts +20 -0
- 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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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>;
|