pinokiod 3.134.0 → 3.135.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/kernel/util.js +124 -2
- package/package.json +1 -1
- package/server/public/common.js +315 -6
- package/server/socket.js +58 -1
package/kernel/util.js
CHANGED
|
@@ -30,6 +30,70 @@ const {
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
const platform = os.platform()
|
|
33
|
+
const WINDOWS_TOAST_APP_ID = process.env.PINOKIO_WINDOWS_APP_ID || 'computer.pinokio'
|
|
34
|
+
const DEFAULT_CHIME_URL_PATH = '/chime.mp3'
|
|
35
|
+
const pushListeners = new Set()
|
|
36
|
+
|
|
37
|
+
function registerPushListener(listener) {
|
|
38
|
+
if (typeof listener !== 'function') {
|
|
39
|
+
throw new TypeError('push listener must be a function')
|
|
40
|
+
}
|
|
41
|
+
pushListeners.add(listener)
|
|
42
|
+
return () => pushListeners.delete(listener)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function emitPushEvent(event) {
|
|
46
|
+
if (!event) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
pushListeners.forEach((listener) => {
|
|
50
|
+
try {
|
|
51
|
+
listener(event)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Push listener error:', err)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePublicAssetUrl(filePath) {
|
|
59
|
+
if (!filePath) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
if (/^https?:\/\//i.test(filePath)) {
|
|
63
|
+
return filePath
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const absolute = path.resolve(filePath)
|
|
67
|
+
const publicRoot = path.resolve(__dirname, '../server/public')
|
|
68
|
+
if (absolute === publicRoot) {
|
|
69
|
+
return '/'
|
|
70
|
+
}
|
|
71
|
+
if (absolute.startsWith(publicRoot + path.sep) || absolute === publicRoot) {
|
|
72
|
+
const relative = path.relative(publicRoot, absolute).replace(/\\/g, '/')
|
|
73
|
+
return '/' + relative
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// ignore resolution failures
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function soundTargetToClientUrl(target) {
|
|
82
|
+
if (!target) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
if (target.kind === 'url') {
|
|
86
|
+
return target.value
|
|
87
|
+
}
|
|
88
|
+
if (target.kind === 'file') {
|
|
89
|
+
const resolved = path.resolve(target.value)
|
|
90
|
+
if (resolved === DEFAULT_CHIME_PATH) {
|
|
91
|
+
return DEFAULT_CHIME_URL_PATH
|
|
92
|
+
}
|
|
93
|
+
return resolvePublicAssetUrl(resolved)
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
33
97
|
function ensureNotifierBinaries() {
|
|
34
98
|
if (platform !== 'darwin') {
|
|
35
99
|
return
|
|
@@ -729,10 +793,39 @@ function push(params) {
|
|
|
729
793
|
if (!notifyParams.contentImage) {
|
|
730
794
|
notifyParams.contentImage = path.resolve(__dirname, "../server/public/pinokio-black.png")
|
|
731
795
|
}
|
|
796
|
+
if (platform === 'win32') {
|
|
797
|
+
// Ensure Windows toast branding aligns with Pinokio assets.
|
|
798
|
+
if (!notifyParams.icon && notifyParams.contentImage) {
|
|
799
|
+
notifyParams.icon = notifyParams.contentImage
|
|
800
|
+
}
|
|
801
|
+
if (!notifyParams.appID && !notifyParams.appName) {
|
|
802
|
+
notifyParams.appID = WINDOWS_TOAST_APP_ID
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const clientSoundUrl = soundTargetToClientUrl(customSoundTarget)
|
|
732
807
|
if (customSoundTarget) {
|
|
733
808
|
notifyParams.sound = false
|
|
734
|
-
|
|
809
|
+
const shouldPlayLocally = platform !== 'win32' || !clientSoundUrl
|
|
810
|
+
if (shouldPlayLocally) {
|
|
811
|
+
scheduleSoundPlayback(customSoundTarget)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const clientImage = resolvePublicAssetUrl(notifyParams.contentImage) || resolvePublicAssetUrl(notifyParams.image)
|
|
816
|
+
const eventPayload = {
|
|
817
|
+
id: randomUUID(),
|
|
818
|
+
title: notifyParams.title,
|
|
819
|
+
subtitle: notifyParams.subtitle || null,
|
|
820
|
+
message: notifyParams.message || '',
|
|
821
|
+
image: clientImage,
|
|
822
|
+
sound: clientSoundUrl,
|
|
823
|
+
timestamp: Date.now(),
|
|
824
|
+
platform,
|
|
735
825
|
}
|
|
826
|
+
|
|
827
|
+
emitPushEvent(eventPayload)
|
|
828
|
+
|
|
736
829
|
console.log("notifyParams", notifyParams)
|
|
737
830
|
notifier.notify(notifyParams)
|
|
738
831
|
}
|
|
@@ -1076,5 +1169,34 @@ const rewrite_localhost= (kernel, obj, source) => {
|
|
|
1076
1169
|
|
|
1077
1170
|
|
|
1078
1171
|
module.exports = {
|
|
1079
|
-
parse_env,
|
|
1172
|
+
parse_env,
|
|
1173
|
+
log_path,
|
|
1174
|
+
api_path,
|
|
1175
|
+
api_name,
|
|
1176
|
+
update_env,
|
|
1177
|
+
parse_env_detail,
|
|
1178
|
+
openfs,
|
|
1179
|
+
port_running,
|
|
1180
|
+
du,
|
|
1181
|
+
is_port_available,
|
|
1182
|
+
find_python,
|
|
1183
|
+
find_venv,
|
|
1184
|
+
fill_object,
|
|
1185
|
+
run,
|
|
1186
|
+
openURL,
|
|
1187
|
+
u2p,
|
|
1188
|
+
p2u,
|
|
1189
|
+
log,
|
|
1190
|
+
diffLinesWithContext,
|
|
1191
|
+
classifyChange,
|
|
1192
|
+
push,
|
|
1193
|
+
filepicker,
|
|
1194
|
+
exists,
|
|
1195
|
+
clipboard,
|
|
1196
|
+
mergeLines,
|
|
1197
|
+
ignore_subrepos,
|
|
1198
|
+
rewrite_localhost,
|
|
1199
|
+
symlink,
|
|
1200
|
+
file_type,
|
|
1201
|
+
registerPushListener,
|
|
1080
1202
|
}
|
package/package.json
CHANGED
package/server/public/common.js
CHANGED
|
@@ -1398,14 +1398,323 @@ const open_url2 = (href, target, features) => {
|
|
|
1398
1398
|
window.open(href, "_self", features)
|
|
1399
1399
|
}
|
|
1400
1400
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1401
|
+
if (typeof hotkeys === 'function') {
|
|
1402
|
+
hotkeys("ctrl+t,cmd+t,ctrl+n,cmd+n", (e) => {
|
|
1403
|
+
let agent = document.body.getAttribute("data-agent")
|
|
1404
|
+
if (agent === "electron") {
|
|
1405
|
+
window.open(location.href, "_blank", "pinokio")
|
|
1406
|
+
} else {
|
|
1407
|
+
window.open(location.href, "_blank")
|
|
1408
|
+
}
|
|
1409
|
+
})
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
(function initNotificationAudioBridge() {
|
|
1413
|
+
if (typeof window === 'undefined') {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (window.__pinokioNotificationAudioInitialized) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
window.__pinokioNotificationAudioInitialized = true;
|
|
1420
|
+
|
|
1421
|
+
const CHANNEL_ID = 'kernel.notifications';
|
|
1422
|
+
const pendingSounds = [];
|
|
1423
|
+
let isPlaying = false;
|
|
1424
|
+
let currentSocket = null;
|
|
1425
|
+
let reconnectTimeout = null;
|
|
1426
|
+
let activeAudio = null;
|
|
1427
|
+
|
|
1428
|
+
const leaderStorageKey = 'pinokio.notification.leader';
|
|
1429
|
+
const leaderHeartbeatMs = 5000;
|
|
1430
|
+
const leaderStaleMs = 15000;
|
|
1431
|
+
const tabId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1432
|
+
const storageEnabled = (() => {
|
|
1433
|
+
try {
|
|
1434
|
+
const testKey = '__pinokio_notification_test__';
|
|
1435
|
+
localStorage.setItem(testKey, '1');
|
|
1436
|
+
localStorage.removeItem(testKey);
|
|
1437
|
+
return true;
|
|
1438
|
+
} catch (_) {
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
})();
|
|
1442
|
+
let isLeader = false;
|
|
1443
|
+
let heartbeatTimer = null;
|
|
1444
|
+
let leadershipCheckTimer = null;
|
|
1445
|
+
|
|
1446
|
+
const parseLeaderValue = (value) => {
|
|
1447
|
+
if (!value) return null;
|
|
1448
|
+
try {
|
|
1449
|
+
return JSON.parse(value);
|
|
1450
|
+
} catch (_) {
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
const writeHeartbeat = () => {
|
|
1456
|
+
try {
|
|
1457
|
+
localStorage.setItem(leaderStorageKey, JSON.stringify({ id: tabId, ts: Date.now() }));
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
console.warn('Notification leader heartbeat failed:', err);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
const clearHeartbeat = () => {
|
|
1464
|
+
if (heartbeatTimer) {
|
|
1465
|
+
clearInterval(heartbeatTimer);
|
|
1466
|
+
heartbeatTimer = null;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
const stopSocket = () => {
|
|
1471
|
+
if (currentSocket && typeof currentSocket.close === 'function') {
|
|
1472
|
+
try {
|
|
1473
|
+
currentSocket.close();
|
|
1474
|
+
} catch (_) {
|
|
1475
|
+
// ignore
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
currentSocket = null;
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const stopAudio = () => {
|
|
1482
|
+
pendingSounds.length = 0;
|
|
1483
|
+
if (activeAudio) {
|
|
1484
|
+
try {
|
|
1485
|
+
activeAudio.pause();
|
|
1486
|
+
activeAudio.currentTime = 0;
|
|
1487
|
+
} catch (_) {
|
|
1488
|
+
// ignore
|
|
1489
|
+
}
|
|
1490
|
+
activeAudio = null;
|
|
1491
|
+
}
|
|
1492
|
+
isPlaying = false;
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
const resignLeadership = () => {
|
|
1496
|
+
if (!isLeader) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
isLeader = false;
|
|
1500
|
+
clearHeartbeat();
|
|
1501
|
+
if (reconnectTimeout != null) {
|
|
1502
|
+
clearTimeout(reconnectTimeout);
|
|
1503
|
+
reconnectTimeout = null;
|
|
1504
|
+
}
|
|
1505
|
+
stopSocket();
|
|
1506
|
+
stopAudio();
|
|
1507
|
+
try {
|
|
1508
|
+
const stored = parseLeaderValue(localStorage.getItem(leaderStorageKey));
|
|
1509
|
+
if (stored && stored.id === tabId) {
|
|
1510
|
+
localStorage.removeItem(leaderStorageKey);
|
|
1511
|
+
}
|
|
1512
|
+
} catch (_) {
|
|
1513
|
+
// ignore
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
const startLeadership = () => {
|
|
1518
|
+
if (isLeader) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
isLeader = true;
|
|
1522
|
+
writeHeartbeat();
|
|
1523
|
+
if (!heartbeatTimer) {
|
|
1524
|
+
heartbeatTimer = setInterval(writeHeartbeat, leaderHeartbeatMs);
|
|
1525
|
+
}
|
|
1526
|
+
connect();
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
const playNextSound = () => {
|
|
1530
|
+
if (isPlaying) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const next = pendingSounds.shift();
|
|
1534
|
+
if (!next) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
isPlaying = true;
|
|
1538
|
+
activeAudio = new Audio(next);
|
|
1539
|
+
activeAudio.preload = 'auto';
|
|
1540
|
+
const cleanup = () => {
|
|
1541
|
+
if (!activeAudio) {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
activeAudio.removeEventListener('ended', handleEnded);
|
|
1545
|
+
activeAudio.removeEventListener('error', handleError);
|
|
1546
|
+
activeAudio = null;
|
|
1547
|
+
isPlaying = false;
|
|
1548
|
+
playNextSound();
|
|
1549
|
+
};
|
|
1550
|
+
const handleEnded = () => cleanup();
|
|
1551
|
+
const handleError = (err) => {
|
|
1552
|
+
console.error('Notification audio playback failed:', err);
|
|
1553
|
+
cleanup();
|
|
1554
|
+
};
|
|
1555
|
+
activeAudio.addEventListener('ended', handleEnded, { once: true });
|
|
1556
|
+
activeAudio.addEventListener('error', handleError, { once: true });
|
|
1557
|
+
const playPromise = activeAudio.play();
|
|
1558
|
+
if (playPromise && typeof playPromise.catch === 'function') {
|
|
1559
|
+
playPromise.catch((err) => {
|
|
1560
|
+
console.error('Notification audio play() rejected:', err);
|
|
1561
|
+
cleanup();
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const enqueueSound = (url) => {
|
|
1567
|
+
if (!url) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
pendingSounds.push(url);
|
|
1571
|
+
playNextSound();
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
const handlePacket = (packet) => {
|
|
1575
|
+
if (!packet || packet.id !== CHANNEL_ID || packet.type !== 'notification') {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const payload = packet.data || {};
|
|
1579
|
+
if (typeof payload.sound === 'string' && payload.sound) {
|
|
1580
|
+
enqueueSound(payload.sound);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
const attemptLeadership = () => {
|
|
1585
|
+
if (isLeader) {
|
|
1586
|
+
writeHeartbeat();
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
let current = null;
|
|
1590
|
+
try {
|
|
1591
|
+
current = parseLeaderValue(localStorage.getItem(leaderStorageKey));
|
|
1592
|
+
} catch (_) {
|
|
1593
|
+
current = null;
|
|
1594
|
+
}
|
|
1595
|
+
const now = Date.now();
|
|
1596
|
+
const isStale = !current || !current.ts || (now - current.ts) > leaderStaleMs;
|
|
1597
|
+
if (isStale || (current && current.id === tabId)) {
|
|
1598
|
+
writeHeartbeat();
|
|
1599
|
+
const freshlyStored = parseLeaderValue(localStorage.getItem(leaderStorageKey));
|
|
1600
|
+
if (freshlyStored && freshlyStored.id === tabId) {
|
|
1601
|
+
startLeadership();
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
const scheduleReconnect = (delay) => {
|
|
1607
|
+
if (!isLeader) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (reconnectTimeout != null) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
reconnectTimeout = setTimeout(() => {
|
|
1614
|
+
reconnectTimeout = null;
|
|
1615
|
+
connect();
|
|
1616
|
+
}, delay);
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
const connect = () => {
|
|
1620
|
+
if (!isLeader) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const SocketCtor = typeof window.Socket === 'function' ? window.Socket : (typeof Socket === 'function' ? Socket : null);
|
|
1624
|
+
if (!SocketCtor) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (typeof WebSocket === 'undefined') {
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (currentSocket && currentSocket.ws && currentSocket.ws.readyState === WebSocket.OPEN) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (currentSocket && typeof currentSocket.close === 'function') {
|
|
1634
|
+
try {
|
|
1635
|
+
currentSocket.close();
|
|
1636
|
+
} catch (_) {
|
|
1637
|
+
// ignore
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
const socket = new SocketCtor();
|
|
1641
|
+
try {
|
|
1642
|
+
const promise = socket.run(
|
|
1643
|
+
{
|
|
1644
|
+
method: CHANNEL_ID,
|
|
1645
|
+
mode: 'listen',
|
|
1646
|
+
},
|
|
1647
|
+
handlePacket
|
|
1648
|
+
);
|
|
1649
|
+
currentSocket = socket;
|
|
1650
|
+
promise.then(() => {
|
|
1651
|
+
// Attempt to reconnect after a brief delay when the socket closes normally.
|
|
1652
|
+
if (currentSocket === socket) {
|
|
1653
|
+
currentSocket = null;
|
|
1654
|
+
scheduleReconnect(1500);
|
|
1655
|
+
}
|
|
1656
|
+
}).catch((err) => {
|
|
1657
|
+
console.warn('Notification listener socket closed with error:', err);
|
|
1658
|
+
if (currentSocket === socket) {
|
|
1659
|
+
currentSocket = null;
|
|
1660
|
+
scheduleReconnect(2500);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
window.__pinokioNotificationSocket = socket;
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
console.error('Failed to establish notification listener socket:', err);
|
|
1666
|
+
scheduleReconnect(3000);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
if (!storageEnabled) {
|
|
1671
|
+
isLeader = true;
|
|
1672
|
+
const start = () => connect();
|
|
1673
|
+
if (document.readyState === 'loading') {
|
|
1674
|
+
document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
1675
|
+
} else {
|
|
1676
|
+
connect();
|
|
1677
|
+
}
|
|
1678
|
+
window.addEventListener('beforeunload', () => {
|
|
1679
|
+
stopSocket();
|
|
1680
|
+
stopAudio();
|
|
1681
|
+
});
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (document.readyState === 'loading') {
|
|
1686
|
+
document.addEventListener('DOMContentLoaded', connect, { once: true });
|
|
1405
1687
|
} else {
|
|
1406
|
-
|
|
1688
|
+
connect();
|
|
1407
1689
|
}
|
|
1408
|
-
|
|
1690
|
+
|
|
1691
|
+
if (!leadershipCheckTimer) {
|
|
1692
|
+
leadershipCheckTimer = setInterval(attemptLeadership, leaderHeartbeatMs);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
window.addEventListener('storage', (event) => {
|
|
1696
|
+
if (event.key !== leaderStorageKey) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const data = parseLeaderValue(event.newValue);
|
|
1700
|
+
if (data && data.id === tabId) {
|
|
1701
|
+
startLeadership();
|
|
1702
|
+
} else {
|
|
1703
|
+
resignLeadership();
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
window.addEventListener('beforeunload', () => {
|
|
1708
|
+
if (leadershipCheckTimer) {
|
|
1709
|
+
clearInterval(leadershipCheckTimer);
|
|
1710
|
+
leadershipCheckTimer = null;
|
|
1711
|
+
}
|
|
1712
|
+
resignLeadership();
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// Attempt to become leader immediately on load.
|
|
1716
|
+
attemptLeadership();
|
|
1717
|
+
})();
|
|
1409
1718
|
const refreshParent = (e) => {
|
|
1410
1719
|
// if (window.parent === window.top) {
|
|
1411
1720
|
window.parent.postMessage(e, "*")
|
package/server/socket.js
CHANGED
|
@@ -3,6 +3,7 @@ const WebSocket = require('ws');
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const Util = require("../kernel/util")
|
|
5
5
|
const Environment = require("../kernel/environment")
|
|
6
|
+
const NOTIFICATION_CHANNEL = 'kernel.notifications'
|
|
6
7
|
class Socket {
|
|
7
8
|
constructor(parent) {
|
|
8
9
|
this.buffer = {}
|
|
@@ -15,6 +16,8 @@ class Socket {
|
|
|
15
16
|
// this.kernel = parent.kernel
|
|
16
17
|
const wss = new WebSocket.Server({ server: this.parent.server })
|
|
17
18
|
this.subscriptions = new Map(); // Initialize a Map to store the WebSocket connections interested in each event
|
|
19
|
+
this.notificationChannel = NOTIFICATION_CHANNEL
|
|
20
|
+
this.notificationBridgeDispose = null
|
|
18
21
|
this.parent.kernel.api.listen("server.socket", this.trigger.bind(this))
|
|
19
22
|
wss.on('connection', (ws, request) => {
|
|
20
23
|
ws._headers = request.headers;
|
|
@@ -25,8 +28,15 @@ class Socket {
|
|
|
25
28
|
ws._origin = request.headers.origin;
|
|
26
29
|
ws.on('close', () => {
|
|
27
30
|
this.subscriptions.forEach((set, eventName) => {
|
|
31
|
+
if (!set) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
28
34
|
set.delete(ws);
|
|
35
|
+
if (set.size === 0) {
|
|
36
|
+
this.subscriptions.delete(eventName);
|
|
37
|
+
}
|
|
29
38
|
});
|
|
39
|
+
this.checkNotificationBridge();
|
|
30
40
|
});
|
|
31
41
|
ws.on('message', async (message, isBinary) => {
|
|
32
42
|
let req
|
|
@@ -217,7 +227,11 @@ class Socket {
|
|
|
217
227
|
if (!this.subscriptions.has(id)) {
|
|
218
228
|
this.subscriptions.set(id, new Set());
|
|
219
229
|
}
|
|
220
|
-
this.subscriptions.get(id)
|
|
230
|
+
const set = this.subscriptions.get(id)
|
|
231
|
+
set.add(ws);
|
|
232
|
+
if (set.size === 1 && id === this.notificationChannel) {
|
|
233
|
+
this.ensureNotificationBridge();
|
|
234
|
+
}
|
|
221
235
|
}
|
|
222
236
|
trigger(e) {
|
|
223
237
|
// send to id session
|
|
@@ -321,6 +335,49 @@ class Socket {
|
|
|
321
335
|
}
|
|
322
336
|
}
|
|
323
337
|
}
|
|
338
|
+
|
|
339
|
+
broadcastNotification(payload) {
|
|
340
|
+
if (!payload) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
const subscribers = this.subscriptions.get(this.notificationChannel)
|
|
344
|
+
if (!subscribers || subscribers.size === 0) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
const envelope = {
|
|
348
|
+
id: this.notificationChannel,
|
|
349
|
+
type: 'notification',
|
|
350
|
+
data: payload,
|
|
351
|
+
}
|
|
352
|
+
const frame = JSON.stringify(envelope)
|
|
353
|
+
subscribers.forEach((subscriber) => {
|
|
354
|
+
if (subscriber.readyState === WebSocket.OPEN) {
|
|
355
|
+
subscriber.send(frame)
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
ensureNotificationBridge() {
|
|
361
|
+
if (this.notificationBridgeDispose) {
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
this.notificationBridgeDispose = Util.registerPushListener((payload) => {
|
|
365
|
+
this.broadcastNotification(payload)
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
checkNotificationBridge() {
|
|
370
|
+
const subscribers = this.subscriptions.get(this.notificationChannel)
|
|
371
|
+
if ((!subscribers || subscribers.size === 0) && this.notificationBridgeDispose) {
|
|
372
|
+
try {
|
|
373
|
+
this.notificationBridgeDispose()
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.error('Failed to dispose notification bridge:', err)
|
|
376
|
+
}
|
|
377
|
+
this.notificationBridgeDispose = null
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
324
381
|
async log_buffer(key, buf) {
|
|
325
382
|
|
|
326
383
|
/*
|