@launchdarkly/js-sdk-common 2.17.0 → 2.18.0
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/CHANGELOG.md +7 -0
- package/dist/cjs/AttributeReference.d.ts +1 -0
- package/dist/cjs/AttributeReference.d.ts.map +1 -1
- package/dist/cjs/Context.d.ts +9 -0
- package/dist/cjs/Context.d.ts.map +1 -1
- package/dist/cjs/api/subsystem/DataSystem/DataSource.d.ts +1 -1
- package/dist/cjs/index.cjs +129 -9
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/internal/events/EventProcessor.d.ts +1 -1
- package/dist/cjs/internal/events/EventProcessor.d.ts.map +1 -1
- package/dist/cjs/internal/events/LDEventSummarizer.d.ts +32 -0
- package/dist/cjs/internal/events/LDEventSummarizer.d.ts.map +1 -0
- package/dist/cjs/internal/events/MultiEventSummarizer.d.ts +13 -0
- package/dist/cjs/internal/events/MultiEventSummarizer.d.ts.map +1 -0
- package/dist/cjs/internal/index.d.ts +1 -0
- package/dist/cjs/internal/index.d.ts.map +1 -1
- package/dist/cjs/internal/json/canonicalize.d.ts +10 -0
- package/dist/cjs/internal/json/canonicalize.d.ts.map +1 -0
- package/dist/cjs/internal/json/index.d.ts +2 -0
- package/dist/cjs/internal/json/index.d.ts.map +1 -0
- package/dist/esm/AttributeReference.d.ts +1 -0
- package/dist/esm/AttributeReference.d.ts.map +1 -1
- package/dist/esm/Context.d.ts +9 -0
- package/dist/esm/Context.d.ts.map +1 -1
- package/dist/esm/api/subsystem/DataSystem/DataSource.d.ts +1 -1
- package/dist/esm/index.mjs +129 -9
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/internal/events/EventProcessor.d.ts +1 -1
- package/dist/esm/internal/events/EventProcessor.d.ts.map +1 -1
- package/dist/esm/internal/events/LDEventSummarizer.d.ts +32 -0
- package/dist/esm/internal/events/LDEventSummarizer.d.ts.map +1 -0
- package/dist/esm/internal/events/MultiEventSummarizer.d.ts +13 -0
- package/dist/esm/internal/events/MultiEventSummarizer.d.ts.map +1 -0
- package/dist/esm/internal/index.d.ts +1 -0
- package/dist/esm/internal/index.d.ts.map +1 -1
- package/dist/esm/internal/json/canonicalize.d.ts +10 -0
- package/dist/esm/internal/json/canonicalize.d.ts.map +1 -0
- package/dist/esm/internal/json/index.d.ts +2 -0
- package/dist/esm/internal/json/index.d.ts.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@launchdarkly/js-sdk-common` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [2.18.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.17.0...js-sdk-common-v2.18.0) (2025-05-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* Add support for per-context summary events. ([#859](https://github.com/launchdarkly/js-core/issues/859)) ([c9fa5c4](https://github.com/launchdarkly/js-core/commit/c9fa5c45f3ac2cbaad2f2e6312d5231c3f671d98))
|
|
11
|
+
|
|
5
12
|
## [2.17.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.16.0...js-sdk-common-v2.17.0) (2025-04-29)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttributeReference.d.ts","sourceRoot":"","sources":["../src/AttributeReference.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAiChE,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAgB,OAAO,UAAC;IAExB;;;OAGG;IACH,SAAgB,aAAa,SAAC;IAE9B;;OAEG;IACH,gBAAuB,gBAAgB,qBAA8B;IAErE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IAEvC;;;;;;;;;;;;;OAaG;gBACgB,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAkC1D,GAAG,CAAC,MAAM,EAAE,eAAe;IA+B3B,YAAY,CAAC,KAAK,EAAE,MAAM;IAIjC,IAAW,KAAK,WAEf;IAED,IAAW,MAAM,IAAI,OAAO,CAE3B;IAEM,OAAO,CAAC,KAAK,EAAE,kBAAkB;
|
|
1
|
+
{"version":3,"file":"AttributeReference.d.ts","sourceRoot":"","sources":["../src/AttributeReference.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAiChE,MAAM,CAAC,OAAO,OAAO,kBAAkB;IACrC,SAAgB,OAAO,UAAC;IAExB;;;OAGG;IACH,SAAgB,aAAa,SAAC;IAE9B;;OAEG;IACH,gBAAuB,gBAAgB,qBAA8B;IAErE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IAEvC;;;;;;;;;;;;;OAaG;gBACgB,YAAY,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe;IAkC1D,GAAG,CAAC,MAAM,EAAE,eAAe;IA+B3B,YAAY,CAAC,KAAK,EAAE,MAAM;IAIjC,IAAW,KAAK,WAEf;IAED,IAAW,MAAM,IAAI,OAAO,CAE3B;IAEM,OAAO,CAAC,KAAK,EAAE,kBAAkB;IAOxC,IAAW,UAAU,aAEpB;CACF"}
|
package/dist/cjs/Context.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export default class Context {
|
|
|
12
12
|
private _wasLegacy;
|
|
13
13
|
private _contexts;
|
|
14
14
|
private _privateAttributeReferences?;
|
|
15
|
+
private _cachedCanonicalJson?;
|
|
15
16
|
readonly kind: string;
|
|
16
17
|
/**
|
|
17
18
|
* Is this a valid context. If a valid context cannot be created, then this flag will be true.
|
|
@@ -91,5 +92,13 @@ export default class Context {
|
|
|
91
92
|
*/
|
|
92
93
|
getContexts(): [string, LDContextCommon][];
|
|
93
94
|
get legacy(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Get the serialized canonical JSON for this context. This is not filtered for use in events.
|
|
97
|
+
*
|
|
98
|
+
* This method will cache the result.
|
|
99
|
+
*
|
|
100
|
+
* @returns The serialized canonical JSON or undefined if it cannot be serialized.
|
|
101
|
+
*/
|
|
102
|
+
canonicalUnfilteredJson(): string | undefined;
|
|
94
103
|
}
|
|
95
104
|
//# sourceMappingURL=Context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Context.d.ts","sourceRoot":"","sources":["../src/Context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAIhB,MAAM,OAAO,CAAC;AACf,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"Context.d.ts","sourceRoot":"","sources":["../src/Context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAIhB,MAAM,OAAO,CAAC;AACf,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAkJtD;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,OAAO,CAAC,QAAQ,CAAC,CAAkB;IAEnC,OAAO,CAAC,QAAQ,CAAkB;IAElC,OAAO,CAAC,OAAO,CAAkB;IAEjC,OAAO,CAAC,UAAU,CAAkB;IAEpC,OAAO,CAAC,SAAS,CAAuC;IAExD,OAAO,CAAC,2BAA2B,CAAC,CAAuC;IAE3E,OAAO,CAAC,oBAAoB,CAAC,CAAS;IAEtC,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAgB,KAAK,EAAE,OAAO,CAAC;IAE/B,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAgB;IAEhD;;;;;;OAMG;IACH,OAAO;IAMP,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAI/B,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAcnC,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAwDpC,OAAO,CAAC,MAAM,CAAC,sBAAsB;IAyBrC,OAAO,CAAC,MAAM,CAAC,eAAe;IAgB9B;;;;;OAKG;WACW,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO;IAiBxD;;;;OAIG;WACW,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS;IAoBlE;;;;;OAKG;IACI,YAAY,CAAC,SAAS,EAAE,kBAAkB,EAAE,IAAI,GAAE,MAAqB,GAAG,GAAG,GAAG,SAAS;IAOhG;;;;OAIG;IACI,GAAG,CAAC,IAAI,GAAE,MAAqB,GAAG,MAAM,GAAG,SAAS;IAI3D;;OAEG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAWhC;IAED;;OAEG;IACH,IAAW,KAAK,IAAI,MAAM,EAAE,CAK3B;IAED;;OAEG;IACH,IAAW,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhD;IAED;;;;OAIG;IACI,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAI5D;;;;;;OAMG;IACI,WAAW,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE;IAOjD,IAAW,MAAM,IAAI,OAAO,CAE3B;IAED;;;;;;OAMG;IACI,uBAAuB,IAAI,MAAM,GAAG,SAAS;CAcrD"}
|
|
@@ -13,7 +13,7 @@ export interface DataSource {
|
|
|
13
13
|
*/
|
|
14
14
|
start(dataCallback: (basis: boolean, data: any) => void, statusCallback: (status: DataSourceState, err?: any) => void): void;
|
|
15
15
|
/**
|
|
16
|
-
* May be called any number of times, if already stopped, has no effect.
|
|
16
|
+
* May be called any number of times, if already stopped, has no effect. Datasource will not make any additional callbacks after stop returns.
|
|
17
17
|
*/
|
|
18
18
|
stop(): void;
|
|
19
19
|
}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -117,6 +117,9 @@ class AttributeReference {
|
|
|
117
117
|
return (this.depth === other.depth &&
|
|
118
118
|
this._components.every((value, index) => value === other.getComponent(index)));
|
|
119
119
|
}
|
|
120
|
+
get components() {
|
|
121
|
+
return [...this._components];
|
|
122
|
+
}
|
|
120
123
|
}
|
|
121
124
|
/**
|
|
122
125
|
* For use as invalid references when deserializing Flag/Segment data.
|
|
@@ -316,6 +319,41 @@ function isLegacyUser(context) {
|
|
|
316
319
|
return !('kind' in context) || context.kind === null || context.kind === undefined;
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Given some object to serialize product a canonicalized JSON string.
|
|
324
|
+
* https://www.rfc-editor.org/rfc/rfc8785.html
|
|
325
|
+
*
|
|
326
|
+
* We do not support custom toJSON methods on objects. Objects should be limited to basic types.
|
|
327
|
+
*
|
|
328
|
+
* @param object The object to serialize.
|
|
329
|
+
*/
|
|
330
|
+
function canonicalize(object, visited = []) {
|
|
331
|
+
// For JavaScript the default JSON serialization will produce canonicalized output for basic types.
|
|
332
|
+
if (object === null || typeof object !== 'object') {
|
|
333
|
+
return JSON.stringify(object);
|
|
334
|
+
}
|
|
335
|
+
if (visited.includes(object)) {
|
|
336
|
+
throw new Error('Cycle detected');
|
|
337
|
+
}
|
|
338
|
+
if (Array.isArray(object)) {
|
|
339
|
+
const values = object
|
|
340
|
+
.map((item) => canonicalize(item, [...visited, object]))
|
|
341
|
+
.map((item) => (item === undefined ? 'null' : item));
|
|
342
|
+
return `[${values.join(',')}]`;
|
|
343
|
+
}
|
|
344
|
+
const values = Object.keys(object)
|
|
345
|
+
.sort()
|
|
346
|
+
.map((key) => {
|
|
347
|
+
const value = canonicalize(object[key], [...visited, object]);
|
|
348
|
+
if (value !== undefined) {
|
|
349
|
+
return `${JSON.stringify(key)}:${value}`;
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
})
|
|
353
|
+
.filter((item) => item !== undefined);
|
|
354
|
+
return `{${values.join(',')}}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
319
357
|
// The general strategy for the context is to transform the passed in context
|
|
320
358
|
// as little as possible. We do convert the legacy users to a single kind
|
|
321
359
|
// context, but we do not translate all passed contexts into a rigid structure.
|
|
@@ -690,6 +728,28 @@ class Context {
|
|
|
690
728
|
get legacy() {
|
|
691
729
|
return this._wasLegacy;
|
|
692
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* Get the serialized canonical JSON for this context. This is not filtered for use in events.
|
|
733
|
+
*
|
|
734
|
+
* This method will cache the result.
|
|
735
|
+
*
|
|
736
|
+
* @returns The serialized canonical JSON or undefined if it cannot be serialized.
|
|
737
|
+
*/
|
|
738
|
+
canonicalUnfilteredJson() {
|
|
739
|
+
if (!this.valid) {
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
742
|
+
if (this._cachedCanonicalJson) {
|
|
743
|
+
return this._cachedCanonicalJson;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
this._cachedCanonicalJson = canonicalize(Context.toLDContext(this));
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// Indicated by undefined being returned.
|
|
750
|
+
}
|
|
751
|
+
return this._cachedCanonicalJson;
|
|
752
|
+
}
|
|
693
753
|
}
|
|
694
754
|
Context.UserKind = DEFAULT_KIND;
|
|
695
755
|
|
|
@@ -2353,7 +2413,9 @@ function counterKey(event) {
|
|
|
2353
2413
|
* @internal
|
|
2354
2414
|
*/
|
|
2355
2415
|
class EventSummarizer {
|
|
2356
|
-
constructor() {
|
|
2416
|
+
constructor(_singleContext = false, _contextFilter) {
|
|
2417
|
+
this._singleContext = _singleContext;
|
|
2418
|
+
this._contextFilter = _contextFilter;
|
|
2357
2419
|
this._startDate = 0;
|
|
2358
2420
|
this._endDate = 0;
|
|
2359
2421
|
this._counters = {};
|
|
@@ -2361,6 +2423,9 @@ class EventSummarizer {
|
|
|
2361
2423
|
}
|
|
2362
2424
|
summarizeEvent(event) {
|
|
2363
2425
|
if (isFeature(event) && !event.excludeFromSummaries) {
|
|
2426
|
+
if (!this._context) {
|
|
2427
|
+
this._context = event.context;
|
|
2428
|
+
}
|
|
2364
2429
|
const countKey = counterKey(event);
|
|
2365
2430
|
const counter = this._counters[countKey];
|
|
2366
2431
|
let kinds = this._contextKinds[event.key];
|
|
@@ -2410,14 +2475,19 @@ class EventSummarizer {
|
|
|
2410
2475
|
flagSummary.counters.push(counterOut);
|
|
2411
2476
|
return acc;
|
|
2412
2477
|
}, {});
|
|
2413
|
-
|
|
2478
|
+
const event = {
|
|
2414
2479
|
startDate: this._startDate,
|
|
2415
2480
|
endDate: this._endDate,
|
|
2416
2481
|
features,
|
|
2417
2482
|
kind: 'summary',
|
|
2483
|
+
context: this._context !== undefined && this._singleContext
|
|
2484
|
+
? this._contextFilter?.filter(this._context)
|
|
2485
|
+
: undefined,
|
|
2418
2486
|
};
|
|
2487
|
+
this._clearSummary();
|
|
2488
|
+
return event;
|
|
2419
2489
|
}
|
|
2420
|
-
|
|
2490
|
+
_clearSummary() {
|
|
2421
2491
|
this._startDate = 0;
|
|
2422
2492
|
this._endDate = 0;
|
|
2423
2493
|
this._counters = {};
|
|
@@ -2432,6 +2502,38 @@ class LDInvalidSDKKeyError extends Error {
|
|
|
2432
2502
|
}
|
|
2433
2503
|
}
|
|
2434
2504
|
|
|
2505
|
+
class MultiEventSummarizer {
|
|
2506
|
+
constructor(_contextFilter, _logger) {
|
|
2507
|
+
this._contextFilter = _contextFilter;
|
|
2508
|
+
this._logger = _logger;
|
|
2509
|
+
this._summarizers = {};
|
|
2510
|
+
}
|
|
2511
|
+
summarizeEvent(event) {
|
|
2512
|
+
if (isFeature(event)) {
|
|
2513
|
+
const key = event.context.canonicalUnfilteredJson();
|
|
2514
|
+
if (!key) {
|
|
2515
|
+
if (event.context.valid) {
|
|
2516
|
+
// The context appeared valid, but it could not be hashed.
|
|
2517
|
+
// This is likely because of a cycle in the data.
|
|
2518
|
+
this._logger?.error('Unable to serialize context, likely the context contains a cycle.');
|
|
2519
|
+
}
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
let summarizer = this._summarizers[key];
|
|
2523
|
+
if (!summarizer) {
|
|
2524
|
+
this._summarizers[key] = new EventSummarizer(true, this._contextFilter);
|
|
2525
|
+
summarizer = this._summarizers[key];
|
|
2526
|
+
}
|
|
2527
|
+
summarizer.summarizeEvent(event);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
getSummaries() {
|
|
2531
|
+
const summarizersToFlush = this._summarizers;
|
|
2532
|
+
this._summarizers = {};
|
|
2533
|
+
return Object.values(summarizersToFlush).map((summarizer) => summarizer.getSummary());
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2435
2537
|
/**
|
|
2436
2538
|
* The contents of this file are for event sampling. They are not used for
|
|
2437
2539
|
* any purpose requiring cryptographic security.
|
|
@@ -2452,12 +2554,14 @@ function shouldSample(ratio) {
|
|
|
2452
2554
|
return Math.floor(Math.random() * truncated) === 0;
|
|
2453
2555
|
}
|
|
2454
2556
|
|
|
2557
|
+
function isMultiEventSummarizer(summarizer) {
|
|
2558
|
+
return summarizer.getSummaries !== undefined;
|
|
2559
|
+
}
|
|
2455
2560
|
class EventProcessor {
|
|
2456
|
-
constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true) {
|
|
2561
|
+
constructor(_config, clientContext, baseHeaders, _contextDeduplicator, _diagnosticsManager, start = true, summariesPerContext = false) {
|
|
2457
2562
|
this._config = _config;
|
|
2458
2563
|
this._contextDeduplicator = _contextDeduplicator;
|
|
2459
2564
|
this._diagnosticsManager = _diagnosticsManager;
|
|
2460
|
-
this._summarizer = new EventSummarizer();
|
|
2461
2565
|
this._queue = [];
|
|
2462
2566
|
this._lastKnownPastTime = 0;
|
|
2463
2567
|
this._droppedEvents = 0;
|
|
@@ -2470,6 +2574,12 @@ class EventProcessor {
|
|
|
2470
2574
|
this._logger = clientContext.basicConfiguration.logger;
|
|
2471
2575
|
this._eventSender = new EventSender(clientContext, baseHeaders);
|
|
2472
2576
|
this._contextFilter = new ContextFilter(_config.allAttributesPrivate, _config.privateAttributes.map((ref) => new AttributeReference(ref)));
|
|
2577
|
+
if (summariesPerContext) {
|
|
2578
|
+
this._summarizer = new MultiEventSummarizer(this._contextFilter, this._logger);
|
|
2579
|
+
}
|
|
2580
|
+
else {
|
|
2581
|
+
this._summarizer = new EventSummarizer();
|
|
2582
|
+
}
|
|
2473
2583
|
if (start) {
|
|
2474
2584
|
this.start();
|
|
2475
2585
|
}
|
|
@@ -2521,10 +2631,19 @@ class EventProcessor {
|
|
|
2521
2631
|
}
|
|
2522
2632
|
const eventsToFlush = this._queue;
|
|
2523
2633
|
this._queue = [];
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2634
|
+
if (isMultiEventSummarizer(this._summarizer)) {
|
|
2635
|
+
const summaries = this._summarizer.getSummaries();
|
|
2636
|
+
summaries.forEach((summary) => {
|
|
2637
|
+
if (Object.keys(summary.features).length) {
|
|
2638
|
+
eventsToFlush.push(summary);
|
|
2639
|
+
}
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
else {
|
|
2643
|
+
const summary = this._summarizer.getSummary();
|
|
2644
|
+
if (Object.keys(summary.features).length) {
|
|
2645
|
+
eventsToFlush.push(summary);
|
|
2646
|
+
}
|
|
2528
2647
|
}
|
|
2529
2648
|
if (!eventsToFlush.length) {
|
|
2530
2649
|
return;
|
|
@@ -3115,6 +3234,7 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
3115
3234
|
NullEventProcessor: NullEventProcessor,
|
|
3116
3235
|
PayloadProcessor: PayloadProcessor,
|
|
3117
3236
|
PayloadStreamReader: PayloadStreamReader,
|
|
3237
|
+
canonicalize: canonicalize,
|
|
3118
3238
|
initMetadataFromHeaders: initMetadataFromHeaders,
|
|
3119
3239
|
isLegacyUser: isLegacyUser,
|
|
3120
3240
|
isMultiKind: isMultiKind,
|