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 +151 -47
- package/dist/index.js +151 -47
- package/dist/server/controllers/bootstrap.d.ts +6 -6
- package/dist/server/controllers/live.d.ts +5 -3
- package/dist/server/controllers/live.d.ts.map +1 -1
- package/dist/server/controllers/status.d.ts +6 -1
- package/dist/server/controllers/status.d.ts.map +1 -1
- package/dist/server/lib/sse.d.ts.map +1 -1
- package/dist/server/lib/types.d.ts +2 -0
- package/dist/server/lib/types.d.ts.map +1 -1
- package/dist/server/services/bootstrap.d.ts.map +1 -1
- package/dist/server/services/index.d.ts +2 -2
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/live-poller.d.ts +103 -0
- package/dist/server/services/live-poller.d.ts.map +1 -0
- package/dist/server/services/live.d.ts +4 -79
- package/dist/server/services/live.d.ts.map +1 -1
- package/dist/server/services/status.d.ts.map +1 -1
- package/dist/server/services/types/index.d.ts +1 -1
- package/dist/server/services/types/index.d.ts.map +1 -1
- package/dist/server/services/types/live.d.ts +3 -14
- package/dist/server/services/types/live.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
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")
|
|
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
|
-
}).
|
|
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
|
|
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.
|
|
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
|
-
|
|
22971
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
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
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
|
|
23483
|
-
|
|
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
|
-
|
|
23514
|
+
};
|
|
23515
|
+
}
|
|
23516
|
+
async getLastBroadcastAfterInitialTick() {
|
|
23517
|
+
if (this.initialTickPromise) {
|
|
23518
|
+
await this.initialTickPromise;
|
|
23488
23519
|
}
|
|
23489
|
-
return
|
|
23520
|
+
return this.lastBroadcast;
|
|
23490
23521
|
}
|
|
23491
|
-
|
|
23492
|
-
|
|
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
|
|
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
|
|
24037
|
-
let
|
|
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
|
-
|
|
24044
|
-
|
|
24045
|
-
|
|
24046
|
-
|
|
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
|
|
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")
|
|
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
|
-
}).
|
|
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
|
|
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.
|
|
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
|
-
|
|
20861
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
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
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
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
|
-
|
|
21404
|
+
};
|
|
21405
|
+
}
|
|
21406
|
+
async getLastBroadcastAfterInitialTick() {
|
|
21407
|
+
if (this.initialTickPromise) {
|
|
21408
|
+
await this.initialTickPromise;
|
|
21378
21409
|
}
|
|
21379
|
-
return
|
|
21410
|
+
return this.lastBroadcast;
|
|
21380
21411
|
}
|
|
21381
|
-
|
|
21382
|
-
|
|
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
|
|
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
|
|
21927
|
-
let
|
|
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
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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/
|
|
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
|
|
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,
|
|
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;
|
|
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;
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
7
|
+
* State for tracking when to check each data source.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
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
|
|
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 {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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.
|
|
30
|
+
"@timeback/core": "0.1.5",
|
|
31
31
|
"c12": "^3.3.3",
|
|
32
32
|
"colorette": "^2.0.20",
|
|
33
33
|
"commander": "^14.0.2",
|