clay-server 4.2.0 → 4.3.0-beta.10
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/lib/driver-continuation-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +86 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +3 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +117 -135
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-pair-result-capture.js +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-sessions.js +4 -0
- package/lib/project-user-message.js +21 -0
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +87 -3
- package/lib/public/app.js +1 -0
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/mobile-nav.css +29 -14
- package/lib/public/css/overlays.css +54 -25
- package/lib/public/css/pane.css +66 -12
- package/lib/public/css/sidebar.css +62 -7
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +165 -74
- package/lib/public/modules/app-messages.js +31 -3
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/message-delivery-ui.js +63 -0
- package/lib/public/modules/message-delivery.js +163 -6
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/permission-control.js +10 -5
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/session-creation-cta.js +46 -0
- package/lib/public/modules/sidebar-mobile.js +42 -30
- package/lib/public/modules/sidebar-sessions.js +77 -52
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-lifecycle.js +178 -0
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/server.js +1 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +46 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +30 -9
- package/lib/session-split-group-anchors.js +126 -0
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +293 -133
- package/lib/sessions.js +89 -22
- package/lib/ws-schema.js +8 -0
- package/package.json +1 -1
|
@@ -1,151 +1,71 @@
|
|
|
1
1
|
var crypto = require("crypto");
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
|
|
2
|
+
var durableAnchors = require("./session-split-group-anchors");
|
|
3
|
+
var splitRoles = require("./session-split-group-roles");
|
|
4
|
+
var versionedStore = require("./session-split-group-v2-store");
|
|
5
|
+
var multiWorkerFeature = require("./multi-worker-feature");
|
|
6
|
+
var groupPersistence = require("./session-split-group-persistence");
|
|
5
7
|
function truncateTitle(title) {
|
|
6
8
|
var value = String(title || "New Session");
|
|
7
9
|
return value.length > 20 ? value.slice(0, 19) + "…" : value;
|
|
8
10
|
}
|
|
9
|
-
|
|
10
11
|
function autoGroupName(leftTitle, rightTitle) {
|
|
11
12
|
return truncateTitle(leftTitle) + " | " + truncateTitle(rightTitle);
|
|
12
13
|
}
|
|
13
|
-
|
|
14
14
|
function createSplitGroupStore(opts) {
|
|
15
15
|
var sessions = opts.sessions;
|
|
16
|
-
var groupsFile = path.join(opts.sessionsDir, "split-groups.json");
|
|
17
16
|
var usersModule = opts.usersModule;
|
|
18
17
|
var broadcast = opts.broadcast || function () {};
|
|
19
18
|
var onPairChanged = opts.onPairChanged || function () {};
|
|
20
19
|
var groups = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// every record and remap members from them at load.
|
|
26
|
-
function currentCliIds(group) {
|
|
27
|
-
var left = sessions.get(group.members[0]);
|
|
28
|
-
var right = sessions.get(group.members[1]);
|
|
29
|
-
var prev = Array.isArray(group.memberCliIds) ? group.memberCliIds : [null, null];
|
|
30
|
-
return [
|
|
31
|
-
(left && left.cliSessionId) || prev[0] || null,
|
|
32
|
-
(right && right.cliSessionId) || prev[1] || null,
|
|
33
|
-
];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function refreshPairAnchors(group) {
|
|
37
|
-
if (!group.pair) return;
|
|
38
|
-
var driver = sessions.get(group.pair.driverId);
|
|
39
|
-
var worker = sessions.get(group.pair.workerId);
|
|
40
|
-
group.pairCliIds = [
|
|
41
|
-
(driver && driver.cliSessionId) || (group.pairCliIds && group.pairCliIds[0]) || null,
|
|
42
|
-
(worker && worker.cliSessionId) || (group.pairCliIds && group.pairCliIds[1]) || null,
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function sessionByCliId(cliId) {
|
|
47
|
-
if (!cliId) return null;
|
|
48
|
-
var found = null;
|
|
49
|
-
sessions.forEach(function (s) {
|
|
50
|
-
if (s.cliSessionId === cliId) found = s;
|
|
51
|
-
});
|
|
52
|
-
return found;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function save() {
|
|
56
|
-
for (var gi = 0; gi < groups.length; gi++) {
|
|
57
|
-
groups[gi].memberCliIds = currentCliIds(groups[gi]);
|
|
58
|
-
refreshPairAnchors(groups[gi]);
|
|
59
|
-
}
|
|
60
|
-
var tmp = groupsFile + ".tmp." + process.pid;
|
|
61
|
-
fs.mkdirSync(opts.sessionsDir, { recursive: true });
|
|
62
|
-
fs.writeFileSync(tmp, JSON.stringify(groups, null, 2) + "\n");
|
|
63
|
-
if (process.platform !== "win32") {
|
|
64
|
-
try { fs.chmodSync(tmp, 0o600); } catch (e) {}
|
|
65
|
-
}
|
|
66
|
-
fs.renameSync(tmp, groupsFile);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function validRecord(group) {
|
|
70
|
-
return group && typeof group.id === "string" && Array.isArray(group.members) &&
|
|
71
|
-
group.members.length === 2 && group.members[0] !== group.members[1] &&
|
|
72
|
-
Number.isInteger(group.members[0]) && Number.isInteger(group.members[1]) &&
|
|
73
|
-
sessions.has(group.members[0]) && sessions.has(group.members[1]);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function load() {
|
|
77
|
-
var loaded = [];
|
|
78
|
-
var shouldRewrite = false;
|
|
79
|
-
try {
|
|
80
|
-
loaded = JSON.parse(fs.readFileSync(groupsFile, "utf8"));
|
|
81
|
-
if (!Array.isArray(loaded)) loaded = [];
|
|
82
|
-
} catch (e) {
|
|
83
|
-
if (e.code !== "ENOENT") shouldRewrite = true;
|
|
84
|
-
}
|
|
85
|
-
var claimed = {};
|
|
86
|
-
groups = loaded.filter(function (group) {
|
|
87
|
-
// Remap members from the durable cliSessionIds; the stored localIds
|
|
88
|
-
// are only trusted for legacy records that predate memberCliIds.
|
|
89
|
-
if (group && Array.isArray(group.memberCliIds)) {
|
|
90
|
-
var left = sessionByCliId(group.memberCliIds[0]);
|
|
91
|
-
var right = sessionByCliId(group.memberCliIds[1]);
|
|
92
|
-
if (!left || !right || left === right) return false;
|
|
93
|
-
if (group.members[0] !== left.localId || group.members[1] !== right.localId) {
|
|
94
|
-
group.members = [left.localId, right.localId];
|
|
95
|
-
shouldRewrite = true;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (group && group.pair && Array.isArray(group.pairCliIds)) {
|
|
99
|
-
var driver = sessionByCliId(group.pairCliIds[0]);
|
|
100
|
-
var worker = sessionByCliId(group.pairCliIds[1]);
|
|
101
|
-
if (driver && worker) {
|
|
102
|
-
group.pair.driverId = driver.localId;
|
|
103
|
-
group.pair.workerId = worker.localId;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (group && group.pair) {
|
|
107
|
-
var pairValid = Array.isArray(group.members) && group.pair.driverId !== group.pair.workerId &&
|
|
108
|
-
group.members.indexOf(group.pair.driverId) !== -1 &&
|
|
109
|
-
group.members.indexOf(group.pair.workerId) !== -1;
|
|
110
|
-
if (!pairValid) return false;
|
|
111
|
-
}
|
|
112
|
-
if (!validRecord(group) || claimed[group.members[0]] || claimed[group.members[1]]) return false;
|
|
113
|
-
claimed[group.members[0]] = true;
|
|
114
|
-
claimed[group.members[1]] = true;
|
|
115
|
-
return true;
|
|
116
|
-
});
|
|
117
|
-
if (groups.length !== loaded.length) shouldRewrite = true;
|
|
118
|
-
if (shouldRewrite) save();
|
|
119
|
-
}
|
|
120
|
-
|
|
20
|
+
var stagedTransfers = [];
|
|
21
|
+
var versionedEnabled = multiWorkerFeature.isEnabled(opts.multiWorkerFeature);
|
|
22
|
+
var persistence = groupPersistence.attachPersistence(Object.assign({}, opts, { versionedEnabled: versionedEnabled }));
|
|
23
|
+
function save() { return persistence.save(groups); }
|
|
121
24
|
function isMultiUser() {
|
|
122
25
|
return !!(usersModule && usersModule.isMultiUser && usersModule.isMultiUser());
|
|
123
26
|
}
|
|
124
|
-
|
|
125
27
|
function listFor(ws) {
|
|
126
28
|
if (!isMultiUser()) return groups.slice();
|
|
127
29
|
if (!ws || !ws._clayUser) return [];
|
|
128
30
|
return groups.filter(function (group) { return group.ownerId === ws._clayUser.id; });
|
|
129
31
|
}
|
|
130
|
-
|
|
131
32
|
function groupForMember(localId) {
|
|
33
|
+
for (var si = 0; si < stagedTransfers.length; si++) {
|
|
34
|
+
var transfer = stagedTransfers[si];
|
|
35
|
+
if (!exactTransfer(transfer)) continue;
|
|
36
|
+
if (transfer.source.localId === localId) return null;
|
|
37
|
+
if (transfer.target.localId === localId || transfer.worker.localId === localId) return transfer.runtimeGroup;
|
|
38
|
+
}
|
|
132
39
|
for (var i = 0; i < groups.length; i++) {
|
|
133
40
|
if (groups[i].members.indexOf(localId) !== -1) return groups[i];
|
|
134
41
|
}
|
|
135
42
|
return null;
|
|
136
43
|
}
|
|
137
|
-
|
|
44
|
+
function membershipGroup(localId) {
|
|
45
|
+
for (var si = 0; si < stagedTransfers.length; si++) {
|
|
46
|
+
var transfer = stagedTransfers[si];
|
|
47
|
+
if (transfer.source.localId === localId || transfer.target.localId === localId ||
|
|
48
|
+
transfer.worker.localId === localId) return transfer.group;
|
|
49
|
+
}
|
|
50
|
+
for (var i = 0; i < groups.length; i++) if (groups[i].members.indexOf(localId) !== -1) return groups[i];
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function groupCountForMember(localId) {
|
|
54
|
+
var count = 0;
|
|
55
|
+
for (var i = 0; i < groups.length; i++) {
|
|
56
|
+
if (groups[i].members.indexOf(localId) !== -1) count++;
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
138
60
|
function canAccess(ws, session) {
|
|
139
61
|
if (!isMultiUser()) return true;
|
|
140
62
|
return !!(ws && ws._clayUser && usersModule.canAccessSession(
|
|
141
63
|
ws._clayUser.id, session, { visibility: "public" }
|
|
142
64
|
));
|
|
143
65
|
}
|
|
144
|
-
|
|
145
66
|
function canOwn(ws, group) {
|
|
146
67
|
return !isMultiUser() || !!(ws && ws._clayUser && group.ownerId === ws._clayUser.id);
|
|
147
68
|
}
|
|
148
|
-
|
|
149
69
|
function create(ws, msg) {
|
|
150
70
|
var members = msg && msg.members;
|
|
151
71
|
if (!Array.isArray(members) || members.length !== 2) return { ok: false, error: "A split group requires exactly two sessions" };
|
|
@@ -156,7 +76,9 @@ function createSplitGroupStore(opts) {
|
|
|
156
76
|
var right = sessions.get(members[1]);
|
|
157
77
|
if (!left || !right) return { ok: false, error: "Session not found" };
|
|
158
78
|
if (!canAccess(ws, left) || !canAccess(ws, right)) return { ok: false, error: "Session access denied" };
|
|
159
|
-
if (
|
|
79
|
+
if (membershipGroup(members[0]) || membershipGroup(members[1])) return { ok: false, error: "A session can belong to only one split group" };
|
|
80
|
+
var roleCheck = splitRoles.validateStorePair(msg.pair, members);
|
|
81
|
+
if (!roleCheck.ok) return { ok: false, error: roleCheck.error };
|
|
160
82
|
var requestedName = typeof msg.name === "string" ? msg.name.trim().slice(0, 80) : "";
|
|
161
83
|
var group = {
|
|
162
84
|
id: "sg_" + Date.now().toString(36) + "_" + crypto.randomBytes(3).toString("hex"),
|
|
@@ -178,7 +100,6 @@ function createSplitGroupStore(opts) {
|
|
|
178
100
|
if (group.pair) onPairChanged(group);
|
|
179
101
|
return { ok: true, group: group };
|
|
180
102
|
}
|
|
181
|
-
|
|
182
103
|
function createOwned(ownerId, msg) {
|
|
183
104
|
var members = msg && msg.members;
|
|
184
105
|
if (!Array.isArray(members) || members.length !== 2) return { ok: false, error: "A split group requires exactly two sessions" };
|
|
@@ -186,9 +107,10 @@ function createSplitGroupStore(opts) {
|
|
|
186
107
|
var right = sessions.get(members[1]);
|
|
187
108
|
if (!left || !right || left === right) return { ok: false, error: "Session not found" };
|
|
188
109
|
if ((left.ownerId || null) !== (ownerId || null) || (right.ownerId || null) !== (ownerId || null)) return { ok: false, error: "Session owner does not match the scheduled task owner" };
|
|
189
|
-
if (
|
|
110
|
+
if (membershipGroup(left.localId) || membershipGroup(right.localId)) return { ok: false, error: "A session can belong to only one split group" };
|
|
190
111
|
var pair = msg && msg.pair;
|
|
191
|
-
|
|
112
|
+
var roleCheck = splitRoles.validateStorePair(pair, members);
|
|
113
|
+
if (!roleCheck.ok || !pair || pair.driverId !== left.localId || pair.workerId !== right.localId) return { ok: false, error: roleCheck.error || "Pair roles must reference both split group members" };
|
|
192
114
|
var requestedName = typeof msg.name === "string" ? msg.name.trim().slice(0, 80) : "";
|
|
193
115
|
var group = {
|
|
194
116
|
id: "sg_" + Date.now().toString(36) + "_" + crypto.randomBytes(3).toString("hex"),
|
|
@@ -202,7 +124,6 @@ function createSplitGroupStore(opts) {
|
|
|
202
124
|
onPairChanged(group);
|
|
203
125
|
return { ok: true, group: group };
|
|
204
126
|
}
|
|
205
|
-
|
|
206
127
|
function dissolveOwned(ownerId, id) {
|
|
207
128
|
var index = groups.findIndex(function (item) { return item.id === id && (item.ownerId || null) === (ownerId || null); });
|
|
208
129
|
if (index === -1) return false;
|
|
@@ -212,7 +133,158 @@ function createSplitGroupStore(opts) {
|
|
|
212
133
|
onPairChanged(removed);
|
|
213
134
|
return true;
|
|
214
135
|
}
|
|
215
|
-
|
|
136
|
+
function beginOwnedDriverTransfer(ownerId, msg) {
|
|
137
|
+
var group = groups.find(function (item) { return item.id === (msg && msg.id); });
|
|
138
|
+
if (!group || (group.ownerId || null) !== (ownerId || null)) return { ok: false, error: "Split group not found" };
|
|
139
|
+
var transferRoles = splitRoles.normalizePair(group.pair, group.members);
|
|
140
|
+
if (transferRoles.ok && transferRoles.kind === "versioned") {
|
|
141
|
+
return { ok: false, error: "Version 2 Driver transfer is not enabled" };
|
|
142
|
+
}
|
|
143
|
+
if (!group.pair || group.pair.driverId !== msg.sourceDriverId || group.pair.workerId !== msg.workerId) {
|
|
144
|
+
return { ok: false, error: "The split pair changed before Driver transfer" };
|
|
145
|
+
}
|
|
146
|
+
var source = sessions.get(msg.sourceDriverId);
|
|
147
|
+
var target = sessions.get(msg.targetDriverId);
|
|
148
|
+
var worker = sessions.get(msg.workerId);
|
|
149
|
+
if (!source || !target || !worker || source === target || target === worker) return { ok: false, error: "Session not found" };
|
|
150
|
+
if ((source.ownerId || null) !== (ownerId || null) || (target.ownerId || null) !== (ownerId || null) ||
|
|
151
|
+
(worker.ownerId || null) !== (ownerId || null)) return { ok: false, error: "Split pair owner changed" };
|
|
152
|
+
if (membershipGroup(target.localId)) return { ok: false, error: "The successor already belongs to a split group" };
|
|
153
|
+
var sourceIndex = group.members.indexOf(source.localId);
|
|
154
|
+
if (sourceIndex === -1 || group.members.indexOf(worker.localId) === -1) return { ok: false, error: "Split group members changed" };
|
|
155
|
+
var before = {
|
|
156
|
+
members: group.members.slice(),
|
|
157
|
+
memberCliIds: Array.isArray(group.memberCliIds) ? group.memberCliIds.slice() : null,
|
|
158
|
+
memberOriginIds: Array.isArray(group.memberOriginIds) ? group.memberOriginIds.slice() : null,
|
|
159
|
+
pair: Object.assign({}, group.pair),
|
|
160
|
+
pairCliIds: Array.isArray(group.pairCliIds) ? group.pairCliIds.slice() : null,
|
|
161
|
+
pairOriginIds: Array.isArray(group.pairOriginIds) ? group.pairOriginIds.slice() : null,
|
|
162
|
+
};
|
|
163
|
+
var runtimeMembers = group.members.slice();
|
|
164
|
+
runtimeMembers[sourceIndex] = target.localId;
|
|
165
|
+
var runtimeGroup = Object.assign({}, group, { members: runtimeMembers,
|
|
166
|
+
pair: { driverId: target.localId, workerId: worker.localId } });
|
|
167
|
+
var transaction = { group: group, before: before, source: source, target: target, worker: worker,
|
|
168
|
+
runtimeGroup: runtimeGroup,
|
|
169
|
+
ownerId: ownerId || null, sourceOriginId: source.sessionOriginId || null,
|
|
170
|
+
targetOriginId: target.sessionOriginId || null, workerOriginId: worker.sessionOriginId || null,
|
|
171
|
+
workerCliSessionId: worker.cliSessionId || null, staged: true, committed: false };
|
|
172
|
+
stagedTransfers.push(transaction);
|
|
173
|
+
return { ok: true, group: runtimeGroup, transaction: transaction };
|
|
174
|
+
}
|
|
175
|
+
function removeStaged(transaction) {
|
|
176
|
+
var index = stagedTransfers.indexOf(transaction);
|
|
177
|
+
if (index !== -1) stagedTransfers.splice(index, 1);
|
|
178
|
+
}
|
|
179
|
+
function restoreTransfer(transaction) {
|
|
180
|
+
var group = transaction.group;
|
|
181
|
+
group.members = transaction.before.members.slice();
|
|
182
|
+
group.pair = Object.assign({}, transaction.before.pair);
|
|
183
|
+
if (transaction.before.memberCliIds) group.memberCliIds = transaction.before.memberCliIds.slice();
|
|
184
|
+
else delete group.memberCliIds;
|
|
185
|
+
if (transaction.before.memberOriginIds) group.memberOriginIds = transaction.before.memberOriginIds.slice();
|
|
186
|
+
else delete group.memberOriginIds;
|
|
187
|
+
if (transaction.before.pairCliIds) group.pairCliIds = transaction.before.pairCliIds.slice();
|
|
188
|
+
else delete group.pairCliIds;
|
|
189
|
+
if (transaction.before.pairOriginIds) group.pairOriginIds = transaction.before.pairOriginIds.slice();
|
|
190
|
+
else delete group.pairOriginIds;
|
|
191
|
+
}
|
|
192
|
+
function stagedTransfer(transaction) {
|
|
193
|
+
return transaction && transaction.staged && !transaction.committed &&
|
|
194
|
+
stagedTransfers.indexOf(transaction) !== -1 && groups.indexOf(transaction.group) !== -1 &&
|
|
195
|
+
transaction.runtimeGroup.pair && transaction.runtimeGroup.pair.driverId === transaction.target.localId &&
|
|
196
|
+
transaction.runtimeGroup.pair.workerId === transaction.worker.localId &&
|
|
197
|
+
transaction.runtimeGroup.members.length === 2 && transaction.runtimeGroup.members.indexOf(transaction.target.localId) !== -1 &&
|
|
198
|
+
transaction.runtimeGroup.members.indexOf(transaction.worker.localId) !== -1;
|
|
199
|
+
}
|
|
200
|
+
function exactTransfer(transaction) {
|
|
201
|
+
return stagedTransfer(transaction) &&
|
|
202
|
+
sessions.get(transaction.source.localId) === transaction.source &&
|
|
203
|
+
sessions.get(transaction.target.localId) === transaction.target &&
|
|
204
|
+
sessions.get(transaction.worker.localId) === transaction.worker &&
|
|
205
|
+
(transaction.group.ownerId || null) === transaction.ownerId &&
|
|
206
|
+
(transaction.source.ownerId || null) === transaction.ownerId &&
|
|
207
|
+
(transaction.target.ownerId || null) === transaction.ownerId &&
|
|
208
|
+
(transaction.worker.ownerId || null) === transaction.ownerId &&
|
|
209
|
+
(transaction.source.sessionOriginId || null) === transaction.sourceOriginId &&
|
|
210
|
+
(transaction.target.sessionOriginId || null) === transaction.targetOriginId &&
|
|
211
|
+
(transaction.worker.sessionOriginId || null) === transaction.workerOriginId &&
|
|
212
|
+
(transaction.worker.cliSessionId || null) === transaction.workerCliSessionId &&
|
|
213
|
+
transaction.group.pair && transaction.group.pair.driverId === transaction.source.localId &&
|
|
214
|
+
transaction.group.pair.workerId === transaction.worker.localId &&
|
|
215
|
+
transaction.group.members.length === 2 && transaction.group.members.indexOf(transaction.source.localId) !== -1 &&
|
|
216
|
+
transaction.group.members.indexOf(transaction.worker.localId) !== -1 &&
|
|
217
|
+
groupCountForMember(transaction.target.localId) === 0 && groupCountForMember(transaction.worker.localId) === 1 &&
|
|
218
|
+
groupCountForMember(transaction.source.localId) === 1;
|
|
219
|
+
}
|
|
220
|
+
function commitOwnedDriverTransfer(transaction) {
|
|
221
|
+
if (!exactTransfer(transaction)) {
|
|
222
|
+
removeStaged(transaction);
|
|
223
|
+
transaction.staged = false;
|
|
224
|
+
return { ok: false, error: "The staged Driver transfer is no longer exact" };
|
|
225
|
+
}
|
|
226
|
+
if (!transaction.target.cliSessionId ||
|
|
227
|
+
(!transaction.worker.cliSessionId && !transaction.worker.sessionOriginId)) {
|
|
228
|
+
return { ok: false, error: "The transferred pair does not have durable session anchors" };
|
|
229
|
+
}
|
|
230
|
+
transaction.group.members = transaction.runtimeGroup.members.slice();
|
|
231
|
+
transaction.group.pair = Object.assign({}, transaction.runtimeGroup.pair);
|
|
232
|
+
try { save(); }
|
|
233
|
+
catch (error) {
|
|
234
|
+
restoreTransfer(transaction);
|
|
235
|
+
removeStaged(transaction);
|
|
236
|
+
transaction.staged = false;
|
|
237
|
+
return { ok: false, error: error.message || String(error) };
|
|
238
|
+
}
|
|
239
|
+
removeStaged(transaction);
|
|
240
|
+
transaction.committed = true;
|
|
241
|
+
broadcast();
|
|
242
|
+
onPairChanged(transaction.group);
|
|
243
|
+
return { ok: true, group: transaction.group };
|
|
244
|
+
}
|
|
245
|
+
function rollbackOwnedDriverTransfer(transaction) {
|
|
246
|
+
if (stagedTransfer(transaction)) {
|
|
247
|
+
removeStaged(transaction);
|
|
248
|
+
transaction.staged = false;
|
|
249
|
+
return { ok: true, group: transaction.group };
|
|
250
|
+
}
|
|
251
|
+
if (!transaction || !transaction.committed || groups.indexOf(transaction.group) === -1 ||
|
|
252
|
+
transaction.group.pair.driverId !== transaction.target.localId ||
|
|
253
|
+
transaction.group.pair.workerId !== transaction.worker.localId) {
|
|
254
|
+
return { ok: false, error: "The staged Driver transfer is no longer exact" };
|
|
255
|
+
}
|
|
256
|
+
var wasCommitted = transaction.committed;
|
|
257
|
+
var committedState = {
|
|
258
|
+
members: transaction.group.members.slice(),
|
|
259
|
+
memberCliIds: Array.isArray(transaction.group.memberCliIds) ? transaction.group.memberCliIds.slice() : null,
|
|
260
|
+
memberOriginIds: Array.isArray(transaction.group.memberOriginIds) ? transaction.group.memberOriginIds.slice() : null,
|
|
261
|
+
pair: Object.assign({}, transaction.group.pair),
|
|
262
|
+
pairCliIds: Array.isArray(transaction.group.pairCliIds) ? transaction.group.pairCliIds.slice() : null,
|
|
263
|
+
pairOriginIds: Array.isArray(transaction.group.pairOriginIds) ? transaction.group.pairOriginIds.slice() : null,
|
|
264
|
+
};
|
|
265
|
+
restoreTransfer(transaction);
|
|
266
|
+
if (wasCommitted) {
|
|
267
|
+
try { save(); }
|
|
268
|
+
catch (error) {
|
|
269
|
+
transaction.group.members = committedState.members;
|
|
270
|
+
transaction.group.pair = committedState.pair;
|
|
271
|
+
if (committedState.memberCliIds) transaction.group.memberCliIds = committedState.memberCliIds;
|
|
272
|
+
else delete transaction.group.memberCliIds;
|
|
273
|
+
if (committedState.memberOriginIds) transaction.group.memberOriginIds = committedState.memberOriginIds;
|
|
274
|
+
else delete transaction.group.memberOriginIds;
|
|
275
|
+
if (committedState.pairCliIds) transaction.group.pairCliIds = committedState.pairCliIds;
|
|
276
|
+
else delete transaction.group.pairCliIds;
|
|
277
|
+
if (committedState.pairOriginIds) transaction.group.pairOriginIds = committedState.pairOriginIds;
|
|
278
|
+
else delete transaction.group.pairOriginIds;
|
|
279
|
+
return { ok: false, error: error.message || String(error) };
|
|
280
|
+
}
|
|
281
|
+
broadcast();
|
|
282
|
+
onPairChanged(transaction.group);
|
|
283
|
+
}
|
|
284
|
+
transaction.staged = false;
|
|
285
|
+
transaction.committed = false;
|
|
286
|
+
return { ok: true, group: transaction.group };
|
|
287
|
+
}
|
|
216
288
|
function rename(ws, msg) {
|
|
217
289
|
var group = groups.find(function (item) { return item.id === (msg && msg.id); });
|
|
218
290
|
if (!group) return { ok: false, error: "Split group not found" };
|
|
@@ -237,6 +309,7 @@ function createSplitGroupStore(opts) {
|
|
|
237
309
|
if (msg.driverId == null) {
|
|
238
310
|
delete group.pair;
|
|
239
311
|
delete group.pairCliIds;
|
|
312
|
+
delete group.pairOriginIds;
|
|
240
313
|
save();
|
|
241
314
|
broadcast();
|
|
242
315
|
onPairChanged(group);
|
|
@@ -246,6 +319,8 @@ function createSplitGroupStore(opts) {
|
|
|
246
319
|
return { ok: false, error: "Driver must be a member of the split group" };
|
|
247
320
|
}
|
|
248
321
|
var workerId = group.members[0] === msg.driverId ? group.members[1] : group.members[0];
|
|
322
|
+
var roleCheck = splitRoles.validateStorePair({ driverId: msg.driverId, workerId: workerId }, group.members);
|
|
323
|
+
if (!roleCheck.ok) return { ok: false, error: roleCheck.error };
|
|
249
324
|
group.pair = { driverId: msg.driverId, workerId: workerId };
|
|
250
325
|
save();
|
|
251
326
|
broadcast();
|
|
@@ -253,31 +328,73 @@ function createSplitGroupStore(opts) {
|
|
|
253
328
|
return { ok: true, group: group };
|
|
254
329
|
}
|
|
255
330
|
|
|
256
|
-
function dissolve(ws, msg) {
|
|
331
|
+
function dissolve(ws, msg, hooks) {
|
|
257
332
|
var index = groups.findIndex(function (item) { return item.id === (msg && msg.id); });
|
|
258
333
|
if (index === -1) return { ok: false, error: "Split group not found" };
|
|
259
334
|
if (!canOwn(ws, groups[index])) return { ok: false, error: "Only the group owner can separate it" };
|
|
260
335
|
var removed = groups.splice(index, 1)[0];
|
|
261
|
-
save();
|
|
336
|
+
try { save(); }
|
|
337
|
+
catch (error) { groups.splice(index, 0, removed); return { ok: false, error: error.message || String(error) }; }
|
|
338
|
+
if (hooks && typeof hooks.afterPersist === "function") {
|
|
339
|
+
groups.splice(index, 0, removed);
|
|
340
|
+
var accepted;
|
|
341
|
+
try { accepted = hooks.afterPersist(); }
|
|
342
|
+
catch (error) { accepted = { ok: false, error: error.message || String(error) }; }
|
|
343
|
+
if (!accepted || accepted.ok === false) {
|
|
344
|
+
try { save(); }
|
|
345
|
+
catch (rollbackError) {
|
|
346
|
+
return { ok: false, error: (accepted && accepted.error || "close transaction rejected") +
|
|
347
|
+
"; persistence rollback failed: " + (rollbackError.message || String(rollbackError)) };
|
|
348
|
+
}
|
|
349
|
+
return { ok: false, error: accepted && accepted.error || "close transaction rejected" };
|
|
350
|
+
}
|
|
351
|
+
groups.splice(index, 1);
|
|
352
|
+
}
|
|
262
353
|
broadcast();
|
|
263
354
|
return { ok: true, group: removed };
|
|
264
355
|
}
|
|
265
356
|
|
|
266
357
|
function dissolveBySession(localId) {
|
|
267
|
-
var
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
358
|
+
var index = groups.findIndex(function (group) { return group.members.indexOf(localId) !== -1; });
|
|
359
|
+
if (index === -1) return false;
|
|
360
|
+
var group = groups[index];
|
|
361
|
+
var normalized = splitRoles.normalizePair(group.pair, group.members);
|
|
362
|
+
var beforeMembers = group.members.slice();
|
|
363
|
+
var beforePair = group.pair && Object.assign({}, group.pair);
|
|
364
|
+
if (beforePair && Array.isArray(beforePair.workerIds)) beforePair.workerIds = beforePair.workerIds.slice();
|
|
365
|
+
var removed = null;
|
|
366
|
+
if (normalized.ok && normalized.kind === "versioned" && normalized.driverId !== localId &&
|
|
367
|
+
normalized.workerIds.length > 1) {
|
|
368
|
+
var remaining = normalized.workerIds.filter(function (id) { return id !== localId; });
|
|
369
|
+
group.members = [normalized.driverId].concat(remaining);
|
|
370
|
+
group.pair = { version: 2, driverId: normalized.driverId, workerIds: remaining };
|
|
371
|
+
} else {
|
|
372
|
+
removed = groups.splice(index, 1)[0];
|
|
373
|
+
}
|
|
374
|
+
try { save(); }
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (removed) groups.splice(index, 0, removed);
|
|
377
|
+
else { group.members = beforeMembers; group.pair = beforePair; }
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
271
380
|
broadcast();
|
|
381
|
+
onPairChanged(group);
|
|
272
382
|
return true;
|
|
273
383
|
}
|
|
274
384
|
|
|
275
385
|
function refreshAnchors(localId) {
|
|
276
386
|
var group = groupForMember(localId);
|
|
277
387
|
if (!group) return false;
|
|
278
|
-
var fresh =
|
|
279
|
-
|
|
280
|
-
|
|
388
|
+
var fresh = durableAnchors.memberAnchors(group, sessions);
|
|
389
|
+
if (fresh && fresh.version === 2) {
|
|
390
|
+
if (JSON.stringify(fresh) === JSON.stringify(group.memberAnchors || null)) return false;
|
|
391
|
+
save();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
var prevCli = Array.isArray(group.memberCliIds) ? group.memberCliIds : [null, null];
|
|
395
|
+
var prevOrigin = Array.isArray(group.memberOriginIds) ? group.memberOriginIds : [null, null];
|
|
396
|
+
if (fresh.cli[0] === prevCli[0] && fresh.cli[1] === prevCli[1] &&
|
|
397
|
+
fresh.origin[0] === prevOrigin[0] && fresh.origin[1] === prevOrigin[1]) return false;
|
|
281
398
|
save();
|
|
282
399
|
return true;
|
|
283
400
|
}
|
|
@@ -298,8 +415,54 @@ function createSplitGroupStore(opts) {
|
|
|
298
415
|
return true;
|
|
299
416
|
}
|
|
300
417
|
|
|
301
|
-
|
|
302
|
-
|
|
418
|
+
function removeVersionedGroup(group, hooks) {
|
|
419
|
+
var index = groups.indexOf(group);
|
|
420
|
+
if (index === -1) return { ok: false, error: "Split group not found" };
|
|
421
|
+
groups.splice(index, 1);
|
|
422
|
+
try { save(); }
|
|
423
|
+
catch (error) {
|
|
424
|
+
groups.splice(index, 0, group);
|
|
425
|
+
return { ok: false, error: error.message || String(error) };
|
|
426
|
+
}
|
|
427
|
+
if (hooks && typeof hooks.afterPersist === "function") {
|
|
428
|
+
groups.splice(index, 0, group);
|
|
429
|
+
var accepted;
|
|
430
|
+
try { accepted = hooks.afterPersist(); }
|
|
431
|
+
catch (error) { accepted = { ok: false, error: error.message || String(error) }; }
|
|
432
|
+
if (!accepted || accepted.ok === false) {
|
|
433
|
+
try { save(); }
|
|
434
|
+
catch (rollbackError) {
|
|
435
|
+
return { ok: false, error: (accepted && accepted.error || "close transaction rejected") +
|
|
436
|
+
"; persistence rollback failed: " + (rollbackError.message || String(rollbackError)) };
|
|
437
|
+
}
|
|
438
|
+
return { ok: false, error: accepted && accepted.error || "close transaction rejected" };
|
|
439
|
+
}
|
|
440
|
+
groups.splice(index, 1);
|
|
441
|
+
}
|
|
442
|
+
broadcast();
|
|
443
|
+
onPairChanged(group);
|
|
444
|
+
return { ok: true, group: group };
|
|
445
|
+
}
|
|
446
|
+
function versionedChanged(group) {
|
|
447
|
+
broadcast();
|
|
448
|
+
onPairChanged(group);
|
|
449
|
+
}
|
|
450
|
+
var versionedMutations = versionedStore.attachVersionedMutations({
|
|
451
|
+
sessions: sessions,
|
|
452
|
+
enabled: versionedEnabled,
|
|
453
|
+
groups: function () { return groups; },
|
|
454
|
+
membershipGroup: membershipGroup,
|
|
455
|
+
save: save,
|
|
456
|
+
remove: removeVersionedGroup,
|
|
457
|
+
changed: versionedChanged,
|
|
458
|
+
});
|
|
459
|
+
groups = persistence.load();
|
|
460
|
+
return { create: create, createOwned: createOwned, dissolveOwned: dissolveOwned,
|
|
461
|
+
beginOwnedDriverTransfer: beginOwnedDriverTransfer, commitOwnedDriverTransfer: commitOwnedDriverTransfer,
|
|
462
|
+
rollbackOwnedDriverTransfer: rollbackOwnedDriverTransfer,
|
|
463
|
+
rename: rename, setPair: setPair, dissolve: dissolve, dissolveBySession: dissolveBySession,
|
|
464
|
+
addWorker: versionedMutations.addWorker, replaceWorker: versionedMutations.replaceWorker,
|
|
465
|
+
removeWorker: versionedMutations.removeWorker,
|
|
303
466
|
refreshAutoName: refreshAutoName, refreshAnchors: refreshAnchors,
|
|
304
467
|
listFor: listFor, groupForMember: groupForMember, get groups() { return groups; } };
|
|
305
468
|
}
|
|
@@ -311,13 +474,11 @@ function attachSplitGroups(ctx) {
|
|
|
311
474
|
usersModule: ctx.usersModule,
|
|
312
475
|
broadcast: broadcast,
|
|
313
476
|
onPairChanged: ctx.onPairChanged,
|
|
477
|
+
multiWorkerFeature: ctx.multiWorkerFeature,
|
|
314
478
|
});
|
|
315
479
|
|
|
316
480
|
function sendState(ws) {
|
|
317
481
|
if (!ws) return;
|
|
318
|
-
// Pane clients need the same owner-scoped pair roles as the parent shell.
|
|
319
|
-
// The Worker composer lock resolves its role from this projection; hiding
|
|
320
|
-
// the projection from panes leaves a configured Worker looking editable.
|
|
321
482
|
ctx.sendTo(ws, { type: "split_groups", groups: store.listFor(ws) });
|
|
322
483
|
}
|
|
323
484
|
|
|
@@ -345,5 +506,4 @@ function attachSplitGroups(ctx) {
|
|
|
345
506
|
return { handleMessage: handleMessage, sendConnectionState: sendState, store: store };
|
|
346
507
|
}
|
|
347
508
|
|
|
348
|
-
module.exports = { attachSplitGroups: attachSplitGroups, createSplitGroupStore: createSplitGroupStore,
|
|
349
|
-
autoGroupName: autoGroupName };
|
|
509
|
+
module.exports = { attachSplitGroups: attachSplitGroups, createSplitGroupStore: createSplitGroupStore, autoGroupName: autoGroupName };
|