@distrohelena/canton-typescript-sdk 0.1.45 → 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,25 +1,42 @@
|
|
|
1
1
|
import { isProxy } from "node:util/types";
|
|
2
2
|
import { ValidationError } from "../../core/errors/validation-error.js";
|
|
3
|
+
import { TransactionShape } from "../../transports/grpc/generated/canton/com/daml/ledger/api/v2/transaction_filter.js";
|
|
3
4
|
import { mapGrpcQueryContractsRequest } from "../../transports/grpc/mappers/contracts-mapper.js";
|
|
4
5
|
import { QuerySource } from "../query-source.js";
|
|
5
6
|
import { mapGrpcQueryRelationFragment } from "./grpc-relation-mapper.js";
|
|
6
7
|
import { isCanonicalGrpcOffset } from "./grpc-query-snapshot-reader.js";
|
|
8
|
+
const DELTA_DEFAULT_MAX_OFFSET_GAP = 1_000_000;
|
|
9
|
+
const DELTA_MAX_PAGES = 10_000;
|
|
7
10
|
export class GrpcContractCache {
|
|
8
11
|
stateService;
|
|
9
12
|
store;
|
|
10
13
|
ttlMs;
|
|
11
14
|
endpointScope;
|
|
12
15
|
now;
|
|
16
|
+
maxPageSize;
|
|
17
|
+
updateService;
|
|
18
|
+
delta;
|
|
13
19
|
inflight = new Map();
|
|
14
|
-
constructor(stateService, store, ttlMs, endpointScope, now = Date.now) {
|
|
20
|
+
constructor(stateService, store, ttlMs, endpointScope, now = Date.now, maxPageSize, updateService, delta = {}) {
|
|
15
21
|
this.stateService = stateService;
|
|
16
22
|
this.store = store;
|
|
17
23
|
this.ttlMs = ttlMs;
|
|
18
24
|
this.endpointScope = endpointScope;
|
|
19
25
|
this.now = now;
|
|
26
|
+
this.maxPageSize = maxPageSize;
|
|
27
|
+
this.updateService = updateService;
|
|
28
|
+
this.delta = delta;
|
|
20
29
|
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
21
30
|
throw new ValidationError("Contract cache ttlMs must be a positive finite number.");
|
|
22
31
|
}
|
|
32
|
+
else if (maxPageSize !== undefined && (!Number.isFinite(maxPageSize) || !Number.isInteger(maxPageSize) || maxPageSize <= 0)) {
|
|
33
|
+
throw new ValidationError("Contract cache maxPageSize must be a positive integer.");
|
|
34
|
+
}
|
|
35
|
+
for (const [name, value] of Object.entries({ maxOffsetGap: delta.maxOffsetGap, maxUpdates: delta.maxUpdates })) {
|
|
36
|
+
if (value !== undefined && (!Number.isFinite(value) || !Number.isInteger(value) || value < 0)) {
|
|
37
|
+
throw new ValidationError(`Contract cache delta ${name} must be a non-negative integer.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
}
|
|
24
41
|
async cacheContracts(args) {
|
|
25
42
|
const parties = normalizeParties(args);
|
|
@@ -45,7 +62,7 @@ export class GrpcContractCache {
|
|
|
45
62
|
if (snapshot === undefined) {
|
|
46
63
|
return undefined;
|
|
47
64
|
}
|
|
48
|
-
return Object.freeze({ activeAtOffset: snapshot.activeAtOffset, contracts: snapshot.contracts });
|
|
65
|
+
return Object.freeze({ activeAtOffset: snapshot.activeAtOffset, contracts: snapshot.contracts, creationMetadata: snapshot.creationMetadata });
|
|
49
66
|
}
|
|
50
67
|
async invalidateContractsCache(args) {
|
|
51
68
|
const parties = normalizeParties(args);
|
|
@@ -58,7 +75,186 @@ export class GrpcContractCache {
|
|
|
58
75
|
}
|
|
59
76
|
await this.store.deleteAsync(key);
|
|
60
77
|
}
|
|
78
|
+
/** Measures the cached snapshot against the current ledger end, without changing anything. */
|
|
79
|
+
async inspectContractsCacheAsync(args) {
|
|
80
|
+
const parties = normalizeParties(args);
|
|
81
|
+
const base = asCompatibleSnapshot(await this.store.getAsync(cacheKey(this.endpointScope, parties)), this.endpointScope, parties, currentEpochMs(this.now), true);
|
|
82
|
+
if (base === undefined || this.stateService.getLedgerEndAsync === undefined) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const ledgerEnd = parseCacheOffset((await this.stateService.getLedgerEndAsync({})).offset);
|
|
86
|
+
const activeAt = parseCacheOffset(base.activeAtOffset);
|
|
87
|
+
if (ledgerEnd === undefined || activeAt === undefined || ledgerEnd < activeAt) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
activeAtOffset: base.activeAtOffset,
|
|
92
|
+
ledgerEndOffset: ledgerEnd.toString(),
|
|
93
|
+
offsetGap: (ledgerEnd - activeAt).toString(),
|
|
94
|
+
contractCount: base.contracts.length,
|
|
95
|
+
expiresAt: new Date(base.expiresAtEpochMs),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
61
98
|
async populateAsync(parties, key) {
|
|
99
|
+
// An expired snapshot is still a valid delta base: expiry gates queries, not refreshes.
|
|
100
|
+
const base = asCompatibleSnapshot(await this.store.getAsync(key), this.endpointScope, parties, currentEpochMs(this.now), true);
|
|
101
|
+
if (base !== undefined && this.updateService !== undefined && this.delta.enabled === true) {
|
|
102
|
+
const delta = await this.tryDeltaRefreshAsync(parties, key, base);
|
|
103
|
+
if (delta !== undefined) {
|
|
104
|
+
return delta;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return this.fullRefreshAsync(parties, key, base);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Patches the base snapshot forward from the update stream instead of re-downloading the ACS. Returns
|
|
111
|
+
* undefined — meaning "do the full download instead" — whenever correctness cannot be proven cheaply:
|
|
112
|
+
* pruning past the base offset, an offset gap or update count beyond the configured budgets, any
|
|
113
|
+
* reassignment or topology change in the window (multi-synchronizer moves cannot be patched safely), an
|
|
114
|
+
* exercised event where ACS_DELTA promises none, or an archive/create inconsistent with the base rows.
|
|
115
|
+
*/
|
|
116
|
+
async tryDeltaRefreshAsync(parties, key, base) {
|
|
117
|
+
if (this.stateService.getLedgerEndAsync === undefined || this.stateService.getLatestPrunedOffsetsAsync === undefined || this.updateService === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const baseOffset = parseCacheOffset(base.activeAtOffset);
|
|
122
|
+
const pruned = parseCacheOffset((await this.stateService.getLatestPrunedOffsetsAsync({})).participantPrunedUpToInclusive);
|
|
123
|
+
const ledgerEnd = parseCacheOffset((await this.stateService.getLedgerEndAsync({})).offset);
|
|
124
|
+
if (baseOffset === undefined || pruned === undefined || pruned > baseOffset || ledgerEnd === undefined || ledgerEnd < baseOffset) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const offsetGap = ledgerEnd - baseOffset;
|
|
128
|
+
if (offsetGap === 0n) {
|
|
129
|
+
return this.writeSnapshotAsync(parties, key, base.activeAtOffset, base.contracts, base.creationMetadata, { refresh: "noop", offsetGap: "0" });
|
|
130
|
+
}
|
|
131
|
+
const maxOffsetGap = BigInt(this.delta.maxOffsetGap ?? DELTA_DEFAULT_MAX_OFFSET_GAP);
|
|
132
|
+
if (offsetGap > maxOffsetGap) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const window = await this.readDeltaWindowAsync(parties, base, ledgerEnd.toString());
|
|
136
|
+
if (window === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const baseIds = new Set(base.contracts.map((row) => row.contractId));
|
|
140
|
+
const adds = new Map();
|
|
141
|
+
const removes = new Set();
|
|
142
|
+
for (const entry of window.entries) {
|
|
143
|
+
if (entry.kind === "created") {
|
|
144
|
+
if (baseIds.has(entry.event.contractId) || adds.has(entry.event.contractId)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
adds.set(entry.event.contractId, { event: entry.event, synchronizerId: entry.synchronizerId });
|
|
148
|
+
}
|
|
149
|
+
else if (adds.has(entry.contractId)) {
|
|
150
|
+
adds.delete(entry.contractId);
|
|
151
|
+
}
|
|
152
|
+
else if (baseIds.has(entry.contractId)) {
|
|
153
|
+
removes.add(entry.contractId);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const survivors = [...adds.values()].map(({ event, synchronizerId }) => ({
|
|
160
|
+
contractEntry: {
|
|
161
|
+
oneofKind: "activeContract",
|
|
162
|
+
activeContract: { createdEvent: event, synchronizerId, reassignmentCounter: "0" },
|
|
163
|
+
},
|
|
164
|
+
}));
|
|
165
|
+
const fragment = mapGrpcQueryRelationFragment([], survivors);
|
|
166
|
+
const contracts = [...base.contracts.filter((row) => !removes.has(row.contractId)), ...fragment.contracts]
|
|
167
|
+
.sort((left, right) => left.contractId.localeCompare(right.contractId));
|
|
168
|
+
const addedMetadata = fragment.creationIdentities.map((identity) => ({
|
|
169
|
+
contractId: identity.contractId,
|
|
170
|
+
packageName: identity.packageName,
|
|
171
|
+
representativePackageId: identity.representativePackageId,
|
|
172
|
+
}));
|
|
173
|
+
const creationMetadata = [...base.creationMetadata.filter((entry) => !removes.has(entry.contractId)), ...addedMetadata]
|
|
174
|
+
.sort((left, right) => left.contractId.localeCompare(right.contractId));
|
|
175
|
+
return await this.writeSnapshotAsync(parties, key, ledgerEnd.toString(), contracts, creationMetadata, {
|
|
176
|
+
refresh: "delta",
|
|
177
|
+
offsetGap: offsetGap.toString(),
|
|
178
|
+
deltaUpdateCount: window.entries.length,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Any validation failure in the window falls back to the full download, which re-validates from scratch.
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async readDeltaWindowAsync(parties, base, endInclusive) {
|
|
187
|
+
const eventFormat = mapGrpcQueryContractsRequest(parties === undefined ? { allParties: true } : { parties }).eventFormat;
|
|
188
|
+
const maxUpdates = this.delta.maxUpdates ?? Math.max(1_000, 2 * base.contracts.length);
|
|
189
|
+
const entries = [];
|
|
190
|
+
const seenPageTokens = new Set();
|
|
191
|
+
const end = parseCacheOffset(endInclusive);
|
|
192
|
+
let expectedLowestExclusive = parseCacheOffset(base.activeAtOffset);
|
|
193
|
+
let pageToken;
|
|
194
|
+
let updatesSeen = 0;
|
|
195
|
+
for (let pagesRead = 0; pagesRead < DELTA_MAX_PAGES; pagesRead += 1) {
|
|
196
|
+
const request = {
|
|
197
|
+
beginOffsetExclusive: base.activeAtOffset,
|
|
198
|
+
endOffsetInclusive: endInclusive,
|
|
199
|
+
updateFormat: { includeTransactions: { eventFormat, transactionShape: TransactionShape.ACS_DELTA } },
|
|
200
|
+
descendingOrder: false,
|
|
201
|
+
pageToken: pageToken === undefined ? undefined : Uint8Array.from(pageToken),
|
|
202
|
+
};
|
|
203
|
+
const response = await this.updateService.getUpdatesPageAsync(request);
|
|
204
|
+
const lowest = parseCacheOffset(response.lowestPageOffsetExclusive);
|
|
205
|
+
const highest = parseCacheOffset(response.highestPageOffsetInclusive);
|
|
206
|
+
if (lowest === undefined || highest === undefined || lowest !== expectedLowestExclusive || highest < lowest || highest > end) {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
updatesSeen += response.updates.length;
|
|
210
|
+
if (updatesSeen > maxUpdates) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
for (const update of response.updates) {
|
|
214
|
+
const collected = collectDeltaEntries(update, entries);
|
|
215
|
+
if (!collected) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const nextPageToken = response.nextPageToken;
|
|
220
|
+
if (nextPageToken === undefined || nextPageToken.length === 0) {
|
|
221
|
+
return highest === end ? { entries } : undefined;
|
|
222
|
+
}
|
|
223
|
+
else if (highest >= end || highest <= lowest) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const tokenKey = Array.from(nextPageToken).join(",");
|
|
227
|
+
if (seenPageTokens.has(tokenKey)) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
seenPageTokens.add(tokenKey);
|
|
231
|
+
expectedLowestExclusive = highest;
|
|
232
|
+
pageToken = Uint8Array.from(nextPageToken);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
async writeSnapshotAsync(parties, key, activeAtOffset, contracts, creationMetadata, outcome) {
|
|
237
|
+
const expiresAtEpochMs = effectiveExpiryEpochMs(this.now, this.ttlMs);
|
|
238
|
+
const snapshot = {
|
|
239
|
+
version: 2,
|
|
240
|
+
endpointScope: this.endpointScope,
|
|
241
|
+
parties,
|
|
242
|
+
activeAtOffset,
|
|
243
|
+
expiresAtEpochMs,
|
|
244
|
+
contracts: copyRows(contracts),
|
|
245
|
+
creationMetadata: creationMetadata.map((entry) => ({ ...entry })),
|
|
246
|
+
};
|
|
247
|
+
await this.store.setAsync(key, snapshot, this.ttlMs);
|
|
248
|
+
return {
|
|
249
|
+
source: QuerySource.grpc,
|
|
250
|
+
cached: true,
|
|
251
|
+
activeAtOffset,
|
|
252
|
+
contractCount: contracts.length,
|
|
253
|
+
expiresAt: new Date(expiresAtEpochMs),
|
|
254
|
+
...outcome,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async fullRefreshAsync(parties, key, base) {
|
|
62
258
|
const activeContracts = [];
|
|
63
259
|
const seenPageTokens = new Set();
|
|
64
260
|
// Every continuation request must be identical to the first page's request apart from the page
|
|
@@ -66,7 +262,9 @@ export class GrpcContractCache {
|
|
|
66
262
|
// the response's now-explicit activeAtOffset into page 2+ makes it reject the token
|
|
67
263
|
// (INVALID_ACS_PAGE_TOKEN) — the token was prepared for a request with the field absent. The token
|
|
68
264
|
// itself pins the snapshot offset; the echoed offset is only tracked to validate it stays constant.
|
|
69
|
-
const baseRequest = mapGrpcQueryContractsRequest(parties === undefined
|
|
265
|
+
const baseRequest = mapGrpcQueryContractsRequest(parties === undefined
|
|
266
|
+
? { allParties: true, maxPageSize: this.maxPageSize }
|
|
267
|
+
: { parties, maxPageSize: this.maxPageSize });
|
|
70
268
|
let activeAtOffset;
|
|
71
269
|
let pageToken;
|
|
72
270
|
do {
|
|
@@ -90,22 +288,35 @@ export class GrpcContractCache {
|
|
|
90
288
|
seenPageTokens.add(tokenKey);
|
|
91
289
|
}
|
|
92
290
|
} while (pageToken !== undefined && pageToken.length > 0);
|
|
93
|
-
const
|
|
291
|
+
const fragment = mapGrpcQueryRelationFragment([], activeContracts);
|
|
292
|
+
const contracts = fragment.contracts;
|
|
293
|
+
const creationMetadata = fragment.creationIdentities.map((identity) => ({
|
|
294
|
+
contractId: identity.contractId,
|
|
295
|
+
packageName: identity.packageName,
|
|
296
|
+
representativePackageId: identity.representativePackageId,
|
|
297
|
+
}));
|
|
94
298
|
const expiresAtEpochMs = effectiveExpiryEpochMs(this.now, this.ttlMs);
|
|
299
|
+
const baseOffset = base === undefined ? undefined : parseCacheOffset(base.activeAtOffset);
|
|
300
|
+
const newOffset = parseCacheOffset(activeAtOffset);
|
|
95
301
|
const result = {
|
|
96
302
|
source: QuerySource.grpc,
|
|
97
303
|
cached: true,
|
|
98
304
|
activeAtOffset: activeAtOffset,
|
|
99
305
|
contractCount: contracts.length,
|
|
100
306
|
expiresAt: new Date(expiresAtEpochMs),
|
|
307
|
+
refresh: "full",
|
|
308
|
+
...(baseOffset !== undefined && newOffset !== undefined && newOffset >= baseOffset
|
|
309
|
+
? { offsetGap: (newOffset - baseOffset).toString() }
|
|
310
|
+
: {}),
|
|
101
311
|
};
|
|
102
312
|
const snapshot = {
|
|
103
|
-
version:
|
|
313
|
+
version: 2,
|
|
104
314
|
endpointScope: this.endpointScope,
|
|
105
315
|
parties,
|
|
106
316
|
activeAtOffset: activeAtOffset,
|
|
107
317
|
expiresAtEpochMs,
|
|
108
318
|
contracts: copyRows(contracts),
|
|
319
|
+
creationMetadata,
|
|
109
320
|
};
|
|
110
321
|
await this.store.setAsync(key, snapshot, this.ttlMs);
|
|
111
322
|
return result;
|
|
@@ -150,7 +361,43 @@ export function normalizeParties(args) {
|
|
|
150
361
|
function cacheKey(endpointScope, parties) {
|
|
151
362
|
return `grpc-contract-cache:v1:${JSON.stringify([endpointScope, parties])}`;
|
|
152
363
|
}
|
|
153
|
-
|
|
364
|
+
/** Collects created/archived events from one ACS_DELTA update; false means the window cannot be patched. */
|
|
365
|
+
function collectDeltaEntries(update, entries) {
|
|
366
|
+
if (update === null || typeof update !== "object") {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
const oneof = update.update;
|
|
370
|
+
if (oneof === undefined || oneof.oneofKind === "offsetCheckpoint") {
|
|
371
|
+
return oneof !== undefined;
|
|
372
|
+
}
|
|
373
|
+
else if (oneof.oneofKind !== "transaction") {
|
|
374
|
+
// Reassignments and topology changes move contracts between synchronizers/visibility in ways a
|
|
375
|
+
// row-level patch cannot represent safely.
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const transaction = oneof.transaction;
|
|
379
|
+
const synchronizerId = transaction?.synchronizerId;
|
|
380
|
+
if (typeof synchronizerId !== "string" || synchronizerId.length === 0 || !Array.isArray(transaction?.events)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
for (const wrapped of transaction.events) {
|
|
384
|
+
const event = wrapped?.event;
|
|
385
|
+
if (event?.oneofKind === "created" && event.created !== undefined) {
|
|
386
|
+
entries.push({ kind: "created", event: event.created, synchronizerId });
|
|
387
|
+
}
|
|
388
|
+
else if (event?.oneofKind === "archived" && typeof event.archived?.contractId === "string" && event.archived.contractId.length > 0) {
|
|
389
|
+
entries.push({ kind: "archived", contractId: event.archived.contractId });
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
function parseCacheOffset(value) {
|
|
398
|
+
return isCanonicalGrpcOffset(value) ? BigInt(value) : undefined;
|
|
399
|
+
}
|
|
400
|
+
function asCompatibleSnapshot(value, endpointScope, parties, nowEpochMs, ignoreExpiry = false) {
|
|
154
401
|
try {
|
|
155
402
|
if (value === null || typeof value !== "object") {
|
|
156
403
|
return undefined;
|
|
@@ -163,7 +410,8 @@ function asCompatibleSnapshot(value, endpointScope, parties, nowEpochMs) {
|
|
|
163
410
|
const activeAtOffset = candidate.activeAtOffset;
|
|
164
411
|
const expiresAtEpochMs = candidate.expiresAtEpochMs;
|
|
165
412
|
const contracts = materializeContractRows(candidate.contracts);
|
|
166
|
-
|
|
413
|
+
const creationMetadata = materializeCreationMetadata(candidate.creationMetadata, contracts);
|
|
414
|
+
if (version !== 2
|
|
167
415
|
|| storedEndpointScope !== endpointScope
|
|
168
416
|
|| (rawParties !== undefined && storedParties === undefined)
|
|
169
417
|
|| !sameParties(storedParties, parties)
|
|
@@ -174,8 +422,9 @@ function asCompatibleSnapshot(value, endpointScope, parties, nowEpochMs) {
|
|
|
174
422
|
|| typeof expiresAtEpochMs !== "number"
|
|
175
423
|
|| !Number.isFinite(expiresAtEpochMs)
|
|
176
424
|
|| !Number.isFinite(new Date(expiresAtEpochMs).getTime())
|
|
177
|
-
|| expiresAtEpochMs <= nowEpochMs
|
|
178
|
-
|| contracts === undefined
|
|
425
|
+
|| (!ignoreExpiry && expiresAtEpochMs <= nowEpochMs)
|
|
426
|
+
|| contracts === undefined
|
|
427
|
+
|| creationMetadata === undefined) {
|
|
179
428
|
return undefined;
|
|
180
429
|
}
|
|
181
430
|
return {
|
|
@@ -185,6 +434,7 @@ function asCompatibleSnapshot(value, endpointScope, parties, nowEpochMs) {
|
|
|
185
434
|
activeAtOffset,
|
|
186
435
|
expiresAtEpochMs,
|
|
187
436
|
contracts,
|
|
437
|
+
creationMetadata,
|
|
188
438
|
};
|
|
189
439
|
}
|
|
190
440
|
catch {
|
|
@@ -248,6 +498,40 @@ function materializePageToken(value) {
|
|
|
248
498
|
throw new Error("Active-contracts response nextPageToken is invalid.");
|
|
249
499
|
}
|
|
250
500
|
}
|
|
501
|
+
function materializeCreationMetadata(value, contracts) {
|
|
502
|
+
if (contracts === undefined) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
const entries = materializeIndexedValues(value, materializeCreationMetadataEntry);
|
|
506
|
+
if (entries === undefined || entries.some((entry) => entry === undefined)) {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
const materialized = entries;
|
|
510
|
+
// Coherence: exactly one metadata entry per contract row, so the contractType join can never dangle.
|
|
511
|
+
const metadataContractIds = new Set(materialized.map((entry) => entry.contractId));
|
|
512
|
+
if (metadataContractIds.size !== materialized.length
|
|
513
|
+
|| materialized.length !== contracts.length
|
|
514
|
+
|| !contracts.every((row) => metadataContractIds.has(row.contractId))) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
return materialized;
|
|
518
|
+
}
|
|
519
|
+
function materializeCreationMetadataEntry(value) {
|
|
520
|
+
if (value === null || typeof value !== "object") {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
const candidate = value;
|
|
524
|
+
const contractId = candidate.contractId;
|
|
525
|
+
const packageName = candidate.packageName;
|
|
526
|
+
const representativePackageId = candidate.representativePackageId;
|
|
527
|
+
if (typeof contractId !== "string"
|
|
528
|
+
|| typeof packageName !== "string"
|
|
529
|
+
|| packageName.length === 0
|
|
530
|
+
|| (typeof representativePackageId !== "string" && representativePackageId !== null)) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
return { contractId, packageName, representativePackageId };
|
|
534
|
+
}
|
|
251
535
|
function materializeContractRows(value) {
|
|
252
536
|
const rows = materializeIndexedValues(value, materializeContractRow);
|
|
253
537
|
if (rows === undefined) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { PackageServiceClient } from "../../services/package/package-service-client.js";
|
|
2
2
|
import type { StateServiceClient } from "../../services/state/state-service-client.js";
|
|
3
3
|
import type { UpdateServiceClient } from "../../services/update/update-service-client.js";
|
|
4
|
+
import type { CantonLogger } from "../../core/types/canton-logger.js";
|
|
4
5
|
import { type QueryDataset } from "../canonical/query-dataset.js";
|
|
5
6
|
import type { NormalizedAggregateQuery, NormalizedCountQuery, NormalizedFindManyQuery, NormalizedFindUniqueQuery, NormalizedGroupByQuery } from "../canonical/query-ast.js";
|
|
6
|
-
import type { ContractCacheArgs, ContractCacheResult, QueryClient } from "../query-client.js";
|
|
7
|
+
import type { ContractCacheArgs, ContractCacheInspection, ContractCacheResult, QueryClient } from "../query-client.js";
|
|
7
8
|
import { QuerySource } from "../query-source.js";
|
|
8
9
|
import { GrpcContractCache } from "./grpc-contract-cache.js";
|
|
9
10
|
type NormalizedQuery = NormalizedFindManyQuery | NormalizedFindUniqueQuery | NormalizedCountQuery | NormalizedAggregateQuery | NormalizedGroupByQuery;
|
|
@@ -23,6 +24,14 @@ export interface GrpcQueryClientOptions {
|
|
|
23
24
|
* the retained window lives for this client's lifetime and its RAM cost is the full replayed history.
|
|
24
25
|
*/
|
|
25
26
|
readonly incrementalHistory?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in: permit queries that replay ledger history (transactions/events/exercises, and contracts
|
|
29
|
+
* queries reaching archived state). Off by default — such queries throw HistoryWalkRequiredError so the
|
|
30
|
+
* replay cost is never paid implicitly.
|
|
31
|
+
*/
|
|
32
|
+
readonly walkHistory?: boolean;
|
|
33
|
+
/** Receives SDK diagnostics (e.g. the once-per-relation full-replay warning); defaults to console. */
|
|
34
|
+
readonly logger?: CantonLogger;
|
|
26
35
|
}
|
|
27
36
|
export declare class GrpcQueryClient implements QueryClient {
|
|
28
37
|
private readonly options;
|
|
@@ -41,6 +50,7 @@ export declare class GrpcQueryClient implements QueryClient {
|
|
|
41
50
|
$queryRaw<TRow>(_sql: string, _values?: readonly unknown[]): Promise<readonly TRow[]>;
|
|
42
51
|
cacheContracts(args?: ContractCacheArgs): Promise<ContractCacheResult>;
|
|
43
52
|
invalidateContractsCache(args?: ContractCacheArgs): Promise<void>;
|
|
53
|
+
inspectContractsCache(args?: ContractCacheArgs): Promise<ContractCacheInspection | undefined>;
|
|
44
54
|
private delegate;
|
|
45
55
|
private collectionDelegate;
|
|
46
56
|
private execute;
|