@pikku/queue-nats 0.12.1

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,84 @@
1
+ import { headers } from '@nats-io/nats-core';
2
+ import { ATTEMPTS_HEADER, BACKOFF_DELAY_HEADER, BACKOFF_TYPE_HEADER, NATS_MSG_ID_HEADER, PIKKU_USER_ID_HEADER, attemptsHeaderValue, subjectForQueue, } from './utils.js';
3
+ /**
4
+ * JetStream queue service for publishing jobs.
5
+ *
6
+ * Deliberately narrower than the pg-boss and bullmq services in two ways, both
7
+ * forced by the broker rather than by choice — see `supportsResults` and
8
+ * `getJob` below.
9
+ */
10
+ export class NatsQueueService {
11
+ js;
12
+ subjectPrefix;
13
+ /**
14
+ * A work-queue stream deletes a message the moment it is acked, so there is
15
+ * nowhere for a result to live and nothing to read it back from. Producers
16
+ * that need a return value should use an RPC, not a queue.
17
+ */
18
+ supportsResults = false;
19
+ delayedPublisher;
20
+ constructor(js, subjectPrefix, delayedPublisher) {
21
+ this.js = js;
22
+ this.subjectPrefix = subjectPrefix;
23
+ this.delayedPublisher = delayedPublisher;
24
+ }
25
+ /**
26
+ * Attach the delayed publisher after construction. Needed because the pg-boss
27
+ * publisher republishes *through* this service, so the two reference each
28
+ * other and one of them has to be wired second.
29
+ */
30
+ setDelayedPublisher(publisher) {
31
+ this.delayedPublisher = publisher;
32
+ }
33
+ async add(queueName, data, options) {
34
+ const hdrs = headers();
35
+ if (options?.pikkuUserId) {
36
+ hdrs.set(PIKKU_USER_ID_HEADER, options.pikkuUserId);
37
+ }
38
+ // JetStream dedupes on this header within the stream's duplicate window,
39
+ // which is the closest equivalent to pg-boss's singleton `jobId`.
40
+ if (options?.jobId) {
41
+ hdrs.set(NATS_MSG_ID_HEADER, options.jobId);
42
+ }
43
+ // Retry policy travels with the job — see ATTEMPTS_HEADER. The worker
44
+ // enforces it; JetStream itself has no per-message equivalent.
45
+ if (options?.attempts !== undefined) {
46
+ hdrs.set(ATTEMPTS_HEADER, attemptsHeaderValue(options.attempts, queueName));
47
+ }
48
+ if (options?.backoff !== undefined) {
49
+ const backoff = typeof options.backoff === 'string'
50
+ ? { type: options.backoff }
51
+ : options.backoff;
52
+ hdrs.set(BACKOFF_TYPE_HEADER, backoff.type);
53
+ if (backoff.delay !== undefined) {
54
+ hdrs.set(BACKOFF_DELAY_HEADER, String(backoff.delay));
55
+ }
56
+ }
57
+ if (options?.delay) {
58
+ // Silently dropping this is not an option: `delay` is what backs
59
+ // workflow.sleep() and orchestrator backoff, so ignoring it turns a
60
+ // one-hour sleep into a zero-second one and an exponential backoff into a
61
+ // hot retry loop. `NatsServiceFactory` always wires a publisher, so this
62
+ // only fires for a hand-built service.
63
+ if (!this.delayedPublisher) {
64
+ throw new Error(`Queue "${queueName}" was given delay=${options.delay}ms but no delayedPublisher is configured. Construct NatsQueueService with a NatsDelayedPublisher (NatsServiceFactory does this for you) or enqueue without a delay.`);
65
+ }
66
+ return await this.delayedPublisher.publishAfter(queueName, data, options.delay, options);
67
+ }
68
+ const ack = await this.js.publish(subjectForQueue(this.subjectPrefix, queueName), JSON.stringify(data), { headers: hdrs });
69
+ // The stream sequence is the only broker-assigned identity a message has.
70
+ // It is unique per stream and monotonic, which is enough to correlate logs
71
+ // and telemetry — but note it is NOT the `jobId` the caller may have passed.
72
+ return String(ack.seq);
73
+ }
74
+ /**
75
+ * Not supported. A message in a work-queue stream is only reachable through a
76
+ * consumer, and only while it is being delivered — there is no lookup by id,
77
+ * and an acked message no longer exists. Returning null (rather than throwing)
78
+ * keeps callers that poll for optional status working; callers that genuinely
79
+ * need per-job state should read it from telemetry.
80
+ */
81
+ async getJob(_queueName, _jobId) {
82
+ return null;
83
+ }
84
+ }
@@ -0,0 +1,46 @@
1
+ import type { ConsumerConfig, JetStreamClient, JetStreamManager } from '@nats-io/jetstream';
2
+ import type { ConfigValidationResult, PikkuWorkerConfig, QueueConfigMapping, QueueWorkers } from '@pikku/core/queue';
3
+ import type { Logger } from '@pikku/core/services';
4
+ /**
5
+ * Map a pikku worker config onto a JetStream consumer config.
6
+ *
7
+ * The important line is `max_ack_pending`. It is the number of messages the
8
+ * server will let this consumer hold unacked at once — i.e. real per-message
9
+ * concurrency, where a slow message occupies exactly one of the N slots. It is
10
+ * NOT a batch size: nothing here ever hands several messages to one handler and
11
+ * waits for all of them, which is the shape that produces head-of-line blocking
12
+ * on a shared queue. Keep it that way.
13
+ */
14
+ export declare const mapPikkuWorkerToNats: (workerConfig?: PikkuWorkerConfig) => Partial<ConsumerConfig>;
15
+ /**
16
+ * JetStream queue worker implementation.
17
+ *
18
+ * Cron and delayed work are handled by `NatsSchedulerService`, which uses the
19
+ * server's own message scheduler (2.14+) rather than a database.
20
+ */
21
+ export declare class NatsQueueWorkers implements QueueWorkers {
22
+ private readonly js;
23
+ private readonly jsm;
24
+ private readonly streamName;
25
+ private readonly subjectPrefix;
26
+ private readonly defaultConsumerConfig;
27
+ readonly name = "nats";
28
+ readonly supportsResults = false;
29
+ readonly configMappings: QueueConfigMapping;
30
+ private readonly activeConsumers;
31
+ constructor(js: JetStreamClient, jsm: JetStreamManager, streamName: string, subjectPrefix: string, defaultConsumerConfig?: Partial<ConsumerConfig>);
32
+ registerQueues(logger?: Logger): Promise<Record<string, ConfigValidationResult[]>>;
33
+ private handleMessage;
34
+ /** Stop consuming. In-flight handlers are left to settle their own acks. */
35
+ stop(): Promise<void>;
36
+ /**
37
+ * Stop consuming and wait for in-flight jobs to settle, up to `timeoutMs`
38
+ * across all queues. Returns the queues that did NOT finish in time.
39
+ *
40
+ * Use this on shutdown in preference to `stop()`. Without it a redeploy kills
41
+ * handlers mid-job, and every unacked message waits out `ack_wait` before
42
+ * being redelivered — safe, but slow, and it re-runs whatever side effects the
43
+ * handler had already performed.
44
+ */
45
+ drain(timeoutMs: number): Promise<string[]>;
46
+ }
@@ -0,0 +1,199 @@
1
+ import { AckPolicy, DeliverPolicy } from '@nats-io/jetstream';
2
+ import { QueueJobDiscardedError, QueueJobFailedError, registerQueueWorkers, runQueueJob, } from '@pikku/core/queue';
3
+ import { pikkuState } from '@pikku/core/state';
4
+ import { attemptsFor, backoffFor, mapJsMsgToQueueJob } from './utils.js';
5
+ import { consumeQueue } from './consume.js';
6
+ const MS_TO_NS = 1_000_000;
7
+ /**
8
+ * Default abandonment timeout, used when a worker declares no `lockDuration`.
9
+ *
10
+ * Matches pg-boss's `expire_seconds` default of 900s. This MUST be set
11
+ * explicitly: JetStream's own default `ack_wait` is 30s, so leaving it unset
12
+ * silently shortens the abandonment timeout 30x versus pg-boss, and any job
13
+ * running longer than 30s — a workflow orchestrator that polls, say — is
14
+ * redelivered while it is still running and executed a second time.
15
+ */
16
+ const DEFAULT_ACK_WAIT_MS = 900_000;
17
+ /**
18
+ * Map a pikku worker config onto a JetStream consumer config.
19
+ *
20
+ * The important line is `max_ack_pending`. It is the number of messages the
21
+ * server will let this consumer hold unacked at once — i.e. real per-message
22
+ * concurrency, where a slow message occupies exactly one of the N slots. It is
23
+ * NOT a batch size: nothing here ever hands several messages to one handler and
24
+ * waits for all of them, which is the shape that produces head-of-line blocking
25
+ * on a shared queue. Keep it that way.
26
+ */
27
+ export const mapPikkuWorkerToNats = (workerConfig) => {
28
+ const config = {
29
+ ack_policy: AckPolicy.Explicit,
30
+ deliver_policy: DeliverPolicy.All,
31
+ max_ack_pending: workerConfig?.batchSize ?? 10,
32
+ };
33
+ // Unlike pg-boss (where expiry is a fixed ceiling from job start), JetStream
34
+ // redelivers purely on ack_wait — so this doubles as the abandonment timeout.
35
+ // It must exceed the longest a handler can legitimately run, or a still-running
36
+ // job gets redelivered and executed twice. Always set, never left to the
37
+ // server default — see DEFAULT_ACK_WAIT_MS.
38
+ config.ack_wait =
39
+ (workerConfig?.lockDuration ?? DEFAULT_ACK_WAIT_MS) * MS_TO_NS;
40
+ return config;
41
+ };
42
+ /**
43
+ * JetStream queue worker implementation.
44
+ *
45
+ * Cron and delayed work are handled by `NatsSchedulerService`, which uses the
46
+ * server's own message scheduler (2.14+) rather than a database.
47
+ */
48
+ export class NatsQueueWorkers {
49
+ js;
50
+ jsm;
51
+ streamName;
52
+ subjectPrefix;
53
+ defaultConsumerConfig;
54
+ name = 'nats';
55
+ supportsResults = false;
56
+ configMappings = {
57
+ supported: {
58
+ batchSize: {
59
+ queueProperty: 'max_ack_pending',
60
+ description: 'Number of messages this consumer may hold unacked at once — per-message concurrency',
61
+ },
62
+ lockDuration: {
63
+ queueProperty: 'ack_wait',
64
+ transform: (value) => value * MS_TO_NS,
65
+ description: 'How long the server waits for an ack before redelivering (converted from ms to ns)',
66
+ },
67
+ },
68
+ unsupported: {
69
+ name: {
70
+ reason: 'Worker names are not supported in JetStream',
71
+ explanation: 'Consumers are identified by their durable name, which is derived from the queue name',
72
+ },
73
+ autorun: {
74
+ reason: 'Autorun is not configurable',
75
+ explanation: 'Consumers begin pulling as soon as they are registered',
76
+ },
77
+ drainDelay: {
78
+ reason: 'Drain delay is not configurable in JetStream',
79
+ explanation: 'Shutdown is handled by stopping consumption and letting in-flight acks settle',
80
+ },
81
+ pollInterval: {
82
+ reason: 'Pull consumers are not timer-driven',
83
+ explanation: 'The server pushes messages to a waiting pull request as they arrive, so there is no poll interval to tune',
84
+ },
85
+ groupConcurrency: {
86
+ reason: 'JetStream has no per-group fairness primitive',
87
+ explanation: 'Fairness across groups would need either a stream per group or application-side accounting',
88
+ },
89
+ },
90
+ fallbacks: {
91
+ removeOnComplete: {
92
+ reason: 'Work-queue streams delete messages on ack',
93
+ explanation: 'A completed message is removed from the stream immediately, so there is no retention to configure',
94
+ fallbackValue: 'Always removed on ack',
95
+ },
96
+ removeOnFail: {
97
+ reason: 'Exhausted messages leave via max_deliver, not retention',
98
+ explanation: 'A message that exceeds max_deliver raises a MAX_DELIVERIES advisory and is dropped from the stream',
99
+ fallbackValue: 'Managed by max_deliver',
100
+ },
101
+ },
102
+ };
103
+ activeConsumers = new Map();
104
+ constructor(js, jsm, streamName, subjectPrefix, defaultConsumerConfig = {}) {
105
+ this.js = js;
106
+ this.jsm = jsm;
107
+ this.streamName = streamName;
108
+ this.subjectPrefix = subjectPrefix;
109
+ this.defaultConsumerConfig = defaultConsumerConfig;
110
+ }
111
+ async registerQueues(logger) {
112
+ if (!logger) {
113
+ logger = pikkuState(null, 'package', 'singletonServices')?.logger;
114
+ }
115
+ if (!logger) {
116
+ throw new Error('Logger is required for registerQueues — pass it explicitly or ensure singleton services are initialized first');
117
+ }
118
+ const log = logger;
119
+ return await registerQueueWorkers(this.configMappings, log, async (queueName, processor) => {
120
+ const handle = await consumeQueue({
121
+ js: this.js,
122
+ jsm: this.jsm,
123
+ streamName: this.streamName,
124
+ subjectPrefix: this.subjectPrefix,
125
+ queueName,
126
+ config: {
127
+ ...this.defaultConsumerConfig,
128
+ ...mapPikkuWorkerToNats(processor.config),
129
+ },
130
+ handler: (msg) => this.handleMessage(queueName, msg, log),
131
+ logger: log,
132
+ });
133
+ this.activeConsumers.set(queueName, handle);
134
+ });
135
+ }
136
+ async handleMessage(queueName, msg, logger) {
137
+ try {
138
+ await runQueueJob({ job: mapJsMsgToQueueJob(queueName, msg) });
139
+ msg.ack();
140
+ }
141
+ catch (error) {
142
+ if (error instanceof QueueJobDiscardedError) {
143
+ // Permanently done, do not redeliver — the JetStream equivalent of
144
+ // pg-boss completing a job in order to discard it.
145
+ logger.info(`NATS message ${msg.seq} on ${queueName} discarded: ${error.message}`);
146
+ msg.term();
147
+ return;
148
+ }
149
+ const message = error instanceof QueueJobFailedError || error instanceof Error
150
+ ? error.message
151
+ : 'Unknown error';
152
+ // Enforce the job's own retry policy. This CANNOT be left to the consumer:
153
+ // `attempts` is per-job (the workflow engine sets it per step, and uses 1
154
+ // to mean "never retry"), while max_deliver is per-consumer — and an unset
155
+ // max_deliver means unlimited, so without this a step marked no-retry
156
+ // would be redelivered forever.
157
+ const attempts = attemptsFor(msg);
158
+ if (attempts !== undefined && msg.info.deliveryCount >= attempts) {
159
+ logger.error(`NATS message ${msg.seq} on ${queueName} failed after ${msg.info.deliveryCount}/${attempts} attempts, giving up: ${message}`);
160
+ msg.term();
161
+ return;
162
+ }
163
+ const delayMs = backoffFor(msg);
164
+ logger.error(`NATS message ${msg.seq} on ${queueName} failed (attempt ${msg.info.deliveryCount}${attempts ? `/${attempts}` : ''}), retrying in ${delayMs}ms: ${message}`);
165
+ // nak(delay) is what makes backoff real. A bare nak() redelivers as fast
166
+ // as the server can, which turns an exponential-backoff policy into a hot
167
+ // loop against whatever is already failing.
168
+ msg.nak(delayMs);
169
+ }
170
+ }
171
+ /** Stop consuming. In-flight handlers are left to settle their own acks. */
172
+ async stop() {
173
+ for (const handle of this.activeConsumers.values()) {
174
+ handle.stop();
175
+ }
176
+ this.activeConsumers.clear();
177
+ }
178
+ /**
179
+ * Stop consuming and wait for in-flight jobs to settle, up to `timeoutMs`
180
+ * across all queues. Returns the queues that did NOT finish in time.
181
+ *
182
+ * Use this on shutdown in preference to `stop()`. Without it a redeploy kills
183
+ * handlers mid-job, and every unacked message waits out `ack_wait` before
184
+ * being redelivered — safe, but slow, and it re-runs whatever side effects the
185
+ * handler had already performed.
186
+ */
187
+ async drain(timeoutMs) {
188
+ const deadline = Date.now() + timeoutMs;
189
+ const entries = [...this.activeConsumers.entries()];
190
+ this.activeConsumers.clear();
191
+ // Drained concurrently, not in sequence: one slow queue must not eat the
192
+ // whole budget and leave the rest no time at all.
193
+ const results = await Promise.all(entries.map(async ([queueName, handle]) => {
194
+ const drained = await handle.drain(Math.max(0, deadline - Date.now()));
195
+ return drained ? null : queueName;
196
+ }));
197
+ return results.filter((name) => name !== null);
198
+ }
199
+ }
@@ -0,0 +1,156 @@
1
+ import type { JetStreamClient, JetStreamManager } from '@nats-io/jetstream';
2
+ import type { CoreUserSession } from '@pikku/core/types';
3
+ import type { ScheduledTaskInfo, ScheduledTaskSummary } from '@pikku/core/services';
4
+ import { SchedulerService } from '@pikku/core/services';
5
+ /**
6
+ * Split a crontab expression into the schedule NATS wants and the timezone it
7
+ * was written in.
8
+ *
9
+ * The timezone has to come out of the expression because pikku's
10
+ * `CoreScheduledTask` has no timezone field — the cron string is the only place
11
+ * an author can say what zone they meant, and `TZ=`/`CRON_TZ=` is how crontab
12
+ * has always said it. It cannot be left in place either: NATS carries the zone
13
+ * in a header, and a prefix left in the expression lands in the seconds field,
14
+ * where it is rejected outright (`TZ=... 0 3 * * *`) or, from a 4-field
15
+ * expression, silently shifts every field along.
16
+ */
17
+ export declare const parseNatsSchedule: (cronExpr: string) => {
18
+ schedule: string;
19
+ timezone?: string;
20
+ };
21
+ /**
22
+ * Translate a 5-field crontab expression into the 6-field form NATS requires.
23
+ *
24
+ * NATS cron is seconds-first and rejects anything that is not exactly 6 fields.
25
+ * Pikku's `wireScheduler` schedules are ordinary 5-field crontab, so the
26
+ * leading `0` is added here rather than pushed onto every task definition.
27
+ *
28
+ * A `TZ=`/`CRON_TZ=` prefix is stripped, since it belongs in a header rather
29
+ * than in the expression — use `parseNatsSchedule` to get the zone as well.
30
+ */
31
+ export declare const toNatsCron: (cronExpr: string) => string;
32
+ /**
33
+ * Scheduler backed by JetStream's own message scheduler (server 2.14+).
34
+ *
35
+ * Mirrors `PgBossSchedulerService`, with the schedules living in the broker
36
+ * instead of a `pgboss.schedule` table. A schedule *is* a retained message on a
37
+ * reserved subject: publishing registers it, republishing to the same subject
38
+ * replaces it, and purging the subject cancels it. The server then produces the
39
+ * message body onto `Nats-Schedule-Target` on each occurrence, where an
40
+ * ordinary consumer picks it up.
41
+ *
42
+ * Two behaviours differ from a database-backed scheduler, both deliberate:
43
+ *
44
+ * - A missed occurrence is skipped, not caught up. This matches pg-boss, whose
45
+ * cron monitor only fires a schedule whose previous occurrence is under a
46
+ * minute old — so an instance that was down over an occurrence has always
47
+ * dropped it rather than replaying it.
48
+ * - There is no job history. A WorkQueue stream deletes a message on ack, so
49
+ * "did this task run" comes from telemetry, not from the broker.
50
+ */
51
+ export declare class NatsSchedulerService extends SchedulerService {
52
+ private readonly js;
53
+ private readonly jsm;
54
+ private readonly streamName;
55
+ private readonly subjectPrefix;
56
+ /**
57
+ * Zone for tasks that do not name one themselves. Applies to every task
58
+ * without a `TZ=` prefix; absent means the server's own zone, which is UTC
59
+ * unless it was configured otherwise.
60
+ */
61
+ private readonly defaultTimezone?;
62
+ private consumer?;
63
+ /**
64
+ * Tasks whose schedule could not be registered on the last `start()`, by name.
65
+ *
66
+ * A schedule that fails to register is the worst failure this service has:
67
+ * every other task keeps firing, so the service and the process both look
68
+ * healthy while one task silently never runs. Kept so a caller can assert on
69
+ * it — a health check, or a boot that would rather fail than run degraded.
70
+ */
71
+ readonly failedSchedules: Map<string, string>;
72
+ constructor(js: JetStreamClient, jsm: JetStreamManager, streamName: string, subjectPrefix: string,
73
+ /**
74
+ * Zone for tasks that do not name one themselves. Applies to every task
75
+ * without a `TZ=` prefix; absent means the server's own zone, which is UTC
76
+ * unless it was configured otherwise.
77
+ */
78
+ defaultTimezone?: string | undefined);
79
+ /** No-op: the connection and stream are owned by NatsServiceFactory. */
80
+ init(): Promise<void>;
81
+ /**
82
+ * Schedule a one-off delayed RPC.
83
+ *
84
+ * The returned task id is the schedule's own subject, which is what makes
85
+ * `unschedule` and `getTask` exact — there is no id to look up, the subject
86
+ * *is* the handle. `@at` schedules are purged by the server once they fire,
87
+ * so a fired task simply stops being findable.
88
+ */
89
+ scheduleRPC(delay: number | string, rpcName: string, data?: any, session?: CoreUserSession): Promise<string>;
90
+ /**
91
+ * Cancel a scheduled task. Idempotent — purging a subject that holds no
92
+ * message is not an error, so cancelling a task that already fired returns
93
+ * false rather than throwing.
94
+ */
95
+ unschedule(taskId: string): Promise<boolean>;
96
+ getTask(taskId: string): Promise<ScheduledTaskInfo | null>;
97
+ /**
98
+ * List every schedule this service owns.
99
+ *
100
+ * Unlike the pg-boss implementation — which returned an empty array because
101
+ * pg-boss has no cheap way to enumerate pending jobs — this is exact: the
102
+ * stream's subject list *is* the set of live schedules, so one
103
+ * `subjects_filter` call answers it. That listing is also what prunes
104
+ * orphans in `start()`.
105
+ */
106
+ getAllTasks(): Promise<ScheduledTaskSummary[]>;
107
+ /** No-op: the connection is owned by NatsServiceFactory. */
108
+ close(): Promise<void>;
109
+ /**
110
+ * Register every code-declared recurring task with the broker and start
111
+ * consuming their fires.
112
+ *
113
+ * Rewriting all schedules on every boot is the point, not waste: a schedule
114
+ * lives in the broker and the code is the only source of truth for what
115
+ * should exist, so republishing reconciles a changed cron expression, and the
116
+ * prune below reconciles a deleted task. Without the prune a task removed
117
+ * from code would keep firing forever onto a queue whose handler no longer
118
+ * recognises it — the NATS-side version of the pg-boss bug where a removed
119
+ * task flooded a queue with stuck jobs.
120
+ */
121
+ start(): Promise<void>;
122
+ /**
123
+ * Deregister every recurring schedule and drop the fires already queued.
124
+ *
125
+ * A runner that drives crons some other way has to call this, because a
126
+ * schedule lives in the broker rather than in the process: one left behind by
127
+ * an earlier deployment keeps firing on time into a queue nothing consumes,
128
+ * and on a work-queue stream those fires are never reclaimed. `start()`
129
+ * re-registers everything it owns, so a runner that does drive crons here can
130
+ * call this first at no cost.
131
+ */
132
+ pruneAll(): Promise<number>;
133
+ /**
134
+ * Stop consuming fires, leaving the schedules registered.
135
+ *
136
+ * Deliberately not symmetric with `start()`: the pg-boss implementation
137
+ * unscheduled everything on stop, which meant a rolling deploy briefly had no
138
+ * schedules at all and any occurrence in that window was lost. Schedules are
139
+ * shared state that outlive one instance; only the consumer is per-instance.
140
+ */
141
+ stop(): Promise<void>;
142
+ /** Run one recurring fire. Owns the message's terminal call — `consumeQueue`
143
+ * launches handlers without awaiting them, so it cannot ack on their behalf. */
144
+ private runFire;
145
+ /** Stable schedule subject for a code-declared recurring task. */
146
+ private recurringSubjectFor;
147
+ /** Guards against a caller passing an id from some other scheduler, which
148
+ * would otherwise purge an arbitrary subject in this stream. */
149
+ private ownsSubject;
150
+ /**
151
+ * Subjects currently holding a schedule, optionally narrowed to one queue.
152
+ * `subjects_filter` makes the server return its per-subject message counts,
153
+ * which for schedule subjects is exactly the live schedule set.
154
+ */
155
+ private scheduleSubjects;
156
+ }