@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/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('kubun_p2p_groups').values({
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('kubun_p2p_groups').selectAll().where('id', '=', id).executeTakeFirst();
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('kubun_p2p_groups').set(values).where('id', '=', id);
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('kubun_p2p_groups').set({
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('kubun_p2p_groups').set({
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('kubun_p2p_group_leave_requests').select('hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
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('kubun_p2p_group_leave_requests').values({
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('kubun_p2p_group_leave_requests').set({
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('kubun_p2p_log_handler_completion').where('group_id', '=', id).execute();
71
- await trx.deleteFrom('kubun_p2p_catalog_offers').where('group_id', '=', id).execute();
72
- await trx.deleteFrom('kubun_p2p_catalog_routes').where('group_id', '=', id).execute();
73
- await trx.deleteFrom('kubun_p2p_groups').where('id', '=', id).execute();
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
- if (db.isTransaction) await remove(db);
78
- else await db.transaction().execute(remove);
77
+ await withAppOutboxLock(context, id, remove);
79
78
  },
80
79
  async listGroups () {
81
- return await db.selectFrom('kubun_p2p_groups').selectAll().where('status', '=', 'joined').execute();
80
+ return await db.selectFrom('p2p_groups').selectAll().where('status', '=', 'joined').execute();
82
81
  },
83
82
  async listLeavingGroups () {
84
- return await db.selectFrom('kubun_p2p_groups').selectAll().where('status', '=', 'leaving').execute();
83
+ return await db.selectFrom('p2p_groups').selectAll().where('status', '=', 'leaving').execute();
85
84
  },
86
85
  async listAllGroups () {
87
- return await db.selectFrom('kubun_p2p_groups').selectAll().execute();
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('kubun_p2p_log_handler_completion').where('group_id', '=', id).execute();
96
+ await tx.deleteFrom('p2p_log_handler_completion').where('group_id', '=', id).execute();
98
97
  }
99
- await tx.updateTable('kubun_p2p_groups').set({
98
+ await tx.updateTable('p2p_groups').set({
100
99
  status
101
100
  }).where('id', '=', id).execute();
102
101
  return cleared;
103
102
  };
104
- if (db.isTransaction) return await update(db);
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('kubun_p2p_groups').selectAll().where('status', '=', 'joined').orderBy('name');
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('kubun_p2p_group_members').values({
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('kubun_p2p_group_members.removed_at_hlc', 'is', null),
146
- eb('kubun_p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
143
+ eb('p2p_group_members.removed_at_hlc', 'is', null),
144
+ eb('p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
147
145
  ]),
148
- eb('kubun_p2p_group_members.removed_at_hlc', '<', eb.ref('excluded.hlc'))
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('kubun_p2p_group_members').values(members.map((member)=>({
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('kubun_p2p_group_members.removed_at_hlc', 'is', null),
172
- eb('kubun_p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
169
+ eb('p2p_group_members.removed_at_hlc', 'is', null),
170
+ eb('p2p_group_members.hlc', '<', eb.ref('excluded.hlc'))
173
171
  ]),
174
- eb('kubun_p2p_group_members.removed_at_hlc', '<', eb.ref('excluded.hlc'))
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('kubun_p2p_group_members').set({
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('kubun_p2p_group_members').set({
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('kubun_p2p_group_members').select([
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('kubun_p2p_group_members').select('removed_at_hlc').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).executeTakeFirst();
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('kubun_p2p_group_members').select([
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('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
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('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(memberDID)).where('removed_at_hlc', 'is', null).executeTakeFirst();
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('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did));
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('kubun_p2p_group_members').select('member_did').where('group_id', '=', groupID).where('member_did', '=', normalizeDID(did)).where('role', '=', 'admin');
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('kubun_p2p_group_members').set({
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('kubun_p2p_group_members').set({
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('kubun_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();
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 `kubun_p2p_groups` —
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('kubun_p2p_group_members').select('group_id').where('member_did', '=', normalizeDID(memberDID)).where('group_id', 'in', groupIDs).where('removed_at_hlc', 'is', null).execute();
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('kubun_p2p_group_members').selectAll().where('group_id', '=', groupID).where('removed_at_hlc', 'is', null).execute();
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('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').selectAll('kubun_p2p_groups').where('kubun_p2p_group_members.member_did', '=', normalizeDID(memberDID)).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
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('kubun_p2p_group_members').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_members.group_id').select([
326
- 'kubun_p2p_group_members.member_did as member_did',
327
- 'kubun_p2p_groups.id as id',
328
- 'kubun_p2p_groups.name as name',
329
- 'kubun_p2p_groups.description as description',
330
- 'kubun_p2p_groups.created_by as created_by',
331
- 'kubun_p2p_groups.status as status',
332
- 'kubun_p2p_groups.kind as kind',
333
- 'kubun_p2p_groups.projections_dirty as projections_dirty',
334
- 'kubun_p2p_groups.projections_epoch as projections_epoch',
335
- 'kubun_p2p_groups.hlc as hlc',
336
- 'kubun_p2p_groups.created_at as created_at',
337
- 'kubun_p2p_groups.updated_at as updated_at'
338
- ]).where('kubun_p2p_group_members.member_did', 'in', normalizedDIDs).where('kubun_p2p_group_members.removed_at_hlc', 'is', null).execute();
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 writeAtomically(db, async (tx)=>{
44
+ await withStoreTransaction(db, async (tx)=>{
64
45
  let designate = false;
65
46
  if (claimDesignation) {
66
- const existing = await tx.selectFrom('kubun_p2p_group_hubs').select('hub_id').where('group_id', '=', binding.group_id).where('is_commit_hub', '=', 1).executeTakeFirst();
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('kubun_p2p_group_hubs').values({
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('kubun_p2p_hubs').values({
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('kubun_p2p_hubs').selectAll().where('id', '=', id).executeTakeFirst();
73
+ return await db.selectFrom('p2p_hubs').selectAll().where('id', '=', id).executeTakeFirst();
93
74
  },
94
75
  async getHubByURL (url) {
95
- return await db.selectFrom('kubun_p2p_hubs').selectAll().where('url', '=', canonicalHubURL(url)).executeTakeFirst();
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('kubun_p2p_hubs').set(values).where('id', '=', id).execute();
84
+ await db.updateTable('p2p_hubs').set(values).where('id', '=', id).execute();
104
85
  },
105
86
  async deleteHub (id) {
106
- await db.deleteFrom('kubun_p2p_hubs').where('id', '=', id).execute();
87
+ await db.deleteFrom('p2p_hubs').where('id', '=', id).execute();
107
88
  },
108
89
  async listHubs () {
109
- return await db.selectFrom('kubun_p2p_hubs').selectAll().execute();
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('kubun_p2p_group_hubs').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
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('kubun_p2p_group_hubs').select('group_id').where('hub_id', '=', hubID).execute();
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('kubun_p2p_group_hubs').innerJoin('kubun_p2p_hubs', 'kubun_p2p_hubs.id', 'kubun_p2p_group_hubs.hub_id').selectAll('kubun_p2p_hubs').where('kubun_p2p_group_hubs.group_id', '=', groupID).execute();
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('kubun_p2p_group_hubs').innerJoin('kubun_p2p_groups', 'kubun_p2p_groups.id', 'kubun_p2p_group_hubs.group_id').select([
147
- 'kubun_p2p_groups.id as id',
148
- 'kubun_p2p_groups.name as name',
149
- 'kubun_p2p_groups.description as description',
150
- 'kubun_p2p_groups.created_by as created_by',
151
- 'kubun_p2p_groups.status as status',
152
- 'kubun_p2p_groups.kind as kind',
153
- 'kubun_p2p_groups.projections_dirty as projections_dirty',
154
- 'kubun_p2p_groups.projections_epoch as projections_epoch',
155
- 'kubun_p2p_groups.hlc as hlc',
156
- 'kubun_p2p_groups.created_at as created_at',
157
- 'kubun_p2p_groups.updated_at as updated_at'
158
- ]).where('kubun_p2p_group_hubs.hub_id', '=', hubID).execute();
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('kubun_p2p_group_hubs').where('hub_id', '=', hubID).execute();
143
+ await db.deleteFrom('p2p_group_hubs').where('hub_id', '=', hubID).execute();
163
144
  },
164
145
  async getCommitHub (groupID) {
165
- return await db.selectFrom('kubun_p2p_group_hubs').innerJoin('kubun_p2p_hubs', 'kubun_p2p_hubs.id', 'kubun_p2p_group_hubs.hub_id').selectAll('kubun_p2p_hubs').where('kubun_p2p_group_hubs.group_id', '=', groupID).where('kubun_p2p_group_hubs.is_commit_hub', '=', 1).executeTakeFirst();
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 writeAtomically(db, async (tx)=>{
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('kubun_p2p_group_hubs').select('hub_id').where('group_id', '=', params.groupID).where('hub_id', '=', params.hubID).executeTakeFirst();
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('kubun_p2p_group_hubs').set({
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('kubun_p2p_group_hubs').set({
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 writeAtomically(db, async (tx)=>{
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('kubun_p2p_group_hubs').innerJoin('kubun_p2p_hubs', 'kubun_p2p_hubs.id', 'kubun_p2p_group_hubs.hub_id').select('kubun_p2p_group_hubs.hub_id as hub_id').where('kubun_p2p_group_hubs.group_id', '=', groupID).where('kubun_p2p_hubs.url', '=', canonicalHubURL(commitHub.url));
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('kubun_p2p_hubs.server_did', '=', commitHub.serverDID),
208
- eb('kubun_p2p_hubs.server_did', 'is', null)
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('kubun_p2p_group_hubs').set({
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('kubun_p2p_group_hubs').set({
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('kubun_p2p_log_handler_completion').values({
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('kubun_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();
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('kubun_p2p_log_handler_completion').values({
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 { Adapter } from '@kubun/db-adapter';
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 = {