timeback-studio 0.1.7 → 0.1.8
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 +122 -36
- package/dist/index.js +122 -36
- package/dist/server/controllers/live.d.ts +5 -3
- package/dist/server/controllers/live.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/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/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -22937,6 +22937,7 @@ function createLogger(options = {}) {
|
|
|
22937
22937
|
}
|
|
22938
22938
|
// src/server/services/bootstrap.ts
|
|
22939
22939
|
var log = createLogger({ scope: "studio:service:bootstrap" });
|
|
22940
|
+
var hasLoggedInitialLoad = false;
|
|
22940
22941
|
|
|
22941
22942
|
class BootstrapService {
|
|
22942
22943
|
client;
|
|
@@ -22967,8 +22968,11 @@ class BootstrapService {
|
|
|
22967
22968
|
stats,
|
|
22968
22969
|
errors: errors3.length
|
|
22969
22970
|
});
|
|
22970
|
-
|
|
22971
|
-
|
|
22971
|
+
if (!hasLoggedInitialLoad) {
|
|
22972
|
+
const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
|
|
22973
|
+
M2.success(message);
|
|
22974
|
+
hasLoggedInitialLoad = true;
|
|
22975
|
+
}
|
|
22972
22976
|
return {
|
|
22973
22977
|
user,
|
|
22974
22978
|
courses,
|
|
@@ -23448,48 +23452,107 @@ class EventsService {
|
|
|
23448
23452
|
return result;
|
|
23449
23453
|
}
|
|
23450
23454
|
}
|
|
23451
|
-
// src/server/services/live.ts
|
|
23452
|
-
var log4 = createLogger({ scope: "studio:service:live" });
|
|
23455
|
+
// src/server/services/live-poller.ts
|
|
23456
|
+
var log4 = createLogger({ scope: "studio:service:live-poller" });
|
|
23457
|
+
var pollers = new Map;
|
|
23458
|
+
function getOrCreatePoller(environment, statusService, eventsService) {
|
|
23459
|
+
let poller = pollers.get(environment);
|
|
23460
|
+
if (!poller) {
|
|
23461
|
+
poller = new LivePoller({ environment, statusService, eventsService });
|
|
23462
|
+
pollers.set(environment, poller);
|
|
23463
|
+
}
|
|
23464
|
+
return poller;
|
|
23465
|
+
}
|
|
23453
23466
|
|
|
23454
|
-
class
|
|
23467
|
+
class LivePoller {
|
|
23468
|
+
environment;
|
|
23455
23469
|
statusService;
|
|
23456
23470
|
eventsService;
|
|
23471
|
+
subscribers = new Set;
|
|
23472
|
+
interval = null;
|
|
23473
|
+
state;
|
|
23474
|
+
lastBroadcast;
|
|
23475
|
+
initialTickPromise = null;
|
|
23476
|
+
ticking = false;
|
|
23457
23477
|
constructor(options) {
|
|
23478
|
+
this.environment = options.environment;
|
|
23458
23479
|
this.statusService = options.statusService;
|
|
23459
23480
|
this.eventsService = options.eventsService;
|
|
23460
|
-
}
|
|
23461
|
-
static createInitialState() {
|
|
23462
23481
|
const now = Date.now();
|
|
23463
|
-
|
|
23482
|
+
this.state = {
|
|
23464
23483
|
lastStatusHash: "",
|
|
23465
23484
|
lastEventTime: new Date().toISOString(),
|
|
23466
23485
|
lastStatusTick: now,
|
|
23467
23486
|
lastEventsTick: now
|
|
23468
23487
|
};
|
|
23469
23488
|
}
|
|
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;
|
|
23489
|
+
subscribe(callback) {
|
|
23490
|
+
this.subscribers.add(callback);
|
|
23491
|
+
if (this.subscribers.size === 1) {
|
|
23492
|
+
this.start();
|
|
23478
23493
|
}
|
|
23479
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
updates.push(eventsUpdate.update);
|
|
23485
|
-
newState.lastEventTime = eventsUpdate.newEventTime;
|
|
23494
|
+
return () => {
|
|
23495
|
+
this.subscribers.delete(callback);
|
|
23496
|
+
if (this.subscribers.size === 0) {
|
|
23497
|
+
this.stop();
|
|
23498
|
+
pollers.delete(this.environment);
|
|
23486
23499
|
}
|
|
23487
|
-
|
|
23500
|
+
};
|
|
23501
|
+
}
|
|
23502
|
+
async getLastBroadcastAfterInitialTick() {
|
|
23503
|
+
if (this.initialTickPromise) {
|
|
23504
|
+
await this.initialTickPromise;
|
|
23488
23505
|
}
|
|
23489
|
-
return
|
|
23506
|
+
return this.lastBroadcast;
|
|
23490
23507
|
}
|
|
23491
|
-
|
|
23492
|
-
|
|
23508
|
+
start() {
|
|
23509
|
+
log4.debug("Poller started", { environment: this.environment });
|
|
23510
|
+
this.initialTickPromise = this.tick();
|
|
23511
|
+
this.interval = setInterval(() => this.tick(), constants.sse.defaultTickIntervalMs);
|
|
23512
|
+
}
|
|
23513
|
+
stop() {
|
|
23514
|
+
if (this.interval) {
|
|
23515
|
+
clearInterval(this.interval);
|
|
23516
|
+
this.interval = null;
|
|
23517
|
+
}
|
|
23518
|
+
this.lastBroadcast = undefined;
|
|
23519
|
+
this.initialTickPromise = null;
|
|
23520
|
+
log4.debug("Poller stopped", { environment: this.environment });
|
|
23521
|
+
}
|
|
23522
|
+
async tick() {
|
|
23523
|
+
if (this.ticking)
|
|
23524
|
+
return;
|
|
23525
|
+
this.ticking = true;
|
|
23526
|
+
try {
|
|
23527
|
+
const updates = [];
|
|
23528
|
+
const now = Date.now();
|
|
23529
|
+
const statusUpdate = await this.fetchStatusIfChanged(this.state.lastStatusHash);
|
|
23530
|
+
if (statusUpdate) {
|
|
23531
|
+
updates.push(statusUpdate.update);
|
|
23532
|
+
this.state.lastStatusHash = statusUpdate.hash;
|
|
23533
|
+
}
|
|
23534
|
+
this.state.lastStatusTick = now;
|
|
23535
|
+
const shouldCheckEvents = now - this.state.lastEventsTick >= constants.events.polling.tickIntervalMs;
|
|
23536
|
+
if (shouldCheckEvents) {
|
|
23537
|
+
const eventsUpdate = await this.fetchEventsIfNew(this.state.lastEventTime);
|
|
23538
|
+
if (eventsUpdate) {
|
|
23539
|
+
updates.push(eventsUpdate.update);
|
|
23540
|
+
this.state.lastEventTime = eventsUpdate.newEventTime;
|
|
23541
|
+
}
|
|
23542
|
+
this.state.lastEventsTick = now;
|
|
23543
|
+
}
|
|
23544
|
+
if (updates.length > 0) {
|
|
23545
|
+
const data = JSON.stringify(updates);
|
|
23546
|
+
this.lastBroadcast = data;
|
|
23547
|
+
for (const callback of this.subscribers) {
|
|
23548
|
+
callback(data);
|
|
23549
|
+
}
|
|
23550
|
+
}
|
|
23551
|
+
} catch (error48) {
|
|
23552
|
+
log4.error("Tick failed", { environment: this.environment, error: error48 });
|
|
23553
|
+
} finally {
|
|
23554
|
+
this.ticking = false;
|
|
23555
|
+
}
|
|
23493
23556
|
}
|
|
23494
23557
|
async fetchStatusIfChanged(lastHash) {
|
|
23495
23558
|
try {
|
|
@@ -23516,7 +23579,8 @@ class LiveService {
|
|
|
23516
23579
|
}
|
|
23517
23580
|
const message = `${underline(newEvents.length)} new ${pluralize(newEvents.length, "event")}`;
|
|
23518
23581
|
M2.message(message, { symbol: magenta("◆") });
|
|
23519
|
-
const mostRecentTime = newEvents.map((e2) => e2.eventTime).sort().pop();
|
|
23582
|
+
const mostRecentTime = newEvents.map((e2) => new Date(e2.eventTime).getTime()).sort((a, b3) => a - b3).pop();
|
|
23583
|
+
const newEventTime = mostRecentTime === undefined ? lastEventTime : new Date(mostRecentTime + 1).toISOString();
|
|
23520
23584
|
return {
|
|
23521
23585
|
update: {
|
|
23522
23586
|
type: "events",
|
|
@@ -23525,7 +23589,7 @@ class LiveService {
|
|
|
23525
23589
|
timestamp: new Date().toISOString()
|
|
23526
23590
|
}
|
|
23527
23591
|
},
|
|
23528
|
-
newEventTime
|
|
23592
|
+
newEventTime
|
|
23529
23593
|
};
|
|
23530
23594
|
} catch (error48) {
|
|
23531
23595
|
log4.error("Failed to fetch events", { error: error48 });
|
|
@@ -23930,6 +23994,7 @@ function runSSE(c, options) {
|
|
|
23930
23994
|
} catch {
|
|
23931
23995
|
abort("write error");
|
|
23932
23996
|
}
|
|
23997
|
+
options.onClose?.();
|
|
23933
23998
|
unregisterConnection(key, () => abort("superseded"));
|
|
23934
23999
|
log8.debug("SSE connection closed", { ...meta3, reason: closeReason });
|
|
23935
24000
|
});
|
|
@@ -24032,20 +24097,41 @@ function handleEventsStream(c) {
|
|
|
24032
24097
|
}
|
|
24033
24098
|
// src/server/controllers/live.ts
|
|
24034
24099
|
function handleLive(c) {
|
|
24100
|
+
const env2 = c.get("env");
|
|
24035
24101
|
const { status: statusService, events: eventsService } = c.get("services");
|
|
24036
|
-
const
|
|
24037
|
-
let
|
|
24102
|
+
const poller = getOrCreatePoller(env2, statusService, eventsService);
|
|
24103
|
+
let pending;
|
|
24104
|
+
let isFirstTick = true;
|
|
24105
|
+
const takePending = () => {
|
|
24106
|
+
const data = pending;
|
|
24107
|
+
pending = undefined;
|
|
24108
|
+
return data;
|
|
24109
|
+
};
|
|
24110
|
+
const unsubscribe = poller.subscribe((data) => {
|
|
24111
|
+
pending = data;
|
|
24112
|
+
});
|
|
24038
24113
|
return runSSE(c, {
|
|
24039
24114
|
event: "live",
|
|
24040
24115
|
intervalMs: constants.sse.defaultTickIntervalMs,
|
|
24041
24116
|
sendInitial: true,
|
|
24117
|
+
onClose: unsubscribe,
|
|
24042
24118
|
tick: async () => {
|
|
24043
|
-
|
|
24044
|
-
|
|
24045
|
-
|
|
24046
|
-
|
|
24119
|
+
if (isFirstTick) {
|
|
24120
|
+
isFirstTick = false;
|
|
24121
|
+
const immediate = takePending();
|
|
24122
|
+
if (immediate !== undefined) {
|
|
24123
|
+
return immediate;
|
|
24124
|
+
}
|
|
24125
|
+
const initial = await poller.getLastBroadcastAfterInitialTick();
|
|
24126
|
+
if (pending !== undefined && pending === initial) {
|
|
24127
|
+
pending = undefined;
|
|
24128
|
+
}
|
|
24129
|
+
if (initial !== undefined) {
|
|
24130
|
+
return initial;
|
|
24131
|
+
}
|
|
24132
|
+
return takePending();
|
|
24047
24133
|
}
|
|
24048
|
-
return
|
|
24134
|
+
return takePending();
|
|
24049
24135
|
}
|
|
24050
24136
|
});
|
|
24051
24137
|
}
|
package/dist/index.js
CHANGED
|
@@ -20827,6 +20827,7 @@ function createLogger(options = {}) {
|
|
|
20827
20827
|
}
|
|
20828
20828
|
// src/server/services/bootstrap.ts
|
|
20829
20829
|
var log = createLogger({ scope: "studio:service:bootstrap" });
|
|
20830
|
+
var hasLoggedInitialLoad = false;
|
|
20830
20831
|
|
|
20831
20832
|
class BootstrapService {
|
|
20832
20833
|
client;
|
|
@@ -20857,8 +20858,11 @@ class BootstrapService {
|
|
|
20857
20858
|
stats,
|
|
20858
20859
|
errors: errors3.length
|
|
20859
20860
|
});
|
|
20860
|
-
|
|
20861
|
-
|
|
20861
|
+
if (!hasLoggedInitialLoad) {
|
|
20862
|
+
const message = `Loaded ${underline(stats.totalCourses)} ${pluralize(stats.totalCourses, "course")}, ${underline(stats.totalStudents)} ${pluralize(stats.totalStudents, "student")}`;
|
|
20863
|
+
M2.success(message);
|
|
20864
|
+
hasLoggedInitialLoad = true;
|
|
20865
|
+
}
|
|
20862
20866
|
return {
|
|
20863
20867
|
user,
|
|
20864
20868
|
courses,
|
|
@@ -21338,48 +21342,107 @@ class EventsService {
|
|
|
21338
21342
|
return result;
|
|
21339
21343
|
}
|
|
21340
21344
|
}
|
|
21341
|
-
// src/server/services/live.ts
|
|
21342
|
-
var log4 = createLogger({ scope: "studio:service:live" });
|
|
21345
|
+
// src/server/services/live-poller.ts
|
|
21346
|
+
var log4 = createLogger({ scope: "studio:service:live-poller" });
|
|
21347
|
+
var pollers = new Map;
|
|
21348
|
+
function getOrCreatePoller(environment, statusService, eventsService) {
|
|
21349
|
+
let poller = pollers.get(environment);
|
|
21350
|
+
if (!poller) {
|
|
21351
|
+
poller = new LivePoller({ environment, statusService, eventsService });
|
|
21352
|
+
pollers.set(environment, poller);
|
|
21353
|
+
}
|
|
21354
|
+
return poller;
|
|
21355
|
+
}
|
|
21343
21356
|
|
|
21344
|
-
class
|
|
21357
|
+
class LivePoller {
|
|
21358
|
+
environment;
|
|
21345
21359
|
statusService;
|
|
21346
21360
|
eventsService;
|
|
21361
|
+
subscribers = new Set;
|
|
21362
|
+
interval = null;
|
|
21363
|
+
state;
|
|
21364
|
+
lastBroadcast;
|
|
21365
|
+
initialTickPromise = null;
|
|
21366
|
+
ticking = false;
|
|
21347
21367
|
constructor(options) {
|
|
21368
|
+
this.environment = options.environment;
|
|
21348
21369
|
this.statusService = options.statusService;
|
|
21349
21370
|
this.eventsService = options.eventsService;
|
|
21350
|
-
}
|
|
21351
|
-
static createInitialState() {
|
|
21352
21371
|
const now = Date.now();
|
|
21353
|
-
|
|
21372
|
+
this.state = {
|
|
21354
21373
|
lastStatusHash: "",
|
|
21355
21374
|
lastEventTime: new Date().toISOString(),
|
|
21356
21375
|
lastStatusTick: now,
|
|
21357
21376
|
lastEventsTick: now
|
|
21358
21377
|
};
|
|
21359
21378
|
}
|
|
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;
|
|
21379
|
+
subscribe(callback) {
|
|
21380
|
+
this.subscribers.add(callback);
|
|
21381
|
+
if (this.subscribers.size === 1) {
|
|
21382
|
+
this.start();
|
|
21368
21383
|
}
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
updates.push(eventsUpdate.update);
|
|
21375
|
-
newState.lastEventTime = eventsUpdate.newEventTime;
|
|
21384
|
+
return () => {
|
|
21385
|
+
this.subscribers.delete(callback);
|
|
21386
|
+
if (this.subscribers.size === 0) {
|
|
21387
|
+
this.stop();
|
|
21388
|
+
pollers.delete(this.environment);
|
|
21376
21389
|
}
|
|
21377
|
-
|
|
21390
|
+
};
|
|
21391
|
+
}
|
|
21392
|
+
async getLastBroadcastAfterInitialTick() {
|
|
21393
|
+
if (this.initialTickPromise) {
|
|
21394
|
+
await this.initialTickPromise;
|
|
21378
21395
|
}
|
|
21379
|
-
return
|
|
21396
|
+
return this.lastBroadcast;
|
|
21380
21397
|
}
|
|
21381
|
-
|
|
21382
|
-
|
|
21398
|
+
start() {
|
|
21399
|
+
log4.debug("Poller started", { environment: this.environment });
|
|
21400
|
+
this.initialTickPromise = this.tick();
|
|
21401
|
+
this.interval = setInterval(() => this.tick(), constants.sse.defaultTickIntervalMs);
|
|
21402
|
+
}
|
|
21403
|
+
stop() {
|
|
21404
|
+
if (this.interval) {
|
|
21405
|
+
clearInterval(this.interval);
|
|
21406
|
+
this.interval = null;
|
|
21407
|
+
}
|
|
21408
|
+
this.lastBroadcast = undefined;
|
|
21409
|
+
this.initialTickPromise = null;
|
|
21410
|
+
log4.debug("Poller stopped", { environment: this.environment });
|
|
21411
|
+
}
|
|
21412
|
+
async tick() {
|
|
21413
|
+
if (this.ticking)
|
|
21414
|
+
return;
|
|
21415
|
+
this.ticking = true;
|
|
21416
|
+
try {
|
|
21417
|
+
const updates = [];
|
|
21418
|
+
const now = Date.now();
|
|
21419
|
+
const statusUpdate = await this.fetchStatusIfChanged(this.state.lastStatusHash);
|
|
21420
|
+
if (statusUpdate) {
|
|
21421
|
+
updates.push(statusUpdate.update);
|
|
21422
|
+
this.state.lastStatusHash = statusUpdate.hash;
|
|
21423
|
+
}
|
|
21424
|
+
this.state.lastStatusTick = now;
|
|
21425
|
+
const shouldCheckEvents = now - this.state.lastEventsTick >= constants.events.polling.tickIntervalMs;
|
|
21426
|
+
if (shouldCheckEvents) {
|
|
21427
|
+
const eventsUpdate = await this.fetchEventsIfNew(this.state.lastEventTime);
|
|
21428
|
+
if (eventsUpdate) {
|
|
21429
|
+
updates.push(eventsUpdate.update);
|
|
21430
|
+
this.state.lastEventTime = eventsUpdate.newEventTime;
|
|
21431
|
+
}
|
|
21432
|
+
this.state.lastEventsTick = now;
|
|
21433
|
+
}
|
|
21434
|
+
if (updates.length > 0) {
|
|
21435
|
+
const data = JSON.stringify(updates);
|
|
21436
|
+
this.lastBroadcast = data;
|
|
21437
|
+
for (const callback of this.subscribers) {
|
|
21438
|
+
callback(data);
|
|
21439
|
+
}
|
|
21440
|
+
}
|
|
21441
|
+
} catch (error48) {
|
|
21442
|
+
log4.error("Tick failed", { environment: this.environment, error: error48 });
|
|
21443
|
+
} finally {
|
|
21444
|
+
this.ticking = false;
|
|
21445
|
+
}
|
|
21383
21446
|
}
|
|
21384
21447
|
async fetchStatusIfChanged(lastHash) {
|
|
21385
21448
|
try {
|
|
@@ -21406,7 +21469,8 @@ class LiveService {
|
|
|
21406
21469
|
}
|
|
21407
21470
|
const message = `${underline(newEvents.length)} new ${pluralize(newEvents.length, "event")}`;
|
|
21408
21471
|
M2.message(message, { symbol: magenta("◆") });
|
|
21409
|
-
const mostRecentTime = newEvents.map((e2) => e2.eventTime).sort().pop();
|
|
21472
|
+
const mostRecentTime = newEvents.map((e2) => new Date(e2.eventTime).getTime()).sort((a, b3) => a - b3).pop();
|
|
21473
|
+
const newEventTime = mostRecentTime === undefined ? lastEventTime : new Date(mostRecentTime + 1).toISOString();
|
|
21410
21474
|
return {
|
|
21411
21475
|
update: {
|
|
21412
21476
|
type: "events",
|
|
@@ -21415,7 +21479,7 @@ class LiveService {
|
|
|
21415
21479
|
timestamp: new Date().toISOString()
|
|
21416
21480
|
}
|
|
21417
21481
|
},
|
|
21418
|
-
newEventTime
|
|
21482
|
+
newEventTime
|
|
21419
21483
|
};
|
|
21420
21484
|
} catch (error48) {
|
|
21421
21485
|
log4.error("Failed to fetch events", { error: error48 });
|
|
@@ -21820,6 +21884,7 @@ function runSSE(c, options) {
|
|
|
21820
21884
|
} catch {
|
|
21821
21885
|
abort("write error");
|
|
21822
21886
|
}
|
|
21887
|
+
options.onClose?.();
|
|
21823
21888
|
unregisterConnection(key, () => abort("superseded"));
|
|
21824
21889
|
log8.debug("SSE connection closed", { ...meta3, reason: closeReason });
|
|
21825
21890
|
});
|
|
@@ -21922,20 +21987,41 @@ function handleEventsStream(c) {
|
|
|
21922
21987
|
}
|
|
21923
21988
|
// src/server/controllers/live.ts
|
|
21924
21989
|
function handleLive(c) {
|
|
21990
|
+
const env2 = c.get("env");
|
|
21925
21991
|
const { status: statusService, events: eventsService } = c.get("services");
|
|
21926
|
-
const
|
|
21927
|
-
let
|
|
21992
|
+
const poller = getOrCreatePoller(env2, statusService, eventsService);
|
|
21993
|
+
let pending;
|
|
21994
|
+
let isFirstTick = true;
|
|
21995
|
+
const takePending = () => {
|
|
21996
|
+
const data = pending;
|
|
21997
|
+
pending = undefined;
|
|
21998
|
+
return data;
|
|
21999
|
+
};
|
|
22000
|
+
const unsubscribe = poller.subscribe((data) => {
|
|
22001
|
+
pending = data;
|
|
22002
|
+
});
|
|
21928
22003
|
return runSSE(c, {
|
|
21929
22004
|
event: "live",
|
|
21930
22005
|
intervalMs: constants.sse.defaultTickIntervalMs,
|
|
21931
22006
|
sendInitial: true,
|
|
22007
|
+
onClose: unsubscribe,
|
|
21932
22008
|
tick: async () => {
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
21936
|
-
|
|
22009
|
+
if (isFirstTick) {
|
|
22010
|
+
isFirstTick = false;
|
|
22011
|
+
const immediate = takePending();
|
|
22012
|
+
if (immediate !== undefined) {
|
|
22013
|
+
return immediate;
|
|
22014
|
+
}
|
|
22015
|
+
const initial = await poller.getLastBroadcastAfterInitialTick();
|
|
22016
|
+
if (pending !== undefined && pending === initial) {
|
|
22017
|
+
pending = undefined;
|
|
22018
|
+
}
|
|
22019
|
+
if (initial !== undefined) {
|
|
22020
|
+
return initial;
|
|
22021
|
+
}
|
|
22022
|
+
return takePending();
|
|
21937
22023
|
}
|
|
21938
|
-
return
|
|
22024
|
+
return takePending();
|
|
21939
22025
|
}
|
|
21940
22026
|
});
|
|
21941
22027
|
}
|
|
@@ -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"}
|
|
@@ -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"}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "timeback-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|