@kumiai/hub-server 0.11.0 → 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 CHANGED
@@ -84,6 +84,11 @@ request belongs to the store, because the store is what refuses the subscribe
84
84
  pin a topic's frames forever with `subscribe({ retention: 2 ** 31 })`. A subscribe above the ceiling
85
85
  is refused with `RetentionExceededError`, never clamped.
86
86
 
87
+ When the store supports subscription reaping, each tick also removes inactive subscriptions after message purge settles.
88
+ `purge.subscriptionOlderThan` sets the inactivity window in seconds, defaulting to `7776000` (90 days).
89
+ The effective window is `Math.max(subscriptionOlderThan, olderThan)`, so subscriptions outlive the message retention window.
90
+ Both operations report failures through `onStoreError` independently. Disposal waits for both operations.
91
+
87
92
  `createMemoryStore`'s `maxDepth` (default 1000) evicts the oldest **log** frames beyond that count.
88
93
  Mailbox frames on the same topic are not counted, so a member cannot evict the log with a mailbox
89
94
  flood.
package/lib/handlers.d.ts CHANGED
@@ -98,6 +98,9 @@ export type AuthorizeHook = (req: AuthorizeRequest) => AuthorizeDecision | Promi
98
98
  export type HubStoreErrorEvent = {
99
99
  method: 'purge';
100
100
  error: unknown;
101
+ } | {
102
+ method: 'purgeSubscriptions';
103
+ error: unknown;
101
104
  }
102
105
  /** The DID whose ack the store refused. */
103
106
  | {
package/lib/handlers.js CHANGED
@@ -21,6 +21,7 @@ function normalizeAuthorizeDecision(decision) {
21
21
  fetchLastResortKeyPackage: 'The fetch returned what the pool could serve, without the last-resort top-up. A read that ' + 'keeps failing means the availability floor the last-resort slot exists to provide is absent, ' + 'and joins fail downstream at the inviter with no signal here.',
22
22
  ack: 'The frame stays pending and the client re-acks on the next round. An ack that keeps failing ' + 'redelivers every frame forever.',
23
23
  purge: 'Retried on the next interval. A purge that keeps failing means the store grows without bound.',
24
+ purgeSubscriptions: 'Retried on the next interval. Persistent failure leaves stale subscriptions consuming storage and quota.',
24
25
  getSubscribers: 'The frame is committed and queued, but the live push to connected subscribers was skipped: ' + 'each of them stops receiving new frames until it reconnects. A getSubscribers that keeps ' + 'failing means the hub has silently degraded from push to pull for every publish.',
25
26
  wake: 'The offline device was not pinged for this frame. It still redelivers on the next reconnect ' + '(store-and-forward), so nothing is lost — but a wake that keeps failing means the device ' + 'stays asleep until something else wakes it.'
26
27
  };
@@ -32,6 +33,7 @@ const reportStoreError = getReporter([
32
33
  * methods, a topic for fan-out, nothing for a store-wide purge. */ function subjectOf(event) {
33
34
  switch(event.method){
34
35
  case 'purge':
36
+ case 'purgeSubscriptions':
35
37
  return '';
36
38
  case 'getSubscribers':
37
39
  return ` on topic ${event.topicID}`;
package/lib/hub.d.ts CHANGED
@@ -20,6 +20,11 @@ export type HubPurgeOptions = {
20
20
  * store's, since the store is the thing that refuses the subscribe.
21
21
  */
22
22
  olderThan?: number;
23
+ /**
24
+ * Subscription inactivity window in seconds. Default: 7776000 (90 days).
25
+ * Clamped to at least olderThan so subscriptions outlive the message retention window.
26
+ */
27
+ subscriptionOlderThan?: number;
23
28
  };
24
29
  /** Replay settings a host may tune. The hub always enables replay checks and rejects stale tokens. */
25
30
  export type HubReplayOptions = Omit<ReplayOptions, 'enabled' | 'rejectStale'>;
package/lib/hub.js CHANGED
@@ -69,16 +69,31 @@ export function createHub(params) {
69
69
  if (params.purge !== false) {
70
70
  const interval = params.purge?.interval ?? 3_600_000;
71
71
  const olderThan = params.purge?.olderThan ?? 604_800;
72
+ const subscriptionOlderThan = Math.max(params.purge?.subscriptionOlderThan ?? 7_776_000, olderThan);
73
+ const store = params.store;
72
74
  purgeTimer = setInterval(()=>{
73
- const run = params.store.purge({
74
- olderThan
75
- }).then(()=>{}).catch((error)=>{
76
- // Purge failures are non-fatal; retried on the next interval
77
- storeErrorReporter({
78
- method: 'purge',
79
- error
80
- });
81
- });
75
+ const run = (async ()=>{
76
+ try {
77
+ await store.purge({
78
+ olderThan
79
+ });
80
+ } catch (error) {
81
+ storeErrorReporter({
82
+ method: 'purge',
83
+ error
84
+ });
85
+ }
86
+ try {
87
+ await store.purgeSubscriptions?.({
88
+ olderThan: subscriptionOlderThan
89
+ });
90
+ } catch (error) {
91
+ storeErrorReporter({
92
+ method: 'purgeSubscriptions',
93
+ error
94
+ });
95
+ }
96
+ })();
82
97
  purgesInFlight.add(run);
83
98
  void run.finally(()=>{
84
99
  purgesInFlight.delete(run);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumiai/hub-server",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Hub server for Enkaku E2EE group message routing",
5
5
  "keywords": [
6
6
  "hub",
@@ -25,20 +25,20 @@
25
25
  "lib/*"
26
26
  ],
27
27
  "dependencies": {
28
- "@enkaku/protocol": "^0.22.1",
29
- "@enkaku/server": "^0.22.1",
30
- "@kokuin/capability": "^0.5.1",
31
- "@kokuin/token": "^0.6.1",
32
- "@kumiai/hub-protocol": "^0.11.0",
28
+ "@enkaku/protocol": "^0.22.2",
29
+ "@enkaku/server": "^0.22.2",
30
+ "@kokuin/capability": "^0.5.2",
31
+ "@kokuin/token": "^0.6.2",
32
+ "@kumiai/hub-protocol": "^0.12.0",
33
33
  "@sozai/codec": "^0.4.3",
34
34
  "@sozai/event": "^0.1.6",
35
35
  "@sozai/log": "^0.4.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@enkaku/client": "^0.22.1",
39
- "@enkaku/transport": "^0.22.1",
40
- "@kumiai/hub-conformance": "^0.11.0",
41
- "@kumiai/hub-wake": "^0.11.0"
38
+ "@enkaku/client": "^0.22.2",
39
+ "@enkaku/transport": "^0.22.2",
40
+ "@kumiai/hub-conformance": "^0.12.0",
41
+ "@kumiai/hub-wake": "^0.12.0"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"