@tryinget/pi-activity-strip 0.3.0 → 0.5.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/README.md +96 -152
- package/bin/pi-activity-strip.mjs +31 -62
- package/examples/simulate-sessions.mjs +1 -1
- package/extensions/activity-strip.js +2 -2
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +303 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +234 -0
- package/native/panel/src/runtime.rs +110 -0
- package/native/panel/src/style.css +176 -0
- package/package.json +20 -7
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +53 -11
- package/src/client/broker-client.mjs +25 -4
- package/src/client/session-telemetry.mjs +123 -13
- package/src/common/activity-order.mjs +15 -9
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +53 -0
- package/src/common/compatibility.mjs +15 -13
- package/src/common/constants.mjs +12 -28
- package/src/common/contracts.d.ts +54 -5
- package/src/common/contracts.ts +54 -5
- package/src/common/niri-focus.mjs +79 -79
- package/src/common/protocol.mjs +49 -14
- package/src/common/session-cards.mjs +102 -0
- package/src/common/status-report.mjs +2 -1
- package/src/common/telemetry.mjs +11 -1
- package/src/common/terminal-identity.mjs +149 -0
- package/src/native/main.mjs +393 -0
- package/src/native/panel-projection.mjs +99 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +4 -1
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -484
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
2
|
+
window {
|
|
3
|
+
background: transparent;
|
|
4
|
+
color: rgba(247, 250, 255, 0.96);
|
|
5
|
+
font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.brand,
|
|
9
|
+
.cards-panel {
|
|
10
|
+
background: linear-gradient(to bottom, rgba(12, 19, 33, 0.96), rgba(8, 13, 22, 0.92));
|
|
11
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
12
|
+
border-radius: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.brand {
|
|
16
|
+
padding: 10px 12px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.brand-eyebrow,
|
|
20
|
+
.meta,
|
|
21
|
+
.card-state,
|
|
22
|
+
.card-tool,
|
|
23
|
+
.inspector-key {
|
|
24
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.brand-eyebrow {
|
|
28
|
+
color: #79b8ff;
|
|
29
|
+
font-size: 9px;
|
|
30
|
+
letter-spacing: 1px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.brand-title {
|
|
34
|
+
color: rgba(247, 250, 255, 0.96);
|
|
35
|
+
font-size: 18px;
|
|
36
|
+
font-weight: 650;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.brand-subtitle {
|
|
40
|
+
color: rgba(194, 205, 224, 0.74);
|
|
41
|
+
font-size: 10px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.meta {
|
|
45
|
+
color: rgba(167, 180, 203, 0.58);
|
|
46
|
+
font-size: 9px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
50
|
+
.cards-panel > viewport {
|
|
51
|
+
background: linear-gradient(to bottom, rgba(10, 16, 28, 0.94), rgba(6, 10, 18, 0.9));
|
|
52
|
+
border-radius: 20px;
|
|
53
|
+
padding: 6px 8px 8px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
57
|
+
.cards-panel scrollbar {
|
|
58
|
+
opacity: 0;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
min-height: 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card {
|
|
64
|
+
color: rgba(247, 250, 255, 0.96);
|
|
65
|
+
background: linear-gradient(to bottom, rgba(13, 20, 35, 0.98), rgba(9, 14, 24, 0.96));
|
|
66
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
67
|
+
border-left: 3px solid #79b8ff;
|
|
68
|
+
border-radius: 16px;
|
|
69
|
+
padding: 8px 10px 8px 12px;
|
|
70
|
+
box-shadow: none;
|
|
71
|
+
transition: 120ms ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.card:hover,
|
|
75
|
+
.card:focus-visible,
|
|
76
|
+
.card.open {
|
|
77
|
+
background: linear-gradient(to bottom, rgba(18, 28, 47, 0.99), rgba(10, 16, 28, 0.97));
|
|
78
|
+
border-color: rgba(121, 184, 255, 0.72);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.card.current {
|
|
82
|
+
border-left-width: 5px;
|
|
83
|
+
border-color: rgba(150, 202, 255, 0.92);
|
|
84
|
+
outline: 1px solid rgba(121, 184, 255, 0.38);
|
|
85
|
+
outline-offset: -3px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.card.stalled {
|
|
89
|
+
opacity: 0.72;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.card.stalled .card-phase {
|
|
93
|
+
text-decoration-line: line-through;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.card.state-thinking {
|
|
97
|
+
border-left-color: #8ab4ff;
|
|
98
|
+
}
|
|
99
|
+
.card.state-tool {
|
|
100
|
+
border-left-color: #ffd166;
|
|
101
|
+
}
|
|
102
|
+
.card.state-waiting {
|
|
103
|
+
border-left-color: #ff9f7a;
|
|
104
|
+
}
|
|
105
|
+
.card.state-success {
|
|
106
|
+
border-left-color: #57d9a3;
|
|
107
|
+
}
|
|
108
|
+
.card.state-error {
|
|
109
|
+
border-left-color: #ff7d7d;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.card-repo {
|
|
113
|
+
color: rgba(247, 250, 255, 0.96);
|
|
114
|
+
font-size: 12px;
|
|
115
|
+
font-weight: 650;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.card-phase,
|
|
119
|
+
.inspector-value {
|
|
120
|
+
color: rgba(194, 205, 224, 0.76);
|
|
121
|
+
font-size: 10px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.card-state {
|
|
125
|
+
border: 1px solid rgba(121, 184, 255, 0.36);
|
|
126
|
+
border-radius: 999px;
|
|
127
|
+
color: #79b8ff;
|
|
128
|
+
font-size: 8px;
|
|
129
|
+
padding: 4px 6px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.state-tool .card-state {
|
|
133
|
+
color: #ffd166;
|
|
134
|
+
border-color: rgba(255, 209, 102, 0.42);
|
|
135
|
+
}
|
|
136
|
+
.state-waiting .card-state {
|
|
137
|
+
color: #ff9f7a;
|
|
138
|
+
border-color: rgba(255, 159, 122, 0.42);
|
|
139
|
+
}
|
|
140
|
+
.state-success .card-state {
|
|
141
|
+
color: #57d9a3;
|
|
142
|
+
border-color: rgba(87, 217, 163, 0.42);
|
|
143
|
+
}
|
|
144
|
+
.state-error .card-state {
|
|
145
|
+
color: #ff7d7d;
|
|
146
|
+
border-color: rgba(255, 125, 125, 0.42);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.card-tool,
|
|
150
|
+
.card-elapsed,
|
|
151
|
+
.inspector-key {
|
|
152
|
+
color: rgba(167, 180, 203, 0.58);
|
|
153
|
+
font-size: 8px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.inspector {
|
|
157
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
158
|
+
padding-top: 9px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.inspector-value {
|
|
162
|
+
line-height: 1.25;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.activation {
|
|
166
|
+
color: rgba(167, 180, 203, 0.72);
|
|
167
|
+
font-size: 9px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.activation.success {
|
|
171
|
+
color: #57d9a3;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.activation.error {
|
|
175
|
+
color: #ff7d7d;
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryinget/pi-activity-strip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "pi extension package for activity-strip workflows in monorepo runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=22.19.0"
|
|
28
28
|
},
|
|
29
|
+
"os": [
|
|
30
|
+
"linux"
|
|
31
|
+
],
|
|
32
|
+
"cpu": [
|
|
33
|
+
"x64"
|
|
34
|
+
],
|
|
29
35
|
"scripts": {
|
|
30
36
|
"fix": "bash ./scripts/quality-gate.sh fix",
|
|
31
37
|
"lint": "bash ./scripts/quality-gate.sh lint",
|
|
@@ -35,11 +41,13 @@
|
|
|
35
41
|
"quality:ci": "bash ./scripts/quality-gate.sh ci",
|
|
36
42
|
"check": "npm run quality:ci",
|
|
37
43
|
"test": "npm run quality:ci",
|
|
38
|
-
"docs:list": "
|
|
39
|
-
"docs:list:workspace": "
|
|
40
|
-
"docs:list:json": "
|
|
44
|
+
"docs:list": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs",
|
|
45
|
+
"docs:list:workspace": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --workspace --discover",
|
|
46
|
+
"docs:list:json": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --json",
|
|
41
47
|
"release:check": "bash ./scripts/release-check.sh",
|
|
42
48
|
"release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
|
|
49
|
+
"native:check": "cd native/panel && RUSTUP_TOOLCHAIN=stable cargo fmt --check && RUSTUP_TOOLCHAIN=stable cargo test",
|
|
50
|
+
"native:build": "bash ./scripts/build-native-panel.sh",
|
|
43
51
|
"strip:open": "node ./bin/pi-activity-strip.mjs open",
|
|
44
52
|
"strip:status": "node ./bin/pi-activity-strip.mjs status",
|
|
45
53
|
"strip:doctor": "node ./bin/pi-activity-strip.mjs doctor",
|
|
@@ -56,6 +64,10 @@
|
|
|
56
64
|
"extensions/activity-strip.js",
|
|
57
65
|
"extensions/activity-strip.ts",
|
|
58
66
|
"src",
|
|
67
|
+
"native/bin",
|
|
68
|
+
"native/panel/Cargo.toml",
|
|
69
|
+
"native/panel/Cargo.lock",
|
|
70
|
+
"native/panel/src",
|
|
59
71
|
"prompts",
|
|
60
72
|
"examples",
|
|
61
73
|
"policy/security-policy.json"
|
|
@@ -76,15 +88,16 @@
|
|
|
76
88
|
},
|
|
77
89
|
"devDependencies": {
|
|
78
90
|
"@biomejs/biome": "2.4.4",
|
|
79
|
-
"@earendil-works/pi-
|
|
80
|
-
"@earendil-works/pi-
|
|
91
|
+
"@earendil-works/pi-agent-core": "0.84.3",
|
|
92
|
+
"@earendil-works/pi-ai": "0.84.3",
|
|
93
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
81
94
|
"@types/node": "22.14.0",
|
|
82
95
|
"@typescript/native-preview": "7.0.0-dev.20260417.1",
|
|
83
96
|
"typebox": "1.3.7",
|
|
84
97
|
"typescript": "5.9.2"
|
|
85
98
|
},
|
|
86
99
|
"overrides": {
|
|
87
|
-
"fast-xml-parser": "5.
|
|
100
|
+
"fast-xml-parser": "5.7.0"
|
|
88
101
|
},
|
|
89
102
|
"peerDependencies": {
|
|
90
103
|
"@earendil-works/pi-ai": "*",
|
package/src/broker/server.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import net from "node:net";
|
|
|
10
10
|
/** @typedef {import("../common/contracts.ts").ActivityStripBrokerOptions} ActivityStripBrokerOptions */
|
|
11
11
|
import {
|
|
12
12
|
ACTIVITY_STRIP_BROADCAST_TICK_MS,
|
|
13
|
+
ACTIVITY_STRIP_CLIENT_IDLE_TIMEOUT_MS,
|
|
14
|
+
ACTIVITY_STRIP_MAX_MESSAGE_BYTES,
|
|
13
15
|
ACTIVITY_STRIP_SOCKET_DIR,
|
|
14
16
|
ACTIVITY_STRIP_SOCKET_PATH,
|
|
15
17
|
} from "../common/constants.mjs";
|
|
@@ -49,6 +51,8 @@ export class ActivityStripBroker extends EventEmitter {
|
|
|
49
51
|
this.getRuntimeStatus = options.getRuntimeStatus ?? (() => undefined);
|
|
50
52
|
this.focusSession =
|
|
51
53
|
options.focusSession ?? (async () => ({ ok: false, error: "Focus unavailable" }));
|
|
54
|
+
this.focusStrip =
|
|
55
|
+
options.focusStrip ?? (async () => ({ ok: false, error: "Strip focus unavailable" }));
|
|
52
56
|
this.server = net.createServer((socket) => this.handleConnection(socket));
|
|
53
57
|
this.tick = null;
|
|
54
58
|
}
|
|
@@ -98,12 +102,18 @@ export class ActivityStripBroker extends EventEmitter {
|
|
|
98
102
|
handleConnection(socket) {
|
|
99
103
|
let buffer = "";
|
|
100
104
|
socket.setEncoding("utf8");
|
|
105
|
+
socket.setTimeout?.(ACTIVITY_STRIP_CLIENT_IDLE_TIMEOUT_MS, () => socket.destroy());
|
|
101
106
|
socket.on("error", (error) => {
|
|
102
107
|
if (!isExpectedClientDisconnect(error)) this.emit("client-error", error);
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
socket.on("data", (chunk) => {
|
|
106
111
|
buffer += chunk;
|
|
112
|
+
if (Buffer.byteLength(buffer, "utf8") > ACTIVITY_STRIP_MAX_MESSAGE_BYTES) {
|
|
113
|
+
this.reply(socket, { ok: false, error: "Activity strip broker message is too large." });
|
|
114
|
+
socket.destroy();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
107
117
|
let newlineIndex = buffer.indexOf("\n");
|
|
108
118
|
while (newlineIndex >= 0) {
|
|
109
119
|
const line = buffer.slice(0, newlineIndex);
|
|
@@ -139,26 +149,52 @@ export class ActivityStripBroker extends EventEmitter {
|
|
|
139
149
|
});
|
|
140
150
|
return;
|
|
141
151
|
case "focus":
|
|
142
|
-
Promise.resolve(this.focusSession(String(message.sessionId ?? "")))
|
|
152
|
+
Promise.resolve(this.focusSession(String(message.cardId ?? message.sessionId ?? "")))
|
|
143
153
|
.then((result) => this.reply(socket, { type: "focus", ...result }))
|
|
144
154
|
.catch(() =>
|
|
145
155
|
this.reply(socket, { ok: false, type: "focus", error: "Focus failed closed." }),
|
|
146
156
|
);
|
|
147
157
|
return;
|
|
158
|
+
case "focus-strip":
|
|
159
|
+
Promise.resolve(this.focusStrip())
|
|
160
|
+
.then((result) => this.reply(socket, { type: "focus-strip", ...result }))
|
|
161
|
+
.catch(() =>
|
|
162
|
+
this.reply(socket, {
|
|
163
|
+
ok: false,
|
|
164
|
+
type: "focus-strip",
|
|
165
|
+
error: "Strip focus failed closed.",
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
148
169
|
case "shutdown":
|
|
149
170
|
this.reply(socket, { ok: true, type: "shutdown" });
|
|
150
171
|
setTimeout(() => {
|
|
151
172
|
this.emit("shutdown-requested");
|
|
152
173
|
}, 20);
|
|
153
174
|
return;
|
|
154
|
-
case "remove":
|
|
155
|
-
this.store.remove(
|
|
156
|
-
|
|
175
|
+
case "remove": {
|
|
176
|
+
const accepted = this.store.remove(
|
|
177
|
+
String(message.sessionId ?? ""),
|
|
178
|
+
String(message.publisherId ?? ""),
|
|
179
|
+
);
|
|
180
|
+
if (accepted) this.emitSnapshot();
|
|
181
|
+
this.reply(socket, {
|
|
182
|
+
ok: accepted,
|
|
183
|
+
type: "remove",
|
|
184
|
+
...(accepted ? {} : { error: "Activity strip broker rejected remove." }),
|
|
185
|
+
});
|
|
157
186
|
return;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.
|
|
187
|
+
}
|
|
188
|
+
case "upsert": {
|
|
189
|
+
const accepted = this.store.upsert(message.session);
|
|
190
|
+
if (accepted) this.emitSnapshot();
|
|
191
|
+
this.reply(socket, {
|
|
192
|
+
ok: accepted,
|
|
193
|
+
type: "upsert",
|
|
194
|
+
...(accepted ? {} : { error: "Activity strip broker rejected upsert." }),
|
|
195
|
+
});
|
|
161
196
|
return;
|
|
197
|
+
}
|
|
162
198
|
default:
|
|
163
199
|
this.reply(socket, { ok: false, error: `Unsupported message type: ${message.type}` });
|
|
164
200
|
}
|
|
@@ -4,43 +4,85 @@
|
|
|
4
4
|
// - "changing session retention, upsert semantics, or broker snapshot ordering"
|
|
5
5
|
// ---
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ACTIVITY_STRIP_MAX_SESSIONS,
|
|
9
|
+
ACTIVITY_STRIP_STALE_AFTER_MS,
|
|
10
|
+
} from "../common/constants.mjs";
|
|
8
11
|
import { normalizeSessionSnapshot, sortSessions } from "../common/protocol.mjs";
|
|
9
12
|
|
|
10
13
|
/** @typedef {import("../common/contracts.ts").SessionSnapshot} SessionSnapshot */
|
|
11
14
|
/** @typedef {import("../common/contracts.ts").SessionStoreOptions} SessionStoreOptions */
|
|
12
15
|
/** @typedef {import("../common/contracts.ts").BrokerSnapshot} BrokerSnapshot */
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Stable store key. Sessions resumed into a second process keep publishing under
|
|
19
|
+
* their own publisherId so two live processes never fight over one card; legacy
|
|
20
|
+
* publishers without a publisherId keep the bare sessionId key.
|
|
21
|
+
* @param {SessionSnapshot} session
|
|
22
|
+
*/
|
|
23
|
+
function sessionKey(session) {
|
|
24
|
+
return session.publisherId ? `${session.sessionId}|${session.publisherId}` : session.sessionId;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
export class SessionStore {
|
|
15
28
|
/** @param {SessionStoreOptions} [options] */
|
|
16
29
|
constructor(options = {}) {
|
|
17
30
|
this.staleAfterMs = options.staleAfterMs ?? ACTIVITY_STRIP_STALE_AFTER_MS;
|
|
18
|
-
|
|
31
|
+
this.maxSessions = options.maxSessions ?? ACTIVITY_STRIP_MAX_SESSIONS;
|
|
32
|
+
/** @type {Map<string, {session: SessionSnapshot; receivedAt: number}>} */
|
|
19
33
|
this.sessions = new Map();
|
|
34
|
+
/** @type {Map<string, number>} */
|
|
35
|
+
this.tombstones = new Map();
|
|
20
36
|
}
|
|
21
37
|
|
|
22
|
-
/** @param {Partial<SessionSnapshot> | Record<string, unknown>} session */
|
|
23
|
-
upsert(session) {
|
|
38
|
+
/** @param {Partial<SessionSnapshot> | Record<string, unknown>} session @param {number} [receivedAt] */
|
|
39
|
+
upsert(session, receivedAt = Date.now()) {
|
|
40
|
+
if (!session || typeof session !== "object") return false;
|
|
41
|
+
this.purge(receivedAt);
|
|
24
42
|
const normalized = normalizeSessionSnapshot(session);
|
|
25
43
|
if (!normalized.sessionId) return false;
|
|
26
|
-
|
|
44
|
+
const key = sessionKey(normalized);
|
|
45
|
+
if (this.tombstones.has(key)) return false;
|
|
46
|
+
const current = this.sessions.get(key);
|
|
47
|
+
if (
|
|
48
|
+
current &&
|
|
49
|
+
current.session.publisherSequence > 0 &&
|
|
50
|
+
(normalized.publisherSequence === 0 ||
|
|
51
|
+
normalized.publisherSequence <= current.session.publisherSequence)
|
|
52
|
+
) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!current && this.sessions.size >= this.maxSessions) return false;
|
|
56
|
+
this.sessions.set(key, { session: normalized, receivedAt });
|
|
27
57
|
return true;
|
|
28
58
|
}
|
|
29
59
|
|
|
30
|
-
/** @param {string} sessionId */
|
|
31
|
-
remove(sessionId) {
|
|
32
|
-
|
|
60
|
+
/** @param {string} sessionId @param {string} [publisherId] @param {number} [removedAt] */
|
|
61
|
+
remove(sessionId, publisherId, removedAt = Date.now()) {
|
|
62
|
+
const id = String(sessionId ?? "");
|
|
63
|
+
if (!id) return false;
|
|
64
|
+
if (!publisherId) return this.sessions.delete(id);
|
|
65
|
+
const key = `${id}|${String(publisherId)}`;
|
|
66
|
+
this.purge(removedAt);
|
|
67
|
+
if (!this.sessions.has(key)) return false;
|
|
68
|
+
if (!this.tombstones.has(key) && this.tombstones.size >= this.maxSessions) return false;
|
|
69
|
+
this.sessions.delete(key);
|
|
70
|
+
this.tombstones.set(key, removedAt);
|
|
71
|
+
return true;
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
/** @param {number} [now] */
|
|
36
75
|
purge(now = Date.now()) {
|
|
37
76
|
let changed = false;
|
|
38
|
-
for (const [sessionId,
|
|
39
|
-
if (now -
|
|
77
|
+
for (const [sessionId, entry] of this.sessions.entries()) {
|
|
78
|
+
if (now - entry.receivedAt > this.staleAfterMs) {
|
|
40
79
|
this.sessions.delete(sessionId);
|
|
41
80
|
changed = true;
|
|
42
81
|
}
|
|
43
82
|
}
|
|
83
|
+
for (const [sessionId, removedAt] of this.tombstones.entries()) {
|
|
84
|
+
if (now - removedAt > this.staleAfterMs) this.tombstones.delete(sessionId);
|
|
85
|
+
}
|
|
44
86
|
return changed;
|
|
45
87
|
}
|
|
46
88
|
|
|
@@ -49,7 +91,7 @@ export class SessionStore {
|
|
|
49
91
|
this.purge(now);
|
|
50
92
|
return {
|
|
51
93
|
generatedAt: now,
|
|
52
|
-
sessions: sortSessions([...this.sessions.values()]),
|
|
94
|
+
sessions: sortSessions([...this.sessions.values()].map((entry) => entry.session)),
|
|
53
95
|
};
|
|
54
96
|
}
|
|
55
97
|
}
|
|
@@ -112,10 +112,31 @@ export async function requestBrokerShutdown(options = {}) {
|
|
|
112
112
|
|
|
113
113
|
/** @param {SessionSnapshot} session @param {BrokerClientOptions} [options] */
|
|
114
114
|
export async function publishSessionSnapshot(session, options = {}) {
|
|
115
|
-
await sendBrokerMessage(makeMessage("upsert", { session }),
|
|
115
|
+
const result = await sendBrokerMessage(makeMessage("upsert", { session }), {
|
|
116
|
+
...options,
|
|
117
|
+
expectReply: true,
|
|
118
|
+
});
|
|
119
|
+
if (result?.ok !== true)
|
|
120
|
+
throw new Error(result?.error || "Activity strip broker rejected upsert.");
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
/** @param {string}
|
|
119
|
-
export async function removeSession(
|
|
120
|
-
|
|
123
|
+
/** @param {{ sessionId: string; publisherId: string }} session @param {BrokerClientOptions} [options] */
|
|
124
|
+
export async function removeSession(session, options = {}) {
|
|
125
|
+
/** @type {Record<string, unknown>} */
|
|
126
|
+
let record = {};
|
|
127
|
+
let fallbackId = "";
|
|
128
|
+
if (session && typeof session === "object") {
|
|
129
|
+
record = session;
|
|
130
|
+
} else {
|
|
131
|
+
fallbackId = String(session ?? "");
|
|
132
|
+
}
|
|
133
|
+
const result = await sendBrokerMessage(
|
|
134
|
+
makeMessage("remove", {
|
|
135
|
+
sessionId: String(record.sessionId ?? fallbackId),
|
|
136
|
+
publisherId: String(record.publisherId ?? ""),
|
|
137
|
+
}),
|
|
138
|
+
{ ...options, expectReply: true },
|
|
139
|
+
);
|
|
140
|
+
if (result?.ok !== true)
|
|
141
|
+
throw new Error(result?.error || "Activity strip broker rejected remove.");
|
|
121
142
|
}
|