timeback-studio 0.1.7 → 0.1.9

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/bin.js CHANGED
@@ -16691,7 +16691,7 @@ var CredentialsSchema = exports_external.object({
16691
16691
  clientSecret: exports_external.string().min(1, "Client secret is required"),
16692
16692
  email: exports_external.email("Valid email is required").optional()
16693
16693
  });
16694
- var ClientIdSchema = exports_external.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers").length(26, "Client ID must be exactly 26 characters");
16694
+ var ClientIdSchema = exports_external.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers");
16695
16695
  var ClientSecretSchema = exports_external.string().min(1, "Client secret is required").regex(/^[a-z0-9]+$/, "Client secret must contain only lowercase letters and numbers").max(53, "Client secret must be less than 53 characters");
16696
16696
  // ../internal/cli-infra/src/ui.ts
16697
16697
  function intro(title) {
@@ -17327,7 +17327,12 @@ var TimebackConfig = exports_external.object({
17327
17327
  }).optional(),
17328
17328
  courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
17329
17329
  sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
17330
- launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
17330
+ launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional(),
17331
+ studio: exports_external.object({
17332
+ telemetry: exports_external.boolean().meta({
17333
+ description: "Enable anonymous usage telemetry for Studio (default: true)"
17334
+ }).optional().default(true)
17335
+ }).meta({ description: "Studio-specific configuration" }).optional()
17331
17336
  }).meta({
17332
17337
  id: "TimebackConfig",
17333
17338
  title: "Timeback Config",
@@ -18183,7 +18188,7 @@ var QtiItemMetadata = exports_external.object({
18183
18188
  grade: TimebackGrade.optional(),
18184
18189
  difficulty: QtiDifficulty.optional(),
18185
18190
  learningObjectiveSet: exports_external.array(QtiLearningObjectiveSet).optional()
18186
- }).strict();
18191
+ }).loose();
18187
18192
  var QtiModalFeedback = exports_external.object({
18188
18193
  outcomeIdentifier: exports_external.string().min(1),
18189
18194
  identifier: exports_external.string().min(1),
@@ -18220,7 +18225,12 @@ var QtiPaginationParams = exports_external.object({
18220
18225
  sort: exports_external.string().optional(),
18221
18226
  order: exports_external.enum(["asc", "desc"]).optional()
18222
18227
  }).strict();
18223
- var QtiAssessmentItemCreateInput = exports_external.object({
18228
+ var QtiAssessmentItemXmlCreateInput = exports_external.object({
18229
+ format: exports_external.string().pipe(exports_external.literal("xml")),
18230
+ xml: exports_external.string().min(1),
18231
+ metadata: QtiItemMetadata.optional()
18232
+ }).strict();
18233
+ var QtiAssessmentItemJsonCreateInput = exports_external.object({
18224
18234
  identifier: exports_external.string().min(1),
18225
18235
  title: exports_external.string().min(1),
18226
18236
  type: QtiAssessmentItemType,
@@ -18235,6 +18245,10 @@ var QtiAssessmentItemCreateInput = exports_external.object({
18235
18245
  feedbackInline: exports_external.array(QtiFeedbackInline).optional(),
18236
18246
  feedbackBlock: exports_external.array(QtiFeedbackBlock).optional()
18237
18247
  }).strict();
18248
+ var QtiAssessmentItemCreateInput = exports_external.union([
18249
+ QtiAssessmentItemXmlCreateInput,
18250
+ QtiAssessmentItemJsonCreateInput
18251
+ ]);
18238
18252
  var QtiAssessmentItemUpdateInput = exports_external.object({
18239
18253
  identifier: exports_external.string().min(1).optional(),
18240
18254
  title: exports_external.string().min(1),
@@ -18271,9 +18285,9 @@ var QtiAssessmentSection = exports_external.object({
18271
18285
  }).strict();
18272
18286
  var QtiTestPart = exports_external.object({
18273
18287
  identifier: exports_external.string().min(1),
18274
- navigationMode: QtiNavigationMode,
18275
- submissionMode: QtiSubmissionMode,
18276
- "qti-assessment-section": exports_external.union([QtiAssessmentSection, exports_external.array(QtiAssessmentSection)])
18288
+ navigationMode: exports_external.string().pipe(QtiNavigationMode),
18289
+ submissionMode: exports_external.string().pipe(QtiSubmissionMode),
18290
+ "qti-assessment-section": exports_external.array(QtiAssessmentSection)
18277
18291
  }).strict();
18278
18292
  var QtiReorderItemsInput = exports_external.object({
18279
18293
  items: exports_external.array(QtiAssessmentItemRef).min(1)
@@ -18291,7 +18305,7 @@ var QtiAssessmentTestCreateInput = exports_external.object({
18291
18305
  maxAttempts: exports_external.number().optional(),
18292
18306
  toolsEnabled: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
18293
18307
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
18294
- "qti-test-part": QtiTestPart,
18308
+ "qti-test-part": exports_external.array(QtiTestPart),
18295
18309
  "qti-outcome-declaration": exports_external.array(QtiTestOutcomeDeclaration).optional()
18296
18310
  }).strict();
18297
18311
  var QtiAssessmentTestUpdateInput = exports_external.object({
@@ -18304,7 +18318,7 @@ var QtiAssessmentTestUpdateInput = exports_external.object({
18304
18318
  maxAttempts: exports_external.number().optional(),
18305
18319
  toolsEnabled: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
18306
18320
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
18307
- "qti-test-part": QtiTestPart,
18321
+ "qti-test-part": exports_external.array(QtiTestPart),
18308
18322
  "qti-outcome-declaration": exports_external.array(QtiTestOutcomeDeclaration).optional()
18309
18323
  }).strict();
18310
18324
  var QtiStimulusCreateInput = exports_external.object({
@@ -22937,6 +22951,7 @@ function createLogger(options = {}) {
22937
22951
  }
22938
22952
  // src/server/services/bootstrap.ts
22939
22953
  var log = createLogger({ scope: "studio:service:bootstrap" });
22954
+ var hasLoggedInitialLoad = false;
22940
22955
 
22941
22956
  class BootstrapService {
22942
22957
  client;
@@ -22967,8 +22982,11 @@ class BootstrapService {
22967
22982
  stats,
22968
22983
  errors: errors3.length
22969
22984
  });
22970
- const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
22971
- M2.success(message);
22985
+ if (!hasLoggedInitialLoad) {
22986
+ const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
22987
+ M2.success(message);
22988
+ hasLoggedInitialLoad = true;
22989
+ }
22972
22990
  return {
22973
22991
  user,
22974
22992
  courses,
@@ -23255,7 +23273,7 @@ class EnrollmentService {
23255
23273
  sourcedId: userId,
23256
23274
  role: "student"
23257
23275
  });
23258
- log2.info("Student enrolled", {
23276
+ log2.debug("Student enrolled", {
23259
23277
  classId,
23260
23278
  userId,
23261
23279
  enrollmentId: result.sourcedIdPairs?.allocatedSourcedId
@@ -23448,48 +23466,107 @@ class EventsService {
23448
23466
  return result;
23449
23467
  }
23450
23468
  }
23451
- // src/server/services/live.ts
23452
- var log4 = createLogger({ scope: "studio:service:live" });
23469
+ // src/server/services/live-poller.ts
23470
+ var log4 = createLogger({ scope: "studio:service:live-poller" });
23471
+ var pollers = new Map;
23472
+ function getOrCreatePoller(environment, statusService, eventsService) {
23473
+ let poller = pollers.get(environment);
23474
+ if (!poller) {
23475
+ poller = new LivePoller({ environment, statusService, eventsService });
23476
+ pollers.set(environment, poller);
23477
+ }
23478
+ return poller;
23479
+ }
23453
23480
 
23454
- class LiveService {
23481
+ class LivePoller {
23482
+ environment;
23455
23483
  statusService;
23456
23484
  eventsService;
23485
+ subscribers = new Set;
23486
+ interval = null;
23487
+ state;
23488
+ lastBroadcast;
23489
+ initialTickPromise = null;
23490
+ ticking = false;
23457
23491
  constructor(options) {
23492
+ this.environment = options.environment;
23458
23493
  this.statusService = options.statusService;
23459
23494
  this.eventsService = options.eventsService;
23460
- }
23461
- static createInitialState() {
23462
23495
  const now = Date.now();
23463
- return {
23496
+ this.state = {
23464
23497
  lastStatusHash: "",
23465
23498
  lastEventTime: new Date().toISOString(),
23466
23499
  lastStatusTick: now,
23467
23500
  lastEventsTick: now
23468
23501
  };
23469
23502
  }
23470
- async tick(state) {
23471
- const updates = [];
23472
- const now = Date.now();
23473
- const newState = { ...state };
23474
- const statusUpdate = await this.fetchStatusIfChanged(state.lastStatusHash);
23475
- if (statusUpdate) {
23476
- updates.push(statusUpdate.update);
23477
- newState.lastStatusHash = statusUpdate.hash;
23503
+ subscribe(callback) {
23504
+ this.subscribers.add(callback);
23505
+ if (this.subscribers.size === 1) {
23506
+ this.start();
23478
23507
  }
23479
- newState.lastStatusTick = now;
23480
- const shouldCheckEvents = this.hasIntervalElapsed(state.lastEventsTick, now, constants.events.polling.tickIntervalMs);
23481
- if (shouldCheckEvents) {
23482
- const eventsUpdate = await this.fetchEventsIfNew(state.lastEventTime);
23483
- if (eventsUpdate) {
23484
- updates.push(eventsUpdate.update);
23485
- newState.lastEventTime = eventsUpdate.newEventTime;
23508
+ return () => {
23509
+ this.subscribers.delete(callback);
23510
+ if (this.subscribers.size === 0) {
23511
+ this.stop();
23512
+ pollers.delete(this.environment);
23486
23513
  }
23487
- newState.lastEventsTick = now;
23514
+ };
23515
+ }
23516
+ async getLastBroadcastAfterInitialTick() {
23517
+ if (this.initialTickPromise) {
23518
+ await this.initialTickPromise;
23488
23519
  }
23489
- return { updates, newState };
23520
+ return this.lastBroadcast;
23490
23521
  }
23491
- hasIntervalElapsed(lastTick, now, intervalMs) {
23492
- return now - lastTick >= intervalMs;
23522
+ start() {
23523
+ log4.debug("Poller started", { environment: this.environment });
23524
+ this.initialTickPromise = this.tick();
23525
+ this.interval = setInterval(() => this.tick(), constants.sse.defaultTickIntervalMs);
23526
+ }
23527
+ stop() {
23528
+ if (this.interval) {
23529
+ clearInterval(this.interval);
23530
+ this.interval = null;
23531
+ }
23532
+ this.lastBroadcast = undefined;
23533
+ this.initialTickPromise = null;
23534
+ log4.debug("Poller stopped", { environment: this.environment });
23535
+ }
23536
+ async tick() {
23537
+ if (this.ticking)
23538
+ return;
23539
+ this.ticking = true;
23540
+ try {
23541
+ const updates = [];
23542
+ const now = Date.now();
23543
+ const statusUpdate = await this.fetchStatusIfChanged(this.state.lastStatusHash);
23544
+ if (statusUpdate) {
23545
+ updates.push(statusUpdate.update);
23546
+ this.state.lastStatusHash = statusUpdate.hash;
23547
+ }
23548
+ this.state.lastStatusTick = now;
23549
+ const shouldCheckEvents = now - this.state.lastEventsTick >= constants.events.polling.tickIntervalMs;
23550
+ if (shouldCheckEvents) {
23551
+ const eventsUpdate = await this.fetchEventsIfNew(this.state.lastEventTime);
23552
+ if (eventsUpdate) {
23553
+ updates.push(eventsUpdate.update);
23554
+ this.state.lastEventTime = eventsUpdate.newEventTime;
23555
+ }
23556
+ this.state.lastEventsTick = now;
23557
+ }
23558
+ if (updates.length > 0) {
23559
+ const data = JSON.stringify(updates);
23560
+ this.lastBroadcast = data;
23561
+ for (const callback of this.subscribers) {
23562
+ callback(data);
23563
+ }
23564
+ }
23565
+ } catch (error48) {
23566
+ log4.error("Tick failed", { environment: this.environment, error: error48 });
23567
+ } finally {
23568
+ this.ticking = false;
23569
+ }
23493
23570
  }
23494
23571
  async fetchStatusIfChanged(lastHash) {
23495
23572
  try {
@@ -23516,7 +23593,8 @@ class LiveService {
23516
23593
  }
23517
23594
  const message = `${underline(newEvents.length)} new ${pluralize(newEvents.length, "event")}`;
23518
23595
  M2.message(message, { symbol: magenta("◆") });
23519
- const mostRecentTime = newEvents.map((e2) => e2.eventTime).sort().pop();
23596
+ const mostRecentTime = newEvents.map((e2) => new Date(e2.eventTime).getTime()).sort((a, b3) => a - b3).pop();
23597
+ const newEventTime = mostRecentTime === undefined ? lastEventTime : new Date(mostRecentTime + 1).toISOString();
23520
23598
  return {
23521
23599
  update: {
23522
23600
  type: "events",
@@ -23525,7 +23603,7 @@ class LiveService {
23525
23603
  timestamp: new Date().toISOString()
23526
23604
  }
23527
23605
  },
23528
- newEventTime: mostRecentTime ?? lastEventTime
23606
+ newEventTime
23529
23607
  };
23530
23608
  } catch (error48) {
23531
23609
  log4.error("Failed to fetch events", { error: error48 });
@@ -23545,11 +23623,15 @@ class StatusService {
23545
23623
  getSavedCredentials("staging"),
23546
23624
  getSavedCredentials("production")
23547
23625
  ]);
23626
+ const defaultCreds = this.ctx.defaultEnvironment === "staging" ? stagingCreds : this.ctx.defaultEnvironment === "production" ? productionCreds : null;
23627
+ const clientId = defaultCreds?.clientId ?? stagingCreds?.clientId ?? productionCreds?.clientId ?? this.ctx.credentials.staging?.clientId ?? this.ctx.credentials.production?.clientId;
23548
23628
  return {
23549
23629
  config: this.ctx.userConfig,
23550
23630
  environment: this.ctx.defaultEnvironment,
23551
23631
  configuredEnvironments,
23552
- hasEmail: !!stagingCreds?.email || !!productionCreds?.email
23632
+ hasEmail: !!stagingCreds?.email || !!productionCreds?.email,
23633
+ clientId,
23634
+ sensors: this.ctx.derivedSensors
23553
23635
  };
23554
23636
  }
23555
23637
  }
@@ -23930,6 +24012,7 @@ function runSSE(c, options) {
23930
24012
  } catch {
23931
24013
  abort("write error");
23932
24014
  }
24015
+ options.onClose?.();
23933
24016
  unregisterConnection(key, () => abort("superseded"));
23934
24017
  log8.debug("SSE connection closed", { ...meta3, reason: closeReason });
23935
24018
  });
@@ -24032,20 +24115,41 @@ function handleEventsStream(c) {
24032
24115
  }
24033
24116
  // src/server/controllers/live.ts
24034
24117
  function handleLive(c) {
24118
+ const env2 = c.get("env");
24035
24119
  const { status: statusService, events: eventsService } = c.get("services");
24036
- const liveService = new LiveService({ statusService, eventsService });
24037
- let state = LiveService.createInitialState();
24120
+ const poller = getOrCreatePoller(env2, statusService, eventsService);
24121
+ let pending;
24122
+ let isFirstTick = true;
24123
+ const takePending = () => {
24124
+ const data = pending;
24125
+ pending = undefined;
24126
+ return data;
24127
+ };
24128
+ const unsubscribe = poller.subscribe((data) => {
24129
+ pending = data;
24130
+ });
24038
24131
  return runSSE(c, {
24039
24132
  event: "live",
24040
24133
  intervalMs: constants.sse.defaultTickIntervalMs,
24041
24134
  sendInitial: true,
24135
+ onClose: unsubscribe,
24042
24136
  tick: async () => {
24043
- const result = await liveService.tick(state);
24044
- state = result.newState;
24045
- if (result.updates.length === 0) {
24046
- return;
24137
+ if (isFirstTick) {
24138
+ isFirstTick = false;
24139
+ const immediate = takePending();
24140
+ if (immediate !== undefined) {
24141
+ return immediate;
24142
+ }
24143
+ const initial = await poller.getLastBroadcastAfterInitialTick();
24144
+ if (pending !== undefined && pending === initial) {
24145
+ pending = undefined;
24146
+ }
24147
+ if (initial !== undefined) {
24148
+ return initial;
24149
+ }
24150
+ return takePending();
24047
24151
  }
24048
- return JSON.stringify(result.updates);
24152
+ return takePending();
24049
24153
  }
24050
24154
  });
24051
24155
  }
package/dist/index.js CHANGED
@@ -14581,7 +14581,7 @@ var CredentialsSchema = exports_external.object({
14581
14581
  clientSecret: exports_external.string().min(1, "Client secret is required"),
14582
14582
  email: exports_external.email("Valid email is required").optional()
14583
14583
  });
14584
- var ClientIdSchema = exports_external.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers").length(26, "Client ID must be exactly 26 characters");
14584
+ var ClientIdSchema = exports_external.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers");
14585
14585
  var ClientSecretSchema = exports_external.string().min(1, "Client secret is required").regex(/^[a-z0-9]+$/, "Client secret must contain only lowercase letters and numbers").max(53, "Client secret must be less than 53 characters");
14586
14586
  // ../internal/cli-infra/src/ui.ts
14587
14587
  function intro(title) {
@@ -15217,7 +15217,12 @@ var TimebackConfig = exports_external.object({
15217
15217
  }).optional(),
15218
15218
  courses: exports_external.array(CourseConfig).min(1, "At least one course is required").meta({ description: "Courses available in this app" }),
15219
15219
  sensor: exports_external.url().meta({ description: "Default Caliper sensor endpoint URL for all courses" }).optional(),
15220
- launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional()
15220
+ launchUrl: exports_external.url().meta({ description: "Default LTI launch URL for all courses" }).optional(),
15221
+ studio: exports_external.object({
15222
+ telemetry: exports_external.boolean().meta({
15223
+ description: "Enable anonymous usage telemetry for Studio (default: true)"
15224
+ }).optional().default(true)
15225
+ }).meta({ description: "Studio-specific configuration" }).optional()
15221
15226
  }).meta({
15222
15227
  id: "TimebackConfig",
15223
15228
  title: "Timeback Config",
@@ -16073,7 +16078,7 @@ var QtiItemMetadata = exports_external.object({
16073
16078
  grade: TimebackGrade.optional(),
16074
16079
  difficulty: QtiDifficulty.optional(),
16075
16080
  learningObjectiveSet: exports_external.array(QtiLearningObjectiveSet).optional()
16076
- }).strict();
16081
+ }).loose();
16077
16082
  var QtiModalFeedback = exports_external.object({
16078
16083
  outcomeIdentifier: exports_external.string().min(1),
16079
16084
  identifier: exports_external.string().min(1),
@@ -16110,7 +16115,12 @@ var QtiPaginationParams = exports_external.object({
16110
16115
  sort: exports_external.string().optional(),
16111
16116
  order: exports_external.enum(["asc", "desc"]).optional()
16112
16117
  }).strict();
16113
- var QtiAssessmentItemCreateInput = exports_external.object({
16118
+ var QtiAssessmentItemXmlCreateInput = exports_external.object({
16119
+ format: exports_external.string().pipe(exports_external.literal("xml")),
16120
+ xml: exports_external.string().min(1),
16121
+ metadata: QtiItemMetadata.optional()
16122
+ }).strict();
16123
+ var QtiAssessmentItemJsonCreateInput = exports_external.object({
16114
16124
  identifier: exports_external.string().min(1),
16115
16125
  title: exports_external.string().min(1),
16116
16126
  type: QtiAssessmentItemType,
@@ -16125,6 +16135,10 @@ var QtiAssessmentItemCreateInput = exports_external.object({
16125
16135
  feedbackInline: exports_external.array(QtiFeedbackInline).optional(),
16126
16136
  feedbackBlock: exports_external.array(QtiFeedbackBlock).optional()
16127
16137
  }).strict();
16138
+ var QtiAssessmentItemCreateInput = exports_external.union([
16139
+ QtiAssessmentItemXmlCreateInput,
16140
+ QtiAssessmentItemJsonCreateInput
16141
+ ]);
16128
16142
  var QtiAssessmentItemUpdateInput = exports_external.object({
16129
16143
  identifier: exports_external.string().min(1).optional(),
16130
16144
  title: exports_external.string().min(1),
@@ -16161,9 +16175,9 @@ var QtiAssessmentSection = exports_external.object({
16161
16175
  }).strict();
16162
16176
  var QtiTestPart = exports_external.object({
16163
16177
  identifier: exports_external.string().min(1),
16164
- navigationMode: QtiNavigationMode,
16165
- submissionMode: QtiSubmissionMode,
16166
- "qti-assessment-section": exports_external.union([QtiAssessmentSection, exports_external.array(QtiAssessmentSection)])
16178
+ navigationMode: exports_external.string().pipe(QtiNavigationMode),
16179
+ submissionMode: exports_external.string().pipe(QtiSubmissionMode),
16180
+ "qti-assessment-section": exports_external.array(QtiAssessmentSection)
16167
16181
  }).strict();
16168
16182
  var QtiReorderItemsInput = exports_external.object({
16169
16183
  items: exports_external.array(QtiAssessmentItemRef).min(1)
@@ -16181,7 +16195,7 @@ var QtiAssessmentTestCreateInput = exports_external.object({
16181
16195
  maxAttempts: exports_external.number().optional(),
16182
16196
  toolsEnabled: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
16183
16197
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
16184
- "qti-test-part": QtiTestPart,
16198
+ "qti-test-part": exports_external.array(QtiTestPart),
16185
16199
  "qti-outcome-declaration": exports_external.array(QtiTestOutcomeDeclaration).optional()
16186
16200
  }).strict();
16187
16201
  var QtiAssessmentTestUpdateInput = exports_external.object({
@@ -16194,7 +16208,7 @@ var QtiAssessmentTestUpdateInput = exports_external.object({
16194
16208
  maxAttempts: exports_external.number().optional(),
16195
16209
  toolsEnabled: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
16196
16210
  metadata: exports_external.record(exports_external.string(), exports_external.unknown()).optional(),
16197
- "qti-test-part": QtiTestPart,
16211
+ "qti-test-part": exports_external.array(QtiTestPart),
16198
16212
  "qti-outcome-declaration": exports_external.array(QtiTestOutcomeDeclaration).optional()
16199
16213
  }).strict();
16200
16214
  var QtiStimulusCreateInput = exports_external.object({
@@ -20827,6 +20841,7 @@ function createLogger(options = {}) {
20827
20841
  }
20828
20842
  // src/server/services/bootstrap.ts
20829
20843
  var log = createLogger({ scope: "studio:service:bootstrap" });
20844
+ var hasLoggedInitialLoad = false;
20830
20845
 
20831
20846
  class BootstrapService {
20832
20847
  client;
@@ -20857,8 +20872,11 @@ class BootstrapService {
20857
20872
  stats,
20858
20873
  errors: errors3.length
20859
20874
  });
20860
- const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
20861
- M2.success(message);
20875
+ if (!hasLoggedInitialLoad) {
20876
+ const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
20877
+ M2.success(message);
20878
+ hasLoggedInitialLoad = true;
20879
+ }
20862
20880
  return {
20863
20881
  user,
20864
20882
  courses,
@@ -21145,7 +21163,7 @@ class EnrollmentService {
21145
21163
  sourcedId: userId,
21146
21164
  role: "student"
21147
21165
  });
21148
- log2.info("Student enrolled", {
21166
+ log2.debug("Student enrolled", {
21149
21167
  classId,
21150
21168
  userId,
21151
21169
  enrollmentId: result.sourcedIdPairs?.allocatedSourcedId
@@ -21338,48 +21356,107 @@ class EventsService {
21338
21356
  return result;
21339
21357
  }
21340
21358
  }
21341
- // src/server/services/live.ts
21342
- var log4 = createLogger({ scope: "studio:service:live" });
21359
+ // src/server/services/live-poller.ts
21360
+ var log4 = createLogger({ scope: "studio:service:live-poller" });
21361
+ var pollers = new Map;
21362
+ function getOrCreatePoller(environment, statusService, eventsService) {
21363
+ let poller = pollers.get(environment);
21364
+ if (!poller) {
21365
+ poller = new LivePoller({ environment, statusService, eventsService });
21366
+ pollers.set(environment, poller);
21367
+ }
21368
+ return poller;
21369
+ }
21343
21370
 
21344
- class LiveService {
21371
+ class LivePoller {
21372
+ environment;
21345
21373
  statusService;
21346
21374
  eventsService;
21375
+ subscribers = new Set;
21376
+ interval = null;
21377
+ state;
21378
+ lastBroadcast;
21379
+ initialTickPromise = null;
21380
+ ticking = false;
21347
21381
  constructor(options) {
21382
+ this.environment = options.environment;
21348
21383
  this.statusService = options.statusService;
21349
21384
  this.eventsService = options.eventsService;
21350
- }
21351
- static createInitialState() {
21352
21385
  const now = Date.now();
21353
- return {
21386
+ this.state = {
21354
21387
  lastStatusHash: "",
21355
21388
  lastEventTime: new Date().toISOString(),
21356
21389
  lastStatusTick: now,
21357
21390
  lastEventsTick: now
21358
21391
  };
21359
21392
  }
21360
- async tick(state) {
21361
- const updates = [];
21362
- const now = Date.now();
21363
- const newState = { ...state };
21364
- const statusUpdate = await this.fetchStatusIfChanged(state.lastStatusHash);
21365
- if (statusUpdate) {
21366
- updates.push(statusUpdate.update);
21367
- newState.lastStatusHash = statusUpdate.hash;
21393
+ subscribe(callback) {
21394
+ this.subscribers.add(callback);
21395
+ if (this.subscribers.size === 1) {
21396
+ this.start();
21368
21397
  }
21369
- newState.lastStatusTick = now;
21370
- const shouldCheckEvents = this.hasIntervalElapsed(state.lastEventsTick, now, constants.events.polling.tickIntervalMs);
21371
- if (shouldCheckEvents) {
21372
- const eventsUpdate = await this.fetchEventsIfNew(state.lastEventTime);
21373
- if (eventsUpdate) {
21374
- updates.push(eventsUpdate.update);
21375
- newState.lastEventTime = eventsUpdate.newEventTime;
21398
+ return () => {
21399
+ this.subscribers.delete(callback);
21400
+ if (this.subscribers.size === 0) {
21401
+ this.stop();
21402
+ pollers.delete(this.environment);
21376
21403
  }
21377
- newState.lastEventsTick = now;
21404
+ };
21405
+ }
21406
+ async getLastBroadcastAfterInitialTick() {
21407
+ if (this.initialTickPromise) {
21408
+ await this.initialTickPromise;
21378
21409
  }
21379
- return { updates, newState };
21410
+ return this.lastBroadcast;
21380
21411
  }
21381
- hasIntervalElapsed(lastTick, now, intervalMs) {
21382
- return now - lastTick >= intervalMs;
21412
+ start() {
21413
+ log4.debug("Poller started", { environment: this.environment });
21414
+ this.initialTickPromise = this.tick();
21415
+ this.interval = setInterval(() => this.tick(), constants.sse.defaultTickIntervalMs);
21416
+ }
21417
+ stop() {
21418
+ if (this.interval) {
21419
+ clearInterval(this.interval);
21420
+ this.interval = null;
21421
+ }
21422
+ this.lastBroadcast = undefined;
21423
+ this.initialTickPromise = null;
21424
+ log4.debug("Poller stopped", { environment: this.environment });
21425
+ }
21426
+ async tick() {
21427
+ if (this.ticking)
21428
+ return;
21429
+ this.ticking = true;
21430
+ try {
21431
+ const updates = [];
21432
+ const now = Date.now();
21433
+ const statusUpdate = await this.fetchStatusIfChanged(this.state.lastStatusHash);
21434
+ if (statusUpdate) {
21435
+ updates.push(statusUpdate.update);
21436
+ this.state.lastStatusHash = statusUpdate.hash;
21437
+ }
21438
+ this.state.lastStatusTick = now;
21439
+ const shouldCheckEvents = now - this.state.lastEventsTick >= constants.events.polling.tickIntervalMs;
21440
+ if (shouldCheckEvents) {
21441
+ const eventsUpdate = await this.fetchEventsIfNew(this.state.lastEventTime);
21442
+ if (eventsUpdate) {
21443
+ updates.push(eventsUpdate.update);
21444
+ this.state.lastEventTime = eventsUpdate.newEventTime;
21445
+ }
21446
+ this.state.lastEventsTick = now;
21447
+ }
21448
+ if (updates.length > 0) {
21449
+ const data = JSON.stringify(updates);
21450
+ this.lastBroadcast = data;
21451
+ for (const callback of this.subscribers) {
21452
+ callback(data);
21453
+ }
21454
+ }
21455
+ } catch (error48) {
21456
+ log4.error("Tick failed", { environment: this.environment, error: error48 });
21457
+ } finally {
21458
+ this.ticking = false;
21459
+ }
21383
21460
  }
21384
21461
  async fetchStatusIfChanged(lastHash) {
21385
21462
  try {
@@ -21406,7 +21483,8 @@ class LiveService {
21406
21483
  }
21407
21484
  const message = `${underline(newEvents.length)} new ${pluralize(newEvents.length, "event")}`;
21408
21485
  M2.message(message, { symbol: magenta("◆") });
21409
- const mostRecentTime = newEvents.map((e2) => e2.eventTime).sort().pop();
21486
+ const mostRecentTime = newEvents.map((e2) => new Date(e2.eventTime).getTime()).sort((a, b3) => a - b3).pop();
21487
+ const newEventTime = mostRecentTime === undefined ? lastEventTime : new Date(mostRecentTime + 1).toISOString();
21410
21488
  return {
21411
21489
  update: {
21412
21490
  type: "events",
@@ -21415,7 +21493,7 @@ class LiveService {
21415
21493
  timestamp: new Date().toISOString()
21416
21494
  }
21417
21495
  },
21418
- newEventTime: mostRecentTime ?? lastEventTime
21496
+ newEventTime
21419
21497
  };
21420
21498
  } catch (error48) {
21421
21499
  log4.error("Failed to fetch events", { error: error48 });
@@ -21435,11 +21513,15 @@ class StatusService {
21435
21513
  getSavedCredentials("staging"),
21436
21514
  getSavedCredentials("production")
21437
21515
  ]);
21516
+ const defaultCreds = this.ctx.defaultEnvironment === "staging" ? stagingCreds : this.ctx.defaultEnvironment === "production" ? productionCreds : null;
21517
+ const clientId = defaultCreds?.clientId ?? stagingCreds?.clientId ?? productionCreds?.clientId ?? this.ctx.credentials.staging?.clientId ?? this.ctx.credentials.production?.clientId;
21438
21518
  return {
21439
21519
  config: this.ctx.userConfig,
21440
21520
  environment: this.ctx.defaultEnvironment,
21441
21521
  configuredEnvironments,
21442
- hasEmail: !!stagingCreds?.email || !!productionCreds?.email
21522
+ hasEmail: !!stagingCreds?.email || !!productionCreds?.email,
21523
+ clientId,
21524
+ sensors: this.ctx.derivedSensors
21443
21525
  };
21444
21526
  }
21445
21527
  }
@@ -21820,6 +21902,7 @@ function runSSE(c, options) {
21820
21902
  } catch {
21821
21903
  abort("write error");
21822
21904
  }
21905
+ options.onClose?.();
21823
21906
  unregisterConnection(key, () => abort("superseded"));
21824
21907
  log8.debug("SSE connection closed", { ...meta3, reason: closeReason });
21825
21908
  });
@@ -21922,20 +22005,41 @@ function handleEventsStream(c) {
21922
22005
  }
21923
22006
  // src/server/controllers/live.ts
21924
22007
  function handleLive(c) {
22008
+ const env2 = c.get("env");
21925
22009
  const { status: statusService, events: eventsService } = c.get("services");
21926
- const liveService = new LiveService({ statusService, eventsService });
21927
- let state = LiveService.createInitialState();
22010
+ const poller = getOrCreatePoller(env2, statusService, eventsService);
22011
+ let pending;
22012
+ let isFirstTick = true;
22013
+ const takePending = () => {
22014
+ const data = pending;
22015
+ pending = undefined;
22016
+ return data;
22017
+ };
22018
+ const unsubscribe = poller.subscribe((data) => {
22019
+ pending = data;
22020
+ });
21928
22021
  return runSSE(c, {
21929
22022
  event: "live",
21930
22023
  intervalMs: constants.sse.defaultTickIntervalMs,
21931
22024
  sendInitial: true,
22025
+ onClose: unsubscribe,
21932
22026
  tick: async () => {
21933
- const result = await liveService.tick(state);
21934
- state = result.newState;
21935
- if (result.updates.length === 0) {
21936
- return;
22027
+ if (isFirstTick) {
22028
+ isFirstTick = false;
22029
+ const immediate = takePending();
22030
+ if (immediate !== undefined) {
22031
+ return immediate;
22032
+ }
22033
+ const initial = await poller.getLastBroadcastAfterInitialTick();
22034
+ if (pending !== undefined && pending === initial) {
22035
+ pending = undefined;
22036
+ }
22037
+ if (initial !== undefined) {
22038
+ return initial;
22039
+ }
22040
+ return takePending();
21937
22041
  }
21938
- return JSON.stringify(result.updates);
22042
+ return takePending();
21939
22043
  }
21940
22044
  });
21941
22045
  }
@@ -92,7 +92,7 @@ export declare function handleBootstrap(c: Context<{
92
92
  preferredMiddleName?: string | null | undefined;
93
93
  preferredLastName?: string | null | undefined;
94
94
  pronouns?: string | null | undefined;
95
- grades?: import("@timeback/types").TimebackGrade[] | undefined;
95
+ grades?: import("@timeback/oneroster/types").TimebackGrade[] | undefined;
96
96
  password?: string | null | undefined;
97
97
  sms?: string | null | undefined;
98
98
  phone?: string | null | undefined;
@@ -147,8 +147,8 @@ export declare function handleBootstrap(c: Context<{
147
147
  } | null | undefined;
148
148
  title: string;
149
149
  courseCode?: string | undefined;
150
- grades?: import("@timeback/types").TimebackGrade[] | undefined;
151
- subjects?: import("@timeback/caliper").TimebackSubject[] | undefined;
150
+ grades?: import("@timeback/oneroster/types").TimebackGrade[] | undefined;
151
+ subjects?: import("@timeback/oneroster/types").TimebackSubject[] | undefined;
152
152
  subjectCodes?: string[] | undefined;
153
153
  org: {
154
154
  sourcedId: string;
@@ -431,8 +431,8 @@ export declare function handleBootstrap(c: Context<{
431
431
  classCode?: string | null | undefined;
432
432
  classType?: "homeroom" | "scheduled" | undefined;
433
433
  location?: string | null | undefined;
434
- grades?: import("@timeback/types").TimebackGrade[] | undefined;
435
- subjects?: import("@timeback/caliper").TimebackSubject[] | undefined;
434
+ grades?: import("@timeback/oneroster/types").TimebackGrade[] | undefined;
435
+ subjects?: import("@timeback/oneroster/types").TimebackSubject[] | undefined;
436
436
  subjectCodes?: string[] | undefined;
437
437
  periods?: string[] | undefined;
438
438
  terms?: {
@@ -576,7 +576,7 @@ export declare function handleBootstrap(c: Context<{
576
576
  preferredMiddleName?: string | null | undefined;
577
577
  preferredLastName?: string | null | undefined;
578
578
  pronouns?: string | null | undefined;
579
- grades?: import("@timeback/types").TimebackGrade[] | undefined;
579
+ grades?: import("@timeback/oneroster/types").TimebackGrade[] | undefined;
580
580
  password?: string | null | undefined;
581
581
  sms?: string | null | undefined;
582
582
  phone?: string | null | undefined;
@@ -3,15 +3,17 @@
3
3
  *
4
4
  * GET /api/live - Multiplexed SSE stream combining status and events.
5
5
  *
6
- * See LiveService for detailed documentation on how tick intervals work.
6
+ * Uses a shared LivePoller per environment so all SSE connections receive
7
+ * the same data from a single polling loop. See LivePoller for details.
7
8
  */
8
9
  import type { Context } from 'hono';
9
10
  import type { EnvVariables } from '../lib';
10
11
  /**
11
12
  * GET /api/live - Multiplexed SSE stream for status and events.
12
13
  *
13
- * Combines status updates (on change) and event updates (every 30s) into
14
- * a single SSE connection. Emits an array of LiveUpdate objects.
14
+ * Subscribes to the shared LivePoller for the current environment.
15
+ * The poller broadcasts serialized updates which are forwarded to
16
+ * the client via the SSE tick function.
15
17
  *
16
18
  * @param c - Hono context with env variables
17
19
  * @returns SSE stream
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../src/server/controllers/live.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAE1C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,YAqBjE"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../src/server/controllers/live.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC,YAiDjE"}
@@ -63,7 +63,7 @@ export declare function handleStatus(c: Context, ctx: AppContext): Promise<(Resp
63
63
  totalGrades?: number | undefined;
64
64
  } | undefined;
65
65
  } | undefined;
66
- subject: import("@timeback/caliper").TimebackSubject;
66
+ subject: import("@timeback/types").TimebackSubject;
67
67
  grade?: import("@timeback/types").TimebackGrade | undefined;
68
68
  ids?: {
69
69
  staging?: string | undefined;
@@ -126,6 +126,9 @@ export declare function handleStatus(c: Context, ctx: AppContext): Promise<(Resp
126
126
  }[];
127
127
  sensor?: string | undefined;
128
128
  launchUrl?: string | undefined;
129
+ studio?: {
130
+ telemetry?: boolean | undefined;
131
+ } | undefined;
129
132
  version: string;
130
133
  path: string;
131
134
  courseIds: {
@@ -136,6 +139,8 @@ export declare function handleStatus(c: Context, ctx: AppContext): Promise<(Resp
136
139
  environment: import("../../config").StudioEnvironment;
137
140
  configuredEnvironments: import("../../config").StudioEnvironment[];
138
141
  hasEmail: boolean;
142
+ clientId?: string | undefined;
143
+ sensors?: string[] | undefined;
139
144
  }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<null, 304, "body">)>;
140
145
  /**
141
146
  * GET /api/status/stream - SSE stream for status updates.
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/server/controllers/status.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mIAY7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,YAoB7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/server/controllers/status.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mIAY7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,YAoB7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../../src/server/lib/sse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AA6C5C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAMD;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,YA4ExD"}
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../../src/server/lib/sse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AA6C5C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAMD;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,YA6ExD"}
@@ -60,5 +60,7 @@ export interface RunSSEOptions {
60
60
  sendInitial?: boolean;
61
61
  /** Return a string to emit, or undefined to skip this tick */
62
62
  tick: () => Promise<string | undefined>;
63
+ /** Called when the stream closes for any reason (superseded, client disconnect, error) */
64
+ onClose?: () => void;
63
65
  }
64
66
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/server/lib/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EACX,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,MAAM,aAAa,CAAA;AAEpB,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAM/D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,gBAAgB,CAAA;IAC3B,UAAU,EAAE,iBAAiB,CAAA;IAC7B,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,aAAa,CAAA;IACrB,aAAa,EAAE,oBAAoB,CAAA;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,+CAA+C;IAC/C,UAAU,EAAE,UAAU,CAAA;IACtB,oCAAoC;IACpC,MAAM,EAAE,cAAc,CAAA;IACtB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAMD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,WAAW,CAAA;IAChB,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAC7B,QAAQ,EAAE,cAAc,CAAA;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,KAAK,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,EAAE,CAAA;CACrB;AAMD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/server/lib/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EACX,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,MAAM,aAAa,CAAA;AAEpB,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAM/D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,gBAAgB,CAAA;IAC3B,UAAU,EAAE,iBAAiB,CAAA;IAC7B,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,aAAa,CAAA;IACrB,aAAa,EAAE,oBAAoB,CAAA;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,+CAA+C;IAC/C,UAAU,EAAE,UAAU,CAAA;IACtB,oCAAoC;IACpC,MAAM,EAAE,cAAc,CAAA;IACtB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAMD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,WAAW,CAAA;IAChB,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAC7B,QAAQ,EAAE,cAAc,CAAA;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,KAAK,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,EAAE,CAAA;CACrB;AAMD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACvC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/services/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAGpD,OAAO,KAAK,EACX,eAAe,EAIf,mBAAmB,EACnB,MAAM,SAAS,CAAA;AAyBhB;;;;GAIG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,YAAY,MAAM,EAAE,cAAc,EAEjC;IAED;;;;;OAKG;IACG,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CA8CzE;IAMD;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;YAkBN,oBAAoB;YAoEpB,0BAA0B;YA8F1B,cAAc;YAgGd,YAAY;YAgCZ,gBAAgB;YA2BhB,gBAAgB;CAmH9B"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/services/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAGpD,OAAO,KAAK,EACX,eAAe,EAIf,mBAAmB,EACnB,MAAM,SAAS,CAAA;AAiChB;;;;GAIG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC,YAAY,MAAM,EAAE,cAAc,EAEjC;IAED;;;;;OAKG;IACG,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiDzE;IAMD;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;YAkBN,oBAAoB;YAoEpB,0BAA0B;YA8F1B,cAAc;YAgGd,YAAY;YAgCZ,gBAAgB;YA2BhB,gBAAgB;CAmH9B"}
@@ -6,8 +6,8 @@
6
6
  export { BootstrapService } from './bootstrap';
7
7
  export { EnrollmentService } from './enrollment';
8
8
  export { EventsService } from './events';
9
- export { LiveService } from './live';
9
+ export { getOrCreatePoller } from './live';
10
10
  export { StatusService } from './status';
11
11
  export { StudentSearchService } from './student-search';
12
- export type { BootstrapResult, BootstrapStats, EnrichedComponentResource, EnrichedEnrollment, GetBootstrapOptions, EnrollmentActionOptions, EnrollmentResult, EnrollStudentOptions, GetNewEventsOptions, GetRecentEventsOptions, TickResult, TickState, SearchStudentsOptions, StudentSearchResponse, StudentSearchResult, } from './types';
12
+ export type { BootstrapResult, BootstrapStats, EnrichedComponentResource, EnrichedEnrollment, GetBootstrapOptions, EnrollmentActionOptions, EnrollmentResult, EnrollStudentOptions, GetNewEventsOptions, GetRecentEventsOptions, TickState, SearchStudentsOptions, StudentSearchResponse, StudentSearchResult, } from './types';
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAMvD,YAAY,EAEX,eAAe,EACf,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EAEnB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EAEpB,mBAAmB,EACnB,sBAAsB,EAEtB,UAAU,EACV,SAAS,EAET,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAMvD,YAAY,EAEX,eAAe,EACf,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,EAEnB,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EAEpB,mBAAmB,EACnB,sBAAsB,EAEtB,SAAS,EAET,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,SAAS,CAAA"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Live Poller
3
+ *
4
+ * Shared polling loop for the multiplexed /api/live SSE endpoint.
5
+ *
6
+ * ## Why This Exists
7
+ *
8
+ * The Studio server is a single-user process — every browser tab connects
9
+ * to the same backend and sees the same data. Rather than each SSE connection
10
+ * running its own polling loop (duplicating API calls), a single LivePoller
11
+ * per environment polls once and broadcasts to all subscribers.
12
+ *
13
+ * ## How It Works
14
+ *
15
+ * ```
16
+ * LivePoller (one per environment)
17
+ * => ticks every 1s
18
+ * => ALWAYS fetch status, emit only if hash changed
19
+ * => Every 30s, fetch events, emit if any new
20
+ * => broadcast serialized updates to all subscribers
21
+ * ```
22
+ *
23
+ * ## Lifecycle
24
+ *
25
+ * Ref-counted: the poller starts when the first SSE connection subscribes,
26
+ * and stops when the last one disconnects. No polling when nobody is listening.
27
+ *
28
+ * ## Data Sources
29
+ *
30
+ * | Source | Check Frequency | Emit Condition |
31
+ * |---------|-----------------|----------------------|
32
+ * | Status | Every tick (1s) | Payload hash changed |
33
+ * | Events | Every 30s | New events exist |
34
+ */
35
+ import type { EventsService } from './events';
36
+ import type { StatusService } from './status';
37
+ /**
38
+ * Get an existing poller for the environment, or create one.
39
+ *
40
+ * The poller is shared across all SSE connections for the same environment.
41
+ * It is removed from the registry when its last subscriber disconnects.
42
+ *
43
+ * @param environment - Environment key (e.g. 'staging', 'production')
44
+ * @param statusService - Status service instance
45
+ * @param eventsService - Events service instance
46
+ * @returns The shared LivePoller for this environment
47
+ */
48
+ export declare function getOrCreatePoller(environment: string, statusService: StatusService, eventsService: EventsService): LivePoller;
49
+ /**
50
+ * Options for creating a LivePoller.
51
+ */
52
+ interface LivePollerOptions {
53
+ environment: string;
54
+ statusService: StatusService;
55
+ eventsService: EventsService;
56
+ }
57
+ /** Callback invoked when the poller has new data to broadcast. */
58
+ type Subscriber = (data: string) => void;
59
+ /**
60
+ * Shared polling loop for a single environment.
61
+ *
62
+ * Owns the tick state (status hash, event cursor) and broadcasts
63
+ * serialized updates to all subscribed SSE connections.
64
+ */
65
+ export declare class LivePoller {
66
+ private readonly environment;
67
+ private readonly statusService;
68
+ private readonly eventsService;
69
+ private readonly subscribers;
70
+ private interval;
71
+ private state;
72
+ private lastBroadcast;
73
+ private initialTickPromise;
74
+ private ticking;
75
+ constructor(options: LivePollerOptions);
76
+ /**
77
+ * Subscribe to updates from this poller.
78
+ *
79
+ * Starts the polling loop on the first subscriber.
80
+ * Returns an unsubscribe function that stops polling when the last subscriber leaves.
81
+ *
82
+ * @param callback - Called with serialized LiveUpdate[] JSON when new data is available
83
+ * @returns Unsubscribe function
84
+ */
85
+ subscribe(callback: Subscriber): () => void;
86
+ /**
87
+ * Wait for the initial tick (triggered when polling starts) and then
88
+ * return the most recent broadcast payload.
89
+ *
90
+ * This is used by newly connected SSE clients so the initial send can
91
+ * include up-to-date data even if the first poll is still in progress.
92
+ *
93
+ * @returns Last broadcast JSON string, or undefined
94
+ */
95
+ getLastBroadcastAfterInitialTick(): Promise<string | undefined>;
96
+ private start;
97
+ private stop;
98
+ private tick;
99
+ private fetchStatusIfChanged;
100
+ private fetchEventsIfNew;
101
+ }
102
+ export {};
103
+ //# sourceMappingURL=live-poller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-poller.d.ts","sourceRoot":"","sources":["../../../src/server/services/live-poller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAW7C;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAChC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,GAC1B,UAAU,CASZ;AAMD;;GAEG;AACH,UAAU,iBAAiB;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,aAAa,CAAA;IAC5B,aAAa,EAAE,aAAa,CAAA;CAC5B;AAED,kEAAkE;AAClE,KAAK,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAExC;;;;;GAKG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAE7C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAA8C;IAC9D,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,OAAO,CAAQ;IAEvB,YAAY,OAAO,EAAE,iBAAiB,EAYrC;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,IAAI,CAe1C;IAED;;;;;;;;OAQG;IACG,gCAAgC,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAKpE;IAMD,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,IAAI;YAeE,IAAI;YA8DJ,oBAAoB;YA4BpB,gBAAgB;CA2C9B"}
@@ -1,83 +1,8 @@
1
1
  /**
2
- * Live Service
2
+ * Live Service (re-export)
3
3
  *
4
- * Orchestrates multiple data sources for the multiplexed /api/live SSE endpoint.
5
- *
6
- * ## How It Works
7
- *
8
- * The SSE connection ticks every 1 second (base interval). On each tick,
9
- * this service decides what to check based on each data source's rules:
10
- *
11
- * ```
12
- * runSSE setInterval(1s)
13
- * => every tick, calls liveService.tick(state)
14
- *
15
- * liveService.tick():
16
- * => ALWAYS fetch status, but only emit if hash changed
17
- * => Check if 30s elapsed since lastEventsTick:
18
- * - YES: fetch events, emit if any new
19
- * - NO: skip events entirely
20
- *
21
- * => return { updates: [...], newState }
22
- * ```
23
- *
24
- * ## Data Sources
25
- *
26
- * | Source | Check Frequency | Emit Condition |
27
- * |---------|-----------------|---------------------|
28
- * | Status | Every tick (1s) | Payload hash changed |
29
- * | Events | Every 30s | New events exist |
30
- *
31
- * This means 29 out of 30 ticks only check status. The 30th tick checks both.
32
- */
33
- import type { EventsService } from './events';
34
- import type { StatusService } from './status';
35
- import type { TickResult, TickState } from './types';
36
- /**
37
- * Options for creating a LiveService.
38
- */
39
- interface LiveServiceOptions {
40
- statusService: StatusService;
41
- eventsService: EventsService;
42
- }
43
- /**
44
- * Service that orchestrates status and event updates for the multiplexed SSE endpoint.
45
- *
46
- * Each SSE connection gets its own instance. The service manages independent tick
47
- * intervals: status is checked every tick (1s) but only emitted on hash change,
48
- * while events are fetched every 30s and emitted when new events exist.
4
+ * The shared LivePoller replaces the per-connection LiveService.
5
+ * This file re-exports for barrel compatibility.
49
6
  */
50
- export declare class LiveService {
51
- private readonly statusService;
52
- private readonly eventsService;
53
- constructor(options: LiveServiceOptions);
54
- /**
55
- * Create initial tick state for a new SSE connection.
56
- *
57
- * @returns Fresh tick state with empty hash and current timestamps
58
- */
59
- static createInitialState(): TickState;
60
- /**
61
- * Perform a tick operation.
62
- *
63
- * Called every base tick (1s) by the SSE loop. Checks each data source
64
- * according to its rules and returns any updates to emit.
65
- *
66
- * @param state - Current tick state from previous tick
67
- * @returns Updates to emit and new state for next tick
68
- */
69
- tick(state: TickState): Promise<TickResult>;
70
- /**
71
- * Check if enough time has passed since the last check.
72
- *
73
- * @param lastTick - Timestamp (ms) of last check
74
- * @param now - Current timestamp (ms)
75
- * @param intervalMs - Required interval between checks
76
- * @returns True if interval has elapsed
77
- */
78
- private hasIntervalElapsed;
79
- private fetchStatusIfChanged;
80
- private fetchEventsIfNew;
81
- }
82
- export {};
7
+ export { getOrCreatePoller } from './live-poller';
83
8
  //# sourceMappingURL=live.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../src/server/services/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAYH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIpD;;GAEG;AACH,UAAU,kBAAkB;IAC3B,aAAa,EAAE,aAAa,CAAA;IAC5B,aAAa,EAAE,aAAa,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAE7C,YAAY,OAAO,EAAE,kBAAkB,EAGtC;IAED;;;;OAIG;IACH,MAAM,CAAC,kBAAkB,IAAI,SAAS,CAQrC;IAED;;;;;;;;OAQG;IACG,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAoChD;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;YAUZ,oBAAoB;YA4BpB,gBAAgB;CAiC9B"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../src/server/services/live.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/server/services/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;GAKG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAEhC,YAAY,GAAG,EAAE,UAAU,EAE1B;IAED;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAc7C;CACD"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/server/services/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAErD;;;;;GAKG;AACH,qBAAa,aAAa;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAEhC,YAAY,GAAG,EAAE,UAAU,EAE1B;IAED;;;;;;;OAOG;IACG,SAAS,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA4B7C;CACD"}
@@ -7,5 +7,5 @@ export type { BootstrapResult, BootstrapStats, EnrichedComponentResource, Enrich
7
7
  export type { EnrollmentActionOptions, EnrollmentResult, EnrollStudentOptions } from './enrollment';
8
8
  export type { GetNewEventsOptions, GetRecentEventsOptions } from './events';
9
9
  export type { SearchStudentsOptions, StudentSearchResponse, StudentSearchResult, } from './student-search';
10
- export type { TickResult, TickState } from './live';
10
+ export type { TickState } from './live';
11
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/server/services/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,eAAe,EACf,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,GACnB,MAAM,aAAa,CAAA;AAEpB,YAAY,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEnG,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAE3E,YAAY,EACX,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/server/services/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,eAAe,EACf,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,mBAAmB,GACnB,MAAM,aAAa,CAAA;AAEpB,YAAY,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAEnG,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAE3E,YAAY,EACX,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAA"}
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Live Service Types
2
+ * Live Poller Types
3
3
  *
4
4
  * Types for the multiplexed SSE endpoint that combines status and events.
5
5
  */
6
- import type { LiveUpdate } from '../../../types';
7
6
  /**
8
- * Per-connection state for tracking when to check each data source.
7
+ * State for tracking when to check each data source.
9
8
  *
10
- * Each SSE connection maintains its own TickState. The LiveService uses
11
- * this state to determine:
9
+ * Owned by the shared LivePoller (one per environment). Used to determine:
12
10
  * - Status: Has the payload hash changed since last emit?
13
11
  * - Events: Has 30s elapsed since last check?
14
12
  */
@@ -22,13 +20,4 @@ export interface TickState {
22
20
  /** Timestamp (ms) of last events check. Used to enforce 30s interval. */
23
21
  lastEventsTick: number;
24
22
  }
25
- /**
26
- * Result of a tick operation.
27
- */
28
- export interface TickResult {
29
- /** Updates to emit to the client (may be empty if nothing changed) */
30
- updates: LiveUpdate[];
31
- /** Updated state to pass to the next tick */
32
- newState: TickState;
33
- }
34
23
  //# sourceMappingURL=live.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../../src/server/services/types/live.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IAKzB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAA;IAEtB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IAMtB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAA;IAErB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,OAAO,EAAE,UAAU,EAAE,CAAA;IAErB,6CAA6C;IAC7C,QAAQ,EAAE,SAAS,CAAA;CACnB"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../../../../src/server/services/types/live.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IAKzB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAA;IAEtB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAA;IAMtB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAA;IAErB,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAA;CACtB"}
package/dist/types.d.ts CHANGED
@@ -32,6 +32,8 @@ export interface StatusEventPayload {
32
32
  environment: StudioEnvironment;
33
33
  configuredEnvironments: StudioEnvironment[];
34
34
  hasEmail: boolean;
35
+ clientId?: string;
36
+ sensors?: string[];
35
37
  }
36
38
  /** Payload for 'events' SSE events (Caliper events stream) */
37
39
  export interface EventsEventPayload {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAGzE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EAAE,gBAAgB,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAMtE;;GAEG;AACH,MAAM,MAAM,eAAe,GAExB,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,GACzB,kCAAkC,GAClC,wBAAwB,GACxB,yBAAyB,GACzB,sBAAsB,GACtB,0BAA0B,GAC1B,2BAA2B,GAC3B,wBAAwB,GAExB,oBAAoB,GACpB,aAAa,GAEb,mBAAmB,GACnB,qBAAqB,GAErB,eAAe,CAAA;AAElB;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,2CAA2C;IAC3C,IAAI,EAAE,eAAe,CAAA;IACrB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAMD,kCAAkC;AAClC,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,gBAAgB,CAAA;IACxB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,sBAAsB,EAAE,iBAAiB,EAAE,CAAA;IAC3C,QAAQ,EAAE,OAAO,CAAA;CACjB;AAED,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IAClC,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,CAAA;IAC7B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;CACjB;AAMD,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEhD,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,kBAAkB,CAAA;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,kBAAkB,CAAA;CACxB;AAED,qCAAqC;AACrC,MAAM,MAAM,UAAU,GAAG,gBAAgB,GAAG,gBAAgB,CAAA;AAM5D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,IAAI,EAAE,UAAU,EAAE,CAAA;CAClB;AAED,yCAAyC;AACzC,MAAM,MAAM,eAAe,GAAG,MAAM,cAAc,CAAA;AAMlD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI;KAC7E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI;CAC5C,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAGzE,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EAAE,gBAAgB,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAMtE;;GAEG;AACH,MAAM,MAAM,eAAe,GAExB,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,GACrB,qBAAqB,GACrB,yBAAyB,GACzB,kCAAkC,GAClC,wBAAwB,GACxB,yBAAyB,GACzB,sBAAsB,GACtB,0BAA0B,GAC1B,2BAA2B,GAC3B,wBAAwB,GAExB,oBAAoB,GACpB,aAAa,GAEb,mBAAmB,GACnB,qBAAqB,GAErB,eAAe,CAAA;AAElB;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,2CAA2C;IAC3C,IAAI,EAAE,eAAe,CAAA;IACrB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAMD,kCAAkC;AAClC,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,gBAAgB,CAAA;IACxB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,sBAAsB,EAAE,iBAAiB,EAAE,CAAA;IAC3C,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IAClC,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,CAAA;IAC7B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;CACjB;AAMD,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEhD,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,kBAAkB,CAAA;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE,kBAAkB,CAAA;CACxB;AAED,qCAAqC;AACrC,MAAM,MAAM,UAAU,GAAG,gBAAgB,GAAG,gBAAgB,CAAA;AAM5D;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,MAAM,EAAE,kBAAkB,CAAA;IAC1B,IAAI,EAAE,UAAU,EAAE,CAAA;CAClB;AAED,yCAAyC;AACzC,MAAM,MAAM,eAAe,GAAG,MAAM,cAAc,CAAA;AAMlD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,IAAI;KAC7E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI;CAC5C,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timeback-studio",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -27,7 +27,7 @@
27
27
  "dependencies": {
28
28
  "@clack/prompts": "^0.11.0",
29
29
  "@hono/node-server": "^1.19.7",
30
- "@timeback/core": "0.1.4",
30
+ "@timeback/core": "0.1.5",
31
31
  "c12": "^3.3.3",
32
32
  "colorette": "^2.0.20",
33
33
  "commander": "^14.0.2",