@threadbase-sh/streamer 1.47.2 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +361 -160
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +214 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +214 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -6
package/dist/index.d.cts
CHANGED
|
@@ -1954,6 +1954,7 @@ declare class StreamerServer {
|
|
|
1954
1954
|
private apnsClient;
|
|
1955
1955
|
private liveActivityNotifier;
|
|
1956
1956
|
private liveActivityRenewal;
|
|
1957
|
+
private waitingInputNotifier;
|
|
1957
1958
|
private discoveryCache;
|
|
1958
1959
|
private discoveryInFlight;
|
|
1959
1960
|
private cacheDir;
|
|
@@ -2008,6 +2009,18 @@ declare class StreamerServer {
|
|
|
2008
2009
|
* on disk; neither it nor any device token is ever logged.
|
|
2009
2010
|
*/
|
|
2010
2011
|
private initLiveActivityPush;
|
|
2012
|
+
/**
|
|
2013
|
+
* Bring up "your turn" notifications over Expo's relay (#528).
|
|
2014
|
+
*
|
|
2015
|
+
* Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
|
|
2016
|
+
* credentials, so a self-hosted streamer needs no credential of its own. The
|
|
2017
|
+
* access token is optional and only relevant if the Expo project has enhanced
|
|
2018
|
+
* security enabled — requiring one would lock out every self-hoster, since
|
|
2019
|
+
* they do not own the project. It is never logged.
|
|
2020
|
+
*/
|
|
2021
|
+
private initWaitingInputPush;
|
|
2022
|
+
/** Whether any live socket is subscribed to this session — "someone is looking". */
|
|
2023
|
+
private hasSessionSubscriber;
|
|
2011
2024
|
/**
|
|
2012
2025
|
* Classify sessions left behind by previous streamer runs (C1 Phase 3a).
|
|
2013
2026
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1954,6 +1954,7 @@ declare class StreamerServer {
|
|
|
1954
1954
|
private apnsClient;
|
|
1955
1955
|
private liveActivityNotifier;
|
|
1956
1956
|
private liveActivityRenewal;
|
|
1957
|
+
private waitingInputNotifier;
|
|
1957
1958
|
private discoveryCache;
|
|
1958
1959
|
private discoveryInFlight;
|
|
1959
1960
|
private cacheDir;
|
|
@@ -2008,6 +2009,18 @@ declare class StreamerServer {
|
|
|
2008
2009
|
* on disk; neither it nor any device token is ever logged.
|
|
2009
2010
|
*/
|
|
2010
2011
|
private initLiveActivityPush;
|
|
2012
|
+
/**
|
|
2013
|
+
* Bring up "your turn" notifications over Expo's relay (#528).
|
|
2014
|
+
*
|
|
2015
|
+
* Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
|
|
2016
|
+
* credentials, so a self-hosted streamer needs no credential of its own. The
|
|
2017
|
+
* access token is optional and only relevant if the Expo project has enhanced
|
|
2018
|
+
* security enabled — requiring one would lock out every self-hoster, since
|
|
2019
|
+
* they do not own the project. It is never logged.
|
|
2020
|
+
*/
|
|
2021
|
+
private initWaitingInputPush;
|
|
2022
|
+
/** Whether any live socket is subscribed to this session — "someone is looking". */
|
|
2023
|
+
private hasSessionSubscriber;
|
|
2011
2024
|
/**
|
|
2012
2025
|
* Classify sessions left behind by previous streamer runs (C1 Phase 3a).
|
|
2013
2026
|
*
|
package/dist/index.js
CHANGED
|
@@ -8415,10 +8415,10 @@ function fingerprintOf(ids) {
|
|
|
8415
8415
|
return `sha256:${createHash3("sha256").update(sorted.join("\n")).digest("hex")}`;
|
|
8416
8416
|
}
|
|
8417
8417
|
var CacheIntegrityMonitor = class {
|
|
8418
|
-
constructor(cache, wsHub,
|
|
8418
|
+
constructor(cache, wsHub, log10, cacheDir, rescan, runDuringReset) {
|
|
8419
8419
|
this.cache = cache;
|
|
8420
8420
|
this.wsHub = wsHub;
|
|
8421
|
-
this.log =
|
|
8421
|
+
this.log = log10;
|
|
8422
8422
|
this.cacheDir = cacheDir;
|
|
8423
8423
|
this.rescan = rescan;
|
|
8424
8424
|
this.runDuringReset = runDuringReset;
|
|
@@ -9217,6 +9217,101 @@ var ApnsClient = class {
|
|
|
9217
9217
|
}
|
|
9218
9218
|
};
|
|
9219
9219
|
|
|
9220
|
+
// src/services/push/expoPushSender.ts
|
|
9221
|
+
var log5 = getLogger("expo-push");
|
|
9222
|
+
var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
|
|
9223
|
+
var EXPO_PUSH_BATCH_SIZE = 100;
|
|
9224
|
+
var DEAD_TOKEN_ERROR = "DeviceNotRegistered";
|
|
9225
|
+
var ExpoPushSender = class {
|
|
9226
|
+
/**
|
|
9227
|
+
* @param accessToken Expo access token, when the project has enhanced
|
|
9228
|
+
* security enabled. Optional on purpose: a self-hoster does not own the Expo
|
|
9229
|
+
* project and cannot obtain one, so requiring it would break the deployment
|
|
9230
|
+
* this transport exists to serve.
|
|
9231
|
+
*/
|
|
9232
|
+
constructor(repo, accessToken) {
|
|
9233
|
+
this.repo = repo;
|
|
9234
|
+
this.accessToken = accessToken;
|
|
9235
|
+
}
|
|
9236
|
+
repo;
|
|
9237
|
+
accessToken;
|
|
9238
|
+
/**
|
|
9239
|
+
* Send one message to every deliverable Expo token.
|
|
9240
|
+
*
|
|
9241
|
+
* Sends are independent: Expo returns a ticket per token in one response, so
|
|
9242
|
+
* a dead device is recorded against its own row and never silences the other
|
|
9243
|
+
* devices in the batch.
|
|
9244
|
+
*/
|
|
9245
|
+
async send(message, now = Date.now()) {
|
|
9246
|
+
const rows = this.repo.listDeliverable();
|
|
9247
|
+
const outcome = { attempted: rows.length, succeeded: 0, retired: 0 };
|
|
9248
|
+
if (rows.length === 0) return outcome;
|
|
9249
|
+
for (let i = 0; i < rows.length; i += EXPO_PUSH_BATCH_SIZE) {
|
|
9250
|
+
const chunk = rows.slice(i, i + EXPO_PUSH_BATCH_SIZE);
|
|
9251
|
+
await this.sendChunk(chunk, message, now, outcome);
|
|
9252
|
+
}
|
|
9253
|
+
return outcome;
|
|
9254
|
+
}
|
|
9255
|
+
async sendChunk(rows, message, now, outcome) {
|
|
9256
|
+
let payload;
|
|
9257
|
+
try {
|
|
9258
|
+
const res = await fetch(EXPO_PUSH_ENDPOINT, {
|
|
9259
|
+
method: "POST",
|
|
9260
|
+
headers: {
|
|
9261
|
+
"content-type": "application/json",
|
|
9262
|
+
accept: "application/json",
|
|
9263
|
+
...this.accessToken && { authorization: `Bearer ${this.accessToken}` }
|
|
9264
|
+
},
|
|
9265
|
+
body: JSON.stringify(rows.map((row) => ({ to: row.token, ...message })))
|
|
9266
|
+
});
|
|
9267
|
+
if (!res.ok) {
|
|
9268
|
+
const code = `HTTP_${res.status}`;
|
|
9269
|
+
for (const row of rows) this.repo.recordFailure(row.token, code, now);
|
|
9270
|
+
log5.warn("expo_push.request_rejected", {
|
|
9271
|
+
event: "expo_push.request_rejected",
|
|
9272
|
+
status: res.status,
|
|
9273
|
+
tokens: rows.length
|
|
9274
|
+
});
|
|
9275
|
+
return;
|
|
9276
|
+
}
|
|
9277
|
+
payload = await res.json();
|
|
9278
|
+
} catch (err) {
|
|
9279
|
+
for (const row of rows) this.repo.recordFailure(row.token, "SendError", now);
|
|
9280
|
+
log5.error("expo_push.send_failed", {
|
|
9281
|
+
event: "expo_push.send_failed",
|
|
9282
|
+
tokens: rows.length,
|
|
9283
|
+
err: String(err)
|
|
9284
|
+
});
|
|
9285
|
+
return;
|
|
9286
|
+
}
|
|
9287
|
+
const tickets = payload?.data;
|
|
9288
|
+
rows.forEach((row, index) => {
|
|
9289
|
+
const ticket = Array.isArray(tickets) ? tickets[index] : void 0;
|
|
9290
|
+
if (!ticket) {
|
|
9291
|
+
this.repo.recordFailure(row.token, "NoTicket", now);
|
|
9292
|
+
return;
|
|
9293
|
+
}
|
|
9294
|
+
if (ticket.status === "ok") {
|
|
9295
|
+
this.repo.recordSuccess(row.token, now);
|
|
9296
|
+
outcome.succeeded += 1;
|
|
9297
|
+
return;
|
|
9298
|
+
}
|
|
9299
|
+
const code = ticket.details?.error ?? "PushError";
|
|
9300
|
+
this.repo.recordFailure(row.token, code, now);
|
|
9301
|
+
if (code === DEAD_TOKEN_ERROR) {
|
|
9302
|
+
this.repo.revoke(row.token, now);
|
|
9303
|
+
outcome.retired += 1;
|
|
9304
|
+
}
|
|
9305
|
+
log5.warn("expo_push.send_rejected", {
|
|
9306
|
+
event: "expo_push.send_rejected",
|
|
9307
|
+
code,
|
|
9308
|
+
message: ticket.message,
|
|
9309
|
+
retired: code === DEAD_TOKEN_ERROR
|
|
9310
|
+
});
|
|
9311
|
+
});
|
|
9312
|
+
}
|
|
9313
|
+
};
|
|
9314
|
+
|
|
9220
9315
|
// src/services/push/liveActivityContentState.ts
|
|
9221
9316
|
var LAST_OUTPUT_MAX_LENGTH = 90;
|
|
9222
9317
|
function toLiveActivityStatus(status) {
|
|
@@ -9228,7 +9323,7 @@ function truncateLastOutput(raw) {
|
|
|
9228
9323
|
}
|
|
9229
9324
|
|
|
9230
9325
|
// src/services/push/liveActivityNotifier.ts
|
|
9231
|
-
var
|
|
9326
|
+
var log6 = getLogger("live-activity");
|
|
9232
9327
|
function contentStateForSession(args) {
|
|
9233
9328
|
const status = toLiveActivityStatus(args.session.status);
|
|
9234
9329
|
if (!status) return null;
|
|
@@ -9288,7 +9383,7 @@ var LiveActivityNotifier = class {
|
|
|
9288
9383
|
}
|
|
9289
9384
|
await this.maybeSendName(session);
|
|
9290
9385
|
} catch (err) {
|
|
9291
|
-
|
|
9386
|
+
log6.error("live_activity.notify_failed", {
|
|
9292
9387
|
event: "live_activity.notify_failed",
|
|
9293
9388
|
sessionId: session.id,
|
|
9294
9389
|
status: session.status,
|
|
@@ -9310,7 +9405,7 @@ var LiveActivityNotifier = class {
|
|
|
9310
9405
|
});
|
|
9311
9406
|
this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
|
|
9312
9407
|
if (outcome.attempted > 0) {
|
|
9313
|
-
|
|
9408
|
+
log6.info("live_activity.updated", {
|
|
9314
9409
|
event: "live_activity.updated",
|
|
9315
9410
|
sessionId: session.id,
|
|
9316
9411
|
status: contentState.status,
|
|
@@ -9334,7 +9429,7 @@ var LiveActivityNotifier = class {
|
|
|
9334
9429
|
});
|
|
9335
9430
|
open2.sessionNameSent = true;
|
|
9336
9431
|
if (outcome.attempted > 0) {
|
|
9337
|
-
|
|
9432
|
+
log6.info("live_activity.updated", {
|
|
9338
9433
|
event: "live_activity.updated",
|
|
9339
9434
|
sessionId: session.id,
|
|
9340
9435
|
status: contentState.status,
|
|
@@ -9353,7 +9448,7 @@ var LiveActivityNotifier = class {
|
|
|
9353
9448
|
if (!contentState) return;
|
|
9354
9449
|
const outcome = await this.sender.end({ sessionId: session.id, contentState });
|
|
9355
9450
|
if (outcome.attempted > 0) {
|
|
9356
|
-
|
|
9451
|
+
log6.info("live_activity.ended", {
|
|
9357
9452
|
event: "live_activity.ended",
|
|
9358
9453
|
sessionId: session.id,
|
|
9359
9454
|
...outcome
|
|
@@ -9367,7 +9462,7 @@ var LiveActivityNotifier = class {
|
|
|
9367
9462
|
};
|
|
9368
9463
|
|
|
9369
9464
|
// src/services/push/liveActivitySender.ts
|
|
9370
|
-
var
|
|
9465
|
+
var log7 = getLogger("live-activity");
|
|
9371
9466
|
var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
|
|
9372
9467
|
function buildActivityKitPayload(args) {
|
|
9373
9468
|
return {
|
|
@@ -9431,7 +9526,7 @@ var LiveActivitySender = class {
|
|
|
9431
9526
|
);
|
|
9432
9527
|
for (const { row, result, error } of results) {
|
|
9433
9528
|
if (error) {
|
|
9434
|
-
|
|
9529
|
+
log7.error("live_activity.send_failed", {
|
|
9435
9530
|
event: "live_activity.send_failed",
|
|
9436
9531
|
sessionId: args.sessionId,
|
|
9437
9532
|
activityId: row.activity_id,
|
|
@@ -9452,7 +9547,7 @@ var LiveActivitySender = class {
|
|
|
9452
9547
|
this.repo.expire(row.token, now);
|
|
9453
9548
|
outcome.retired += 1;
|
|
9454
9549
|
}
|
|
9455
|
-
|
|
9550
|
+
log7.warn("live_activity.send_rejected", {
|
|
9456
9551
|
event: "live_activity.send_rejected",
|
|
9457
9552
|
sessionId: args.sessionId,
|
|
9458
9553
|
activityId: row.activity_id,
|
|
@@ -9497,7 +9592,7 @@ var LiveActivitySender = class {
|
|
|
9497
9592
|
};
|
|
9498
9593
|
|
|
9499
9594
|
// src/services/push/liveActivityRenewal.ts
|
|
9500
|
-
var
|
|
9595
|
+
var log8 = getLogger("live-activity");
|
|
9501
9596
|
var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
|
|
9502
9597
|
var MAX_TIMER_MS = 60 * 60 * 1e3;
|
|
9503
9598
|
function renewalDueAt(row) {
|
|
@@ -9545,7 +9640,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
9545
9640
|
await this.renew(row, now);
|
|
9546
9641
|
}
|
|
9547
9642
|
} catch (err) {
|
|
9548
|
-
|
|
9643
|
+
log8.error("live_activity.renewal_sweep_failed", {
|
|
9549
9644
|
event: "live_activity.renewal_sweep_failed",
|
|
9550
9645
|
err: String(err)
|
|
9551
9646
|
});
|
|
@@ -9574,7 +9669,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
9574
9669
|
if (!session || !status) {
|
|
9575
9670
|
this.deps.repo.claimRenewal(row.token, now);
|
|
9576
9671
|
this.deps.repo.expire(row.token, now);
|
|
9577
|
-
|
|
9672
|
+
log8.info("live_activity.renewal_skipped", {
|
|
9578
9673
|
event: "live_activity.renewal_skipped",
|
|
9579
9674
|
sessionId: row.session_id,
|
|
9580
9675
|
activityId: row.activity_id,
|
|
@@ -9609,7 +9704,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
9609
9704
|
startedAt,
|
|
9610
9705
|
now
|
|
9611
9706
|
});
|
|
9612
|
-
|
|
9707
|
+
log8.info("live_activity.renewed", {
|
|
9613
9708
|
event: "live_activity.renewed",
|
|
9614
9709
|
sessionId: session.id,
|
|
9615
9710
|
activityId: row.activity_id,
|
|
@@ -9619,7 +9714,7 @@ var LiveActivityRenewalScheduler = class {
|
|
|
9619
9714
|
replacementRequested: started
|
|
9620
9715
|
});
|
|
9621
9716
|
} catch (err) {
|
|
9622
|
-
|
|
9717
|
+
log8.error("live_activity.renewal_failed", {
|
|
9623
9718
|
event: "live_activity.renewal_failed",
|
|
9624
9719
|
sessionId: session.id,
|
|
9625
9720
|
activityId: row.activity_id,
|
|
@@ -9664,6 +9759,77 @@ var LiveActivityRenewalScheduler = class {
|
|
|
9664
9759
|
}
|
|
9665
9760
|
};
|
|
9666
9761
|
|
|
9762
|
+
// src/services/push/waitingInputNotifier.ts
|
|
9763
|
+
var log9 = getLogger("expo-push");
|
|
9764
|
+
function waitingInputMessage(session, serverId) {
|
|
9765
|
+
return {
|
|
9766
|
+
title: session.projectName || "Threadbase",
|
|
9767
|
+
body: "Waiting for your input",
|
|
9768
|
+
data: { sessionId: session.id, serverId }
|
|
9769
|
+
};
|
|
9770
|
+
}
|
|
9771
|
+
var WaitingInputNotifier = class {
|
|
9772
|
+
/**
|
|
9773
|
+
* @param isWatched Whether a client is currently subscribed to this session
|
|
9774
|
+
* over WebSocket. Mobile subscribes while the session screen is open and the
|
|
9775
|
+
* socket dies when the app is backgrounded, so this is the available signal
|
|
9776
|
+
* for "the user is already looking" — and a push to someone already reading
|
|
9777
|
+
* the output is pure noise.
|
|
9778
|
+
*/
|
|
9779
|
+
constructor(sender, serverId, isWatched) {
|
|
9780
|
+
this.sender = sender;
|
|
9781
|
+
this.serverId = serverId;
|
|
9782
|
+
this.isWatched = isWatched;
|
|
9783
|
+
}
|
|
9784
|
+
sender;
|
|
9785
|
+
serverId;
|
|
9786
|
+
isWatched;
|
|
9787
|
+
/** Sessions with a turn the user started that has not yet been answered. */
|
|
9788
|
+
openTurn = /* @__PURE__ */ new Set();
|
|
9789
|
+
/**
|
|
9790
|
+
* React to a session status change.
|
|
9791
|
+
*
|
|
9792
|
+
* Fire-and-forget by design: a push must never delay or fail a session
|
|
9793
|
+
* transition, so this returns a promise the caller may ignore and every error
|
|
9794
|
+
* is logged rather than propagated.
|
|
9795
|
+
*/
|
|
9796
|
+
async onStatusChange(session, previousStatus) {
|
|
9797
|
+
try {
|
|
9798
|
+
if (session.status === "running") {
|
|
9799
|
+
if (previousStatus === "waiting_input") this.openTurn.add(session.id);
|
|
9800
|
+
return;
|
|
9801
|
+
}
|
|
9802
|
+
if (session.status !== "waiting_input") {
|
|
9803
|
+
this.openTurn.delete(session.id);
|
|
9804
|
+
return;
|
|
9805
|
+
}
|
|
9806
|
+
if (!this.openTurn.delete(session.id)) return;
|
|
9807
|
+
if (this.isWatched(session.id)) {
|
|
9808
|
+
log9.debug("expo_push.suppressed_watched", {
|
|
9809
|
+
event: "expo_push.suppressed_watched",
|
|
9810
|
+
sessionId: session.id
|
|
9811
|
+
});
|
|
9812
|
+
return;
|
|
9813
|
+
}
|
|
9814
|
+
const outcome = await this.sender.send(waitingInputMessage(session, this.serverId));
|
|
9815
|
+
if (outcome.attempted > 0) {
|
|
9816
|
+
log9.info("expo_push.waiting_input", {
|
|
9817
|
+
event: "expo_push.waiting_input",
|
|
9818
|
+
sessionId: session.id,
|
|
9819
|
+
...outcome
|
|
9820
|
+
});
|
|
9821
|
+
}
|
|
9822
|
+
} catch (err) {
|
|
9823
|
+
log9.error("expo_push.notify_failed", {
|
|
9824
|
+
event: "expo_push.notify_failed",
|
|
9825
|
+
sessionId: session.id,
|
|
9826
|
+
status: session.status,
|
|
9827
|
+
err: String(err)
|
|
9828
|
+
});
|
|
9829
|
+
}
|
|
9830
|
+
}
|
|
9831
|
+
};
|
|
9832
|
+
|
|
9667
9833
|
// src/services/questions/parseStatusLine.ts
|
|
9668
9834
|
var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
|
|
9669
9835
|
var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
|
|
@@ -10974,6 +11140,10 @@ var StreamerServer = class {
|
|
|
10974
11140
|
apnsClient = null;
|
|
10975
11141
|
liveActivityNotifier = null;
|
|
10976
11142
|
liveActivityRenewal = null;
|
|
11143
|
+
// "Your turn" notifications over Expo's relay (#528). Needs no credential of
|
|
11144
|
+
// its own, so unlike the Live Activity path it is on wherever the cache DB
|
|
11145
|
+
// opened — with no registered device it simply sends nothing.
|
|
11146
|
+
waitingInputNotifier = null;
|
|
10977
11147
|
discoveryCache = null;
|
|
10978
11148
|
// Single-flight for process discovery. Mobile polls GET /api/sessions and
|
|
10979
11149
|
// retries on timeout; without this, every concurrent request starts its own
|
|
@@ -11273,6 +11443,7 @@ var StreamerServer = class {
|
|
|
11273
11443
|
this.wsHub.broadcast({ type: "session_update", session: resp });
|
|
11274
11444
|
}
|
|
11275
11445
|
void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
|
|
11446
|
+
void this.waitingInputNotifier?.onStatusChange(session, previousStatus);
|
|
11276
11447
|
this.sessionStatusBus.emit(`status:${session.id}`, session.status, session);
|
|
11277
11448
|
}
|
|
11278
11449
|
});
|
|
@@ -11585,6 +11756,33 @@ var StreamerServer = class {
|
|
|
11585
11756
|
topic: `${creds.bundleId}.push-type.liveactivity`
|
|
11586
11757
|
});
|
|
11587
11758
|
}
|
|
11759
|
+
/**
|
|
11760
|
+
* Bring up "your turn" notifications over Expo's relay (#528).
|
|
11761
|
+
*
|
|
11762
|
+
* Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
|
|
11763
|
+
* credentials, so a self-hosted streamer needs no credential of its own. The
|
|
11764
|
+
* access token is optional and only relevant if the Expo project has enhanced
|
|
11765
|
+
* security enabled — requiring one would lock out every self-hoster, since
|
|
11766
|
+
* they do not own the project. It is never logged.
|
|
11767
|
+
*/
|
|
11768
|
+
initWaitingInputPush(pushRepo) {
|
|
11769
|
+
const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
|
|
11770
|
+
const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname3();
|
|
11771
|
+
this.waitingInputNotifier = new WaitingInputNotifier(
|
|
11772
|
+
sender,
|
|
11773
|
+
serverId,
|
|
11774
|
+
(id) => this.hasSessionSubscriber(id)
|
|
11775
|
+
);
|
|
11776
|
+
}
|
|
11777
|
+
/** Whether any live socket is subscribed to this session — "someone is looking". */
|
|
11778
|
+
hasSessionSubscriber(sessionId) {
|
|
11779
|
+
const subs = this.sessionSubscribers.get(sessionId);
|
|
11780
|
+
if (!subs) return false;
|
|
11781
|
+
for (const ws of subs) {
|
|
11782
|
+
if (ws.readyState === ws.OPEN) return true;
|
|
11783
|
+
}
|
|
11784
|
+
return false;
|
|
11785
|
+
}
|
|
11588
11786
|
/**
|
|
11589
11787
|
* Classify sessions left behind by previous streamer runs (C1 Phase 3a).
|
|
11590
11788
|
*
|
|
@@ -12187,6 +12385,7 @@ var StreamerServer = class {
|
|
|
12187
12385
|
this.pushRepo = new PushRepository(db);
|
|
12188
12386
|
this.devicesRepo = new DevicesRepository(db);
|
|
12189
12387
|
this.initLiveActivityPush(this.pushRepo);
|
|
12388
|
+
this.initWaitingInputPush(this.pushRepo);
|
|
12190
12389
|
this.cacheMonitor = new CacheIntegrityMonitor(
|
|
12191
12390
|
this.cache,
|
|
12192
12391
|
this.wsHub,
|