@datadog/apps-frontend 0.0.1 → 0.0.3

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.
@@ -0,0 +1,8 @@
1
+ /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0.
2
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
3
+ * Copyright 2019-Present Datadog, Inc.
4
+ */
5
+
6
+ import type { ComponentType, PropsWithChildren } from 'react';
7
+ /** Owns the shared embedding connection for all descendant Datadog App hooks. */
8
+ export declare const DatadogAppProvider: ComponentType<PropsWithChildren>;
@@ -0,0 +1,29 @@
1
+ /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0.
2
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
3
+ * Copyright 2019-Present Datadog, Inc.
4
+ */
5
+
6
+ import type { EmbeddingMetadata } from '../index.js';
7
+ export interface UseDatadogAppEmbeddingOptions {
8
+ /**
9
+ * Suspend (throw a promise) while the connection is still settling
10
+ * (`connecting`/`reconnecting`), and throw an `Error` for a terminal
11
+ * failure (`rejected`/`closed`) so the nearest error boundary catches it.
12
+ * Only `connected`/`not-embedded` -- both legitimate states to render
13
+ * against -- are ever returned. Defaults to `false`.
14
+ */
15
+ readonly suspense?: boolean;
16
+ }
17
+ type SettledEmbeddingMetadata = Extract<EmbeddingMetadata, {
18
+ state: 'connected' | 'not-embedded';
19
+ }>;
20
+ /**
21
+ * Tracks this app's embedding connection: its lifecycle state, any connection
22
+ * error, and which product surface the host embedded it into. Must be called
23
+ * below `DatadogAppProvider`.
24
+ */
25
+ export declare function useDatadogAppEmbedding(options: Readonly<{
26
+ suspense: true;
27
+ }>): SettledEmbeddingMetadata;
28
+ export declare function useDatadogAppEmbedding(options?: UseDatadogAppEmbeddingOptions): EmbeddingMetadata;
29
+ export {};
@@ -0,0 +1,392 @@
1
+ /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0.
2
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
3
+ * Copyright 2019-Present Datadog, Inc.
4
+ */
5
+
6
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
7
+ export type Unsubscribe = () => void;
8
+ export type Result<TValue, TError> = Readonly<{
9
+ ok: true;
10
+ value: TValue;
11
+ }> | Readonly<{
12
+ ok: false;
13
+ error: TError;
14
+ }>;
15
+ /**
16
+ * A synchronous, deterministic, validation-only Standard Schema.
17
+ *
18
+ * Both embedding peers validate independently. Schemas must therefore have
19
+ * identical input/output types and must not coerce or transform accepted
20
+ * values.
21
+ */
22
+ export type ValueSchema<TValue> = StandardSchemaV1<TValue, TValue>;
23
+ export type AnySchema = StandardSchemaV1;
24
+ export type InferOf<TSchema extends AnySchema> = StandardSchemaV1.InferOutput<TSchema>;
25
+ export type ValidationOnlySchema<TSchema extends AnySchema> = StandardSchemaV1.InferInput<TSchema> extends StandardSchemaV1.InferOutput<TSchema> ? StandardSchemaV1.InferOutput<TSchema> extends StandardSchemaV1.InferInput<TSchema> ? TSchema : never : never;
26
+ export type ContractVersion = number;
27
+ export interface ValueField<TSchema extends AnySchema> {
28
+ readonly kind: 'value';
29
+ readonly schema: ValidationOnlySchema<TSchema>;
30
+ }
31
+ export interface FunctionField<TInputs extends Readonly<Record<string, AnySchema>>, TResult extends AnySchema> {
32
+ readonly kind: 'function';
33
+ readonly inputs: {
34
+ readonly [TName in keyof TInputs]: ValidationOnlySchema<TInputs[TName]>;
35
+ };
36
+ readonly result: ValidationOnlySchema<TResult>;
37
+ /**
38
+ * How long this function may run before it times out, overriding the
39
+ * default `FUNCTION_CALL_TIMEOUT_MS`. A fast lookup can ask for seconds
40
+ * instead of inheriting a ten-minute budget.
41
+ *
42
+ * Never sent over the wire. The app arms its wait from the contract it
43
+ * claims and the host arms its watchdog from the contract it registers, so
44
+ * two peers on different contract revisions may use different numbers.
45
+ * That needs no reconciling: whichever watchdog fires first wins through
46
+ * the normal `TIMED_OUT` path, as it already did with the default alone.
47
+ *
48
+ * Must be positive and no larger than `MAX_TIMER_DELAY_MS`. Outside that
49
+ * range no call could ever meet it -- at `0` the host's deadline checks are
50
+ * already past, and beyond the timer ceiling `setTimeout` overflows and
51
+ * fires almost at once -- so `defineContract()` rejects it outright.
52
+ */
53
+ readonly timeoutMs?: number;
54
+ }
55
+ export type AnyFunctionField = FunctionField<Readonly<Record<string, AnySchema>>, AnySchema>;
56
+ export type AnyField = ValueField<AnySchema> | AnyFunctionField;
57
+ export interface Contract<TFields extends Record<string, AnyField>> {
58
+ readonly identifier: string;
59
+ readonly version: ContractVersion;
60
+ readonly fields: TFields;
61
+ }
62
+ export type AnyContract = Contract<Record<string, AnyField>>;
63
+ export interface LiveValue<TValue> {
64
+ get(): TValue;
65
+ subscribe(onChange: () => void): Unsubscribe;
66
+ }
67
+ export interface HostFunctionContext {
68
+ /**
69
+ * Aborts once the call is cancelled or its deadline passes. An async
70
+ * implementation should check or listen for this to stop early. A
71
+ * *synchronous* implementation has no way to observe it mid-call: JS
72
+ * can't preempt a running function, so a synchronous handler that blocks
73
+ * past the deadline still runs to completion and produces its value --
74
+ * the caller is simply told the call timed out and never sees that
75
+ * value, rather than the value being prevented from being computed.
76
+ */
77
+ readonly signal: AbortSignal;
78
+ }
79
+ export interface HostFunction {
80
+ invoke(inputs: Readonly<Record<string, unknown>>, context: HostFunctionContext): unknown;
81
+ }
82
+ export type FunctionInputs<TInputs extends Readonly<Record<string, AnySchema>>> = {
83
+ readonly [TName in keyof TInputs as undefined extends InferOf<TInputs[TName]> ? TName : never]?: InferOf<TInputs[TName]>;
84
+ } & {
85
+ readonly [TName in keyof TInputs as undefined extends InferOf<TInputs[TName]> ? never : TName]: InferOf<TInputs[TName]>;
86
+ };
87
+ /** Resolves a contract field to the plain value/callable shape a claimant receives, ignoring subscription. */
88
+ type FieldValue<TField extends AnyField> = TField extends ValueField<infer TSchema> ? InferOf<TSchema> : TField extends FunctionField<infer TInputs, infer TResult> ? (inputs: FunctionInputs<TInputs>) => Promise<InferOf<TResult>> : never;
89
+ type ClaimedField<TField extends AnyField> = TField extends ValueField<infer TSchema> ? LiveValue<InferOf<TSchema>> : FieldValue<TField>;
90
+ /** The live, per-field view of a claim: subscribable `LiveValue`s for value fields, callables for function fields. */
91
+ export type ClaimedFields<TContract extends AnyContract> = {
92
+ readonly [TKey in keyof TContract['fields']]: ClaimedField<TContract['fields'][TKey]>;
93
+ };
94
+ declare const hostModuleBrand: unique symbol;
95
+ /**
96
+ * One contract version, live and ready to serve claims. Nominally branded --
97
+ * `hostModuleBrand` is declared but never exported, so only this package can
98
+ * produce a `HostModule`, and only through `createHostModule()`. That is what
99
+ * keeps a host from assembling a module for one hand-picked version: the
100
+ * single supported way in is to provide an inputs definition, which mints a
101
+ * module for *every* version that definition still converts to. The brand has
102
+ * no runtime representation.
103
+ */
104
+ export interface HostModule {
105
+ readonly [hostModuleBrand]: true;
106
+ readonly contract: AnyContract;
107
+ readonly implementation: Readonly<Record<string, LiveValue<unknown> | HostFunction>>;
108
+ readonly signal?: AbortSignal;
109
+ }
110
+ export interface Issue {
111
+ readonly message: string;
112
+ readonly path: readonly (string | number)[];
113
+ }
114
+ export type AnyError = Readonly<{
115
+ code: 'ORIGIN_REJECTED';
116
+ origin: string;
117
+ }> | Readonly<{
118
+ code: 'APP_PROTOCOL_TOO_OLD';
119
+ appProtocols: readonly number[];
120
+ hostProtocols: readonly number[];
121
+ }> | Readonly<{
122
+ code: 'HOST_PROTOCOL_TOO_OLD';
123
+ appProtocols: readonly number[];
124
+ hostProtocols: readonly number[];
125
+ }> | Readonly<{
126
+ code: 'PROTOCOL_INCOMPATIBLE';
127
+ appProtocols: readonly number[];
128
+ hostProtocols: readonly number[];
129
+ }> | Readonly<{
130
+ code: 'PEER_LACKS_MODULE';
131
+ identifier: string;
132
+ }> | Readonly<{
133
+ code: 'APP_MAJOR_TOO_OLD';
134
+ wanted: ContractVersion;
135
+ hostVersions: readonly ContractVersion[];
136
+ }> | Readonly<{
137
+ code: 'HOST_MAJOR_TOO_OLD';
138
+ wanted: ContractVersion;
139
+ hostVersions: readonly ContractVersion[];
140
+ }> | Readonly<{
141
+ code: 'CONTRACT_INCOMPATIBLE';
142
+ field: string;
143
+ }> | Readonly<{
144
+ code: 'INVALID_ARGUMENT';
145
+ issues: readonly Issue[];
146
+ }> | Readonly<{
147
+ code: 'FUNCTION_FAILED';
148
+ field: string;
149
+ }> | Readonly<{
150
+ code: 'INVALID_RESULT';
151
+ issues: readonly Issue[];
152
+ }> | Readonly<{
153
+ code: 'TIMED_OUT';
154
+ timeoutMs: number;
155
+ }> | Readonly<{
156
+ code: 'TRANSPORT_FAILED';
157
+ cause: unknown;
158
+ }> | Readonly<{
159
+ code: 'ABORTED';
160
+ }> | Readonly<{
161
+ code: 'DISCONNECTED';
162
+ }> | Readonly<{
163
+ code: 'UNRECOGNIZED';
164
+ rawCode: string;
165
+ raw: unknown;
166
+ }>;
167
+ export type ErrorOf<TCode extends AnyError['code']> = Extract<AnyError, {
168
+ code: TCode;
169
+ }>;
170
+ export type ConnectError = ErrorOf<'ORIGIN_REJECTED' | 'APP_PROTOCOL_TOO_OLD' | 'HOST_PROTOCOL_TOO_OLD' | 'PROTOCOL_INCOMPATIBLE' | 'TIMED_OUT' | 'TRANSPORT_FAILED' | 'ABORTED' | 'DISCONNECTED' | 'UNRECOGNIZED'>;
171
+ export type WireClaimError = ErrorOf<'PEER_LACKS_MODULE' | 'APP_MAJOR_TOO_OLD' | 'HOST_MAJOR_TOO_OLD' | 'CONTRACT_INCOMPATIBLE' | 'INVALID_RESULT' | 'TIMED_OUT' | 'ABORTED' | 'DISCONNECTED' | 'UNRECOGNIZED'>;
172
+ export type WireFunctionError = ErrorOf<'CONTRACT_INCOMPATIBLE' | 'INVALID_ARGUMENT' | 'FUNCTION_FAILED' | 'INVALID_RESULT' | 'TIMED_OUT' | 'ABORTED' | 'DISCONNECTED' | 'UNRECOGNIZED'>;
173
+ export type ClaimInvalidationError = ErrorOf<'INVALID_RESULT' | 'DISCONNECTED' | 'UNRECOGNIZED'>;
174
+ export interface Claimed<TContract extends AnyContract> {
175
+ readonly fields: ClaimedFields<TContract>;
176
+ release(): void;
177
+ onValuesChanged(listener: () => void): Unsubscribe;
178
+ onInvalidated(listener: (error: ClaimInvalidationError) => void): Unsubscribe;
179
+ }
180
+ export interface ClaimOptions {
181
+ /**
182
+ * Local claim deadline. Must be finite, non-negative, and no larger than
183
+ * the platform timer ceiling (`2_147_483_647`).
184
+ */
185
+ readonly timeoutMs?: number;
186
+ readonly signal?: AbortSignal;
187
+ }
188
+ export interface AppConnection {
189
+ claim<TContract extends AnyContract>(contract: TContract, options?: ClaimOptions): Promise<Result<Claimed<TContract>, WireClaimError>>;
190
+ }
191
+ /**
192
+ * Identifies which product surface a Datadog App is embedded into (e.g. the
193
+ * Service Catalog side panel vs. IDP), so an app can render surface-specific
194
+ * content. Each value matches the `identifier` of the inputs schema that
195
+ * surface provides -- see `../inputs/schemas`. Sent host-to-app as part of
196
+ * the embedding connection handshake.
197
+ */
198
+ /**
199
+ * Every surface, as a tuple rather than a bare union so it can also serve as a
200
+ * type: `DatadogAppInputsSchema` defaults its `surfaces` to this, which makes
201
+ * an IDE print the members in full on hover instead of collapsing them to the
202
+ * `SurfaceId` alias.
203
+ */
204
+ export declare const EVERY_SURFACE: readonly [
205
+ 'datadog.idp.homepage',
206
+ 'datadog.idp.service-panel',
207
+ 'datadog.idp.self-service',
208
+ 'datadog.app-builder',
209
+ 'datadog.appsec',
210
+ 'datadog.notebook',
211
+ 'datadog.dashboard'
212
+ ];
213
+ /**
214
+ * Where an app is running. Derived from `EVERY_SURFACE`, so there is one
215
+ * list, but widened to arbitrary strings: it is inert metadata forwarded to
216
+ * `listen()`, never used to gate whether embedding connects, so a host is
217
+ * free to pass a value with no matching surface (e.g. an unmapped view
218
+ * context) rather than going without one.
219
+ */
220
+ export type SurfaceId = (typeof EVERY_SURFACE)[number] | (string & {});
221
+ export type AppConnectionState = Readonly<{
222
+ state: 'connecting';
223
+ error?: ConnectError;
224
+ }> | Readonly<{
225
+ state: 'connected';
226
+ connection: AppConnection;
227
+ surfaceId?: SurfaceId;
228
+ }> | Readonly<{
229
+ state: 'reconnecting';
230
+ lastError: ConnectError;
231
+ }> | Readonly<{
232
+ state: 'rejected';
233
+ error: ConnectError;
234
+ }> | Readonly<{
235
+ state: 'not-embedded';
236
+ }> | Readonly<{
237
+ state: 'closed';
238
+ }>;
239
+ /**
240
+ * Public-safe view of `AppConnectionState`: the same lifecycle cases, minus
241
+ * the raw `AppConnection` a consumer would otherwise have no use for outside
242
+ * of claiming a contract.
243
+ */
244
+ export type EmbeddingMetadata = Readonly<{
245
+ state: 'connecting';
246
+ error?: ConnectError;
247
+ }> | Readonly<{
248
+ state: 'connected';
249
+ surfaceId?: SurfaceId;
250
+ }> | Readonly<{
251
+ state: 'reconnecting';
252
+ lastError: ConnectError;
253
+ }> | Readonly<{
254
+ state: 'rejected';
255
+ error: ConnectError;
256
+ }> | Readonly<{
257
+ state: 'not-embedded';
258
+ }> | Readonly<{
259
+ state: 'closed';
260
+ }>;
261
+ export interface EmbeddingMetadataStore {
262
+ readonly getSnapshot: () => EmbeddingMetadata;
263
+ readonly subscribe: (onChange: () => void) => Unsubscribe;
264
+ }
265
+ export interface AppConnectionHandle {
266
+ getSnapshot(): AppConnectionState;
267
+ subscribe(onChange: () => void): Unsubscribe;
268
+ close(): void;
269
+ }
270
+ export interface AppConnectionFactory {
271
+ connect(): AppConnectionHandle;
272
+ }
273
+ export interface HostListenOptions {
274
+ /** Security boundary for the app-created bootstrap port. */
275
+ readonly peer: Readonly<{
276
+ frame: HTMLIFrameElement;
277
+ appOrigin: string;
278
+ /**
279
+ * Set when the iframe is intentionally configured with an opaque
280
+ * sandboxed origin, so the host should accept a `"null"` origin from
281
+ * it in place of an exact match against `appOrigin`.
282
+ */
283
+ allowOpaqueOrigin?: boolean;
284
+ }>;
285
+ readonly modules: readonly HostModule[];
286
+ readonly surfaceId?: SurfaceId;
287
+ /**
288
+ * Local compatibility and establishment deadline for each phase. Must be
289
+ * finite, non-negative, and no larger than the platform timer ceiling
290
+ * (`2_147_483_647`).
291
+ */
292
+ readonly timeoutMs?: number;
293
+ readonly signal?: AbortSignal;
294
+ }
295
+ export interface HostListener {
296
+ onError(listener: (error: ConnectError) => void): Unsubscribe;
297
+ /**
298
+ * Fires exactly once per successful operational session: after permanent
299
+ * bootstrap and compatibility negotiation complete, before any
300
+ * per-module claim resolves. Not a one-shot for the listener's whole
301
+ * lifetime -- if this session is later superseded (a new bootstrap
302
+ * replaces it, see `close()`'s effect on an active connection) and a new
303
+ * session establishes, this fires again for that new session. Never
304
+ * fires for an attempt that fails, or after `close()`. A subscriber that
305
+ * calls `onReady` while a session is already active is notified
306
+ * immediately upon subscribing, so a late subscriber cannot miss an
307
+ * already-established session.
308
+ */
309
+ onReady(listener: () => void): Unsubscribe;
310
+ close(): void;
311
+ }
312
+ export type EmbeddingError = Readonly<{
313
+ code: 'TIMED_OUT';
314
+ message: string;
315
+ action: 'retry';
316
+ }> | Readonly<{
317
+ code: 'ORIGIN_REJECTED';
318
+ message: string;
319
+ action: 'contact-support';
320
+ }> | Readonly<{
321
+ code: 'APP_PROTOCOL_TOO_OLD';
322
+ message: string;
323
+ action: 'update-app';
324
+ }> | Readonly<{
325
+ code: 'HOST_PROTOCOL_TOO_OLD' | 'PROTOCOL_INCOMPATIBLE';
326
+ message: string;
327
+ action: 'contact-support';
328
+ }> | Readonly<{
329
+ code: 'CONNECTION_LOST';
330
+ message: string;
331
+ action: 'retry';
332
+ }> | Readonly<{
333
+ code: 'UNKNOWN';
334
+ message: string;
335
+ action: 'contact-support';
336
+ }>;
337
+ export type ClaimError = EmbeddingError | Readonly<{
338
+ code: 'APP_MAJOR_TOO_OLD';
339
+ action: 'update-app';
340
+ requestedVersion: ContractVersion;
341
+ availableVersions: readonly ContractVersion[];
342
+ message: string;
343
+ }> | Readonly<{
344
+ code: 'HOST_MAJOR_TOO_OLD';
345
+ action: 'contact-support';
346
+ requestedVersion: ContractVersion;
347
+ availableVersions: readonly ContractVersion[];
348
+ message: string;
349
+ }>;
350
+ export type ClaimedValues<TContract extends AnyContract> = Readonly<{
351
+ [TKey in keyof TContract['fields']]: FieldValue<TContract['fields'][TKey]>;
352
+ }>;
353
+ export type ClaimState<TContract extends AnyContract> = Readonly<{
354
+ status: 'pending';
355
+ }> | Readonly<{
356
+ status: 'ready';
357
+ fields: ClaimedValues<TContract>;
358
+ }> | Readonly<{
359
+ status: 'unavailable';
360
+ }> | Readonly<{
361
+ status: 'failed';
362
+ error: ClaimError;
363
+ }>;
364
+ export interface ClaimStore<TContract extends AnyContract> {
365
+ readonly getSnapshot: () => ClaimState<TContract>;
366
+ readonly subscribe: (onChange: () => void) => Unsubscribe;
367
+ }
368
+ /** Framework-neutral claim capability consumed by feature modules. */
369
+ export interface AppClaims {
370
+ getClaim<TContract extends AnyContract>(contract: TContract): ClaimStore<TContract>;
371
+ }
372
+ /** Owns the lifecycle of one concrete embedding implementation. */
373
+ export interface AppEmbedding extends AppClaims {
374
+ start(): Unsubscribe;
375
+ getEmbeddingMetadata(): EmbeddingMetadataStore;
376
+ }
377
+ /**
378
+ * One established connection, owned by the caller that opened it. These say
379
+ * nothing about which operational protocol produced them, so the connection
380
+ * lifecycle does not change when a protocol is added or retired.
381
+ */
382
+ export type ManagedAppConnection = Readonly<{
383
+ connection: AppConnection;
384
+ surfaceId?: SurfaceId;
385
+ closeSignal: AbortSignal;
386
+ close(): void;
387
+ }>;
388
+ export type ManagedHostConnection = Readonly<{
389
+ closeSignal: AbortSignal;
390
+ close(): void;
391
+ }>;
392
+ export {};
@@ -0,0 +1,104 @@
1
+ /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0.
2
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
3
+ * Copyright 2019-Present Datadog, Inc.
4
+ */
5
+
6
+ import type { AnyContract, AnyField, FunctionField, FunctionInputs, HostFunctionContext, HostModule, InferOf, LiveValue, ValueField } from './types.js';
7
+ type HostFieldImplementation<TField extends AnyField> = TField extends ValueField<infer TSchema> ? LiveValue<InferOf<TSchema>> : TField extends FunctionField<infer TInputs, infer TResult> ? (inputs: FunctionInputs<TInputs>, context: HostFunctionContext) => InferOf<TResult> | Promise<InferOf<TResult>> : never;
8
+ /** The live host-side implementation of a raw field map: `LiveValue`s for value fields, plain callables for function fields. */
9
+ export type HostFieldsFor<TFields extends Record<string, AnyField>> = Readonly<{
10
+ [TName in keyof TFields]: HostFieldImplementation<TFields[TName]>;
11
+ }>;
12
+ /** The live host-side implementation of a whole contract's fields. */
13
+ export type HostFields<TContract extends AnyContract> = HostFieldsFor<TContract['fields']>;
14
+ /**
15
+ * A `LiveValue` that recomputes from `sources` on every `get()` rather than
16
+ * caching, so it always reflects whichever of `sources` last changed without
17
+ * needing its own dedupe or disposal bookkeeping -- that already lives on
18
+ * `sources` themselves.
19
+ */
20
+ export declare function deriveLiveValue<TValue>(sources: readonly LiveValue<unknown>[], compute: () => TValue): LiveValue<TValue>;
21
+ /**
22
+ * One older version of `TLatest`: its own frozen contract, the extra live
23
+ * inputs its `convert` needs beyond what `TLatest` already carries, and the
24
+ * function that derives its fields from `TLatest` plus those extras. Runs
25
+ * once, at `buildModules()` time, and must return live fields -- typically
26
+ * built with `deriveLiveValue()` -- so the older version stays exactly as
27
+ * reactive as `TLatest`, with `TLatest`'s own implementation untouched.
28
+ */
29
+ export interface OlderContractVersion<TLatest extends AnyContract, TOlder extends AnyContract, TExtraInputs extends Record<string, AnyField>> {
30
+ readonly contract: TOlder;
31
+ readonly extraInputs: TExtraInputs;
32
+ convert(latest: HostFields<TLatest>, extra: HostFieldsFor<TExtraInputs>): HostFields<TOlder>;
33
+ }
34
+ export type AnyOlderContractVersion<TLatest extends AnyContract> = OlderContractVersion<TLatest, AnyContract, Record<string, AnyField>>;
35
+ /**
36
+ * Declares and validates one `OlderContractVersion`. Takes `latest` as its
37
+ * own argument -- rather than only through `definition.convert`'s parameter
38
+ * type -- so `TLatest` can be inferred directly from a value in covariant
39
+ * position; inference can't otherwise recover it from `convert`'s parameter,
40
+ * a contravariant position, and silently falls back to `AnyContract`.
41
+ */
42
+ export declare function olderVersion<TLatest extends AnyContract, TOlder extends AnyContract, const TExtraInputs extends Record<string, AnyField>>(_latest: TLatest, definition: OlderContractVersion<TLatest, TOlder, TExtraInputs>): OlderContractVersion<TLatest, TOlder, TExtraInputs>;
43
+ export interface VersionedContract<TLatest extends AnyContract, TOlderVersions extends readonly AnyOlderContractVersion<TLatest>[]> {
44
+ readonly identifier: TLatest['identifier'];
45
+ /**
46
+ * The version an up-to-date peer claims. Read by the claiming side to
47
+ * know what it is asking for; never a way to *provide* one version alone,
48
+ * since `buildModules()` is all-or-nothing.
49
+ */
50
+ readonly latest: TLatest;
51
+ readonly olderVersions: TOlderVersions;
52
+ /**
53
+ * `X`: every field needed to serve every version, in one flat namespace.
54
+ * A provider populates exactly this and learns nothing about how many
55
+ * versions sit behind it.
56
+ */
57
+ readonly fields: VersionedContractFields<TLatest, TOlderVersions>;
58
+ /**
59
+ * Mints one live module per version -- latest plus every version that
60
+ * still has a converter -- from one flat field record. There is
61
+ * deliberately no way to ask for a subset: supporting a version and
62
+ * serving it are the same act.
63
+ */
64
+ buildModules(fields: HostFieldsFor<VersionedContractFields<TLatest, TOlderVersions>>, signal?: AbortSignal): readonly HostModule[];
65
+ }
66
+ /**
67
+ * Structural bound for "some versioned contract", used where generic code
68
+ * needs to accept any of them. Deliberately forgetful about `buildModules`'s
69
+ * parameter and about `olderVersions`' element type: both are contravariant,
70
+ * so naming them would make every concrete contract fail to satisfy this.
71
+ * Callers recover the precise field types from the generic parameter itself
72
+ * (see `DatadogAppInputImplementations`), not from this bound.
73
+ */
74
+ export type AnyVersionedContract = Readonly<{
75
+ identifier: string;
76
+ latest: AnyContract;
77
+ olderVersions: readonly unknown[];
78
+ fields: Readonly<Record<string, AnyField>>;
79
+ buildModules: (fields: never, signal?: AbortSignal) => readonly HostModule[];
80
+ }>;
81
+ export type UnionToIntersection<TUnion> = (TUnion extends unknown ? (key: TUnion) => void : never) extends (key: infer TIntersection) => void ? TIntersection : never;
82
+ type OlderVersionsExtraFieldsUnion<TLatest extends AnyContract, TOlderVersions extends readonly AnyOlderContractVersion<TLatest>[]> = {
83
+ [TIndex in keyof TOlderVersions]: TOlderVersions[TIndex] extends OlderContractVersion<TLatest, AnyContract, infer TExtraInputs> ? TExtraInputs : never;
84
+ }[number];
85
+ /**
86
+ * Every field needed to construct every version this contract declares,
87
+ * flattened into one namespace: `latest`'s own fields plus each older
88
+ * version's `extraInputs`. This is `giving` values' whole contract with
89
+ * versioning -- a caller populates this one flat record with no notion that
90
+ * more than one version exists behind it. Field names must therefore be
91
+ * unique across `latest` and every older version's `extraInputs`; two older
92
+ * versions may reuse the same name only when it means the same schema, since
93
+ * flattening gives it exactly one slot.
94
+ */
95
+ export type VersionedContractFields<TLatest extends AnyContract, TOlderVersions extends readonly AnyOlderContractVersion<TLatest>[]> = OlderVersionsExtraFieldsUnion<TLatest, TOlderVersions> extends never ? TLatest['fields'] : TLatest['fields'] & UnionToIntersection<OlderVersionsExtraFieldsUnion<TLatest, TOlderVersions>>;
96
+ /**
97
+ * Ties a latest contract to any number of older versions it stays willing to
98
+ * serve. `buildModules()` derives each older version's fields from the
99
+ * latest implementation plus that version's own extra inputs and returns one
100
+ * independent, fully live `HostModule` per version -- ready to hand to a
101
+ * host listener alongside modules for any other contract.
102
+ */
103
+ export declare function defineVersionedContract<TLatest extends AnyContract, const TOlderVersions extends readonly AnyOlderContractVersion<TLatest>[]>(latest: TLatest, ...olderVersions: TOlderVersions): VersionedContract<TLatest, TOlderVersions>;
104
+ export {};