From 3c0db14904bfa7b5f9e294aec6149ad38d60e7fc Mon Sep 17 00:00:00 2001 From: Sterister Date: Fri, 8 May 2026 22:56:27 +0200 Subject: [PATCH] release(v4.8.5): kill flushOnce 15s success-backoff + per-recipient parallel drain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prism filed a per-recipient-flush-concurrency FR pointing at serial-per-flush. Investigation surfaced the actual culprit: `scheduleFlush` was using a 15 s backoff on **both** the success and failure paths, so envelopes enqueued *during* an in-flight flush sat ~15 s behind the next drain — visible as "10 s of silence then 25-frame burst" on the receiving side under sustained sender output. Two fixes: 1. `scheduleFlush` now uses 0 ms delay when `flushOnce` delivered ≥1 envelope and more is queued (network healthy → drain remainder immediately). 15 s reserved for the actual failure case where every attempt this round failed. `flushOnce` returns `{ delivered, remaining } | null` so concurrent-flush early returns don't double-schedule. 2. `flushOnce` groups the outgoing queue by `recipientAddress` and drains buckets via `Promise.all`. Per-peer order preserved (sequential within a bucket); a slow POST to recipient A no longer head-of-line-blocks frames bound for B. `Inbox.tick` public shape unchanged. `OutgoingQueueStore` implementations see the same per-entry list/remove/bumpAttempts/ size contract; only cross-recipient interleaving changes. Tests cover (1) 25-envelope burst behind a 100 ms slow PUT drains within 1 s, and (2) carol's PUT lands within 150 ms even when bob's PUT stalls 200 ms. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 79 ++++++++++ packages/shade-cli/package.json | 2 +- packages/shade-core/package.json | 2 +- packages/shade-crypto-web/package.json | 2 +- packages/shade-dashboard/package.json | 2 +- packages/shade-files/package.json | 2 +- packages/shade-inbox-server/package.json | 2 +- packages/shade-inbox/package.json | 2 +- packages/shade-inbox/src/inbox.ts | 129 ++++++++++++----- packages/shade-inbox/tests/client.test.ts | 135 ++++++++++++++++++ packages/shade-key-transparency/package.json | 2 +- packages/shade-keychain/package.json | 2 +- packages/shade-observability/package.json | 2 +- packages/shade-observer/package.json | 2 +- packages/shade-proto/package.json | 2 +- packages/shade-recovery/package.json | 2 +- packages/shade-sdk/package.json | 2 +- packages/shade-server/package.json | 2 +- packages/shade-storage-encrypted/package.json | 2 +- packages/shade-storage-indexeddb/package.json | 2 +- packages/shade-storage-postgres/package.json | 2 +- packages/shade-storage-sqlite/package.json | 2 +- packages/shade-streams/package.json | 2 +- packages/shade-transfer/package.json | 2 +- packages/shade-transport-bridge/package.json | 2 +- packages/shade-transport-webrtc/package.json | 2 +- packages/shade-transport/package.json | 2 +- packages/shade-widgets/package.json | 2 +- 28 files changed, 334 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 653b8ac..36e79fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,85 @@ All notable changes to Shade are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.8.5] — 2026-05-08 — `Inbox.flushOnce`: kill the 15 s success-backoff + per-recipient parallel drain + +Prism filed a "typing-into-a-chatty-shell" UX FR pointing at +serial-per-flush behavior. The investigation surfaced a more +important latent bug: `scheduleFlush` was using a 15 s backoff timer +on **both** the success and failure paths, so any envelopes enqueued +*during* an in-flight flush had to wait ~15 s for the next drain to +fire — visible to Prism's web client as "10 s of silence then a +25-frame burst" whenever the PC sidecar was emitting steady output. + +Two fixes ship together: + +**(1) `scheduleFlush` distinguishes healthy-drain from all-failed.** +After `flushOnce` returns, if the round delivered ≥1 envelope and +items are still queued, the next flush fires with **0 ms** delay +(network is fine — drain whatever piled up immediately). The 15 s +backoff is reserved for the actual failure case (every attempt this +round threw / was rejected). `flushOnce` now returns +`{ delivered, remaining } | null` so the scheduler can also tell +"someone else is flushing, don't double-schedule" apart from +"queue is empty, idle." Externally-visible API unchanged +(`Inbox.tick()` still returns `{ flushed, received }`). + +**(2) Per-recipient parallel drain inside `flushOnce`.** The queue +is grouped by `recipientAddress`; each bucket is drained +sequentially (preserves per-peer enqueue order — the relay assigns +`receivedAt` on PUT arrival, so concurrent PUTs to the same peer +would let the second one land first), but distinct buckets run +concurrently via `Promise.all`. Pre-fix, a slow POST to recipient A +head-of-line-blocked every other recipient's frames. Future N-peer +broadcast fan-outs (multiple devices viewing the same Prism PTY) +benefit immediately; single-recipient deployments are unaffected +since N=1 is the trivial parallel case. + +Reported by Prism (multi-device E2EE terminal). Acceptance: under +sustained typing, web's `recv` rate is roughly proportional to PC's +emit rate, no multi-second silences punctuated by burst catch-ups. + +### Fixed + +#### `@shade/inbox` — `scheduleFlush` 15 s success-backoff +- After a successful drain, the next flush is rescheduled with + `delayMs=0` when `delivered > 0`. The 15 s timer is reserved for + rounds where every attempt failed (no progress, avoid tight retry + loop). +- Concurrent `scheduleFlush` calls during an in-flight flush are + detected via `flushOnce` returning `null`; the no-op early return + no longer double-schedules a 15 s retry for a flush that's + already running. + +#### `@shade/inbox` — `flushOnce` per-recipient parallelism +- Outgoing queue is grouped by `recipientAddress`; buckets drain + via `Promise.all`. Per-peer order preserved (sequential within a + bucket); cross-peer order has no guarantee in Shade's wire model + to begin with. +- Failure handling unchanged: per-entry `bumpAttempts` / + `maxAttempts` semantics are identical to V4.8.4. + +### Tests +- `packages/shade-inbox/tests/client.test.ts`: + 1. "burst enqueued during a flush drains immediately, not after + 15 s backoff" — slow first PUT (100 ms), pile 24 more during, + assert `pendingCount === 0` within 1 s. + 2. "per-recipient parallel drain — slow POST to A does not block + POSTs to B" — `bob` PUT stalls 200 ms; `carol` envelope queued + after; assert `inbox.message_delivered` for carol fires within + 150 ms (would be ≥200 ms pre-fix). + +### Migration + +None. `Inbox.flushOnce` is a private method; the +`{ delivered, remaining } | null` shape is internal. `Inbox.tick` +public return `{ flushed, received }` is unchanged. Apps that hand +custom `OutgoingQueueStore` implementations to `Inbox` see no +contract change — `list()` / `remove()` / `bumpAttempts()` / `size()` +are called the same way per entry; only the *order* of `remove()` +calls across distinct recipients changes (interleaved instead of +strictly sequential). + ## [4.8.4] — 2026-05-08 — Server-side cross-channel dedup via `BridgeDeliveryLog` V4.8.3 shipped the *client-side* cross-channel dedup hook diff --git a/packages/shade-cli/package.json b/packages/shade-cli/package.json index 6b6bc1d..736cbe1 100644 --- a/packages/shade-cli/package.json +++ b/packages/shade-cli/package.json @@ -1,6 +1,6 @@ { "name": "@shade/cli", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/cli.ts", "bin": { diff --git a/packages/shade-core/package.json b/packages/shade-core/package.json index f9c6382..0a8c308 100644 --- a/packages/shade-core/package.json +++ b/packages/shade-core/package.json @@ -1,6 +1,6 @@ { "name": "@shade/core", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-crypto-web/package.json b/packages/shade-crypto-web/package.json index 3d94a66..9fd8ab8 100644 --- a/packages/shade-crypto-web/package.json +++ b/packages/shade-crypto-web/package.json @@ -1,6 +1,6 @@ { "name": "@shade/crypto-web", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-dashboard/package.json b/packages/shade-dashboard/package.json index 7e37b6d..2d1d57e 100644 --- a/packages/shade-dashboard/package.json +++ b/packages/shade-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@shade/dashboard", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/shade-files/package.json b/packages/shade-files/package.json index 5c5cebf..b8f9962 100644 --- a/packages/shade-files/package.json +++ b/packages/shade-files/package.json @@ -1,6 +1,6 @@ { "name": "@shade/files", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-inbox-server/package.json b/packages/shade-inbox-server/package.json index 703f438..3cb556e 100644 --- a/packages/shade-inbox-server/package.json +++ b/packages/shade-inbox-server/package.json @@ -1,6 +1,6 @@ { "name": "@shade/inbox-server", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-inbox/package.json b/packages/shade-inbox/package.json index 44c8613..ed70092 100644 --- a/packages/shade-inbox/package.json +++ b/packages/shade-inbox/package.json @@ -1,6 +1,6 @@ { "name": "@shade/inbox", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-inbox/src/inbox.ts b/packages/shade-inbox/src/inbox.ts index 7090d47..8bf689d 100644 --- a/packages/shade-inbox/src/inbox.ts +++ b/packages/shade-inbox/src/inbox.ts @@ -247,7 +247,10 @@ export class Inbox { * after a push-trigger arrives). Does not throw on transient errors. */ async tick(): Promise<{ flushed: number; received: number }> { - const flushed = await this.flushOnce(); + const flushResult = await this.flushOnce(); + // `null` means another flush was concurrent; report 0 newly-flushed + // for this caller (the other flush counted them). + const flushed = flushResult?.delivered ?? 0; const received = await this.pollOnce(); return { flushed, received }; } @@ -301,11 +304,27 @@ export class Inbox { this.flushTimer = setTimeout(() => { this.flushTimer = null; this.flushOnce() - .then(() => { - // If anything is still queued, retry with backoff. - this.queueStore.size().then((n) => { - if (n > 0 && this.started) this.scheduleFlush(15_000); - }); + .then((result) => { + // `result === null` means another flush was already in flight + // and this call early-returned via the `flushing` guard. The + // already-running flush will reschedule itself when it + // finishes; do not double-schedule from here. + if (result === null) return; + if (result.remaining === 0) return; + // V4.8.5 — distinguish healthy-drain-but-more-queued from + // all-attempts-failed. Pre-fix, both cases used a 15 s + // backoff. Under sustained traffic (Prism's typing-into-a- + // chatty-shell pattern), bursts of envelopes enqueued + // *during* a flush would sit ~10–15 s behind the backoff + // timer before the next drain — visible to the receiver as a + // "10 s silence then 25-frame burst" wave. Healthy drain + // (delivered > 0) means the network is fine and we should + // immediately drain whatever piled up; reserve the 15 s + // retry for the case where every attempt this round failed. + if (this.started) { + const delay = result.delivered > 0 ? 0 : 15_000; + this.scheduleFlush(delay); + } }) .catch(() => { if (this.started) this.scheduleFlush(15_000); @@ -324,45 +343,87 @@ export class Inbox { }, delayMs); } - private async flushOnce(): Promise { - if (this.flushing) return 0; + /** + * Drain the outgoing queue. Returns `null` if another flush is already + * in flight (the running flush owns the rescheduling); otherwise + * returns the count of newly-delivered envelopes and the queue size + * after the drain so the caller can decide whether to immediately + * re-flush (more piled up during the drain — healthy network) or + * back off (everything failed). + * + * V4.8.5: drain is parallel-per-recipient. Each `recipientAddress` + * gets its own sequential worker (so per-peer order is preserved), + * but distinct recipients run concurrently. Pre-fix, a single slow + * POST head-of-line-blocked the entire queue — including small + * frames bound for unrelated peers. See Prism FR + * `per-recipient-flush-concurrency-v4.8.md`. + */ + private async flushOnce(): Promise<{ delivered: number; remaining: number } | null> { + if (this.flushing) return null; this.flushing = true; let delivered = 0; try { const entries = await this.queueStore.list(); + // Group by recipient. Within a bucket we drain sequentially so + // per-peer message order matches enqueue order (the relay + // assigns `receivedAt` on PUT arrival; concurrent POSTs to the + // same peer would let the second arrive first and the recipient + // would observe out-of-order envelopes). Across buckets, no + // ordering guarantee exists in Shade's wire model anyway, so + // parallel drain is safe. + const buckets = new Map(); for (const entry of entries) { - try { - const result = await this.client.put({ - recipientAddress: entry.recipientAddress, - senderSigningKey: this.options.signingPublicKey, - envelope: entry.ciphertext, - ttlSeconds: entry.ttlSeconds, - }); - await this.queueStore.remove(entry.recipientAddress, entry.msgId); - delivered++; - this.events.emit('inbox.message_delivered', { - recipientAddress: entry.recipientAddress, - msgId: result.msgId, - idempotent: result.idempotent, - }); - } catch (err) { - await this.queueStore.bumpAttempts(entry.recipientAddress, entry.msgId); - const attempts = entry.attempts + 1; - this.events.emit('inbox.message_failed', { - recipientAddress: entry.recipientAddress, - msgId: entry.msgId, - attempts, - error: (err as Error).message, - }); - if (attempts >= this.maxAttempts) { + let bucket = buckets.get(entry.recipientAddress); + if (!bucket) { + bucket = []; + buckets.set(entry.recipientAddress, bucket); + } + bucket.push(entry); + } + + const drainBucket = async (bucket: OutgoingEntry[]): Promise => { + let count = 0; + for (const entry of bucket) { + try { + const result = await this.client.put({ + recipientAddress: entry.recipientAddress, + senderSigningKey: this.options.signingPublicKey, + envelope: entry.ciphertext, + ttlSeconds: entry.ttlSeconds, + }); await this.queueStore.remove(entry.recipientAddress, entry.msgId); + count++; + this.events.emit('inbox.message_delivered', { + recipientAddress: entry.recipientAddress, + msgId: result.msgId, + idempotent: result.idempotent, + }); + } catch (err) { + await this.queueStore.bumpAttempts(entry.recipientAddress, entry.msgId); + const attempts = entry.attempts + 1; + this.events.emit('inbox.message_failed', { + recipientAddress: entry.recipientAddress, + msgId: entry.msgId, + attempts, + error: (err as Error).message, + }); + if (attempts >= this.maxAttempts) { + await this.queueStore.remove(entry.recipientAddress, entry.msgId); + } } } - } + return count; + }; + + const counts = await Promise.all( + Array.from(buckets.values(), drainBucket), + ); + delivered = counts.reduce((a, b) => a + b, 0); } finally { this.flushing = false; } - return delivered; + const remaining = await this.queueStore.size(); + return { delivered, remaining }; } private async pollOnce(): Promise { diff --git a/packages/shade-inbox/tests/client.test.ts b/packages/shade-inbox/tests/client.test.ts index 6728fd0..f1ee332 100644 --- a/packages/shade-inbox/tests/client.test.ts +++ b/packages/shade-inbox/tests/client.test.ts @@ -284,6 +284,141 @@ describe('Inbox orchestrator', () => { expect(dispatched).toEqual([msgId]); }); + test('burst enqueued during a flush drains immediately, not after 15 s backoff (V4.8.5)', async () => { + // Reproduces Prism FR `per-recipient-flush-concurrency-v4.8`: a + // burst of envelopes enqueued *during* a slow POST used to sit + // ~15 s behind the next flush because both the success path and + // the failure path of `flushOnce` rescheduled with the same 15 s + // backoff. The fix uses 0 ms when the round delivered something + // (network is healthy — drain remainder) and reserves 15 s for + // the all-attempts-failed case. + const store = new MemoryInboxStore(); + const app = createInboxServer({ crypto, store, disableRateLimit: true }); + const bob = await makeIdentity(); + const alice = await makeIdentity(); + const bobClient = new InboxClient({ + baseUrl: 'http://localhost', + crypto, + signingPrivateKey: bob.signingPrivateKey, + fetch: honoFetch(app), + }); + await bobClient.register({ address: 'bob', signingKey: bob.signingPublicKey }); + + // Wrap fetch so the FIRST PUT (only) takes 100 ms — long enough + // for many enqueues to land while it's in flight. + let firstPutSeen = false; + const slowFirstFetch: typeof fetch = (async (input, init) => { + const u = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; + const isPut = u.includes('/v1/inbox/bob') && !u.includes('/fetch'); + if (isPut && !firstPutSeen) { + firstPutSeen = true; + await new Promise((r) => setTimeout(r, 100)); + } + return honoFetch(app)(input, init); + }) as typeof fetch; + + const aliceInbox = new Inbox({ + baseUrl: 'http://localhost', + ownAddress: 'alice', + crypto, + signingPrivateKey: alice.signingPrivateKey, + signingPublicKey: alice.signingPublicKey, + pollIntervalMs: 0, + fetch: slowFirstFetch, + }); + + aliceInbox.start(); + + // First send — this kicks the slow-PUT path. + await aliceInbox.send({ recipientAddress: 'bob', envelope: randBytes(20) }); + + // Pile 24 more on top while the first PUT is still in flight. The + // first PUT will finish at ~T+100 ms; the subsequent 24 should + // drain immediately after, NOT after a 15 s backoff. + for (let i = 0; i < 24; i++) { + await aliceInbox.send({ recipientAddress: 'bob', envelope: randBytes(20) }); + } + + // Wait long enough for the slow first PUT + the immediate + // reschedule + the 24-envelope drain. Pre-fix this would still + // have ≥1 entry pending after 1 s (waiting for the 15 s timer). + await new Promise((r) => setTimeout(r, 1_000)); + expect(await aliceInbox.pendingCount()).toBe(0); + aliceInbox.stop(); + }); + + test('per-recipient parallel drain — slow POST to A does not block POSTs to B (V4.8.5)', async () => { + const store = new MemoryInboxStore(); + const app = createInboxServer({ crypto, store, disableRateLimit: true }); + const alice = await makeIdentity(); + const bob = await makeIdentity(); + const carol = await makeIdentity(); + // Register bob + carol. + const reg = async (name: string, kp: { signingPrivateKey: Uint8Array; signingPublicKey: Uint8Array }) => { + const c = new InboxClient({ + baseUrl: 'http://localhost', + crypto, + signingPrivateKey: kp.signingPrivateKey, + fetch: honoFetch(app), + }); + await c.register({ address: name, signingKey: kp.signingPublicKey }); + }; + await reg('bob', bob); + await reg('carol', carol); + + // bob's PUT route stalls 200 ms; carol's is instant. Pre-fix this + // would head-of-line block carol behind bob. + const slowedFetch: typeof fetch = (async (input, init) => { + const u = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; + const m = (init as RequestInit | undefined)?.method ?? 'GET'; + if (m === 'POST' && u.includes('/v1/inbox/bob') && !u.includes('/fetch')) { + await new Promise((r) => setTimeout(r, 200)); + } + return honoFetch(app)(input, init); + }) as typeof fetch; + + const aliceInbox = new Inbox({ + baseUrl: 'http://localhost', + ownAddress: 'alice', + crypto, + signingPrivateKey: alice.signingPrivateKey, + signingPublicKey: alice.signingPublicKey, + pollIntervalMs: 0, + fetch: slowedFetch, + }); + + const carolDeliveredAt = new Promise((resolve) => { + aliceInbox.on((e) => { + if (e.name === 'inbox.message_delivered' && e.data.recipientAddress === 'carol') { + resolve(Date.now()); + } + }); + }); + + const t0 = Date.now(); + // Bob queue first, carol second — pre-fix carol would wait 200 ms + // behind bob's slow PUT. With per-recipient parallelism, carol's + // PUT runs concurrently and lands first. + await aliceInbox.send({ recipientAddress: 'bob', envelope: randBytes(20) }); + await aliceInbox.send({ recipientAddress: 'carol', envelope: randBytes(20) }); + + aliceInbox.start(); + const carolAt = await carolDeliveredAt; + const carolElapsed = carolAt - t0; + expect(carolElapsed).toBeLessThan(150); + aliceInbox.stop(); + }); + test('flush retries on transient server failure', async () => { const store = new MemoryInboxStore(); const app = createInboxServer({ crypto, store, disableRateLimit: true }); diff --git a/packages/shade-key-transparency/package.json b/packages/shade-key-transparency/package.json index f4046da..4ce261a 100644 --- a/packages/shade-key-transparency/package.json +++ b/packages/shade-key-transparency/package.json @@ -1,6 +1,6 @@ { "name": "@shade/key-transparency", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-keychain/package.json b/packages/shade-keychain/package.json index 626541b..78c5750 100644 --- a/packages/shade-keychain/package.json +++ b/packages/shade-keychain/package.json @@ -1,6 +1,6 @@ { "name": "@shade/keychain", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-observability/package.json b/packages/shade-observability/package.json index 6a7f43d..57faca0 100644 --- a/packages/shade-observability/package.json +++ b/packages/shade-observability/package.json @@ -1,6 +1,6 @@ { "name": "@shade/observability", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-observer/package.json b/packages/shade-observer/package.json index df794f2..15d1b4d 100644 --- a/packages/shade-observer/package.json +++ b/packages/shade-observer/package.json @@ -1,6 +1,6 @@ { "name": "@shade/observer", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-proto/package.json b/packages/shade-proto/package.json index 6d804fa..872cdf2 100644 --- a/packages/shade-proto/package.json +++ b/packages/shade-proto/package.json @@ -1,6 +1,6 @@ { "name": "@shade/proto", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-recovery/package.json b/packages/shade-recovery/package.json index ef8f98b..8faa4f4 100644 --- a/packages/shade-recovery/package.json +++ b/packages/shade-recovery/package.json @@ -1,6 +1,6 @@ { "name": "@shade/recovery", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-sdk/package.json b/packages/shade-sdk/package.json index 29f77d5..e5bcfb7 100644 --- a/packages/shade-sdk/package.json +++ b/packages/shade-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@shade/sdk", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-server/package.json b/packages/shade-server/package.json index 3e544a4..7c40845 100644 --- a/packages/shade-server/package.json +++ b/packages/shade-server/package.json @@ -1,6 +1,6 @@ { "name": "@shade/server", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-encrypted/package.json b/packages/shade-storage-encrypted/package.json index ce5b966..dae8f69 100644 --- a/packages/shade-storage-encrypted/package.json +++ b/packages/shade-storage-encrypted/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-encrypted", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-indexeddb/package.json b/packages/shade-storage-indexeddb/package.json index 5ee33ae..97f1991 100644 --- a/packages/shade-storage-indexeddb/package.json +++ b/packages/shade-storage-indexeddb/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-indexeddb", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-postgres/package.json b/packages/shade-storage-postgres/package.json index d541322..1dcb037 100644 --- a/packages/shade-storage-postgres/package.json +++ b/packages/shade-storage-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-postgres", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-storage-sqlite/package.json b/packages/shade-storage-sqlite/package.json index 97f9429..1ed6956 100644 --- a/packages/shade-storage-sqlite/package.json +++ b/packages/shade-storage-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@shade/storage-sqlite", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-streams/package.json b/packages/shade-streams/package.json index 96d8060..fad6a64 100644 --- a/packages/shade-streams/package.json +++ b/packages/shade-streams/package.json @@ -1,6 +1,6 @@ { "name": "@shade/streams", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transfer/package.json b/packages/shade-transfer/package.json index 47e1580..9da3c56 100644 --- a/packages/shade-transfer/package.json +++ b/packages/shade-transfer/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transfer", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport-bridge/package.json b/packages/shade-transport-bridge/package.json index dc96d78..0b87bdd 100644 --- a/packages/shade-transport-bridge/package.json +++ b/packages/shade-transport-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport-bridge", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport-webrtc/package.json b/packages/shade-transport-webrtc/package.json index aaabb3f..27226c4 100644 --- a/packages/shade-transport-webrtc/package.json +++ b/packages/shade-transport-webrtc/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport-webrtc", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-transport/package.json b/packages/shade-transport/package.json index 8b2905d..47b7ba9 100644 --- a/packages/shade-transport/package.json +++ b/packages/shade-transport/package.json @@ -1,6 +1,6 @@ { "name": "@shade/transport", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/shade-widgets/package.json b/packages/shade-widgets/package.json index 30db2ba..d27305a 100644 --- a/packages/shade-widgets/package.json +++ b/packages/shade-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@shade/widgets", - "version": "4.8.4", + "version": "4.8.5", "type": "module", "main": "src/index.ts", "types": "src/index.ts",