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