import * as kv from "@distilled.cloud/aws/kinesis-video";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
/**
* Raised when a Kinesis Video stream or signaling channel fails to reach
* `ACTIVE` within the bounded polling budget after a create/update.
*/
export class KinesisVideoNotConverged extends Data.TaggedError(
"KinesisVideoNotConverged",
)<{
readonly resource: string;
readonly status: string | undefined;
}> {}
/**
* Raised when `GetSignalingChannelEndpoint` returns no endpoint for the
* requested protocol.
*/
export class SignalingEndpointUnavailable extends Data.TaggedError(
"SignalingEndpointUnavailable",
)<{
readonly channelArn: string;
readonly protocol: string;
}> {}
/**
* Bounded retry through transient `ResourceInUseException` states — e.g.
* deleting a stream or channel that is still `CREATING`/`UPDATING`, or
* re-creating one whose previous incarnation is still `DELETING`.
*
* Expressed as an explicitly-typed module-scope helper: inlining
* `Effect.retry` in lifecycle code leaves its conditional return type
* unresolved in the provider's declaration emit, which widens the
* `AWS.providers()` layer type for every downstream consumer.
*/
export const retryWhileResourceInUse = (
self: Effect.Effect,
): Effect.Effect =>
Effect.retry(self, {
while: (e) => e._tag === "ResourceInUseException",
schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(30)]),
});
/**
* Bounded retry through the transient states a mutation can hit while a
* previous async transition settles: `ResourceInUseException` and the
* synthetic `StreamNotActive` (Kinesis Video overloads
* `ResourceNotFoundException` with "not found or not active" while a
* stream/channel is CREATING/UPDATING — patched into a typed tag in
* distilled). Explicitly typed for the declaration-emit reason above.
*/
export const retryWhileSettling = (
self: Effect.Effect,
): Effect.Effect =>
Effect.retry(self, {
while: (e) =>
e._tag === "ResourceInUseException" || e._tag === "StreamNotActive",
schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(15)]),
});
/**
* Bounded retry through `ResourceNotFoundException` — a freshly-created
* stream/channel can be invisible to `Describe*` for a few seconds
* (eventual consistency). Explicitly typed for the same declaration-emit
* reason as above.
*/
export const retryWhileNotFound = (
self: Effect.Effect,
): Effect.Effect =>
Effect.retry(self, {
while: (e) => e._tag === "ResourceNotFoundException",
schedule: Schedule.max([Schedule.fixed("1 second"), Schedule.recurs(10)]),
});
/**
* Repeat a describe poll until `done` holds (bounded — streams and channels
* typically become active within seconds). Explicitly typed for the
* declaration-emit reason above.
*/
const untilConverged = (
self: Effect.Effect,
done: (a: A) => boolean,
times = 30,
): Effect.Effect =>
Effect.repeat(self, {
schedule: Schedule.spaced("2 seconds"),
until: done,
times,
});
/**
* Poll `DescribeStream` until the stream is `ACTIVE`. Tolerates the brief
* post-create window where the stream is not yet describable.
*
* Mutations (`UpdateStream`/`UpdateDataRetention`) are asynchronous AND bump
* the stream version — a describe issued immediately after can still show
* the pre-mutation `ACTIVE` state with the stale version. Pass
* `previousVersion` after a mutation to also wait for the version bump, so
* the returned `Version` is safe to use in the next versioned call.
*/
export const waitForStreamActive = Effect.fn(
"AWS.KinesisVideo.waitForStreamActive",
)(function* (streamName: string, previousVersion?: string) {
const done = (info: kv.StreamInfo | undefined): boolean =>
info?.Status === "ACTIVE" &&
(previousVersion === undefined || info.Version !== previousVersion);
const info = yield* untilConverged(
retryWhileNotFound(kv.describeStream({ StreamName: streamName })).pipe(
Effect.map((r) => r.StreamInfo),
),
done,
);
if (!done(info)) {
return yield* Effect.fail(
new KinesisVideoNotConverged({
resource: streamName,
status: info?.Status,
}),
);
}
return info!;
});
/**
* Poll `DescribeSignalingChannel` until the channel is `ACTIVE`. Tolerates
* the brief post-create window where the channel is not yet describable.
* Pass `previousVersion` after `UpdateSignalingChannel` to also wait for the
* version bump (see {@link waitForStreamActive}).
*/
export const waitForChannelActive = Effect.fn(
"AWS.KinesisVideo.waitForChannelActive",
)(function* (channelName: string, previousVersion?: string) {
const done = (info: kv.ChannelInfo | undefined): boolean =>
info?.ChannelStatus === "ACTIVE" &&
(previousVersion === undefined || info.Version !== previousVersion);
const info = yield* untilConverged(
retryWhileNotFound(
kv.describeSignalingChannel({ ChannelName: channelName }),
).pipe(Effect.map((r) => r.ChannelInfo)),
done,
);
if (!done(info)) {
return yield* Effect.fail(
new KinesisVideoNotConverged({
resource: channelName,
status: info?.ChannelStatus,
}),
);
}
return info!;
});
/**
* Poll `DescribeStream` until the stream is fully purged (NotFound).
* A stream in `DELETING` blocks re-creation of the same name with
* `ResourceInUseException`, so reconcilers wait it out before recreating.
*/
export const waitForStreamGone = Effect.fn(
"AWS.KinesisVideo.waitForStreamGone",
)(function* (streamName: string) {
const info = yield* untilConverged(
kv.describeStream({ StreamName: streamName }).pipe(
Effect.map((r) => r.StreamInfo),
Effect.catchTag("ResourceNotFoundException", () =>
Effect.succeed(undefined),
),
),
(i) => i === undefined,
// full purge can take noticeably longer than activation (~90s bound)
45,
);
if (info !== undefined) {
return yield* Effect.fail(
new KinesisVideoNotConverged({
resource: streamName,
status: info.Status,
}),
);
}
});
/**
* Poll `DescribeSignalingChannel` until the channel is fully purged
* (NotFound) — see {@link waitForStreamGone}.
*/
export const waitForChannelGone = Effect.fn(
"AWS.KinesisVideo.waitForChannelGone",
)(function* (channelName: string) {
const info = yield* untilConverged(
kv.describeSignalingChannel({ ChannelName: channelName }).pipe(
Effect.map((r) => r.ChannelInfo),
Effect.catchTag("ResourceNotFoundException", () =>
Effect.succeed(undefined),
),
),
(i) => i === undefined,
45,
);
if (info !== undefined) {
return yield* Effect.fail(
new KinesisVideoNotConverged({
resource: channelName,
status: info.ChannelStatus,
}),
);
}
});
/**
* Drop `undefined` values from a distilled tag map (`{ [key]: string |
* undefined }`) so it can be diffed as a plain `Record`.
*/
export const compactTags = (
tags: { [key: string]: string | undefined } | undefined,
): Record => {
const out: Record = {};
for (const [key, value] of Object.entries(tags ?? {})) {
if (value !== undefined) {
out[key] = value;
}
}
return out;
};
// Per-stream data endpoints are stable for the life of the stream — cache
// them per (arn, API) so repeated data-plane calls skip the extra
// GetDataEndpoint round-trip.
const dataEndpointCache = new Map();
/**
* Discover (and cache) the per-stream data endpoint for `apiName`.
* Parameterized over the `GetDataEndpoint` operation so binding layers can
* pass an operation captured via yield-first (`yield* op`) whose calls are
* requirement-free.
*/
export const discoverDataEndpoint = (
streamArn: string,
apiName: kv.APIName,
getDataEndpoint: (
input: kv.GetDataEndpointInput,
) => Effect.Effect,
): Effect.Effect =>
Effect.gen(function* () {
const key = `${streamArn}#${apiName}`;
const cached = dataEndpointCache.get(key);
if (cached !== undefined) {
return cached;
}
const response = yield* getDataEndpoint({
StreamARN: streamArn,
APIName: apiName,
});
const url = response.DataEndpoint!;
dataEndpointCache.set(key, url);
return url;
});
// Signaling endpoints are likewise stable per (channel, protocol, role).
const signalingEndpointCache = new Map();
/**
* Discover (and cache) the per-channel signaling endpoint for `protocol` +
* `role` via `GetSignalingChannelEndpoint`. Parameterized over the operation
* for the same yield-first reason as {@link discoverDataEndpoint}.
*/
export const discoverSignalingEndpoint = (
channelArn: string,
protocol: kv.ChannelProtocol,
role: kv.ChannelRole,
getSignalingChannelEndpoint: (
input: kv.GetSignalingChannelEndpointInput,
) => Effect.Effect,
): Effect.Effect =>
Effect.gen(function* () {
const key = `${channelArn}#${protocol}#${role}`;
const cached = signalingEndpointCache.get(key);
if (cached !== undefined) {
return cached;
}
const response = yield* getSignalingChannelEndpoint({
ChannelARN: channelArn,
SingleMasterChannelEndpointConfiguration: {
Protocols: [protocol],
Role: role,
},
});
const endpoint = (response.ResourceEndpointList ?? []).find(
(item) => item.Protocol === protocol,
)?.ResourceEndpoint;
if (endpoint === undefined) {
return yield* Effect.fail(
new SignalingEndpointUnavailable({ channelArn, protocol }),
);
}
signalingEndpointCache.set(key, endpoint);
return endpoint;
});