@distrohelena/canton-typescript-sdk 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,21 +95,30 @@ class DefaultGrpcQueryDataProvider {
95
95
  const history = await this.snapshots.readHistoryAsync(endInclusive);
96
96
  const transactions = history.updates.flatMap((response) => response.update.oneofKind === "transaction" ? [response.update.transaction] : []);
97
97
  const fragment = (0, grpc_relation_mapper_js_1.mapGrpcQueryRelationFragment)(transactions);
98
- const useCreationsOnlyMetadata = requiresPackageMetadata(closure)
99
- && query.relation !== "packages" && query.relation !== "contractTypes" && query.relation !== "exerciseTypes"
100
- && derivableFromCreationsOnly(closure);
98
+ // Catalog queries rooted at packages/contractTypes/exerciseTypes need every known package's
99
+ // full type inventory, which only the decoded archives provide.
100
+ const catalogRelation = query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes";
101
+ const closureNeedsExercises = closure.has("exercises") || closure.has("exerciseTypes");
102
+ // Non-catalog metadata can usually come straight from the fetched events: creations always name
103
+ // their own package, and direct exercises name their choice owner's. packageMetadataFromEvents
104
+ // returns undefined when the window holds an interface-exercised choice (owner package name is
105
+ // not on the event), and "packages" rows (version) have no event equivalent at all — both fall
106
+ // back to the archive decode.
107
+ const derivedMetadata = requiresPackageMetadata(closure) && !catalogRelation && !closure.has("packages")
108
+ ? closureNeedsExercises
109
+ ? (0, grpc_relation_mapper_js_1.packageMetadataFromEvents)(fragment)
110
+ : (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(fragment.creationIdentities)
111
+ : undefined;
101
112
  const packageMetadata = !requiresPackageMetadata(closure)
102
113
  ? []
103
- : query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes"
114
+ : catalogRelation
104
115
  ? await this.packages.readAllAsync()
105
- : useCreationsOnlyMetadata
106
- ? (0, grpc_relation_mapper_js_1.contractTypeMetadataFromCreations)(fragment.creationIdentities)
107
- : await this.packages.readPackagesAsync((0, grpc_relation_mapper_js_1.referencedGrpcPackageIds)(fragment));
108
- // The closure guarantees this query never reads "exercises" here, but the replayed window can still
109
- // contain unrelated exercises (createGrpcQueryDataset always resolves package metadata for every
110
- // exercise in the fragment). contractTypeMetadataFromCreations only knows about templates, so drop
111
- // the exercises this query doesn't need instead of paying for their canonical keys.
112
- const datasetFragment = useCreationsOnlyMetadata ? { ...fragment, exercises: [] } : fragment;
116
+ : derivedMetadata ?? await this.packages.readPackagesAsync((0, grpc_relation_mapper_js_1.referencedGrpcPackageIds)(fragment));
117
+ // When the closure never reads exercises, the replayed window's unrelated exercises are dropped:
118
+ // createGrpcQueryDataset resolves canonical keys for every exercise present, and creations-only
119
+ // metadata knows nothing about choices. When the closure does read exercises, the event-derived
120
+ // metadata covers them and they stay.
121
+ const datasetFragment = derivedMetadata !== undefined && !closureNeedsExercises ? { ...fragment, exercises: [] } : fragment;
113
122
  return packageMetadata.length === 0
114
123
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
115
124
  : (0, grpc_relation_mapper_js_1.createGrpcQueryDataset)(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
@@ -292,24 +301,6 @@ function isValidTemplateRef(ref) {
292
301
  return false;
293
302
  }
294
303
  }
295
- /**
296
- * True when contractType metadata can be derived from the fetched creation events alone (see
297
- * contractTypeMetadataFromCreations), instead of decoding LF packages. Every CreatedEvent already carries its
298
- * own packageName, so this always holds whenever the query's closure needs only "contractTypes" (plus
299
- * "contracts"/"transactions"/"events", which carry no package linkage of their own). It does NOT extend to
300
- * "exercises"/"exerciseTypes": ExercisedEvent.packageName documents "the package name of the contract" — i.e.
301
- * the template side — so it is not a reliable source for the choice owner's package name when a choice was
302
- * exercised via an interface. "packages" is excluded because its own rows (e.g. version) have no event
303
- * equivalent at all.
304
- *
305
- * The caller is responsible for dropping any exercises the fetched fragment happens to contain before handing
306
- * it to createGrpcQueryDataset: a full-history fragment reflects the *entire* replayed window, not just what
307
- * this query's closure touches, and createGrpcQueryDataset unconditionally resolves package metadata for
308
- * every exercise present — this function only knows about templates, not choices.
309
- */
310
- function derivableFromCreationsOnly(closure) {
311
- return closure.has("contractTypes") && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages");
312
- }
313
304
  function predicateRequiresHistory(relation, predicate) {
314
305
  if (predicate === undefined || predicate.kind === "scalar") {
315
306
  return false;
@@ -4,6 +4,7 @@ exports.mapGrpcQueryRelationFragment = mapGrpcQueryRelationFragment;
4
4
  exports.createGrpcQueryDataset = createGrpcQueryDataset;
5
5
  exports.referencedGrpcPackageIds = referencedGrpcPackageIds;
6
6
  exports.contractTypeMetadataFromCreations = contractTypeMetadataFromCreations;
7
+ exports.packageMetadataFromEvents = packageMetadataFromEvents;
7
8
  const validation_error_js_1 = require("../../core/errors/validation-error.js");
8
9
  const query_dataset_js_1 = require("../canonical/query-dataset.js");
9
10
  const public_identity_js_1 = require("../canonical/public-identity.js");
@@ -248,40 +249,92 @@ function referencedGrpcPackageIds(fragment) {
248
249
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
249
250
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
250
251
  * {contracts, contractTypes} — every contract's own creation event already carries packageName directly,
251
- * so no archive decode is needed. "version" is a placeholder: this path is unreachable from any query that
252
- * can see the "packages" relation, so it is never observed.
252
+ * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
253
+ * any query that can see the "packages" relation, so it is never observed.
253
254
  */
254
255
  function contractTypeMetadataFromCreations(creationIdentities) {
255
256
  const packages = new Map();
256
- for (const creation of creationIdentities) {
257
- const packageId = creation.representativePackageId ?? creation.creationPackageId;
258
- const { moduleName, entityName } = creation.templateId;
259
- let pkg = packages.get(packageId);
260
- if (pkg === undefined) {
261
- pkg = { name: creation.packageName, templates: new Map() };
262
- packages.set(packageId, pkg);
263
- }
264
- else if (pkg.name !== creation.packageName) {
265
- throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
266
- }
267
- const identity = `${moduleName}${entityName}`;
268
- if (!pkg.templates.has(identity)) {
269
- const templateFqn = `${pkg.name}:${moduleName}:${entityName}`;
270
- pkg.templates.set(identity, Object.freeze({
271
- moduleName,
272
- entityName,
273
- payloadType: "template",
274
- aliases: Object.freeze([templateFqn, `${moduleName}:${entityName}`, entityName]),
275
- templateFqn,
276
- choices: Object.freeze([]),
277
- }));
257
+ addDerivedCreationEntries(packages, creationIdentities);
258
+ return finalizeDerivedPackages(packages);
259
+ }
260
+ /**
261
+ * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
262
+ * exercised template, so the event's own packageName names the owner's package and choice/consuming come
263
+ * straight off the event. Returns undefined when the window contains an interface-exercised choice — its
264
+ * owner is the interface, whose package name the event does not carry, so only the decoded archive can
265
+ * produce its canonical choiceFqn. Unobserved choices are simply absent, which is complete for query plans
266
+ * that reach exerciseTypes only through observed exercises (never for exerciseTypes catalog queries).
267
+ */
268
+ function packageMetadataFromEvents(fragment) {
269
+ if (fragment.typeIdentities.some((identity) => identity.choice !== undefined && identity.packageName === undefined)) {
270
+ return undefined;
271
+ }
272
+ const packages = new Map();
273
+ addDerivedCreationEntries(packages, fragment.creationIdentities);
274
+ for (const identity of fragment.typeIdentities) {
275
+ if (identity.packageName === undefined) {
276
+ throw new validation_error_js_1.ValidationError("gRPC query contract type identity is missing its package name");
277
+ }
278
+ const template = derivedTemplateFor(derivedPackageFor(packages, identity.packageId, identity.packageName), identity.templateId.moduleName, identity.templateId.entityName);
279
+ if (identity.choice !== undefined) {
280
+ const consuming = template.choices.get(identity.choice);
281
+ if (consuming !== undefined && consuming !== identity.consuming) {
282
+ throw new validation_error_js_1.ValidationError(`gRPC query choice ${identity.choice} reports conflicting consuming flags`);
283
+ }
284
+ template.choices.set(identity.choice, identity.consuming === true);
278
285
  }
279
286
  }
287
+ return finalizeDerivedPackages(packages);
288
+ }
289
+ function addDerivedCreationEntries(packages, creationIdentities) {
290
+ for (const creation of creationIdentities) {
291
+ derivedTemplateFor(derivedPackageFor(packages, creation.representativePackageId ?? creation.creationPackageId, creation.packageName), creation.templateId.moduleName, creation.templateId.entityName);
292
+ }
293
+ }
294
+ function derivedPackageFor(packages, packageId, packageName) {
295
+ const existing = packages.get(packageId);
296
+ if (existing === undefined) {
297
+ const created = { name: packageName, templates: new Map() };
298
+ packages.set(packageId, created);
299
+ return created;
300
+ }
301
+ else if (existing.name !== packageName) {
302
+ throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
303
+ }
304
+ return existing;
305
+ }
306
+ function derivedTemplateFor(pkg, moduleName, entityName) {
307
+ const identity = `${moduleName} ${entityName}`;
308
+ let template = pkg.templates.get(identity);
309
+ if (template === undefined) {
310
+ template = { moduleName, entityName, choices: new Map() };
311
+ pkg.templates.set(identity, template);
312
+ }
313
+ return template;
314
+ }
315
+ function finalizeDerivedPackages(packages) {
280
316
  return Object.freeze([...packages.entries()].map(([id, pkg]) => Object.freeze({
281
317
  id,
282
318
  name: pkg.name,
283
- version: "unresolved",
284
- templates: Object.freeze([...pkg.templates.values()]),
319
+ // Unique per package: two versions of the same package name can both appear in one window, and
320
+ // duplicate name+version pairs are rejected during dataset normalization.
321
+ version: `unresolved-${id}`,
322
+ templates: Object.freeze([...pkg.templates.values()].map((template) => {
323
+ const templateFqn = `${pkg.name}:${template.moduleName}:${template.entityName}`;
324
+ return Object.freeze({
325
+ moduleName: template.moduleName,
326
+ entityName: template.entityName,
327
+ payloadType: "template",
328
+ aliases: Object.freeze([templateFqn, `${template.moduleName}:${template.entityName}`, template.entityName]),
329
+ templateFqn,
330
+ choices: Object.freeze([...template.choices.entries()].map(([choice, consuming]) => Object.freeze({
331
+ choice,
332
+ consuming,
333
+ aliases: Object.freeze([`${templateFqn}:${choice}`, `${template.moduleName}:${template.entityName}:${choice}`, `${template.entityName}:${choice}`, choice]),
334
+ choiceFqn: `${templateFqn}:${choice}`,
335
+ })).sort((left, right) => left.choice.localeCompare(right.choice))),
336
+ });
337
+ })),
285
338
  })));
286
339
  }
287
340
  function compareTemplateMetadata(left, right) {
@@ -304,7 +357,6 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
304
357
  }
305
358
  const packageValues = Array.from(packages);
306
359
  const packageIds = new Set();
307
- const packageNameVersions = new Set();
308
360
  const normalized = [];
309
361
  for (const pkg of packageValues) {
310
362
  if (pkg === null || typeof pkg !== "object") {
@@ -319,11 +371,10 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
319
371
  throw new validation_error_js_1.ValidationError(`gRPC query has duplicate package metadata ${packageId}`);
320
372
  }
321
373
  packageIds.add(packageId);
322
- const nameVersion = `${packageName}\u0000${packageVersion}`;
323
- if (packageNameVersions.has(nameVersion)) {
324
- throw new validation_error_js_1.ValidationError(`gRPC query has duplicate package metadata ${packageName}@${packageVersion}`);
325
- }
326
- packageNameVersions.add(nameVersion);
374
+ // Same name+version under two different ids is NOT rejected: package ids are content-addressed, so a
375
+ // recompiled or re-uploaded DAR legitimately reappears with a new id and an unchanged manifest. The
376
+ // name-derived canonical rows go through deduplicateCanonicalRows, which throws on any genuine
377
+ // metadata conflict for one canonical key — the only conflict that would actually matter.
327
378
  const templatesValue = value.templates;
328
379
  if (!Array.isArray(templatesValue)) {
329
380
  throw new validation_error_js_1.ValidationError(`gRPC query package ${packageId} templates are invalid`);
@@ -569,12 +620,19 @@ function identitiesFor(event) {
569
620
  }
570
621
  function typeIdentityRows(event, registry) {
571
622
  const template = requiredTemplate(event.templateId, "event template");
572
- const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId };
623
+ const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId, packageName: event.packageName };
573
624
  if (!isExercised(event)) {
574
625
  return [contract];
575
626
  }
576
627
  const owner = exerciseOwner(event);
577
- return [contract, { pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)), templateId: copyTemplate(owner), packageId: owner.packageId, choice: event.choice, consuming: event.consuming }];
628
+ return [contract, {
629
+ pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)),
630
+ templateId: copyTemplate(owner),
631
+ packageId: owner.packageId,
632
+ ...(event.interfaceId === undefined ? { packageName: event.packageName } : {}),
633
+ choice: event.choice,
634
+ consuming: event.consuming,
635
+ }];
578
636
  }
579
637
  function activeContractEntries(responses) {
580
638
  const entries = responses.map((response) => {
@@ -5,7 +5,7 @@ import { createQueryDataset } from "../canonical/query-dataset.js";
5
5
  import { normalizeAggregate, normalizeCount, normalizeFindMany, normalizeFindUnique, normalizeGroupBy } from "../canonical/query-normalizer.js";
6
6
  import { queryRelationEdges, queryRelations } from "../canonical/query-schema.js";
7
7
  import { QuerySource } from "../query-source.js";
8
- import { contractTypeMetadataFromCreations, createGrpcQueryDataset, mapGrpcQueryRelationFragment, referencedGrpcPackageIds } from "./grpc-relation-mapper.js";
8
+ import { contractTypeMetadataFromCreations, createGrpcQueryDataset, mapGrpcQueryRelationFragment, packageMetadataFromEvents, referencedGrpcPackageIds } from "./grpc-relation-mapper.js";
9
9
  import { GrpcPackageRelationReader } from "./grpc-package-relation-reader.js";
10
10
  import { GrpcQuerySnapshotReader } from "./grpc-query-snapshot-reader.js";
11
11
  import { validDottedNameString } from "./grpc-query-value-mapper.js";
@@ -91,21 +91,30 @@ class DefaultGrpcQueryDataProvider {
91
91
  const history = await this.snapshots.readHistoryAsync(endInclusive);
92
92
  const transactions = history.updates.flatMap((response) => response.update.oneofKind === "transaction" ? [response.update.transaction] : []);
93
93
  const fragment = mapGrpcQueryRelationFragment(transactions);
94
- const useCreationsOnlyMetadata = requiresPackageMetadata(closure)
95
- && query.relation !== "packages" && query.relation !== "contractTypes" && query.relation !== "exerciseTypes"
96
- && derivableFromCreationsOnly(closure);
94
+ // Catalog queries rooted at packages/contractTypes/exerciseTypes need every known package's
95
+ // full type inventory, which only the decoded archives provide.
96
+ const catalogRelation = query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes";
97
+ const closureNeedsExercises = closure.has("exercises") || closure.has("exerciseTypes");
98
+ // Non-catalog metadata can usually come straight from the fetched events: creations always name
99
+ // their own package, and direct exercises name their choice owner's. packageMetadataFromEvents
100
+ // returns undefined when the window holds an interface-exercised choice (owner package name is
101
+ // not on the event), and "packages" rows (version) have no event equivalent at all — both fall
102
+ // back to the archive decode.
103
+ const derivedMetadata = requiresPackageMetadata(closure) && !catalogRelation && !closure.has("packages")
104
+ ? closureNeedsExercises
105
+ ? packageMetadataFromEvents(fragment)
106
+ : contractTypeMetadataFromCreations(fragment.creationIdentities)
107
+ : undefined;
97
108
  const packageMetadata = !requiresPackageMetadata(closure)
98
109
  ? []
99
- : query.relation === "packages" || query.relation === "contractTypes" || query.relation === "exerciseTypes"
110
+ : catalogRelation
100
111
  ? await this.packages.readAllAsync()
101
- : useCreationsOnlyMetadata
102
- ? contractTypeMetadataFromCreations(fragment.creationIdentities)
103
- : await this.packages.readPackagesAsync(referencedGrpcPackageIds(fragment));
104
- // The closure guarantees this query never reads "exercises" here, but the replayed window can still
105
- // contain unrelated exercises (createGrpcQueryDataset always resolves package metadata for every
106
- // exercise in the fragment). contractTypeMetadataFromCreations only knows about templates, so drop
107
- // the exercises this query doesn't need instead of paying for their canonical keys.
108
- const datasetFragment = useCreationsOnlyMetadata ? { ...fragment, exercises: [] } : fragment;
112
+ : derivedMetadata ?? await this.packages.readPackagesAsync(referencedGrpcPackageIds(fragment));
113
+ // When the closure never reads exercises, the replayed window's unrelated exercises are dropped:
114
+ // createGrpcQueryDataset resolves canonical keys for every exercise present, and creations-only
115
+ // metadata knows nothing about choices. When the closure does read exercises, the event-derived
116
+ // metadata covers them and they stay.
117
+ const datasetFragment = derivedMetadata !== undefined && !closureNeedsExercises ? { ...fragment, exercises: [] } : fragment;
109
118
  return packageMetadata.length === 0
110
119
  ? fragmentDataset(fragment, endInclusive, this.options.endpointScope ?? "ledger")
111
120
  : createGrpcQueryDataset(datasetFragment, packageMetadata, endInclusive, this.options.endpointScope ?? "ledger");
@@ -288,24 +297,6 @@ function isValidTemplateRef(ref) {
288
297
  return false;
289
298
  }
290
299
  }
291
- /**
292
- * True when contractType metadata can be derived from the fetched creation events alone (see
293
- * contractTypeMetadataFromCreations), instead of decoding LF packages. Every CreatedEvent already carries its
294
- * own packageName, so this always holds whenever the query's closure needs only "contractTypes" (plus
295
- * "contracts"/"transactions"/"events", which carry no package linkage of their own). It does NOT extend to
296
- * "exercises"/"exerciseTypes": ExercisedEvent.packageName documents "the package name of the contract" — i.e.
297
- * the template side — so it is not a reliable source for the choice owner's package name when a choice was
298
- * exercised via an interface. "packages" is excluded because its own rows (e.g. version) have no event
299
- * equivalent at all.
300
- *
301
- * The caller is responsible for dropping any exercises the fetched fragment happens to contain before handing
302
- * it to createGrpcQueryDataset: a full-history fragment reflects the *entire* replayed window, not just what
303
- * this query's closure touches, and createGrpcQueryDataset unconditionally resolves package metadata for
304
- * every exercise present — this function only knows about templates, not choices.
305
- */
306
- function derivableFromCreationsOnly(closure) {
307
- return closure.has("contractTypes") && !closure.has("exercises") && !closure.has("exerciseTypes") && !closure.has("packages");
308
- }
309
300
  function predicateRequiresHistory(relation, predicate) {
310
301
  if (predicate === undefined || predicate.kind === "scalar") {
311
302
  return false;
@@ -11,6 +11,13 @@ export interface GrpcQueryTypeIdentity {
11
11
  entityName: string;
12
12
  }>;
13
13
  readonly packageId: string;
14
+ /**
15
+ * The owning package's name when the event states it reliably: always for contract types
16
+ * (CreatedEvent/ExercisedEvent packageName is the contract's package), and for choice types only on
17
+ * direct exercises — an interface-exercised choice is owned by the interface, whose package name the
18
+ * event does not carry.
19
+ */
20
+ readonly packageName?: string;
14
21
  readonly choice?: string;
15
22
  readonly consuming?: boolean;
16
23
  }
@@ -70,7 +77,16 @@ export declare function referencedGrpcPackageIds(fragment: GrpcQueryRelationFrag
70
77
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
71
78
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
72
79
  * {contracts, contractTypes} — every contract's own creation event already carries packageName directly,
73
- * so no archive decode is needed. "version" is a placeholder: this path is unreachable from any query that
74
- * can see the "packages" relation, so it is never observed.
80
+ * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
81
+ * any query that can see the "packages" relation, so it is never observed.
75
82
  */
76
83
  export declare function contractTypeMetadataFromCreations(creationIdentities: readonly GrpcQueryCreationIdentity[]): readonly GrpcPackageMetadata[];
84
+ /**
85
+ * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
86
+ * exercised template, so the event's own packageName names the owner's package and choice/consuming come
87
+ * straight off the event. Returns undefined when the window contains an interface-exercised choice — its
88
+ * owner is the interface, whose package name the event does not carry, so only the decoded archive can
89
+ * produce its canonical choiceFqn. Unobserved choices are simply absent, which is complete for query plans
90
+ * that reach exerciseTypes only through observed exercises (never for exerciseTypes catalog queries).
91
+ */
92
+ export declare function packageMetadataFromEvents(fragment: Pick<GrpcQueryRelationFragment, "creationIdentities" | "typeIdentities">): readonly GrpcPackageMetadata[] | undefined;
@@ -242,40 +242,92 @@ export function referencedGrpcPackageIds(fragment) {
242
242
  * Derives contractType metadata straight from already-fetched created-contract events instead of the
243
243
  * Package Service. Only valid where the caller has confirmed the query's relation closure is a subset of
244
244
  * {contracts, contractTypes} — every contract's own creation event already carries packageName directly,
245
- * so no archive decode is needed. "version" is a placeholder: this path is unreachable from any query that
246
- * can see the "packages" relation, so it is never observed.
245
+ * so no archive decode is needed. "version" is a per-package placeholder: this path is unreachable from
246
+ * any query that can see the "packages" relation, so it is never observed.
247
247
  */
248
248
  export function contractTypeMetadataFromCreations(creationIdentities) {
249
249
  const packages = new Map();
250
- for (const creation of creationIdentities) {
251
- const packageId = creation.representativePackageId ?? creation.creationPackageId;
252
- const { moduleName, entityName } = creation.templateId;
253
- let pkg = packages.get(packageId);
254
- if (pkg === undefined) {
255
- pkg = { name: creation.packageName, templates: new Map() };
256
- packages.set(packageId, pkg);
257
- }
258
- else if (pkg.name !== creation.packageName) {
259
- throw new ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
260
- }
261
- const identity = `${moduleName}${entityName}`;
262
- if (!pkg.templates.has(identity)) {
263
- const templateFqn = `${pkg.name}:${moduleName}:${entityName}`;
264
- pkg.templates.set(identity, Object.freeze({
265
- moduleName,
266
- entityName,
267
- payloadType: "template",
268
- aliases: Object.freeze([templateFqn, `${moduleName}:${entityName}`, entityName]),
269
- templateFqn,
270
- choices: Object.freeze([]),
271
- }));
250
+ addDerivedCreationEntries(packages, creationIdentities);
251
+ return finalizeDerivedPackages(packages);
252
+ }
253
+ /**
254
+ * Extends the creations-only derivation to exercises: for a direct exercise the choice owner IS the
255
+ * exercised template, so the event's own packageName names the owner's package and choice/consuming come
256
+ * straight off the event. Returns undefined when the window contains an interface-exercised choice — its
257
+ * owner is the interface, whose package name the event does not carry, so only the decoded archive can
258
+ * produce its canonical choiceFqn. Unobserved choices are simply absent, which is complete for query plans
259
+ * that reach exerciseTypes only through observed exercises (never for exerciseTypes catalog queries).
260
+ */
261
+ export function packageMetadataFromEvents(fragment) {
262
+ if (fragment.typeIdentities.some((identity) => identity.choice !== undefined && identity.packageName === undefined)) {
263
+ return undefined;
264
+ }
265
+ const packages = new Map();
266
+ addDerivedCreationEntries(packages, fragment.creationIdentities);
267
+ for (const identity of fragment.typeIdentities) {
268
+ if (identity.packageName === undefined) {
269
+ throw new ValidationError("gRPC query contract type identity is missing its package name");
270
+ }
271
+ const template = derivedTemplateFor(derivedPackageFor(packages, identity.packageId, identity.packageName), identity.templateId.moduleName, identity.templateId.entityName);
272
+ if (identity.choice !== undefined) {
273
+ const consuming = template.choices.get(identity.choice);
274
+ if (consuming !== undefined && consuming !== identity.consuming) {
275
+ throw new ValidationError(`gRPC query choice ${identity.choice} reports conflicting consuming flags`);
276
+ }
277
+ template.choices.set(identity.choice, identity.consuming === true);
272
278
  }
273
279
  }
280
+ return finalizeDerivedPackages(packages);
281
+ }
282
+ function addDerivedCreationEntries(packages, creationIdentities) {
283
+ for (const creation of creationIdentities) {
284
+ derivedTemplateFor(derivedPackageFor(packages, creation.representativePackageId ?? creation.creationPackageId, creation.packageName), creation.templateId.moduleName, creation.templateId.entityName);
285
+ }
286
+ }
287
+ function derivedPackageFor(packages, packageId, packageName) {
288
+ const existing = packages.get(packageId);
289
+ if (existing === undefined) {
290
+ const created = { name: packageName, templates: new Map() };
291
+ packages.set(packageId, created);
292
+ return created;
293
+ }
294
+ else if (existing.name !== packageName) {
295
+ throw new ValidationError(`gRPC query package ${packageId} reports conflicting package names`);
296
+ }
297
+ return existing;
298
+ }
299
+ function derivedTemplateFor(pkg, moduleName, entityName) {
300
+ const identity = `${moduleName} ${entityName}`;
301
+ let template = pkg.templates.get(identity);
302
+ if (template === undefined) {
303
+ template = { moduleName, entityName, choices: new Map() };
304
+ pkg.templates.set(identity, template);
305
+ }
306
+ return template;
307
+ }
308
+ function finalizeDerivedPackages(packages) {
274
309
  return Object.freeze([...packages.entries()].map(([id, pkg]) => Object.freeze({
275
310
  id,
276
311
  name: pkg.name,
277
- version: "unresolved",
278
- templates: Object.freeze([...pkg.templates.values()]),
312
+ // Unique per package: two versions of the same package name can both appear in one window, and
313
+ // duplicate name+version pairs are rejected during dataset normalization.
314
+ version: `unresolved-${id}`,
315
+ templates: Object.freeze([...pkg.templates.values()].map((template) => {
316
+ const templateFqn = `${pkg.name}:${template.moduleName}:${template.entityName}`;
317
+ return Object.freeze({
318
+ moduleName: template.moduleName,
319
+ entityName: template.entityName,
320
+ payloadType: "template",
321
+ aliases: Object.freeze([templateFqn, `${template.moduleName}:${template.entityName}`, template.entityName]),
322
+ templateFqn,
323
+ choices: Object.freeze([...template.choices.entries()].map(([choice, consuming]) => Object.freeze({
324
+ choice,
325
+ consuming,
326
+ aliases: Object.freeze([`${templateFqn}:${choice}`, `${template.moduleName}:${template.entityName}:${choice}`, `${template.entityName}:${choice}`, choice]),
327
+ choiceFqn: `${templateFqn}:${choice}`,
328
+ })).sort((left, right) => left.choice.localeCompare(right.choice))),
329
+ });
330
+ })),
279
331
  })));
280
332
  }
281
333
  function compareTemplateMetadata(left, right) {
@@ -298,7 +350,6 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
298
350
  }
299
351
  const packageValues = Array.from(packages);
300
352
  const packageIds = new Set();
301
- const packageNameVersions = new Set();
302
353
  const normalized = [];
303
354
  for (const pkg of packageValues) {
304
355
  if (pkg === null || typeof pkg !== "object") {
@@ -313,11 +364,10 @@ function normalizeGrpcPackageMetadataUnsafe(packages) {
313
364
  throw new ValidationError(`gRPC query has duplicate package metadata ${packageId}`);
314
365
  }
315
366
  packageIds.add(packageId);
316
- const nameVersion = `${packageName}\u0000${packageVersion}`;
317
- if (packageNameVersions.has(nameVersion)) {
318
- throw new ValidationError(`gRPC query has duplicate package metadata ${packageName}@${packageVersion}`);
319
- }
320
- packageNameVersions.add(nameVersion);
367
+ // Same name+version under two different ids is NOT rejected: package ids are content-addressed, so a
368
+ // recompiled or re-uploaded DAR legitimately reappears with a new id and an unchanged manifest. The
369
+ // name-derived canonical rows go through deduplicateCanonicalRows, which throws on any genuine
370
+ // metadata conflict for one canonical key — the only conflict that would actually matter.
321
371
  const templatesValue = value.templates;
322
372
  if (!Array.isArray(templatesValue)) {
323
373
  throw new ValidationError(`gRPC query package ${packageId} templates are invalid`);
@@ -563,12 +613,19 @@ function identitiesFor(event) {
563
613
  }
564
614
  function typeIdentityRows(event, registry) {
565
615
  const template = requiredTemplate(event.templateId, "event template");
566
- const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId };
616
+ const contract = { pk: registry.get(contractIdentity(template)), templateId: copyTemplate(template), packageId: template.packageId, packageName: event.packageName };
567
617
  if (!isExercised(event)) {
568
618
  return [contract];
569
619
  }
570
620
  const owner = exerciseOwner(event);
571
- return [contract, { pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)), templateId: copyTemplate(owner), packageId: owner.packageId, choice: event.choice, consuming: event.consuming }];
621
+ return [contract, {
622
+ pk: registry.get(exerciseIdentity(owner, event.choice, event.consuming)),
623
+ templateId: copyTemplate(owner),
624
+ packageId: owner.packageId,
625
+ ...(event.interfaceId === undefined ? { packageName: event.packageName } : {}),
626
+ choice: event.choice,
627
+ consuming: event.consuming,
628
+ }];
572
629
  }
573
630
  function activeContractEntries(responses) {
574
631
  const entries = responses.map((response) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distrohelena/canton-typescript-sdk",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",