@kumiai/hub-store 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/lib/migrations.js +32 -0
- package/lib/store.d.ts +6 -3
- package/lib/store.js +45 -5
- package/lib/tables.d.ts +9 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ server with a custom transport or when you need the store API separately.
|
|
|
11
11
|
- `createHubStoreDefinition(options?)` -- the Hozon `StoreDefinition`, named `'kumiai-hub'`
|
|
12
12
|
(`HUB_STORE`).
|
|
13
13
|
- `getHubStore(provider)` -- the `SQLHubStore` from a `HozonDB` or any Hozon `StoreProvider`.
|
|
14
|
-
- `HubStoreOptions`, `HubSubscriptionReaper`, and `SQLHubStore` (
|
|
14
|
+
- `HubStoreOptions`, `HubSubscriptionReaper`, and `SQLHubStore` (the hub contract, subscription reaping, occupancy and persistent store identity).
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
17
|
import { HozonDB } from '@hozon/db'
|
|
@@ -56,3 +56,21 @@ Concurrent retries of one publish ID wait for the accepting transaction and retu
|
|
|
56
56
|
It returns exactly the deleted `{ topicID, subscriberDID }` pairs. A subscription refreshed at the boundary survives.
|
|
57
57
|
The delete and return happen atomically, so a refreshed subscription is neither removed nor reported.
|
|
58
58
|
This operation emits no event. `createHub` calls it after scheduled message purge when the store supports it.
|
|
59
|
+
|
|
60
|
+
## Attribution migration and occupancy
|
|
61
|
+
|
|
62
|
+
Migration `1-attribution` adds `payload_size`, nullable `account`, `actor` and `via` columns, plus the `hub_meta` table.
|
|
63
|
+
It backfills payload sizes and persists a generated `storeID` in `hub_meta`.
|
|
64
|
+
Existing message, retention and delivery semantics remain unchanged.
|
|
65
|
+
|
|
66
|
+
`publish({ ..., attribution? })` accepts `{ account, actor, via: 'direct' | 'delegated' | 'group' }`.
|
|
67
|
+
Without attribution, these columns remain null.
|
|
68
|
+
Legacy and unattributed rows fall back to `sender_did` for both account and actor, with `via: 'direct'`.
|
|
69
|
+
|
|
70
|
+
`store.occupancy()` returns retained payload bytes and frame counts grouped by account, actor and via.
|
|
71
|
+
Each physical frame counts once, regardless of recipient count.
|
|
72
|
+
Ack, trim, purge and unsubscribe affect occupancy through their existing frame deletion rules.
|
|
73
|
+
`store.storeID()` returns the persistent identifier shared by processes using the same store.
|
|
74
|
+
|
|
75
|
+
Publish results include `outcome` and `storedBytes`.
|
|
76
|
+
Only `outcome: 'inserted'` contributes payload bytes. Deduplicated and no-recipient publishes report zero stored bytes.
|
package/lib/migrations.js
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
|
+
import { sql } from '@hozon/db';
|
|
1
2
|
export function hubStoreMigrations(ctx) {
|
|
2
3
|
const t = ctx.types;
|
|
3
4
|
const p = ctx.tablePrefix;
|
|
4
5
|
return {
|
|
6
|
+
'1-attribution': {
|
|
7
|
+
async up (db) {
|
|
8
|
+
await db.schema.alterTable('hub_messages').addColumn('payload_size', 'integer', (col)=>col.notNull().defaultTo(0)).execute();
|
|
9
|
+
for (const column of [
|
|
10
|
+
'account',
|
|
11
|
+
'actor',
|
|
12
|
+
'via'
|
|
13
|
+
]){
|
|
14
|
+
await db.schema.alterTable('hub_messages').addColumn(column, t.text).execute();
|
|
15
|
+
}
|
|
16
|
+
await db.updateTable('hub_messages').set({
|
|
17
|
+
payload_size: sql`length(${sql.ref('payload')})`
|
|
18
|
+
}).execute();
|
|
19
|
+
await db.schema.createTable('hub_meta').addColumn('key', t.text, (col)=>col.notNull().primaryKey()).addColumn('value', t.text, (col)=>col.notNull()).execute();
|
|
20
|
+
await db.insertInto('hub_meta').values({
|
|
21
|
+
key: 'storeID',
|
|
22
|
+
value: crypto.randomUUID()
|
|
23
|
+
}).execute();
|
|
24
|
+
},
|
|
25
|
+
async down (db) {
|
|
26
|
+
await db.schema.dropTable('hub_meta').execute();
|
|
27
|
+
for (const column of [
|
|
28
|
+
'via',
|
|
29
|
+
'actor',
|
|
30
|
+
'account',
|
|
31
|
+
'payload_size'
|
|
32
|
+
]){
|
|
33
|
+
await db.schema.alterTable('hub_messages').dropColumn(column).execute();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
5
37
|
'0-init': {
|
|
6
38
|
async up (db) {
|
|
7
39
|
await db.schema.createTable('hub_messages').addColumn('sequence_id', t.text, (col)=>col.notNull().primaryKey()).addColumn('sender_did', t.text, (col)=>col.notNull()).addColumn('topic_id', t.text, (col)=>col.notNull()).addColumn('payload', t.binary, (col)=>col.notNull()).addColumn('stored_at', t.bigint, (col)=>col.notNull()).addColumn('retain', t.text, (col)=>col.notNull().defaultTo('mailbox')).execute();
|
package/lib/store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Adapter } from '@hozon/adapter';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { Kysely } from '@hozon/db';
|
|
3
|
+
import type { HubStore, OccupancyRow } from '@kumiai/hub-protocol';
|
|
4
4
|
import type { HubTables } from './tables.js';
|
|
5
5
|
export type HubStoreOptions = {
|
|
6
6
|
/** Hub default retention in seconds: the floor for a topic's age bound. Default 0. */
|
|
@@ -36,5 +36,8 @@ export type HubSubscriptionReaper = {
|
|
|
36
36
|
subscriberDID: string;
|
|
37
37
|
}>>;
|
|
38
38
|
};
|
|
39
|
-
export type SQLHubStore = HubStore & HubSubscriptionReaper
|
|
39
|
+
export type SQLHubStore = HubStore & HubSubscriptionReaper & {
|
|
40
|
+
occupancy(): Promise<Array<OccupancyRow>>;
|
|
41
|
+
storeID(): Promise<string>;
|
|
42
|
+
};
|
|
40
43
|
export declare function createHubStore(params: CreateHubStoreParams): SQLHubStore;
|
package/lib/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withStoreTransaction } from '@hozon/db';
|
|
1
|
+
import { sql, withStoreTransaction } from '@hozon/db';
|
|
2
2
|
import { HeadMismatchError, KeyPackageQuotaExceededError, NotSubscribedError, RetentionExceededError, SubscriptionQuotaExceededError } from '@kumiai/hub-protocol';
|
|
3
3
|
import { EventEmitter } from '@sozai/event';
|
|
4
4
|
/**
|
|
@@ -110,7 +110,9 @@ export function createHubStore(params) {
|
|
|
110
110
|
if (existing.sequence_id == null) throw new Error('Incomplete publish-id record');
|
|
111
111
|
return {
|
|
112
112
|
sequenceID: existing.sequence_id,
|
|
113
|
-
deduped: true
|
|
113
|
+
deduped: true,
|
|
114
|
+
outcome: 'deduplicated',
|
|
115
|
+
storedBytes: 0
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -128,12 +130,20 @@ export function createHubStore(params) {
|
|
|
128
130
|
const recipients = subscriberRows.map((r)=>r.subscriber_did).filter((did)=>did !== senderDID);
|
|
129
131
|
// 6. Append. A log frame is stored unconditionally (its reader may not exist yet). A mailbox
|
|
130
132
|
// frame with zero recipients is not stored, but the sequenceID is still consumed.
|
|
131
|
-
|
|
133
|
+
let storedBytes = 0;
|
|
134
|
+
const inserted = retain === 'log' || recipients.length > 0;
|
|
135
|
+
if (inserted) {
|
|
136
|
+
const payload = adapter.encodeBinary(publishParams.payload);
|
|
137
|
+
storedBytes = payload.byteLength;
|
|
132
138
|
await trx.insertInto('hub_messages').values({
|
|
133
139
|
sequence_id: sequenceID,
|
|
134
140
|
sender_did: senderDID,
|
|
135
141
|
topic_id: topicID,
|
|
136
|
-
payload
|
|
142
|
+
payload,
|
|
143
|
+
payload_size: storedBytes,
|
|
144
|
+
account: publishParams.attribution?.account ?? null,
|
|
145
|
+
actor: publishParams.attribution?.actor ?? null,
|
|
146
|
+
via: publishParams.attribution?.via ?? null,
|
|
137
147
|
stored_at: Date.now(),
|
|
138
148
|
retain
|
|
139
149
|
}).execute();
|
|
@@ -185,7 +195,9 @@ export function createHubStore(params) {
|
|
|
185
195
|
}
|
|
186
196
|
return {
|
|
187
197
|
sequenceID,
|
|
188
|
-
deduped: false
|
|
198
|
+
deduped: false,
|
|
199
|
+
outcome: inserted ? 'inserted' : 'no-recipients',
|
|
200
|
+
storedBytes
|
|
189
201
|
};
|
|
190
202
|
});
|
|
191
203
|
}
|
|
@@ -484,7 +496,35 @@ export function createHubStore(params) {
|
|
|
484
496
|
const rows = await db.selectFrom('hub_subscriptions').select('subscriber_did').where('topic_id', '=', topicID).execute();
|
|
485
497
|
return rows.map((r)=>r.subscriber_did);
|
|
486
498
|
}
|
|
499
|
+
async function occupancy() {
|
|
500
|
+
const rows = await db.selectFrom('hub_messages').select((eb)=>{
|
|
501
|
+
return [
|
|
502
|
+
eb.fn.coalesce('account', 'sender_did').as('account'),
|
|
503
|
+
eb.fn.coalesce('actor', 'sender_did').as('actor'),
|
|
504
|
+
eb.fn.coalesce(eb.ref('via'), sql`'direct'`).as('via'),
|
|
505
|
+
eb.fn.sum('payload_size').as('bytes'),
|
|
506
|
+
eb.fn.countAll().as('frames')
|
|
507
|
+
];
|
|
508
|
+
}).groupBy((eb)=>{
|
|
509
|
+
return [
|
|
510
|
+
eb.fn.coalesce('account', 'sender_did'),
|
|
511
|
+
eb.fn.coalesce('actor', 'sender_did'),
|
|
512
|
+
eb.fn.coalesce(eb.ref('via'), sql`'direct'`)
|
|
513
|
+
];
|
|
514
|
+
}).execute();
|
|
515
|
+
return rows.map((row)=>({
|
|
516
|
+
...row,
|
|
517
|
+
bytes: Number(row.bytes),
|
|
518
|
+
frames: Number(row.frames)
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
async function storeID() {
|
|
522
|
+
const row = await db.selectFrom('hub_meta').select('value').where('key', '=', 'storeID').executeTakeFirstOrThrow();
|
|
523
|
+
return row.value;
|
|
524
|
+
}
|
|
487
525
|
return {
|
|
526
|
+
occupancy,
|
|
527
|
+
storeID,
|
|
488
528
|
events,
|
|
489
529
|
publish,
|
|
490
530
|
fetch,
|
package/lib/tables.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { ColumnType } from '@hozon/db';
|
|
2
|
+
import type { HubAttribution } from '@kumiai/hub-protocol';
|
|
2
3
|
export type HubTables = {
|
|
4
|
+
hub_meta: {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
3
8
|
hub_messages: {
|
|
4
9
|
sequence_id: string;
|
|
5
10
|
sender_did: string;
|
|
6
11
|
topic_id: string;
|
|
12
|
+
payload_size: ColumnType<number, number | undefined, number>;
|
|
13
|
+
account: ColumnType<string | null, string | null | undefined, string | null>;
|
|
14
|
+
actor: ColumnType<string | null, string | null | undefined, string | null>;
|
|
15
|
+
via: ColumnType<HubAttribution['via'] | null, HubAttribution['via'] | null | undefined, HubAttribution['via'] | null>;
|
|
7
16
|
payload: Uint8Array;
|
|
8
17
|
stored_at: number;
|
|
9
18
|
retain: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-store",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "SQL hub store for Kumiai hubs, backed by Hozon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@hozon/adapter": "^0.3.0",
|
|
29
29
|
"@hozon/db": "^0.5.1",
|
|
30
|
-
"@kumiai/hub-protocol": "^0.13.
|
|
30
|
+
"@kumiai/hub-protocol": "^0.13.1",
|
|
31
31
|
"@sozai/event": "^0.1.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@enkaku/client": "^0.23.
|
|
35
|
-
"@enkaku/protocol": "^0.23.
|
|
36
|
-
"@enkaku/transport": "^0.23.
|
|
34
|
+
"@enkaku/client": "^0.23.1",
|
|
35
|
+
"@enkaku/protocol": "^0.23.1",
|
|
36
|
+
"@enkaku/transport": "^0.23.1",
|
|
37
37
|
"@hozon/node-sqlite": "^0.3.0",
|
|
38
38
|
"@hozon/postgres": "^0.2.1",
|
|
39
39
|
"@kokuin/token": "^0.7.0",
|
|
40
40
|
"@kumiai/hub-conformance": "^0.13.0",
|
|
41
|
-
"@kumiai/hub-server": "^0.13.
|
|
41
|
+
"@kumiai/hub-server": "^0.13.1",
|
|
42
42
|
"@testcontainers/postgresql": "^12.2.1"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|