@kumiai/hub-store 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -9
- package/lib/definition.d.ts +3 -4
- package/lib/index.d.ts +1 -1
- package/lib/store.d.ts +10 -1
- package/lib/store.js +29 -11
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ server with a custom transport or when you need the store API separately.
|
|
|
10
10
|
|
|
11
11
|
- `createHubStoreDefinition(options?)` -- the Hozon `StoreDefinition`, named `'kumiai-hub'`
|
|
12
12
|
(`HUB_STORE`).
|
|
13
|
-
- `getHubStore(provider)` -- the `
|
|
14
|
-
- `HubStoreOptions
|
|
13
|
+
- `getHubStore(provider)` -- the `SQLHubStore` from a `HozonDB` or any Hozon `StoreProvider`.
|
|
14
|
+
- `HubStoreOptions`, `HubSubscriptionReaper`, and `SQLHubStore` (`HubStore & HubSubscriptionReaper`).
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
17
|
import { HozonDB } from '@hozon/db'
|
|
@@ -28,13 +28,13 @@ definition. Close the `HozonDB` when the host shuts down.
|
|
|
28
28
|
|
|
29
29
|
## Options
|
|
30
30
|
|
|
31
|
-
| Option
|
|
32
|
-
|
|
33
|
-
| `defaultRetention`
|
|
34
|
-
| `maxRetention`
|
|
35
|
-
| `maxDepth`
|
|
36
|
-
| `maxKeyPackagesPerDID`
|
|
37
|
-
| `maxSubscriptionsPerDID` | `1000`
|
|
31
|
+
| Option | Default | Meaning |
|
|
32
|
+
| ------------------------ | ------------------- | -------------------------------------------------------------------------------- |
|
|
33
|
+
| `defaultRetention` | `0` | Floor, in seconds, on how long a topic's frames are kept. |
|
|
34
|
+
| `maxRetention` | `2592000` (30 days) | Ceiling, in seconds, a subscribe may request. Above it the subscribe is refused. |
|
|
35
|
+
| `maxDepth` | `1000` | Retained log frames per topic. The oldest log frames are evicted first. |
|
|
36
|
+
| `maxKeyPackagesPerDID` | `100` | Ordinary key packages one DID may hold. An upload past it is refused. |
|
|
37
|
+
| `maxSubscriptionsPerDID` | `1000` | Distinct topics one DID may subscribe to. |
|
|
38
38
|
|
|
39
39
|
Set `maxRetention` or `maxDepth` to `Infinity` to disable that bound explicitly.
|
|
40
40
|
These defaults match the memory store. Periodic purge applies the age policy.
|
|
@@ -49,3 +49,10 @@ head advance in one transaction. The head advance is a conditional write, so two
|
|
|
49
49
|
one Postgres database cannot both accept a publish at the same head.
|
|
50
50
|
|
|
51
51
|
Concurrent retries of one publish ID wait for the accepting transaction and return its sequence ID with `deduped: true`.
|
|
52
|
+
|
|
53
|
+
## Subscription reaping
|
|
54
|
+
|
|
55
|
+
`store.purgeSubscriptions({ olderThan })` removes subscriptions inactive for strictly more than `olderThan` seconds.
|
|
56
|
+
It returns exactly the deleted `{ topicID, subscriberDID }` pairs. A subscription refreshed at the boundary survives.
|
|
57
|
+
The delete and return happen atomically, so a refreshed subscription is neither removed nor reported.
|
|
58
|
+
This operation emits no event. `createHub` calls it after scheduled message purge when the store supports it.
|
package/lib/definition.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { StoreDefinition, StoreProvider } from '@hozon/db';
|
|
2
|
-
import type
|
|
3
|
-
import { type HubStoreOptions } from './store.js';
|
|
2
|
+
import { type HubStoreOptions, type SQLHubStore } from './store.js';
|
|
4
3
|
import type { HubTables } from './tables.js';
|
|
5
4
|
export declare const HUB_STORE = "kumiai-hub";
|
|
6
|
-
export declare function createHubStoreDefinition(options?: HubStoreOptions): StoreDefinition<HubTables,
|
|
7
|
-
export declare function getHubStore(provider: StoreProvider): Promise<
|
|
5
|
+
export declare function createHubStoreDefinition(options?: HubStoreOptions): StoreDefinition<HubTables, SQLHubStore>;
|
|
6
|
+
export declare function getHubStore(provider: StoreProvider): Promise<SQLHubStore>;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createHubStoreDefinition, getHubStore, HUB_STORE } from './definition.js';
|
|
2
|
-
export type { HubStoreOptions } from './store.js';
|
|
2
|
+
export type { HubStoreOptions, HubSubscriptionReaper, SQLHubStore } from './store.js';
|
package/lib/store.d.ts
CHANGED
|
@@ -28,4 +28,13 @@ export type CreateHubStoreParams = {
|
|
|
28
28
|
adapter: Adapter;
|
|
29
29
|
options?: HubStoreOptions;
|
|
30
30
|
};
|
|
31
|
-
export
|
|
31
|
+
export type HubSubscriptionReaper = {
|
|
32
|
+
purgeSubscriptions(params: {
|
|
33
|
+
olderThan: number;
|
|
34
|
+
}): Promise<Array<{
|
|
35
|
+
topicID: string;
|
|
36
|
+
subscriberDID: string;
|
|
37
|
+
}>>;
|
|
38
|
+
};
|
|
39
|
+
export type SQLHubStore = HubStore & HubSubscriptionReaper;
|
|
40
|
+
export declare function createHubStore(params: CreateHubStoreParams): SQLHubStore;
|
package/lib/store.js
CHANGED
|
@@ -62,8 +62,9 @@ export function createHubStore(params) {
|
|
|
62
62
|
// and only then evicts frames, so a sweep that deleted frames first and wrote the row after
|
|
63
63
|
// would take the same two locks in the opposite order and deadlock on Postgres.
|
|
64
64
|
async function lockTopic(trx, topicID) {
|
|
65
|
-
await trx.selectFrom('hub_topics').select('
|
|
66
|
-
.$if(adapter.kind === 'postgres', (qb)=>qb.forUpdate()).
|
|
65
|
+
const row = await trx.selectFrom('hub_topics').select('head').where('topic_id', '=', topicID)// SQLite has no row locks (and rejects the clause); its one writer already serialises.
|
|
66
|
+
.$if(adapter.kind === 'postgres', (qb)=>qb.forUpdate()).executeTakeFirst();
|
|
67
|
+
return row?.head ?? null;
|
|
67
68
|
}
|
|
68
69
|
// Mint the next sequenceID inside the accepting transaction, from a persistent global counter.
|
|
69
70
|
// Fixed-width zero-padded so lexicographic order matches numeric order (`"10" > "9"`).
|
|
@@ -331,7 +332,7 @@ export function createHubStore(params) {
|
|
|
331
332
|
const effective = Math.max(olderThan, subscriberRetention, defaultRetention);
|
|
332
333
|
const threshold = now - effective * 1000;
|
|
333
334
|
const ids = await withStoreTransaction(db, async (trx)=>{
|
|
334
|
-
await lockTopic(trx, topic_id);
|
|
335
|
+
const head = await lockTopic(trx, topic_id);
|
|
335
336
|
// Log removal is prefix-only by position. A clock that moved backwards can leave a due
|
|
336
337
|
// frame above one that is not; removing it would punch a hole a cursor cannot see.
|
|
337
338
|
const keptRow = await trx.selectFrom('hub_messages').select((eb)=>eb.fn.min('sequence_id').as('firstKept')).where('topic_id', '=', topic_id).where('retain', '=', 'log').where('stored_at', '>', threshold).executeTakeFirst();
|
|
@@ -340,15 +341,20 @@ export function createHubStore(params) {
|
|
|
340
341
|
'sequence_id',
|
|
341
342
|
'retain'
|
|
342
343
|
]).where('topic_id', '=', topic_id).where((eb)=>{
|
|
344
|
+
const mailbox = eb.and([
|
|
345
|
+
eb('retain', '=', 'mailbox'),
|
|
346
|
+
eb('stored_at', '<=', threshold)
|
|
347
|
+
]);
|
|
348
|
+
// A mailbox-only topic has no row to lock. Never admit its first concurrent log.
|
|
349
|
+
if (head == null) return mailbox;
|
|
350
|
+
const log = [
|
|
351
|
+
eb('retain', '=', 'log'),
|
|
352
|
+
eb('sequence_id', '<=', head)
|
|
353
|
+
];
|
|
354
|
+
if (firstKept != null) log.push(eb('sequence_id', '<', firstKept));
|
|
343
355
|
return eb.or([
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
eb('stored_at', '<=', threshold)
|
|
347
|
-
]),
|
|
348
|
-
firstKept == null ? eb('retain', '=', 'log') : eb.and([
|
|
349
|
-
eb('retain', '=', 'log'),
|
|
350
|
-
eb('sequence_id', '<', firstKept)
|
|
351
|
-
])
|
|
356
|
+
mailbox,
|
|
357
|
+
eb.and(log)
|
|
352
358
|
]);
|
|
353
359
|
}).orderBy('sequence_id', 'asc').execute();
|
|
354
360
|
if (frames.length === 0) return [];
|
|
@@ -369,6 +375,17 @@ export function createHubStore(params) {
|
|
|
369
375
|
});
|
|
370
376
|
return removed;
|
|
371
377
|
}
|
|
378
|
+
async function purgeSubscriptions({ olderThan }) {
|
|
379
|
+
// Strict boundary and atomic return: a refreshed subscriber is neither deleted nor reported.
|
|
380
|
+
const rows = await db.deleteFrom('hub_subscriptions').where('last_active_at', '<', Date.now() - olderThan * 1000).returning([
|
|
381
|
+
'topic_id',
|
|
382
|
+
'subscriber_did'
|
|
383
|
+
]).execute();
|
|
384
|
+
return rows.map((row)=>({
|
|
385
|
+
topicID: row.topic_id,
|
|
386
|
+
subscriberDID: row.subscriber_did
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
372
389
|
async function deleteExpiredKeyPackages(ownerDID) {
|
|
373
390
|
await db.deleteFrom('hub_key_packages').where('owner_did', '=', ownerDID).where('not_after', 'is not', null).where('not_after', '<=', nowSeconds()).execute();
|
|
374
391
|
}
|
|
@@ -474,6 +491,7 @@ export function createHubStore(params) {
|
|
|
474
491
|
fetchTopic,
|
|
475
492
|
ack,
|
|
476
493
|
purge,
|
|
494
|
+
purgeSubscriptions,
|
|
477
495
|
trim,
|
|
478
496
|
storeKeyPackage,
|
|
479
497
|
fetchKeyPackages,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "SQL hub store for Kumiai hubs, backed by Hozon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -25,21 +25,21 @@
|
|
|
25
25
|
"lib/*"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@hozon/adapter": "^0.1
|
|
29
|
-
"@hozon/db": "^0.
|
|
30
|
-
"@kumiai/hub-protocol": "^0.
|
|
31
|
-
"@sozai/event": "^0.1.
|
|
28
|
+
"@hozon/adapter": "^0.2.1",
|
|
29
|
+
"@hozon/db": "^0.5.0",
|
|
30
|
+
"@kumiai/hub-protocol": "^0.12.0",
|
|
31
|
+
"@sozai/event": "^0.1.6"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@enkaku/client": "^0.
|
|
35
|
-
"@enkaku/protocol": "^0.
|
|
36
|
-
"@enkaku/transport": "^0.
|
|
37
|
-
"@hozon/node-sqlite": "^0.
|
|
38
|
-
"@hozon/postgres": "^0.
|
|
39
|
-
"@kokuin/token": "^0.
|
|
40
|
-
"@kumiai/hub-conformance": "^0.
|
|
41
|
-
"@kumiai/hub-server": "^0.
|
|
42
|
-
"@testcontainers/postgresql": "^12.1
|
|
34
|
+
"@enkaku/client": "^0.22.2",
|
|
35
|
+
"@enkaku/protocol": "^0.22.2",
|
|
36
|
+
"@enkaku/transport": "^0.22.2",
|
|
37
|
+
"@hozon/node-sqlite": "^0.2.0",
|
|
38
|
+
"@hozon/postgres": "^0.2.0",
|
|
39
|
+
"@kokuin/token": "^0.6.2",
|
|
40
|
+
"@kumiai/hub-conformance": "^0.12.0",
|
|
41
|
+
"@kumiai/hub-server": "^0.12.0",
|
|
42
|
+
"@testcontainers/postgresql": "^12.2.1"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=24"
|