c8y-nitro 0.7.2 → 0.9.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/dist/utils.d.mts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
2
- import { Client, ICredentials, ICurrentUser } from "@c8y/client";
2
+ import { RealtimeClient } from "c8y-realtime";
3
+ import { Client, ICredentials, ICurrentUser, IResultList } from "@c8y/client";
3
4
  import { createError as createError$1, createLogger } from "evlog";
4
5
  import { EventHandler, H3Event, HTTPEvent } from "nitro/h3";
5
6
  import { ServerRequest, TaskContext, TaskPayload } from "nitro/types";
6
- import { C8YRoles, C8YSettingsCategory, C8YTenantOptionKey } from "c8y-nitro/types";
7
+ import { C8YRole, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey } from "c8y-nitro/types";
7
8
  //#region src/utils/client.d.ts
8
9
  /**
9
10
  * Creates a Cumulocity client authenticated with the current user's credentials.\
@@ -51,8 +52,98 @@ declare function useSubscribedTenantClients(): Promise<Record<string, Client>>;
51
52
  */
52
53
  declare function useDeployedTenantClient(): Promise<Client>;
53
54
  //#endregion
55
+ //#region src/utils/paging.d.ts
56
+ /**
57
+ * The first page of a Cumulocity list result, in any of the shapes a caller
58
+ * naturally has on hand:
59
+ *
60
+ * - an already-awaited {@link IResultList} (`await client.inventory.list(...)`)
61
+ * - the pending `list()` promise itself (`client.inventory.list(...)`)
62
+ * - a thunk that produces it lazily (`() => client.inventory.list(...)`)
63
+ *
64
+ * Only the first page has to be supplied. Every subsequent page is fetched via
65
+ * the result's own `paging.next()`, which re-uses the exact client, auth, and
66
+ * filter that produced the first page — so there is no client to pass and no
67
+ * ambiguity between user, tenant, or microservice clients.
68
+ */
69
+ type PageSource<T> = IResultList<T> | Promise<IResultList<T>> | (() => IResultList<T> | Promise<IResultList<T>>);
70
+ /**
71
+ * Optional safety guards for walking large or unbounded Cumulocity collections.
72
+ */
73
+ interface PagingOptions {
74
+ /**
75
+ * Stop after fetching this many pages. Guards against runaway iteration over
76
+ * unexpectedly large collections. Unlimited when omitted.
77
+ */
78
+ maxPages?: number;
79
+ /**
80
+ * Stop once this many items have been produced. The page that crosses the
81
+ * limit is truncated so exactly `maxItems` items are yielded. Unlimited when
82
+ * omitted.
83
+ */
84
+ maxItems?: number;
85
+ }
86
+ /**
87
+ * Lazily walks a Cumulocity list result page by page, yielding each page's data
88
+ * array. This is the engine behind {@link paginate} and {@link fetchAllPages}.
89
+ *
90
+ * Pages are fetched on demand: the next request is only issued when the consumer
91
+ * asks for the next value. Iteration stops when a page comes back shorter than
92
+ * the requested page size (Cumulocity's signal that there is no more data),
93
+ * when the result carries no paging information, or when a {@link PagingOptions}
94
+ * guard is hit.
95
+ *
96
+ * @param source - The first page (result, promise, or thunk). See {@link PageSource}.
97
+ * @param options - Optional {@link PagingOptions} safety guards.
98
+ * @yields Each page as an array of items.
99
+ * @example
100
+ * // Process one page at a time (e.g. bulk insert per page):
101
+ * for await (const page of paginatePages(client.event.list({ pageSize: 2000 }))) {
102
+ * await bulkInsert(page)
103
+ * }
104
+ */
105
+ declare function paginatePages<T>(source: PageSource<T>, options?: PagingOptions): AsyncGenerator<T[]>;
106
+ /**
107
+ * Lazily iterates every item across all pages of a Cumulocity list result,
108
+ * yielding one entry at a time — ideal for streaming large collections with a
109
+ * low memory footprint.
110
+ *
111
+ * Pages are fetched on demand as iteration advances, walking via the result's
112
+ * own `paging.next()` (same client and filter as the first page). Iteration
113
+ * stops when a page comes back shorter than the requested page size.
114
+ *
115
+ * @param source - The first page (result, promise, or thunk). See {@link PageSource}.
116
+ * @param options - Optional {@link PagingOptions} safety guards.
117
+ * @yields Each item, in order, across all pages.
118
+ * @example
119
+ * // Stream every active alarm, one at a time:
120
+ * for await (const alarm of paginate(() => client.alarm.list({ pageSize: 2000, status: 'ACTIVE' }))) {
121
+ * await handleAlarm(alarm)
122
+ * }
123
+ */
124
+ declare function paginate<T>(source: PageSource<T>, options?: PagingOptions): AsyncGenerator<T>;
125
+ /**
126
+ * Collects every item across all pages of a Cumulocity list result into a single
127
+ * array, awaiting until all pages have been fetched.
128
+ *
129
+ * Walks via the result's own `paging.next()` (same client and filter as the
130
+ * first page), stopping when a page comes back shorter than the requested page
131
+ * size. For very large collections prefer {@link paginate} to avoid holding
132
+ * every item in memory at once, or cap the result with
133
+ * {@link PagingOptions.maxItems}.
134
+ *
135
+ * @param source - The first page (result, promise, or thunk). See {@link PageSource}.
136
+ * @param options - Optional {@link PagingOptions} safety guards.
137
+ * @returns All items across all pages, in order.
138
+ * @example
139
+ * // Fetch every managed object matching a fragment:
140
+ * const client = useUserClient(event)
141
+ * const all = await fetchAllPages(client.inventory.list({ pageSize: 2000, fragmentType: 'c8y_Sensor' }))
142
+ */
143
+ declare function fetchAllPages<T>(source: PageSource<T>, options?: PagingOptions): Promise<T[]>;
144
+ //#endregion
54
145
  //#region src/utils/middleware.d.ts
55
- type UserRole = keyof C8YRoles | (string & {});
146
+ type UserRole = keyof C8YRoles | C8YRole;
56
147
  /**
57
148
  * Middleware to check if the current user has the required role.\
58
149
  * If the user doesn't have the required role, throws a 403 Forbidden error.\
@@ -502,110 +593,288 @@ declare function useLogger<T extends object = Record<string, unknown>>(eventOrRe
502
593
  */
503
594
  declare const createError: typeof createError$1;
504
595
  //#endregion
505
- //#region src/utils/schedule.d.ts
506
- type ScheduledTaskPayload = TaskPayload;
507
- type ScheduledTaskContext = TaskContext;
596
+ //#region src/utils/tasks.d.ts
597
+ /**
598
+ * Concurrency policy for a single job.
599
+ * - `'single'`: at most one run of the job is in flight at a time. A tick that
600
+ * fires while the previous run is still going is skipped, and a manual
601
+ * {@link TaskRegistry.triggerJob} while a run is active joins the in-flight run.
602
+ * - `'parallel'`: every tick (or trigger) starts a fresh run, even if a previous
603
+ * run has not finished. Runs of the same job may overlap.
604
+ */
605
+ type JobConcurrency = 'single' | 'parallel';
606
+ type TaskRegistryPayload = TaskPayload;
607
+ type TaskRegistryContext = TaskContext;
608
+ /**
609
+ * The event passed to a task handler on every execution.
610
+ */
611
+ interface TaskEvent<TPayload = TaskRegistryPayload> {
612
+ /**
613
+ * Name of the task (function) being executed.
614
+ */
615
+ task: string;
616
+ /**
617
+ * Name of the job that triggered this run, when the run originates from a scheduled or triggered job.
618
+ */
619
+ job?: string;
620
+ /**
621
+ * Data bound when the job was scheduled (or passed to `run()`/`triggerJob()`).
622
+ */
623
+ payload: TPayload;
624
+ /**
625
+ * Arbitrary context bound alongside the payload.
626
+ */
627
+ context: TaskRegistryContext;
628
+ }
508
629
  /**
509
- * A one-shot schedule definition for `scheduleTask()`.\
510
- * Numbers are treated as seconds from now, strings are parsed as human-readable durations
511
- * such as `"10 minutes"`, and dates are used as exact run times.
630
+ * A task handler function registered via {@link TaskRegistry.createTask}.
512
631
  */
513
- type ScheduledTaskInput = Date | number | string;
632
+ type TaskHandler<TPayload = TaskRegistryPayload, TResult = unknown> = (event: TaskEvent<TPayload>) => TResult | Promise<TResult>;
514
633
  /**
515
- * Options for scheduling a Nitro task to run once in the future.\
516
- * `payload` and `context` are forwarded to the task handler's `run()` method.
634
+ * When a job should run.
635
+ * - `{ cron }`: recurring on a cron expression. 5- or 6-field (with seconds) is
636
+ * supported. Evaluated in UTC by default, or in the job's `timezone` if set.
637
+ * - `{ in }`: one-shot, from now — either a number of seconds (`{ in: 30 }`) or a
638
+ * human duration string (`{ in: '5 minutes' }`, `{ in: '1 hour' }`).
639
+ * - `{ at }`: one-shot, at an exact instant given as a `Date` or ISO 8601 string,
640
+ * taken literally (never re-projected to another timezone). Times in the past
641
+ * fire almost immediately.
517
642
  */
518
- interface ScheduleTaskOptions {
643
+ type JobSchedule = {
644
+ cron: string;
645
+ } | {
646
+ in: number | string;
647
+ } | {
648
+ at: Date | string;
649
+ };
650
+ /**
651
+ * Options for scheduling a job via {@link TaskRegistry.scheduleJob}.
652
+ */
653
+ interface ScheduleJobOptions<TTask extends string = string, TPayload = TaskRegistryPayload, TSchedule extends JobSchedule = JobSchedule> {
654
+ /**
655
+ * Unique, caller-chosen job name. This is the handle used by `cancelJob()`, `getJob()` and `triggerJob()`.
656
+ */
657
+ name: string;
519
658
  /**
520
- * Payload forwarded to the task handler's `TaskEvent.payload`.
659
+ * Name of a task registered via `createTask()` that this job runs.
521
660
  */
522
- payload?: ScheduledTaskPayload;
661
+ task: TTask;
523
662
  /**
524
- * Context forwarded to the task handler's `TaskEvent.context`.
663
+ * When the job should run.
525
664
  */
526
- context?: ScheduledTaskContext;
665
+ schedule: TSchedule;
527
666
  /**
528
- * When the task should run. Numbers are seconds from now.
667
+ * Data forwarded to the handler's `event.payload`.\
668
+ * Prefer stable identifiers (e.g. a config id) over live object references so the
669
+ * handler can resolve current values at run time and the schedule stays serializable.
529
670
  */
530
- schedule: ScheduledTaskInput;
671
+ payload?: TPayload;
672
+ /**
673
+ * Data forwarded to the handler's `event.context`.
674
+ */
675
+ context?: TaskRegistryContext;
676
+ /**
677
+ * Concurrency policy for this job. Defaults to `'single'`.
678
+ */
679
+ concurrency?: JobConcurrency;
680
+ /**
681
+ * Recurring only: the IANA timezone the cron expression is evaluated in
682
+ * (e.g. `'Europe/Berlin'`). Defaults to `'UTC'` so the schedule is deterministic
683
+ * regardless of the container's local time. croner performs the alignment — no
684
+ * conversion is done by `c8y-nitro`. Ignored for one-shot schedules.
685
+ */
686
+ timezone?: string;
687
+ /**
688
+ * Recurring only: also run once immediately on registration, in addition to the cron. Defaults to `false`.
689
+ */
690
+ immediate?: boolean;
691
+ /**
692
+ * Recurring only: stop after this many scheduled runs.
693
+ */
694
+ maxRuns?: number;
695
+ /**
696
+ * Overwrite an existing job with the same name instead of throwing. Defaults to `false`.
697
+ */
698
+ replace?: boolean;
531
699
  }
532
700
  /**
533
- * Public information about a pending scheduled task.\
534
- * Returned by `scheduleTask()` and by `listScheduledTasks()`.
701
+ * Fields common to every job, regardless of kind.
535
702
  */
536
- interface ScheduledTaskInfo {
703
+ interface BaseJobInfo {
537
704
  /**
538
- * Stable UUID used to list or cancel the scheduled task.
705
+ * The job's unique name.
539
706
  */
540
- id: string;
707
+ name: string;
541
708
  /**
542
- * Nitro task name that will be passed to `runTask()`.
709
+ * The task (function) the job runs.
543
710
  */
544
711
  task: string;
545
712
  /**
546
- * Exact execution time as an ISO date string.
713
+ * The job's concurrency policy.
547
714
  */
548
- runAt: string;
715
+ concurrency: JobConcurrency;
716
+ /**
717
+ * Next scheduled execution as an ISO datetime string, or `null` if nothing is pending.
718
+ */
719
+ nextRun: string | null;
720
+ /**
721
+ * Whether a run of this job is currently in flight.
722
+ */
723
+ running: boolean;
724
+ }
725
+ /**
726
+ * Information about a one-shot job (scheduled with `{ in }` or `{ at }`).
727
+ */
728
+ interface OnceJobInfo extends BaseJobInfo {
729
+ kind: 'once';
730
+ /**
731
+ * Always `null` for one-shot jobs.
732
+ */
733
+ cron: null;
734
+ }
735
+ /**
736
+ * Information about a recurring job (scheduled with `{ cron }`).
737
+ */
738
+ interface RecurringJobInfo extends BaseJobInfo {
739
+ kind: 'recurring';
740
+ /**
741
+ * The cron expression the job runs on.
742
+ */
743
+ cron: string;
744
+ /**
745
+ * The IANA timezone the cron expression is evaluated in.
746
+ */
747
+ timezone: string;
748
+ /**
749
+ * A few upcoming executions as ISO datetime strings.
750
+ */
751
+ nextRuns: string[];
549
752
  }
550
753
  /**
551
- * Schedules a Nitro task to run once in the future.\
552
- * Resolves and calls the task handler directly from Nitro's virtual task registry\
553
- * Numbers are treated as seconds, strings are parsed as human-readable durations, and dates are used as exact run times.
754
+ * Public information about a registered job. A discriminated union on `kind` —
755
+ * returned by `scheduleJob()`, `getJob()` and `listJobs()`.
756
+ */
757
+ type JobInfo = OnceJobInfo | RecurringJobInfo;
758
+ /**
759
+ * Maps a {@link JobSchedule} to the {@link JobInfo} variant it produces.
760
+ */
761
+ type JobInfoFor<TSchedule extends JobSchedule> = TSchedule extends {
762
+ cron: string;
763
+ } ? RecurringJobInfo : OnceJobInfo;
764
+ /**
765
+ * Describes the payload and result type of a single registered task.
766
+ */
767
+ interface TaskShape<TPayload = TaskRegistryPayload, TResult = unknown> {
768
+ payload: TPayload;
769
+ result: TResult;
770
+ }
771
+ /**
772
+ * A map of task name to its {@link TaskShape}, accumulated by the builder.
773
+ */
774
+ type TaskMap = Record<string, TaskShape>;
775
+ /**
776
+ * A type-charged registry of tasks (functions) and the jobs that schedule them.
554
777
  *
555
- * @param taskName - The Nitro task name to run (from `tasks/*.ts`)
556
- * @param options - Task payload, context, and the schedule time
557
- * @returns Information about the scheduled task
778
+ * Each `createTask()` call widens the registry's type with the new task name, so
779
+ * `run()`, `scheduleJob()` and `triggerJob()` autocomplete known task names and
780
+ * reject typos at compile time.
781
+ */
782
+ interface TaskRegistry<TTasks extends TaskMap = {}> {
783
+ /**
784
+ * Registers a task (function) under a compile-time-known name.\
785
+ * Passing a name that is already registered is a type error (and throws at runtime).
786
+ *
787
+ * @param name - Unique task name
788
+ * @param handler - The function to run. Annotate its event to type the payload,
789
+ * e.g. `(e: TaskEvent<{ configId: string }>) => ...`.
790
+ */
791
+ createTask: <TName extends string, TPayload extends TaskRegistryPayload = TaskRegistryPayload, TResult = unknown>(name: TName extends keyof TTasks ? never : TName, handler: TaskHandler<TPayload, TResult>) => TaskRegistry<TTasks & Record<TName, TaskShape<TPayload, TResult>>>;
792
+ /**
793
+ * Runs a task immediately and ad-hoc, without registering a job. Always runs
794
+ * fresh (no deduplication). Use a job with `'single'` concurrency when you need
795
+ * overlap protection.
796
+ */
797
+ run: <TName extends keyof TTasks & string>(task: TName, options?: {
798
+ payload?: TTasks[TName]['payload'];
799
+ context?: TaskRegistryContext;
800
+ }) => Promise<TTasks[TName]['result']>;
801
+ /**
802
+ * Schedules a named job that runs a task once or repeatedly. The return type is
803
+ * inferred from the schedule: `{ cron }` yields a {@link RecurringJobInfo},
804
+ * `{ in }`/`{ at }` yield a {@link OnceJobInfo}.
805
+ */
806
+ scheduleJob: <TName extends keyof TTasks & string, TSchedule extends JobSchedule>(options: ScheduleJobOptions<TName, TTasks[TName]['payload'], TSchedule>) => JobInfoFor<TSchedule>;
807
+ /**
808
+ * Runs an already-registered job immediately, respecting its concurrency policy.
809
+ * For a `'single'` job with a run in flight this returns the in-flight promise.
810
+ */
811
+ triggerJob: <TResult = unknown>(name: string, options?: {
812
+ payload?: TaskRegistryPayload;
813
+ context?: TaskRegistryContext;
814
+ }) => Promise<TResult>;
815
+ /**
816
+ * Cancels a scheduled job by name.
817
+ * @returns `true` when a job was found and cancelled, otherwise `false`.
818
+ */
819
+ cancelJob: (name: string) => boolean;
820
+ /**
821
+ * Lists all currently registered jobs.
822
+ */
823
+ listJobs: () => JobInfo[];
824
+ /**
825
+ * Returns information about a single job, or `undefined` if it does not exist.
826
+ */
827
+ getJob: (name: string) => JobInfo | undefined;
828
+ /**
829
+ * Returns `true` when a task with the given name is registered.
830
+ */
831
+ hasTask: (name: string) => boolean;
832
+ }
833
+ /**
834
+ * Creates a new, empty task registry.
558
835
  *
559
836
  * @example
560
- * // Run a task in 30 seconds:
561
- * const scheduled = await scheduleTask('emails:send', {
562
- * payload: { messageId: 'abc123' },
563
- * schedule: 30,
564
- * })
837
+ * import { c8yTasks } from 'c8y-nitro/utils'
565
838
  *
566
- * @example
567
- * // Run a task using a human-readable duration:
568
- * await scheduleTask('reports:generate', {
569
- * payload: { reportId: 'report-1' },
570
- * schedule: '1 hour',
571
- * })
839
+ * export const tasks = c8yTasks()
840
+ * .createTask('sync', async ({ payload }) => {
841
+ * const config = await loadConfig(payload.configId) // resolve live state here
842
+ * await sync(config)
843
+ * })
572
844
  *
573
- * @example
574
- * // Run a task at an exact time:
575
- * await scheduleTask('cleanup:tenant', {
576
- * payload: { tenant: 't12345' },
577
- * schedule: new Date('2026-05-01T12:00:00Z'),
578
- * })
845
+ * // one per config, each an independent job
846
+ * tasks.scheduleJob({ name: 'sync-abc', task: 'sync', payload: { configId: 'abc' }, schedule: { cron: '*\/5 * * * *' } })
847
+ * tasks.scheduleJob({ name: 'sync-def', task: 'sync', payload: { configId: 'def' }, schedule: { cron: '*\/5 * * * *' }, concurrency: 'parallel' })
848
+ *
849
+ * @remarks
850
+ * The registry is a plain runtime singleton — export it from one module and import
851
+ * it wherever you schedule or cancel jobs. Recurring jobs only arm when the module
852
+ * that schedules them executes, so register them from a Nitro plugin (or another
853
+ * startup path) if they must run from boot.
579
854
  */
580
- declare function scheduleTask(taskName: string, options: ScheduleTaskOptions): Promise<ScheduledTaskInfo>;
855
+ declare function c8yTasks(): TaskRegistry;
856
+ //#endregion
857
+ //#region src/utils/realtime.d.ts
581
858
  /**
582
- * Lists all tasks that are currently scheduled and have not started yet.\
583
- * The returned object is keyed by the scheduled task UUID for easy lookup and cancellation.
584
- *
585
- * @returns Object mapping scheduled task IDs to their public task information
586
- *
587
- * @example
588
- * const tasks = await listScheduledTasks()
589
- * for (const [id, task] of Object.entries(tasks)) {
590
- * console.log(id, task.task, task.runAt)
591
- * }
859
+ * Anything a caller might already have on hand that identifies a tenant, from
860
+ * which {@link useTenantRealtimeClient} resolves the tenant's service
861
+ * credentials: a tenant id, that tenant's credentials, a `@c8y/client` scoped to
862
+ * it, or the current request.
592
863
  */
593
- declare function listScheduledTasks(): Promise<Record<string, ScheduledTaskInfo>>;
864
+ type TenantRealtimeInput = string | ICredentials | Client | H3Event | ServerRequest;
594
865
  /**
595
- * Cancels a scheduled task before it starts running.\
596
- * Once the underlying Nitro task has started, it cannot be cancelled with this utility.
597
- *
598
- * @param id - The scheduled task UUID returned by `scheduleTask()` or `listScheduledTasks()`
599
- * @returns `true` when a pending task was cancelled, otherwise `false`
866
+ * Get the pooled Notification 2.0 realtime client for a tenant, creating it on
867
+ * first use and reusing it after. Accepts whatever identifies the tenant — a
868
+ * tenant id, that tenant's credentials, a `@c8y/client`, or the current request
869
+ * — and always connects with the tenant's **service** credentials.
600
870
  *
871
+ * Register handlers directly on the returned client (see `c8y-realtime`):
601
872
  * @example
602
- * const scheduled = await scheduleTask('emails:send', {
603
- * payload: { messageId: 'abc123' },
604
- * schedule: '10 minutes',
605
- * })
606
- *
607
- * const cancelled = await cancelScheduledTask(scheduled.id)
873
+ * const rt = await useTenantRealtimeClient(event)
874
+ * rt.alarms.onCreate('*', alarm => log.info(alarm.severity, alarm.text))
875
+ * rt.measurements.onCreate('device42', m => log.info(m.type, m.time))
876
+ * @param input - A tenant id, `ICredentials`, a `Client`, or an `H3Event`/`ServerRequest`.
608
877
  */
609
- declare function cancelScheduledTask(id: string): Promise<boolean>;
878
+ declare function useTenantRealtimeClient(input: TenantRealtimeInput): Promise<RealtimeClient>;
610
879
  //#endregion
611
- export { ForeignTenantOptionCategory, ForeignTenantOptionKeyInput, ScheduleTaskOptions, ScheduledTaskContext, ScheduledTaskInfo, ScheduledTaskInput, ScheduledTaskPayload, SetEditableOptions, TenantOption, TenantOptionCategory, TenantOptionKeyInput, cancelScheduledTask, createError, createLogger, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, listScheduledTasks, scheduleTask, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };
880
+ export { BaseJobInfo, ForeignTenantOptionCategory, ForeignTenantOptionKeyInput, JobConcurrency, JobInfo, JobInfoFor, JobSchedule, OnceJobInfo, PageSource, PagingOptions, RecurringJobInfo, ScheduleJobOptions, SetEditableOptions, TaskEvent, TaskHandler, TaskMap, TaskRegistry, TaskRegistryContext, TaskRegistryPayload, TaskShape, TenantOption, TenantOptionCategory, TenantOptionKeyInput, type TenantRealtimeInput, c8yTasks, createError, createLogger, fetchAllPages, hasUserRequiredRole, isUserFromAllowedTenant, isUserFromDeployedTenant, paginate, paginatePages, useDeployedTenantClient, useDeployedTenantCredentials, useLogger, useSubscribedTenantClients, useSubscribedTenantCredentials, useTenantOption, useTenantOptions, useTenantRealtimeClient, useUser, useUserClient, useUserRoles, useUserTenantClient, useUserTenantCredentials };