@kubun/store-p2p 0.19.0 → 0.20.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/lib/api.d.ts +5 -2
- package/lib/api.js +10 -6
- package/lib/definition.d.ts +6 -1
- package/lib/definition.js +37 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/migrations.js +90 -90
- package/lib/ops/app-lane.d.ts +1 -1
- package/lib/ops/app-lane.js +18 -12
- package/lib/ops/app-outbox.d.ts +4 -2
- package/lib/ops/app-outbox.js +33 -33
- package/lib/ops/catalog-offers.d.ts +1 -1
- package/lib/ops/catalog-offers.js +9 -10
- package/lib/ops/catalog-routes.js +9 -9
- package/lib/ops/catalog-sync.d.ts +2 -1
- package/lib/ops/catalog-sync.js +39 -42
- package/lib/ops/circles.js +33 -33
- package/lib/ops/control.d.ts +1 -1
- package/lib/ops/control.js +12 -10
- package/lib/ops/delegations.d.ts +2 -1
- package/lib/ops/delegations.js +17 -25
- package/lib/ops/discovery.d.ts +1 -1
- package/lib/ops/discovery.js +23 -10
- package/lib/ops/document-sweep.js +5 -5
- package/lib/ops/groups.d.ts +3 -2
- package/lib/ops/groups.js +60 -62
- package/lib/ops/hubs.js +38 -57
- package/lib/ops/log-handler-completion.js +3 -3
- package/lib/ops/mls.d.ts +1 -1
- package/lib/ops/mls.js +36 -26
- package/lib/ops/own-agents.d.ts +3 -3
- package/lib/ops/own-agents.js +128 -72
- package/lib/ops/sync.d.ts +1 -1
- package/lib/ops/sync.js +17 -8
- package/lib/scope.d.ts +8 -0
- package/lib/scope.js +8 -0
- package/lib/tables.d.ts +52 -42
- package/package.json +14 -13
package/lib/ops/groups.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { normalizeDID } from '@kokuin/token';
|
|
2
2
|
import { chunked } from '@kubun/db-adapter';
|
|
3
|
-
import { clearAppOutboxRows, lockAppOutboxForErasure } from './app-outbox.js';
|
|
4
|
-
export function createGroupOps(db, adapter) {
|
|
3
|
+
import { clearAppOutboxRows, lockAppOutboxForErasure, withAppOutboxLock } from './app-outbox.js';
|
|
4
|
+
export function createGroupOps(db, adapter, context) {
|
|
5
5
|
const ops = {
|
|
6
6
|
async createGroup (group) {
|
|
7
|
-
await db.insertInto('
|
|
7
|
+
await db.insertInto('p2p_groups').values({
|
|
8
8
|
id: group.id,
|
|
9
9
|
name: group.name,
|
|
10
10
|
description: group.description,
|
|
@@ -13,7 +13,7 @@ export function createGroupOps(db, adapter) {
|
|
|
13
13
|
}).execute();
|
|
14
14
|
},
|
|
15
15
|
async getGroup (id) {
|
|
16
|
-
return await db.selectFrom('
|
|
16
|
+
return await db.selectFrom('p2p_groups').selectAll().where('id', '=', id).executeTakeFirst();
|
|
17
17
|
},
|
|
18
18
|
async updateGroup (id, update) {
|
|
19
19
|
const values = {};
|
|
@@ -22,7 +22,7 @@ export function createGroupOps(db, adapter) {
|
|
|
22
22
|
if (update.hlc != null) values.hlc = update.hlc;
|
|
23
23
|
const changed = Object.entries(values);
|
|
24
24
|
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
25
|
-
let query = db.updateTable('
|
|
25
|
+
let query = db.updateTable('p2p_groups').set(values).where('id', '=', id);
|
|
26
26
|
// Rewriting the values a row already holds is not a change: a reproject
|
|
27
27
|
// that agrees with the stored row leaves it, `updated_at` included, alone.
|
|
28
28
|
if (changed.length > 0) {
|
|
@@ -31,25 +31,25 @@ export function createGroupOps(db, adapter) {
|
|
|
31
31
|
await query.execute();
|
|
32
32
|
},
|
|
33
33
|
async markGroupProjectionsDirty (id, token) {
|
|
34
|
-
await db.updateTable('
|
|
34
|
+
await db.updateTable('p2p_groups').set({
|
|
35
35
|
projections_dirty: token
|
|
36
36
|
}).where('id', '=', id).execute();
|
|
37
37
|
},
|
|
38
38
|
async clearGroupProjectionsDirty (id, token, epoch) {
|
|
39
|
-
const result = await db.updateTable('
|
|
39
|
+
const result = await db.updateTable('p2p_groups').set({
|
|
40
40
|
projections_dirty: null,
|
|
41
41
|
projections_epoch: epoch
|
|
42
42
|
}).where('id', '=', id).where('projections_dirty', token == null ? 'is' : '=', token).executeTakeFirst();
|
|
43
43
|
return (result.numUpdatedRows ?? 0n) > 0n;
|
|
44
44
|
},
|
|
45
45
|
async getGroupLeaveRequestHLC (groupID, memberDID) {
|
|
46
|
-
const row = await db.selectFrom('
|
|
46
|
+
const row = await db.selectFrom('p2p_group_leave_requests').select('hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
47
47
|
return row?.hlc;
|
|
48
48
|
},
|
|
49
49
|
async swapGroupLeaveRequestHLC (params) {
|
|
50
50
|
const memberDID = normalizeDID(params.memberDID);
|
|
51
51
|
if (params.expected == null) {
|
|
52
|
-
const inserted = await db.insertInto('
|
|
52
|
+
const inserted = await db.insertInto('p2p_group_leave_requests').values({
|
|
53
53
|
group_id: params.groupID,
|
|
54
54
|
member_did: memberDID,
|
|
55
55
|
hlc: params.hlc
|
|
@@ -59,7 +59,7 @@ export function createGroupOps(db, adapter) {
|
|
|
59
59
|
]).doNothing()).executeTakeFirst();
|
|
60
60
|
return (inserted.numInsertedOrUpdatedRows ?? 0n) > 0n;
|
|
61
61
|
}
|
|
62
|
-
const updated = await db.updateTable('
|
|
62
|
+
const updated = await db.updateTable('p2p_group_leave_requests').set({
|
|
63
63
|
hlc: params.hlc
|
|
64
64
|
}).where('group_id', '=', params.groupID).where('member_did', '=', memberDID).where('hlc', '=', params.expected).executeTakeFirst();
|
|
65
65
|
return (updated.numUpdatedRows ?? 0n) > 0n;
|
|
@@ -67,24 +67,23 @@ export function createGroupOps(db, adapter) {
|
|
|
67
67
|
async deleteGroup (id) {
|
|
68
68
|
const remove = async (trx)=>{
|
|
69
69
|
await clearAppOutboxRows(trx, id);
|
|
70
|
-
await trx.deleteFrom('
|
|
71
|
-
await trx.deleteFrom('
|
|
72
|
-
await trx.deleteFrom('
|
|
73
|
-
await trx.deleteFrom('
|
|
70
|
+
await trx.deleteFrom('p2p_log_handler_completion').where('group_id', '=', id).execute();
|
|
71
|
+
await trx.deleteFrom('p2p_catalog_offers').where('group_id', '=', id).execute();
|
|
72
|
+
await trx.deleteFrom('p2p_catalog_routes').where('group_id', '=', id).execute();
|
|
73
|
+
await trx.deleteFrom('p2p_groups').where('id', '=', id).execute();
|
|
74
74
|
};
|
|
75
75
|
// A caller already inside a provider transaction owns atomicity; opening
|
|
76
76
|
// a nested one is unsupported.
|
|
77
|
-
|
|
78
|
-
else await db.transaction().execute(remove);
|
|
77
|
+
await withAppOutboxLock(context, id, remove);
|
|
79
78
|
},
|
|
80
79
|
async listGroups () {
|
|
81
|
-
return await db.selectFrom('
|
|
80
|
+
return await db.selectFrom('p2p_groups').selectAll().where('status', '=', 'joined').execute();
|
|
82
81
|
},
|
|
83
82
|
async listLeavingGroups () {
|
|
84
|
-
return await db.selectFrom('
|
|
83
|
+
return await db.selectFrom('p2p_groups').selectAll().where('status', '=', 'leaving').execute();
|
|
85
84
|
},
|
|
86
85
|
async listAllGroups () {
|
|
87
|
-
return await db.selectFrom('
|
|
86
|
+
return await db.selectFrom('p2p_groups').selectAll().execute();
|
|
88
87
|
},
|
|
89
88
|
async setGroupStatus (id, status, options) {
|
|
90
89
|
const update = async (tx)=>{
|
|
@@ -94,19 +93,18 @@ export function createGroupOps(db, adapter) {
|
|
|
94
93
|
}
|
|
95
94
|
if (status === 'left') {
|
|
96
95
|
cleared = await clearAppOutboxRows(tx, id, options);
|
|
97
|
-
await tx.deleteFrom('
|
|
96
|
+
await tx.deleteFrom('p2p_log_handler_completion').where('group_id', '=', id).execute();
|
|
98
97
|
}
|
|
99
|
-
await tx.updateTable('
|
|
98
|
+
await tx.updateTable('p2p_groups').set({
|
|
100
99
|
status
|
|
101
100
|
}).where('id', '=', id).execute();
|
|
102
101
|
return cleared;
|
|
103
102
|
};
|
|
104
|
-
|
|
105
|
-
return await db.transaction().execute(update);
|
|
103
|
+
return await withAppOutboxLock(context, id, update);
|
|
106
104
|
},
|
|
107
105
|
async listGroupsOffsetPaged (params) {
|
|
108
106
|
const { limit, offset } = params;
|
|
109
|
-
let query = db.selectFrom('
|
|
107
|
+
let query = db.selectFrom('p2p_groups').selectAll().where('status', '=', 'joined').orderBy('name');
|
|
110
108
|
if (offset != null) {
|
|
111
109
|
query = query.offset(offset);
|
|
112
110
|
}
|
|
@@ -127,7 +125,7 @@ export function createGroupOps(db, adapter) {
|
|
|
127
125
|
},
|
|
128
126
|
// --- Group member operations ---
|
|
129
127
|
async addGroupMember (member) {
|
|
130
|
-
const row = await db.insertInto('
|
|
128
|
+
const row = await db.insertInto('p2p_group_members').values({
|
|
131
129
|
group_id: member.group_id,
|
|
132
130
|
member_did: normalizeDID(member.member_did),
|
|
133
131
|
role: member.role,
|
|
@@ -142,10 +140,10 @@ export function createGroupOps(db, adapter) {
|
|
|
142
140
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
143
141
|
})).where((eb)=>eb.or([
|
|
144
142
|
eb.and([
|
|
145
|
-
eb('
|
|
146
|
-
eb('
|
|
143
|
+
eb('p2p_group_members.removed_at_hlc', 'is', null),
|
|
144
|
+
eb('p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
|
|
147
145
|
]),
|
|
148
|
-
eb('
|
|
146
|
+
eb('p2p_group_members.removed_at_hlc', '<', eb.ref('excluded.hlc'))
|
|
149
147
|
]))).returning('member_did').executeTakeFirst();
|
|
150
148
|
return row != null;
|
|
151
149
|
},
|
|
@@ -153,7 +151,7 @@ export function createGroupOps(db, adapter) {
|
|
|
153
151
|
if (members.length === 0) {
|
|
154
152
|
return new Set();
|
|
155
153
|
}
|
|
156
|
-
const rows = await db.insertInto('
|
|
154
|
+
const rows = await db.insertInto('p2p_group_members').values(members.map((member)=>({
|
|
157
155
|
group_id: member.group_id,
|
|
158
156
|
member_did: normalizeDID(member.member_did),
|
|
159
157
|
role: member.role,
|
|
@@ -168,15 +166,15 @@ export function createGroupOps(db, adapter) {
|
|
|
168
166
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
169
167
|
})).where((eb)=>eb.or([
|
|
170
168
|
eb.and([
|
|
171
|
-
eb('
|
|
172
|
-
eb('
|
|
169
|
+
eb('p2p_group_members.removed_at_hlc', 'is', null),
|
|
170
|
+
eb('p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
|
|
173
171
|
]),
|
|
174
|
-
eb('
|
|
172
|
+
eb('p2p_group_members.removed_at_hlc', '<', eb.ref('excluded.hlc'))
|
|
175
173
|
]))).returning('member_did').execute();
|
|
176
174
|
return new Set(rows.map((row)=>row.member_did));
|
|
177
175
|
},
|
|
178
176
|
async markMemberRemoved (groupID, memberDID, hlc) {
|
|
179
|
-
const row = await db.updateTable('
|
|
177
|
+
const row = await db.updateTable('p2p_group_members').set({
|
|
180
178
|
removed_at_hlc: hlc,
|
|
181
179
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
182
180
|
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).where('hlc', '<', hlc).returning('member_did').executeTakeFirst();
|
|
@@ -186,7 +184,7 @@ export function createGroupOps(db, adapter) {
|
|
|
186
184
|
if (memberDIDs.length === 0) {
|
|
187
185
|
return new Set();
|
|
188
186
|
}
|
|
189
|
-
const rows = await db.updateTable('
|
|
187
|
+
const rows = await db.updateTable('p2p_group_members').set({
|
|
190
188
|
removed_at_hlc: hlc,
|
|
191
189
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
192
190
|
}).where('group_id', '=', groupID).where('member_did', 'in', memberDIDs.map(normalizeDID)).where('removed_at_hlc', 'is', null).where('hlc', '<', hlc).returning('member_did').execute();
|
|
@@ -194,14 +192,14 @@ export function createGroupOps(db, adapter) {
|
|
|
194
192
|
},
|
|
195
193
|
async listGroupMembershipStamps (groupID, memberDIDs) {
|
|
196
194
|
if (memberDIDs.length === 0) return [];
|
|
197
|
-
return await db.selectFrom('
|
|
195
|
+
return await db.selectFrom('p2p_group_members').select([
|
|
198
196
|
'member_did',
|
|
199
197
|
'hlc',
|
|
200
198
|
'removed_at_hlc'
|
|
201
199
|
]).where('group_id', '=', groupID).where('member_did', 'in', memberDIDs.map(normalizeDID)).execute();
|
|
202
200
|
},
|
|
203
201
|
async getMemberRemoval (groupID, memberDID) {
|
|
204
|
-
const row = await db.selectFrom('
|
|
202
|
+
const row = await db.selectFrom('p2p_group_members').select('removed_at_hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
|
|
205
203
|
return row == null ? undefined : {
|
|
206
204
|
removed_at_hlc: row.removed_at_hlc
|
|
207
205
|
};
|
|
@@ -213,7 +211,7 @@ export function createGroupOps(db, adapter) {
|
|
|
213
211
|
];
|
|
214
212
|
const memberDID = normalizeDID(params.memberDID);
|
|
215
213
|
for (const chunk of chunked(groupIDs)){
|
|
216
|
-
const rows = await db.selectFrom('
|
|
214
|
+
const rows = await db.selectFrom('p2p_group_members').select([
|
|
217
215
|
'group_id',
|
|
218
216
|
'removed_at_hlc'
|
|
219
217
|
]).where('group_id', 'in', chunk).where('member_did', '=', memberDID).execute();
|
|
@@ -227,14 +225,14 @@ export function createGroupOps(db, adapter) {
|
|
|
227
225
|
return await ops.markMemberRemoved(groupID, memberDID, hlc);
|
|
228
226
|
},
|
|
229
227
|
async getGroupMember (groupID, memberDID) {
|
|
230
|
-
return await db.selectFrom('
|
|
228
|
+
return await db.selectFrom('p2p_group_members').selectAll().where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
231
229
|
},
|
|
232
230
|
async isGroupMember (groupID, memberDID) {
|
|
233
|
-
const row = await db.selectFrom('
|
|
231
|
+
const row = await db.selectFrom('p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
|
|
234
232
|
return row != null;
|
|
235
233
|
},
|
|
236
234
|
async isGroupMemberAtHLC (groupID, did, atHLC) {
|
|
237
|
-
let query = db.selectFrom('
|
|
235
|
+
let query = db.selectFrom('p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did));
|
|
238
236
|
if (atHLC == null) {
|
|
239
237
|
query = query.where('removed_at_hlc', 'is', null);
|
|
240
238
|
} else {
|
|
@@ -247,7 +245,7 @@ export function createGroupOps(db, adapter) {
|
|
|
247
245
|
return row != null;
|
|
248
246
|
},
|
|
249
247
|
async isGroupAdmin (groupID, did, atHLC) {
|
|
250
|
-
let query = db.selectFrom('
|
|
248
|
+
let query = db.selectFrom('p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did)).where('role', '=', 'admin');
|
|
251
249
|
if (atHLC == null) {
|
|
252
250
|
query = query.where('removed_at_hlc', 'is', null);
|
|
253
251
|
} else {
|
|
@@ -266,7 +264,7 @@ export function createGroupOps(db, adapter) {
|
|
|
266
264
|
// Update-only by design: a missing row leaves the table untouched, so a
|
|
267
265
|
// role grant can never conjure a phantom member. hlc/removed_at_hlc are
|
|
268
266
|
// left alone to keep membership LWW and tombstones intact.
|
|
269
|
-
await db.updateTable('
|
|
267
|
+
await db.updateTable('p2p_group_members').set({
|
|
270
268
|
role,
|
|
271
269
|
updated_at: adapter.encodeTimestamp(new Date())
|
|
272
270
|
}).where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).execute();
|
|
@@ -291,7 +289,7 @@ export function createGroupOps(db, adapter) {
|
|
|
291
289
|
}
|
|
292
290
|
const now = adapter.encodeTimestamp(new Date());
|
|
293
291
|
for (const [role, dids] of didsByRole){
|
|
294
|
-
await db.updateTable('
|
|
292
|
+
await db.updateTable('p2p_group_members').set({
|
|
295
293
|
role,
|
|
296
294
|
updated_at: now
|
|
297
295
|
}).where('group_id', '=', groupID).where('member_did', 'in', dids).execute();
|
|
@@ -299,22 +297,22 @@ export function createGroupOps(db, adapter) {
|
|
|
299
297
|
},
|
|
300
298
|
async isMemberOfAnyGroup (viewerDID, groupIDs) {
|
|
301
299
|
if (groupIDs.length === 0) return false;
|
|
302
|
-
const row = await db.selectFrom('
|
|
300
|
+
const row = await db.selectFrom('p2p_group_members').select('group_id').where('member_did', '=', normalizeDID(viewerDID)).where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).limit(1).executeTakeFirst();
|
|
303
301
|
return row != null;
|
|
304
302
|
},
|
|
305
303
|
// Same predicate as `isGroupMember`, projecting the matching group ids rather
|
|
306
|
-
// than collapsing to one existence answer. No join to `
|
|
304
|
+
// than collapsing to one existence answer. No join to `p2p_groups` —
|
|
307
305
|
// membership stands on its own row.
|
|
308
306
|
async listMemberGroupIDs (memberDID, groupIDs) {
|
|
309
307
|
if (groupIDs.length === 0) return [];
|
|
310
|
-
const rows = await db.selectFrom('
|
|
308
|
+
const rows = await db.selectFrom('p2p_group_members').select('group_id').where('member_did', '=', normalizeDID(memberDID)).where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).execute();
|
|
311
309
|
return rows.map((row)=>row.group_id);
|
|
312
310
|
},
|
|
313
311
|
async listGroupMembers (groupID) {
|
|
314
|
-
return await db.selectFrom('
|
|
312
|
+
return await db.selectFrom('p2p_group_members').selectAll().where('group_id', '=', groupID).where('removed_at_hlc', 'is', null).execute();
|
|
315
313
|
},
|
|
316
314
|
async getGroupsForMember (memberDID) {
|
|
317
|
-
return await db.selectFrom('
|
|
315
|
+
return await db.selectFrom('p2p_group_members').innerJoin('p2p_groups', 'p2p_groups.id', 'p2p_group_members.group_id').selectAll('p2p_groups').where('p2p_group_members.member_did', '=', normalizeDID(memberDID)).where('p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
318
316
|
},
|
|
319
317
|
// Result Map is keyed by the normalized stored `member_did`; callers must
|
|
320
318
|
// normalize before looking up by key (existing callers iterate `.values()`).
|
|
@@ -322,20 +320,20 @@ export function createGroupOps(db, adapter) {
|
|
|
322
320
|
const result = new Map();
|
|
323
321
|
if (memberDIDs.length === 0) return result;
|
|
324
322
|
const normalizedDIDs = memberDIDs.map(normalizeDID);
|
|
325
|
-
const rows = await db.selectFrom('
|
|
326
|
-
'
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
'
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
]).where('
|
|
323
|
+
const rows = await db.selectFrom('p2p_group_members').innerJoin('p2p_groups', 'p2p_groups.id', 'p2p_group_members.group_id').select([
|
|
324
|
+
'p2p_group_members.member_did as member_did',
|
|
325
|
+
'p2p_groups.id as id',
|
|
326
|
+
'p2p_groups.name as name',
|
|
327
|
+
'p2p_groups.description as description',
|
|
328
|
+
'p2p_groups.created_by as created_by',
|
|
329
|
+
'p2p_groups.status as status',
|
|
330
|
+
'p2p_groups.kind as kind',
|
|
331
|
+
'p2p_groups.projections_dirty as projections_dirty',
|
|
332
|
+
'p2p_groups.projections_epoch as projections_epoch',
|
|
333
|
+
'p2p_groups.hlc as hlc',
|
|
334
|
+
'p2p_groups.created_at as created_at',
|
|
335
|
+
'p2p_groups.updated_at as updated_at'
|
|
336
|
+
]).where('p2p_group_members.member_did', 'in', normalizedDIDs).where('p2p_group_members.removed_at_hlc', 'is', null).execute();
|
|
339
337
|
for (const row of rows){
|
|
340
338
|
const { member_did, ...group } = row;
|
|
341
339
|
const list = result.get(member_did);
|
package/lib/ops/hubs.js
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
|
+
import { withStoreTransaction } from '@kubun/db';
|
|
1
2
|
import { sql } from 'kysely';
|
|
2
3
|
import { canonicalHubURL } from '../hub-url.js';
|
|
3
4
|
import { COMMIT_HUB_UNIQUE_INDEX } from '../migrations.js';
|
|
4
|
-
/**
|
|
5
|
-
* Run `write` as a single atomic unit. The store API is built on whatever Kysely
|
|
6
|
-
* instance the provider hands it, which is a transaction-scoped one whenever the
|
|
7
|
-
* caller runs inside an ambient write transaction. Kysely rejects a nested
|
|
8
|
-
* `transaction()` call, and the ambient transaction already provides the
|
|
9
|
-
* atomicity, so in that case the statements run inline; otherwise a transaction
|
|
10
|
-
* is opened around them.
|
|
11
|
-
*
|
|
12
|
-
* Multi-statement invariants (clear-then-set, read-then-insert) must go through
|
|
13
|
-
* this: a crash between two independent statements can leave a group with no
|
|
14
|
-
* commit hub at all, and a device with no commit hub cannot commit.
|
|
15
|
-
*/ async function writeAtomically(db, write) {
|
|
16
|
-
if (db.isTransaction) {
|
|
17
|
-
await write(db);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
await db.transaction().execute(async (tx)=>{
|
|
21
|
-
await write(tx);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
5
|
/**
|
|
25
6
|
* Run `run` so that a failed statement inside it can be caught and recovered
|
|
26
7
|
* from. Postgres aborts the WHOLE transaction on any statement error, so a
|
|
@@ -60,13 +41,13 @@ import { COMMIT_HUB_UNIQUE_INDEX } from '../migrations.js';
|
|
|
60
41
|
* row was actually inserted.
|
|
61
42
|
*/ async function bindGroupHub(db, binding, claimDesignation) {
|
|
62
43
|
let inserted = false;
|
|
63
|
-
await
|
|
44
|
+
await withStoreTransaction(db, async (tx)=>{
|
|
64
45
|
let designate = false;
|
|
65
46
|
if (claimDesignation) {
|
|
66
|
-
const existing = await tx.selectFrom('
|
|
47
|
+
const existing = await tx.selectFrom('p2p_group_hubs').select('hub_id').where('group_id', '=', binding.group_id).where('is_commit_hub', '=', 1).executeTakeFirst();
|
|
67
48
|
designate = existing == null;
|
|
68
49
|
}
|
|
69
|
-
const result = await tx.insertInto('
|
|
50
|
+
const result = await tx.insertInto('p2p_group_hubs').values({
|
|
70
51
|
group_id: binding.group_id,
|
|
71
52
|
hub_id: binding.hub_id,
|
|
72
53
|
is_commit_hub: designate ? 1 : 0
|
|
@@ -81,7 +62,7 @@ import { COMMIT_HUB_UNIQUE_INDEX } from '../migrations.js';
|
|
|
81
62
|
export function createHubOps(db, adapter) {
|
|
82
63
|
return {
|
|
83
64
|
async insertHub (hub) {
|
|
84
|
-
await db.insertInto('
|
|
65
|
+
await db.insertInto('p2p_hubs').values({
|
|
85
66
|
id: hub.id,
|
|
86
67
|
label: hub.label,
|
|
87
68
|
url: canonicalHubURL(hub.url),
|
|
@@ -89,10 +70,10 @@ export function createHubOps(db, adapter) {
|
|
|
89
70
|
}).execute();
|
|
90
71
|
},
|
|
91
72
|
async getHub (id) {
|
|
92
|
-
return await db.selectFrom('
|
|
73
|
+
return await db.selectFrom('p2p_hubs').selectAll().where('id', '=', id).executeTakeFirst();
|
|
93
74
|
},
|
|
94
75
|
async getHubByURL (url) {
|
|
95
|
-
return await db.selectFrom('
|
|
76
|
+
return await db.selectFrom('p2p_hubs').selectAll().where('url', '=', canonicalHubURL(url)).executeTakeFirst();
|
|
96
77
|
},
|
|
97
78
|
async updateHub (id, update) {
|
|
98
79
|
const values = {};
|
|
@@ -100,13 +81,13 @@ export function createHubOps(db, adapter) {
|
|
|
100
81
|
if (update.url !== undefined) values.url = canonicalHubURL(update.url);
|
|
101
82
|
if (update.server_did !== undefined) values.server_did = update.server_did;
|
|
102
83
|
values.updated_at = adapter.encodeTimestamp(new Date());
|
|
103
|
-
await db.updateTable('
|
|
84
|
+
await db.updateTable('p2p_hubs').set(values).where('id', '=', id).execute();
|
|
104
85
|
},
|
|
105
86
|
async deleteHub (id) {
|
|
106
|
-
await db.deleteFrom('
|
|
87
|
+
await db.deleteFrom('p2p_hubs').where('id', '=', id).execute();
|
|
107
88
|
},
|
|
108
89
|
async listHubs () {
|
|
109
|
-
return await db.selectFrom('
|
|
90
|
+
return await db.selectFrom('p2p_hubs').selectAll().execute();
|
|
110
91
|
},
|
|
111
92
|
// --- Group-hub binding operations ---
|
|
112
93
|
async insertGroupHub (binding) {
|
|
@@ -132,55 +113,55 @@ export function createHubOps(db, adapter) {
|
|
|
132
113
|
}
|
|
133
114
|
},
|
|
134
115
|
async deleteGroupHub (params) {
|
|
135
|
-
const result = await db.deleteFrom('
|
|
116
|
+
const result = await db.deleteFrom('p2p_group_hubs').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
|
|
136
117
|
return (result.numDeletedRows ?? 0n) > 0n;
|
|
137
118
|
},
|
|
138
119
|
async listGroupIDsByHubID (hubID) {
|
|
139
|
-
const rows = await db.selectFrom('
|
|
120
|
+
const rows = await db.selectFrom('p2p_group_hubs').select('group_id').where('hub_id', '=', hubID).execute();
|
|
140
121
|
return rows.map((r)=>r.group_id);
|
|
141
122
|
},
|
|
142
123
|
async listHubsByGroupID (groupID) {
|
|
143
|
-
return await db.selectFrom('
|
|
124
|
+
return await db.selectFrom('p2p_group_hubs').innerJoin('p2p_hubs', 'p2p_hubs.id', 'p2p_group_hubs.hub_id').selectAll('p2p_hubs').where('p2p_group_hubs.group_id', '=', groupID).execute();
|
|
144
125
|
},
|
|
145
126
|
async listGroupsByHubID (hubID) {
|
|
146
|
-
const rows = await db.selectFrom('
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
]).where('
|
|
127
|
+
const rows = await db.selectFrom('p2p_group_hubs').innerJoin('p2p_groups', 'p2p_groups.id', 'p2p_group_hubs.group_id').select([
|
|
128
|
+
'p2p_groups.id as id',
|
|
129
|
+
'p2p_groups.name as name',
|
|
130
|
+
'p2p_groups.description as description',
|
|
131
|
+
'p2p_groups.created_by as created_by',
|
|
132
|
+
'p2p_groups.status as status',
|
|
133
|
+
'p2p_groups.kind as kind',
|
|
134
|
+
'p2p_groups.projections_dirty as projections_dirty',
|
|
135
|
+
'p2p_groups.projections_epoch as projections_epoch',
|
|
136
|
+
'p2p_groups.hlc as hlc',
|
|
137
|
+
'p2p_groups.created_at as created_at',
|
|
138
|
+
'p2p_groups.updated_at as updated_at'
|
|
139
|
+
]).where('p2p_group_hubs.hub_id', '=', hubID).execute();
|
|
159
140
|
return rows;
|
|
160
141
|
},
|
|
161
142
|
async deleteGroupHubsByHubID (hubID) {
|
|
162
|
-
await db.deleteFrom('
|
|
143
|
+
await db.deleteFrom('p2p_group_hubs').where('hub_id', '=', hubID).execute();
|
|
163
144
|
},
|
|
164
145
|
async getCommitHub (groupID) {
|
|
165
|
-
return await db.selectFrom('
|
|
146
|
+
return await db.selectFrom('p2p_group_hubs').innerJoin('p2p_hubs', 'p2p_hubs.id', 'p2p_group_hubs.hub_id').selectAll('p2p_hubs').where('p2p_group_hubs.group_id', '=', groupID).where('p2p_group_hubs.is_commit_hub', '=', 1).executeTakeFirst();
|
|
166
147
|
},
|
|
167
148
|
async setCommitHub (params) {
|
|
168
149
|
// Clear then set so at most one bound hub per group carries the flag — the
|
|
169
150
|
// single-designation invariant a fork-free commit log depends on. Both
|
|
170
151
|
// statements run as one unit: a crash after the clear would leave the group
|
|
171
152
|
// with no commit hub, and a device with none cannot commit.
|
|
172
|
-
await
|
|
153
|
+
await withStoreTransaction(db, async (tx)=>{
|
|
173
154
|
// The hub must already be bound. Without this check an unbound hubID
|
|
174
155
|
// clears every flag and sets none — silently leaving the group unable to
|
|
175
156
|
// commit, which reads as a hang rather than an error.
|
|
176
|
-
const bound = await tx.selectFrom('
|
|
157
|
+
const bound = await tx.selectFrom('p2p_group_hubs').select('hub_id').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
|
|
177
158
|
if (bound == null) {
|
|
178
159
|
throw new Error(`Cannot designate commit hub ${params.hubID} for group ${params.groupID}: the hub is not bound to the group`);
|
|
179
160
|
}
|
|
180
|
-
await tx.updateTable('
|
|
161
|
+
await tx.updateTable('p2p_group_hubs').set({
|
|
181
162
|
is_commit_hub: 0
|
|
182
163
|
}).where('group_id', '=', params.groupID).execute();
|
|
183
|
-
await tx.updateTable('
|
|
164
|
+
await tx.updateTable('p2p_group_hubs').set({
|
|
184
165
|
is_commit_hub: 1
|
|
185
166
|
}).where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).execute();
|
|
186
167
|
});
|
|
@@ -192,31 +173,31 @@ export function createHubOps(db, adapter) {
|
|
|
192
173
|
}
|
|
193
174
|
// Match and reflag as one unit: a crash between the clear and the set
|
|
194
175
|
// would leave the group with no commit hub at all.
|
|
195
|
-
await
|
|
176
|
+
await withStoreTransaction(db, async (tx)=>{
|
|
196
177
|
// The bound hub whose URL matches the designation, if this device has
|
|
197
178
|
// bound it. The join is what makes "bound to THIS group with that URL"
|
|
198
179
|
// the match, rather than any hub row that happens to carry the URL. When
|
|
199
180
|
// both sides carry a server DID it must match too: the same URL served
|
|
200
181
|
// by a different server DID is a different hub identity, and flagging it
|
|
201
182
|
// would designate a peer the group never chose.
|
|
202
|
-
let matchQuery = tx.selectFrom('
|
|
183
|
+
let matchQuery = tx.selectFrom('p2p_group_hubs').innerJoin('p2p_hubs', 'p2p_hubs.id', 'p2p_group_hubs.hub_id').select('p2p_group_hubs.hub_id as hub_id').where('p2p_group_hubs.group_id', '=', groupID).where('p2p_hubs.url', '=', canonicalHubURL(commitHub.url));
|
|
203
184
|
if (commitHub.serverDID != null) {
|
|
204
185
|
// A bound row with no recorded server DID stays eligible — it has not
|
|
205
186
|
// claimed a conflicting identity, only an unknown one.
|
|
206
187
|
matchQuery = matchQuery.where((eb)=>eb.or([
|
|
207
|
-
eb('
|
|
208
|
-
eb('
|
|
188
|
+
eb('p2p_hubs.server_did', '=', commitHub.serverDID),
|
|
189
|
+
eb('p2p_hubs.server_did', 'is', null)
|
|
209
190
|
]));
|
|
210
191
|
}
|
|
211
192
|
const match = await matchQuery.executeTakeFirst();
|
|
212
193
|
// Clear every row first so at most one carries the flag — and so a
|
|
213
194
|
// designated-but-unbound hub leaves the device with NO flagged row rather
|
|
214
195
|
// than a stale non-designated one (that fallback is the fork).
|
|
215
|
-
await tx.updateTable('
|
|
196
|
+
await tx.updateTable('p2p_group_hubs').set({
|
|
216
197
|
is_commit_hub: 0
|
|
217
198
|
}).where('group_id', '=', groupID).execute();
|
|
218
199
|
if (match != null) {
|
|
219
|
-
await tx.updateTable('
|
|
200
|
+
await tx.updateTable('p2p_group_hubs').set({
|
|
220
201
|
is_commit_hub: 1
|
|
221
202
|
}).where('group_id', '=', groupID).where('hub_id', '=', match.hub_id).execute();
|
|
222
203
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function createLogHandlerCompletionOps(db) {
|
|
2
2
|
return {
|
|
3
3
|
async beginLogHandlerCompletion ({ key, notify }) {
|
|
4
|
-
const row = await db.insertInto('
|
|
4
|
+
const row = await db.insertInto('p2p_log_handler_completion').values({
|
|
5
5
|
group_id: key.groupID,
|
|
6
6
|
procedure: key.procedure,
|
|
7
7
|
entity: key.entity,
|
|
@@ -18,11 +18,11 @@ export function createLogHandlerCompletionOps(db) {
|
|
|
18
18
|
return row.notify === 1;
|
|
19
19
|
},
|
|
20
20
|
async hasLogHandlerCompleted (key) {
|
|
21
|
-
const row = await db.selectFrom('
|
|
21
|
+
const row = await db.selectFrom('p2p_log_handler_completion').select('hlc').where('group_id', '=', key.groupID).where('procedure', '=', key.procedure).where('entity', '=', key.entity).where('hlc', '=', key.hlc).where('completed', '=', 1).executeTakeFirst();
|
|
22
22
|
return row != null;
|
|
23
23
|
},
|
|
24
24
|
async markLogHandlerCompleted (key) {
|
|
25
|
-
await db.insertInto('
|
|
25
|
+
await db.insertInto('p2p_log_handler_completion').values({
|
|
26
26
|
group_id: key.groupID,
|
|
27
27
|
procedure: key.procedure,
|
|
28
28
|
entity: key.entity,
|
package/lib/ops/mls.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { GroupMLSState, InsertGroupMLSState, P2PStoreTables } from '../tables.js';
|
|
4
4
|
export type ReserveSenderGenerationsParams = {
|