feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from './jobs.svelte';
|
||||
@@ -0,0 +1,78 @@
|
||||
import { BaseApi, classroomio } from '$lib/utils/services/api';
|
||||
|
||||
import type {
|
||||
AssetMediaJobs,
|
||||
CancelMediaJobRequest,
|
||||
GetMediaJobRequest,
|
||||
ListAssetMediaJobsRequest,
|
||||
ListMediaJobsRequest,
|
||||
MediaJobEnvelope,
|
||||
MediaJobsBatch
|
||||
} from '../utils/types';
|
||||
|
||||
class JobsApi extends BaseApi {
|
||||
/** Latest envelope per polled job id. */
|
||||
envelopes = $state<Record<string, MediaJobEnvelope>>({});
|
||||
/** Per-asset latest runs (for lesson editor badges). */
|
||||
envelopesByAsset = $state<Record<string, AssetMediaJobs>>({});
|
||||
|
||||
async getMediaJob(jobId: string): Promise<MediaJobEnvelope | null> {
|
||||
let envelope: MediaJobEnvelope | null = null;
|
||||
await this.execute<GetMediaJobRequest>({
|
||||
requestFn: () => classroomio.jobs.media[':jobId'].$get({ param: { jobId } }),
|
||||
logContext: 'fetching media job',
|
||||
onSuccess: (response) => {
|
||||
envelope = response.data;
|
||||
this.envelopes = { ...this.envelopes, [response.data.job.id]: response.data };
|
||||
}
|
||||
});
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
async getMediaJobs(jobIds: string[]): Promise<MediaJobsBatch> {
|
||||
if (jobIds.length === 0) return {};
|
||||
|
||||
let envelopes: MediaJobsBatch = {};
|
||||
await this.execute<ListMediaJobsRequest>({
|
||||
requestFn: () => classroomio.jobs.media.$get({ query: { ids: jobIds.join(',') } }),
|
||||
logContext: 'fetching media jobs (batch)',
|
||||
onSuccess: (response) => {
|
||||
envelopes = response.data;
|
||||
this.envelopes = { ...this.envelopes, ...response.data };
|
||||
}
|
||||
});
|
||||
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
async getMediaJobsForAsset(assetId: string): Promise<AssetMediaJobs> {
|
||||
let envelopes: AssetMediaJobs = [];
|
||||
await this.execute<ListAssetMediaJobsRequest>({
|
||||
requestFn: () => classroomio.jobs.media.asset[':assetId'].$get({ param: { assetId } }),
|
||||
logContext: 'fetching media jobs for asset',
|
||||
onSuccess: (response) => {
|
||||
envelopes = response.data;
|
||||
this.envelopesByAsset = { ...this.envelopesByAsset, [assetId]: response.data };
|
||||
}
|
||||
});
|
||||
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
async cancelMediaJob(jobId: string): Promise<MediaJobEnvelope | null> {
|
||||
let envelope: MediaJobEnvelope | null = null;
|
||||
await this.execute<CancelMediaJobRequest>({
|
||||
requestFn: () => classroomio.jobs.media[':jobId'].cancel.$post({ param: { jobId } }),
|
||||
logContext: 'cancelling media job',
|
||||
onSuccess: (response) => {
|
||||
envelope = response.data;
|
||||
this.envelopes = { ...this.envelopes, [response.data.job.id]: response.data };
|
||||
}
|
||||
});
|
||||
|
||||
return envelope;
|
||||
}
|
||||
}
|
||||
|
||||
export const jobsApi = new JobsApi();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './api/jobs.svelte';
|
||||
export * from './utils/poller';
|
||||
export type { MediaJobEnvelope, AssetMediaJobs, MediaJobsBatch } from './utils/types';
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Reusable poller for the standardized job envelope. Honors the
|
||||
* `nextPollMs` hint the API returns, stops on terminal states, and exposes
|
||||
* a manual `poke()` that triggers an immediate refresh on user interaction.
|
||||
*
|
||||
* Designed to live in `.svelte.ts` callers so reactive consumers can pass an
|
||||
* `onUpdate` handler that mutates `$state` cleanly.
|
||||
*/
|
||||
export interface JobLikeStatus {
|
||||
status: 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
||||
}
|
||||
|
||||
export interface JobLikeEnvelope<TJob extends JobLikeStatus> {
|
||||
job: TJob;
|
||||
nextPollMs: number;
|
||||
}
|
||||
|
||||
export interface JobPollerOptions<TEnvelope extends JobLikeEnvelope<JobLikeStatus>> {
|
||||
/** Fetcher called on each poll. Should resolve to the envelope or null when missing. */
|
||||
fetch: () => Promise<TEnvelope | null>;
|
||||
/** Called whenever a fresh envelope arrives (including the first response). */
|
||||
onUpdate?: (envelope: TEnvelope) => void;
|
||||
/** Called when the poll throws. Defaults to console.error. */
|
||||
onError?: (error: unknown) => void;
|
||||
/** Override the floor between polls. Defaults to 500ms. */
|
||||
minIntervalMs?: number;
|
||||
/** Override the ceiling between polls. Defaults to 30s. */
|
||||
maxIntervalMs?: number;
|
||||
}
|
||||
|
||||
export class JobPoller<TEnvelope extends JobLikeEnvelope<JobLikeStatus>> {
|
||||
private timer: ReturnType<typeof setTimeout> | null = null;
|
||||
private inFlight = false;
|
||||
private stopped = false;
|
||||
private readonly minIntervalMs: number;
|
||||
private readonly maxIntervalMs: number;
|
||||
|
||||
constructor(private readonly options: JobPollerOptions<TEnvelope>) {
|
||||
this.minIntervalMs = options.minIntervalMs ?? 500;
|
||||
this.maxIntervalMs = options.maxIntervalMs ?? 30_000;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.stopped) return;
|
||||
|
||||
void this.tick();
|
||||
}
|
||||
|
||||
/** Trigger an immediate poll on user interaction (e.g. tab focus). */
|
||||
poke(): void {
|
||||
if (this.stopped) return;
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
void this.tick();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.stopped = true;
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clampInterval(suggestedMs: number): number {
|
||||
return Math.min(Math.max(suggestedMs, this.minIntervalMs), this.maxIntervalMs);
|
||||
}
|
||||
|
||||
private async tick(): Promise<void> {
|
||||
if (this.stopped || this.inFlight) return;
|
||||
|
||||
this.inFlight = true;
|
||||
let envelope: TEnvelope | null = null;
|
||||
try {
|
||||
envelope = await this.options.fetch();
|
||||
if (envelope) this.options.onUpdate?.(envelope);
|
||||
} catch (error) {
|
||||
const handler = this.options.onError ?? ((err) => console.error('JobPoller error:', err));
|
||||
handler(error);
|
||||
} finally {
|
||||
this.inFlight = false;
|
||||
}
|
||||
|
||||
if (this.stopped) return;
|
||||
|
||||
if (
|
||||
envelope &&
|
||||
(envelope.job.status === 'completed' || envelope.job.status === 'failed' || envelope.job.status === 'canceled')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = this.clampInterval(envelope?.nextPollMs ?? 2_000);
|
||||
this.timer = setTimeout(() => void this.tick(), delay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
|
||||
export type GetMediaJobRequest = (typeof classroomio.jobs.media)[':jobId']['$get'];
|
||||
export type GetMediaJobSuccess = Extract<InferResponseType<GetMediaJobRequest>, { success: true }>;
|
||||
export type MediaJobEnvelope = GetMediaJobSuccess['data'];
|
||||
|
||||
export type ListMediaJobsRequest = (typeof classroomio.jobs.media)['$get'];
|
||||
export type ListMediaJobsSuccess = Extract<InferResponseType<ListMediaJobsRequest>, { success: true }>;
|
||||
export type MediaJobsBatch = ListMediaJobsSuccess['data'];
|
||||
|
||||
export type ListAssetMediaJobsRequest = (typeof classroomio.jobs.media)['asset'][':assetId']['$get'];
|
||||
export type ListAssetMediaJobsSuccess = Extract<InferResponseType<ListAssetMediaJobsRequest>, { success: true }>;
|
||||
export type AssetMediaJobs = ListAssetMediaJobsSuccess['data'];
|
||||
|
||||
export type CancelMediaJobRequest = (typeof classroomio.jobs.media)[':jobId']['cancel']['$post'];
|
||||
export type CancelMediaJobSuccess = Extract<InferResponseType<CancelMediaJobRequest>, { success: true }>;
|
||||
|
||||
export type StartAssetTranscriptionRequest =
|
||||
(typeof classroomio.jobs.media)['asset'][':assetId']['transcribe']['$post'];
|
||||
export type StartAssetTranscriptionSuccess = Extract<
|
||||
InferResponseType<StartAssetTranscriptionRequest>,
|
||||
{ success: true }
|
||||
>;
|
||||
Reference in New Issue
Block a user