@kumiai/hub-server 0.11.0 → 0.12.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 CHANGED
@@ -13,6 +13,7 @@ client registry for push fan-out, and an in-memory `HubStore` for development an
13
13
  development; it is also what runs the `@kumiai/hub-conformance` suites in this repo.
14
14
  - `HubClientRegistry` — the live-connection table.
15
15
  - `createRateLimiter`, `DEFAULT_RATE_LIMITS`, `DEFAULT_KEYPACKAGE_FETCH_LIMITS`,
16
+ `DEFAULT_RECEIVE_WINDOW`, `DEFAULT_RECEIVE_WINDOW_BYTES`, `DEFAULT_RECEIVE_ACK_TIMEOUT`,
16
17
  `DEFAULT_HUB_ACCESS_RULES`.
17
18
  - `HubStoreErrorEvent`, `HubStoreErrorHook` — the `onStoreError` event and hook types.
18
19
 
@@ -74,6 +75,34 @@ than assigned, because `logPosition: undefined` becomes a present key with a fal
74
75
  off the wire — and the entire point of the field is that a reader can tell "no place in any log" from
75
76
  a place.
76
77
 
78
+ ## The receive channel is paced by acks
79
+
80
+ `hub/v1/receive` writes a frame only while the frames in flight on that channel stay under a window:
81
+ `receiveWindow` frames (`DEFAULT_RECEIVE_WINDOW`, 256) and `receiveWindowBytes` bytes
82
+ (`DEFAULT_RECEIVE_WINDOW_BYTES`, 512 KiB, counting the base64 payload plus an envelope estimate; one
83
+ frame always goes through when nothing else is in flight). The backlog drain and live pushes share
84
+ the window, and acks are read from the moment the channel opens, so the drain advances as the client
85
+ acks it.
86
+
87
+ The transport cannot do this pacing: `writer.ready` on a channel says nothing about the HTTP
88
+ bridge's per-session buffer, which `@enkaku/http-serve` drops whole once it passes
89
+ `maxSessionBufferBytes`. Unpaced, a backlog larger than that buffer, read by a slow client, ended
90
+ the session on every connect before any ack reached the hub, and the next connect redelivered the
91
+ same backlog. Keep the byte window well below the transport's session buffer.
92
+
93
+ The channel is ordered, so an ack for a frame proves the client has read every frame written before
94
+ it: for the window, an ack releases that frame and all earlier ones. The store still frees only the
95
+ deliveries actually acked. A client can leave frames unacked for good — a topic it subscribed to but
96
+ reads with `hub/v1/topic/fetch`, or one no consumer on the device claims — and those frames must not
97
+ hold the window, or they would come back first on every connect and stall it.
98
+
99
+ A full window that sees no ack at all for `receiveAckTimeout` ms (`DEFAULT_RECEIVE_ACK_TIMEOUT`,
100
+ 15 s; each ack restarts it) is released whole and the drain goes on; the channel is not ended,
101
+ because a window made only of never-acked frames would come back first on the next connect too. A
102
+ client that has stopped reading cannot grow a buffer without bound: the HTTP bridge drops its
103
+ session, and a transport with real backpressure stalls the write until `receiveBufferLimit` tears
104
+ the channel down.
105
+
77
106
  ## Retention: the store refuses, the hub schedules
78
107
 
79
108
  Two different knobs, easily confused. `createHub`'s `purge.olderThan` (default 7 days) is the **age
@@ -84,6 +113,11 @@ request belongs to the store, because the store is what refuses the subscribe
84
113
  pin a topic's frames forever with `subscribe({ retention: 2 ** 31 })`. A subscribe above the ceiling
85
114
  is refused with `RetentionExceededError`, never clamped.
86
115
 
116
+ When the store supports subscription reaping, each tick also removes inactive subscriptions after message purge settles.
117
+ `purge.subscriptionOlderThan` sets the inactivity window in seconds, defaulting to `7776000` (90 days).
118
+ The effective window is `Math.max(subscriptionOlderThan, olderThan)`, so subscriptions outlive the message retention window.
119
+ Both operations report failures through `onStoreError` independently. Disposal waits for both operations.
120
+
87
121
  `createMemoryStore`'s `maxDepth` (default 1000) evicts the oldest **log** frames beyond that count.
88
122
  Mailbox frames on the same topic are not counted, so a member cannot evict the log with a mailbox
89
123
  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
  | {
@@ -163,6 +166,34 @@ export declare const DEFAULT_RECEIVE_BUFFER_LIMIT = 256;
163
166
  * maximum window a removed member can keep draining a topic. See the receive handler's `gate`.
164
167
  */
165
168
  export declare const DEFAULT_RECEIVE_AUTH_CACHE_TTL = 5000;
169
+ /**
170
+ * Max frames in flight on a receive channel: written, and not yet followed by an ack for that frame
171
+ * or a later one. The hub writes the next frame only once acks bring the count back under this
172
+ * window.
173
+ *
174
+ * Without it the drain writes a whole backlog at once, and a transport that buffers per session
175
+ * (the SSE bridge drops a session whose unread buffer passes `maxSessionBufferBytes`) ends the
176
+ * session before the slow client's acks reach the hub: every connect then redelivers the same
177
+ * backlog. The window must stay well above the client's ack batch (10 for Kubun's device client)
178
+ * so batched acks keep the pipe full.
179
+ */
180
+ export declare const DEFAULT_RECEIVE_WINDOW = 256;
181
+ /**
182
+ * Max bytes (base64 payload plus an estimate of the frame's envelope) in flight on a receive
183
+ * channel. Kept well below the HTTP transport's per-session buffer (1 MiB by default in `@enkaku/http-serve`, 8 MiB
184
+ * on Kubun's relay) so a window of large frames cannot overflow it. A single frame is always let
185
+ * through when nothing else is in flight, whatever its size.
186
+ */
187
+ export declare const DEFAULT_RECEIVE_WINDOW_BYTES: number;
188
+ /**
189
+ * How long (ms) a receive channel with a full window waits without any ack before it releases the
190
+ * whole window and writes on. Each ack restarts the wait. The channel is NOT ended: a window full
191
+ * of frames the client will never ack (topics no sink on the device claims) would come back first
192
+ * on every reconnect and livelock it. Long enough for a busy phone to open the first frames of a
193
+ * window and flush an ack batch (10 frames or 500 ms on Kubun's device client); short, because it
194
+ * is the delay a window of never-acked frames costs per connect.
195
+ */
196
+ export declare const DEFAULT_RECEIVE_ACK_TIMEOUT = 15000;
166
197
  export type CreateHandlersParams = {
167
198
  registry: HubClientRegistry;
168
199
  store: HubStore;
@@ -172,6 +203,19 @@ export type CreateHandlersParams = {
172
203
  /** Max frames queued-but-unflushed on a receive channel. See {@link DEFAULT_RECEIVE_BUFFER_LIMIT}
173
204
  * for the >= 50-frame-page floor this should respect. Default: 256 */
174
205
  receiveBufferLimit?: number;
206
+ /** Max frames in flight on a receive channel. Default: {@link DEFAULT_RECEIVE_WINDOW} */
207
+ receiveWindow?: number;
208
+ /**
209
+ * Max bytes (payload plus envelope estimate) in flight on a receive channel. Default:
210
+ * {@link DEFAULT_RECEIVE_WINDOW_BYTES}
211
+ */
212
+ receiveWindowBytes?: number;
213
+ /**
214
+ * How long (ms) a receive channel with a full window waits for an ack before it releases the
215
+ * window and writes on.
216
+ * Default: {@link DEFAULT_RECEIVE_ACK_TIMEOUT}
217
+ */
218
+ receiveAckTimeout?: number;
175
219
  /**
176
220
  * TTL (ms) for the per-(did, topicID) receive delivery-authorization cache. `0` disables reuse
177
221
  * (the hook is consulted for every frame). Non-finite or negative values fall back to
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}`;
@@ -89,6 +91,31 @@ export const DEFAULT_KEYPACKAGE_FETCH_LIMITS = {
89
91
  * Bounds how long an already-resolved allow is reused before the hook is re-consulted — i.e. the
90
92
  * maximum window a removed member can keep draining a topic. See the receive handler's `gate`.
91
93
  */ export const DEFAULT_RECEIVE_AUTH_CACHE_TTL = 5000;
94
+ /**
95
+ * Max frames in flight on a receive channel: written, and not yet followed by an ack for that frame
96
+ * or a later one. The hub writes the next frame only once acks bring the count back under this
97
+ * window.
98
+ *
99
+ * Without it the drain writes a whole backlog at once, and a transport that buffers per session
100
+ * (the SSE bridge drops a session whose unread buffer passes `maxSessionBufferBytes`) ends the
101
+ * session before the slow client's acks reach the hub: every connect then redelivers the same
102
+ * backlog. The window must stay well above the client's ack batch (10 for Kubun's device client)
103
+ * so batched acks keep the pipe full.
104
+ */ export const DEFAULT_RECEIVE_WINDOW = 256;
105
+ /**
106
+ * Max bytes (base64 payload plus an estimate of the frame's envelope) in flight on a receive
107
+ * channel. Kept well below the HTTP transport's per-session buffer (1 MiB by default in `@enkaku/http-serve`, 8 MiB
108
+ * on Kubun's relay) so a window of large frames cannot overflow it. A single frame is always let
109
+ * through when nothing else is in flight, whatever its size.
110
+ */ export const DEFAULT_RECEIVE_WINDOW_BYTES = 512 * 1024;
111
+ /**
112
+ * How long (ms) a receive channel with a full window waits without any ack before it releases the
113
+ * whole window and writes on. Each ack restarts the wait. The channel is NOT ended: a window full
114
+ * of frames the client will never ack (topics no sink on the device claims) would come back first
115
+ * on every reconnect and livelock it. Long enough for a busy phone to open the first frames of a
116
+ * window and flush an ack batch (10 frames or 500 ms on Kubun's device client); short, because it
117
+ * is the delay a window of never-acked frames costs per connect.
118
+ */ export const DEFAULT_RECEIVE_ACK_TIMEOUT = 15_000;
92
119
  function getClientDID(ctx) {
93
120
  const iss = ctx.message.payload.iss;
94
121
  if (typeof iss !== 'string' || iss.length === 0) {
@@ -147,6 +174,9 @@ export function createHandlers(params) {
147
174
  const topicLimiter = createRateLimiter(rateLimits.perTopic);
148
175
  const receiveBufferLimit = params.receiveBufferLimit ?? DEFAULT_RECEIVE_BUFFER_LIMIT;
149
176
  const receiveAuthCacheTTL = resolveReceiveAuthCacheTTL(params.receiveAuthCacheTTL);
177
+ const receiveWindow = Math.max(1, params.receiveWindow ?? DEFAULT_RECEIVE_WINDOW);
178
+ const receiveWindowBytes = params.receiveWindowBytes ?? DEFAULT_RECEIVE_WINDOW_BYTES;
179
+ const receiveAckTimeout = params.receiveAckTimeout ?? DEFAULT_RECEIVE_ACK_TIMEOUT;
150
180
  const storeErrorReporter = createStoreErrorReporter(params.onStoreError);
151
181
  const fetchLimits = {
152
182
  ...DEFAULT_KEYPACKAGE_FETCH_LIMITS,
@@ -492,6 +522,73 @@ export function createHandlers(params) {
492
522
  ;
493
523
  let tornDown = false;
494
524
  let writeChain = Promise.resolve();
525
+ // Ack window (flow control). `writer.ready` says nothing about the transport's per-session
526
+ // buffer (the HTTP bridge's writable is shared by every session and resolves at once), so the
527
+ // hub paces itself on acks instead: at most `receiveWindow` frames and `receiveWindowBytes`
528
+ // bytes are in flight, i.e. written and not yet known to have reached the client. Denied frames
529
+ // are never written, so never counted.
530
+ //
531
+ // The channel is ordered, so an ack for one frame proves the client has read every frame
532
+ // written before it: an ack releases that frame and all earlier ones from the window. A
533
+ // client may leave some frames unacked for good (a topic no sink on the device claims, a sink
534
+ // torn down mid-session); they stay pending in the store but cannot hold the window. This is
535
+ // window accounting only — the store still frees exactly the deliveries acked.
536
+ const inFlight = new Map() // sequenceID -> counted bytes, in write order
537
+ ;
538
+ let inFlightBytes = 0;
539
+ let onWindowChange = null;
540
+ const hasRoom = (bytes)=>inFlight.size === 0 || inFlight.size < receiveWindow && inFlightBytes + bytes <= receiveWindowBytes;
541
+ function releaseAll() {
542
+ inFlight.clear();
543
+ inFlightBytes = 0;
544
+ }
545
+ // Resolves once the frame fits or the channel is torn down. A full window that sees no ack for
546
+ // `receiveAckTimeout` ms is released whole and the drain goes on: the frames in it may be ones
547
+ // the client will never ack, and ending the channel would only redeliver them first on the
548
+ // next connect. A client that has stopped reading is not left to grow a buffer: the HTTP
549
+ // bridge drops a session that falls `maxSessionBufferBytes` behind, and a transport with real
550
+ // backpressure stalls `writer.write` until `receiveBufferLimit` tears the channel down.
551
+ function waitForRoom(bytes) {
552
+ if (tornDown || hasRoom(bytes)) return Promise.resolve();
553
+ return new Promise((resolve)=>{
554
+ let timer;
555
+ const arm = ()=>{
556
+ clearTimeout(timer);
557
+ timer = setTimeout(()=>{
558
+ releaseAll();
559
+ notifyWindow();
560
+ }, receiveAckTimeout);
561
+ };
562
+ onWindowChange = ()=>{
563
+ if (tornDown || hasRoom(bytes)) {
564
+ clearTimeout(timer);
565
+ onWindowChange = null;
566
+ resolve();
567
+ } else {
568
+ arm();
569
+ }
570
+ };
571
+ arm();
572
+ });
573
+ }
574
+ function notifyWindow() {
575
+ onWindowChange?.();
576
+ }
577
+ function release(sequenceIDs) {
578
+ const acked = new Set(sequenceIDs);
579
+ // The acked frame written last, in write order (an ack batch need not be sorted).
580
+ let last;
581
+ for (const id of inFlight.keys()){
582
+ if (acked.has(id)) last = id;
583
+ }
584
+ if (last === undefined) return;
585
+ for (const [id, bytes] of inFlight){
586
+ inFlight.delete(id);
587
+ inFlightBytes -= bytes;
588
+ if (id === last) break;
589
+ }
590
+ notifyWindow();
591
+ }
495
592
  let resolveDone = ()=>{};
496
593
  const done = new Promise((resolve)=>{
497
594
  resolveDone = resolve;
@@ -501,6 +598,7 @@ export function createHandlers(params) {
501
598
  function finish() {
502
599
  if (tornDown) return;
503
600
  tornDown = true;
601
+ notifyWindow();
504
602
  if (receiveToken != null) registry.releaseReceiveWriter(clientDID, receiveToken);
505
603
  registry.unregisterIfIdle(clientDID);
506
604
  reader.cancel().catch(()=>{});
@@ -520,7 +618,13 @@ export function createHandlers(params) {
520
618
  if (tornDown) return;
521
619
  try {
522
620
  if (!await gate(frame.topicID)) return; // denied: skip, leave pending, no ack
621
+ // The payload plus the frame's other fields and the transport's envelope (~256 chars).
622
+ const bytes = frame.payload.length + frame.senderDID.length + frame.topicID.length + 256;
623
+ await waitForRoom(bytes);
624
+ if (tornDown) return;
523
625
  await writer.ready;
626
+ inFlight.set(frame.sequenceID, bytes);
627
+ inFlightBytes += bytes;
524
628
  await writer.write(frame);
525
629
  if (frame.sequenceID > lastServed) lastServed = frame.sequenceID;
526
630
  } catch {
@@ -553,6 +657,38 @@ export function createHandlers(params) {
553
657
  pushWrite(frame);
554
658
  }
555
659
  };
660
+ // Ack loop (M1): a store.ack failure must not stop later acks; only a reader error closes.
661
+ // It runs from connect, alongside the drain: the drain waits on the ack window, so acks for the
662
+ // backlog must be read while it is still being served.
663
+ void (async ()=>{
664
+ while(true){
665
+ let result;
666
+ try {
667
+ result = await reader.read();
668
+ } catch {
669
+ break;
670
+ }
671
+ if (result.done) break;
672
+ const ack = result.value?.ack;
673
+ if (ack != null) {
674
+ // The client has the frames, whatever the store does with the ack.
675
+ release(ack);
676
+ try {
677
+ await store.ack({
678
+ recipientDID: clientDID,
679
+ sequenceIDs: ack
680
+ });
681
+ } catch (error) {
682
+ // Frame stays pending; the client re-acks next round. Do NOT break.
683
+ storeErrorReporter({
684
+ method: 'ack',
685
+ did: clientDID,
686
+ error
687
+ });
688
+ }
689
+ }
690
+ }
691
+ })();
556
692
  try {
557
693
  // Binding evicts whatever held the lane: a reconnect means the old connection broke and the
558
694
  // server learns last, so the stale writer must give way to the live one. See
@@ -563,6 +699,17 @@ export function createHandlers(params) {
563
699
  evicted?.();
564
700
  // The device is draining; a trailing summary would be noise it has already outrun.
565
701
  params.wake?.dispatcher?.online(clientDID);
702
+ // Teardown triggers are armed before the drain: the drain waits on the ack window, so it
703
+ // can last as long as the client takes to ack, and an abort or eviction must end it.
704
+ // H2: an already-aborted signal never fires 'abort', so run teardown now.
705
+ if (ctx.signal.aborted) {
706
+ finish();
707
+ } else {
708
+ void evictedHere.then(finish);
709
+ ctx.signal.addEventListener('abort', finish, {
710
+ once: true
711
+ });
712
+ }
566
713
  // Drain the backlog. Await each page so lastServed is exact before the flush.
567
714
  let cursor = after;
568
715
  while(!tornDown){
@@ -596,49 +743,13 @@ export function createHandlers(params) {
596
743
  } catch (error) {
597
744
  // A synchronous drain error (e.g. store.fetch threw): clean up and reject the handler.
598
745
  tornDown = true;
746
+ notifyWindow();
599
747
  if (receiveToken != null) registry.releaseReceiveWriter(clientDID, receiveToken);
600
748
  registry.unregisterIfIdle(clientDID);
601
749
  reader.cancel().catch(()=>{});
602
750
  writer.abort(error).catch(()=>{});
603
751
  throw error;
604
752
  }
605
- // Ack loop (M1): a store.ack failure must not stop later acks; only a reader error closes.
606
- void (async ()=>{
607
- while(true){
608
- let result;
609
- try {
610
- result = await reader.read();
611
- } catch {
612
- break;
613
- }
614
- if (result.done) break;
615
- const ack = result.value?.ack;
616
- if (ack != null) {
617
- try {
618
- await store.ack({
619
- recipientDID: clientDID,
620
- sequenceIDs: ack
621
- });
622
- } catch (error) {
623
- // Frame stays pending; the client re-acks next round. Do NOT break.
624
- storeErrorReporter({
625
- method: 'ack',
626
- did: clientDID,
627
- error
628
- });
629
- }
630
- }
631
- }
632
- })();
633
- // H2: an already-aborted signal never fires 'abort', so run teardown now.
634
- if (ctx.signal.aborted) {
635
- finish();
636
- return done;
637
- }
638
- void evictedHere.then(finish);
639
- ctx.signal.addEventListener('abort', finish, {
640
- once: true
641
- });
642
753
  return done;
643
754
  },
644
755
  'hub/v1/keypackage/upload': async (ctx)=>{
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/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { AuthorizeDecision, AuthorizeHook, AuthorizeRequest, CreateHandlersParams, HubRateLimits, HubStoreErrorEvent, HubStoreErrorHook, KeyPackageFetchLimits, } from './handlers.js';
2
- export { createHandlers, DEFAULT_KEYPACKAGE_FETCH_LIMITS, DEFAULT_RATE_LIMITS, DEFAULT_RECEIVE_BUFFER_LIMIT, } from './handlers.js';
2
+ export { createHandlers, DEFAULT_KEYPACKAGE_FETCH_LIMITS, DEFAULT_RATE_LIMITS, DEFAULT_RECEIVE_ACK_TIMEOUT, DEFAULT_RECEIVE_BUFFER_LIMIT, DEFAULT_RECEIVE_WINDOW, DEFAULT_RECEIVE_WINDOW_BYTES, } from './handlers.js';
3
3
  export type { CreateHubParams, HubInstance, HubPurgeOptions, HubReplayOptions } from './hub.js';
4
4
  export { createHub, DEFAULT_HUB_ACCESS_RULES } from './hub.js';
5
5
  export { createMemoryStore, type MemoryStoreOptions } from './memoryStore.js';
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { createHandlers, DEFAULT_KEYPACKAGE_FETCH_LIMITS, DEFAULT_RATE_LIMITS, DEFAULT_RECEIVE_BUFFER_LIMIT } from './handlers.js';
1
+ export { createHandlers, DEFAULT_KEYPACKAGE_FETCH_LIMITS, DEFAULT_RATE_LIMITS, DEFAULT_RECEIVE_ACK_TIMEOUT, DEFAULT_RECEIVE_BUFFER_LIMIT, DEFAULT_RECEIVE_WINDOW, DEFAULT_RECEIVE_WINDOW_BYTES } from './handlers.js';
2
2
  export { createHub, DEFAULT_HUB_ACCESS_RULES } from './hub.js';
3
3
  export { createMemoryStore } from './memoryStore.js';
4
4
  export { createRateLimiter } from './rateLimit.js';
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.1",
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",
33
- "@sozai/codec": "^0.4.3",
28
+ "@enkaku/protocol": "^0.22.4",
29
+ "@enkaku/server": "^0.22.4",
30
+ "@kokuin/capability": "^0.5.2",
31
+ "@kokuin/token": "^0.6.4",
32
+ "@kumiai/hub-protocol": "^0.12.0",
33
+ "@sozai/codec": "^0.4.4",
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.4",
39
+ "@enkaku/transport": "^0.22.4",
40
+ "@kumiai/hub-conformance": "^0.12.0",
41
+ "@kumiai/hub-wake": "^0.12.0"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"