@shadow-js/realtime 1.0.0 → 1.0.2

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +35 -0
  3. package/dist/dev/adapter.d.ts +0 -2
  4. package/dist/dev/client/core.d.ts +0 -2
  5. package/dist/dev/client/core.js +105 -108
  6. package/dist/dev/client/core.js.map +1 -1
  7. package/dist/dev/client/presence.js +1 -2
  8. package/dist/dev/client/sse.js +34 -26
  9. package/dist/dev/client/sse.js.map +1 -1
  10. package/dist/dev/client/types.d.ts +6 -2
  11. package/dist/dev/client/util.js +3 -4
  12. package/dist/dev/client/util.js.map +1 -1
  13. package/dist/dev/client/ws.js +1 -2
  14. package/dist/dev/cloudflare/adapter.d.ts +0 -2
  15. package/dist/dev/cloudflare/adapter.js +1 -2
  16. package/dist/dev/cloudflare/core.js +1 -2
  17. package/dist/dev/cloudflare/link.js +70 -10
  18. package/dist/dev/cloudflare/link.js.map +1 -1
  19. package/dist/dev/cloudflare/object.d.ts +0 -2
  20. package/dist/dev/cloudflare/object.js +1 -2
  21. package/dist/dev/cloudflare/types.d.ts +0 -2
  22. package/dist/dev/cloudflare/wire.js +1 -2
  23. package/dist/dev/errors.d.ts +0 -2
  24. package/dist/dev/errors.js +1 -2
  25. package/dist/dev/hooks.d.ts +0 -2
  26. package/dist/dev/hooks.js +1 -2
  27. package/dist/dev/memory.d.ts +11 -2
  28. package/dist/dev/memory.js +39 -12
  29. package/dist/dev/memory.js.map +1 -1
  30. package/dist/dev/protocol.d.ts +0 -2
  31. package/dist/dev/protocol.js +14 -14
  32. package/dist/dev/protocol.js.map +1 -1
  33. package/dist/dev/redis/client.d.ts +0 -2
  34. package/dist/dev/redis/client.js +1 -2
  35. package/dist/dev/redis/scripts.js +1 -2
  36. package/dist/dev/redis/topics.d.ts +6 -3
  37. package/dist/dev/redis/topics.js +5 -3
  38. package/dist/dev/redis/topics.js.map +1 -1
  39. package/dist/dev/redis.d.ts +0 -2
  40. package/dist/dev/redis.js +1 -2
  41. package/dist/dev/server/http.d.ts +0 -2
  42. package/dist/dev/server/http.js +38 -26
  43. package/dist/dev/server/http.js.map +1 -1
  44. package/dist/dev/server/hub.d.ts +0 -2
  45. package/dist/dev/server/hub.js +67 -41
  46. package/dist/dev/server/hub.js.map +1 -1
  47. package/dist/dev/server/index.d.ts +2 -4
  48. package/dist/dev/server/index.js +1 -2
  49. package/dist/dev/server/index.js.map +1 -1
  50. package/dist/dev/server/limits.d.ts +4 -3
  51. package/dist/dev/server/limits.js +1 -2
  52. package/dist/dev/server/limits.js.map +1 -1
  53. package/dist/dev/server/rules.d.ts +0 -2
  54. package/dist/dev/server/rules.js +1 -2
  55. package/dist/dev/server/rules.js.map +1 -1
  56. package/dist/dev/testing.d.ts +0 -2
  57. package/dist/dev/testing.js +1 -2
  58. package/dist/prod/client/core.js +1 -1
  59. package/dist/prod/client/core.js.map +1 -1
  60. package/dist/prod/client/sse.js +1 -4
  61. package/dist/prod/client/sse.js.map +1 -1
  62. package/dist/prod/client/util.js +1 -1
  63. package/dist/prod/client/util.js.map +1 -1
  64. package/dist/prod/cloudflare/link.js +1 -1
  65. package/dist/prod/cloudflare/link.js.map +1 -1
  66. package/dist/prod/memory.js +1 -1
  67. package/dist/prod/memory.js.map +1 -1
  68. package/dist/prod/protocol.js +1 -1
  69. package/dist/prod/protocol.js.map +1 -1
  70. package/dist/prod/redis/topics.js +1 -1
  71. package/dist/prod/redis/topics.js.map +1 -1
  72. package/dist/prod/server/http.js +1 -1
  73. package/dist/prod/server/http.js.map +1 -1
  74. package/dist/prod/server/hub.js +1 -1
  75. package/dist/prod/server/hub.js.map +1 -1
  76. package/dist/prod/server/index.js.map +1 -1
  77. package/dist/prod/server/limits.js +1 -1
  78. package/dist/prod/server/limits.js.map +1 -1
  79. package/dist/prod/server/rules.js +1 -1
  80. package/dist/prod/server/rules.js.map +1 -1
  81. package/package.json +6 -5
@@ -1 +1 @@
1
- {"version":3,"file":"limits.js","names":["DEFAULT_HUB_LIMITS","MAX_HISTORY","resolveLimits","out","name","value","given","floor","\"history\"","\"presenceTtlMs\"","\"presenceHeartbeatMs\"","TokenBucket","#rate","rate","#burst","burst","#tokens","#at","now","elapsed","WindowCounter","#limit","limit","#windowMs","windowMs","#times","cutoff"],"sources":["../../../src/server/limits.ts"],"sourcesContent":["/**\n * The limits of a hub. Every one is bounded, so a hub cannot be made to hold unbounded state (see the realtime protocol reference).\n * @since 1.0.0\n */\nexport interface HubLimits {\n /** Largest frame, in UTF-8 bytes, either way. Default 65,536. */\n readonly maxMessageBytes: number;\n /** Channels one connection may subscribe to. Default 64. */\n readonly maxChannelsPerConnection: number;\n /** Subscribers one channel may have on this server. Default 50,000. */\n readonly maxSubscribersPerChannel: number;\n /** Presence members one channel may have, across servers. Default 1,000. */\n readonly maxPresenceMembers: number;\n /** Largest presence state, in bytes of JSON. Default 1,024. */\n readonly maxStateBytes: number;\n /** Requests one connection may have in flight. Default 32. */\n readonly maxPendingRequests: number;\n /** Frames one connection may have waiting to be handled. Default 256. */\n readonly maxQueuedFrames: number;\n /** Bytes the transport may hold unsent before the connection is closed as a slow consumer. Default 1 MiB. */\n readonly maxBufferedBytes: number;\n /** Publishes per second per connection. Default 20. */\n readonly publishesPerSecond: number;\n /** Largest burst of publishes per connection. Default 40. */\n readonly publishBurst: number;\n /** Publishes per second per channel on this server. Default 500. */\n readonly channelPublishesPerSecond: number;\n /** Milliseconds a request may run. Default 10,000. */\n readonly requestTimeoutMs: number;\n /** Messages kept per channel unless a rule says otherwise. Default 100. */\n readonly history: number;\n /** Milliseconds a message is kept. Default 300,000. */\n readonly historyTtlMs: number;\n /** Milliseconds an idempotency key is remembered. Default 60,000. */\n readonly idempotencyTtlMs: number;\n /** Milliseconds a presence member lives without a heartbeat. Default 45,000. */\n readonly presenceTtlMs: number;\n /** Milliseconds between the heartbeats a client is asked to send (`ready.hb`). Default 30,000. */\n readonly heartbeatMs: number;\n /** Milliseconds between presence heartbeats (and reaping). Default 15,000. */\n readonly presenceHeartbeatMs: number;\n}\n\n/** The defaults of {@link HubLimits}. @since 1.0.0 */\nexport const DEFAULT_HUB_LIMITS: HubLimits = Object.freeze({\n maxMessageBytes: 65_536,\n maxChannelsPerConnection: 64,\n maxSubscribersPerChannel: 50_000,\n maxPresenceMembers: 1_000,\n maxStateBytes: 1_024,\n maxPendingRequests: 32,\n maxQueuedFrames: 256,\n maxBufferedBytes: 1_048_576,\n publishesPerSecond: 20,\n publishBurst: 40,\n channelPublishesPerSecond: 500,\n requestTimeoutMs: 10_000,\n history: 100,\n historyTtlMs: 300_000,\n idempotencyTtlMs: 60_000,\n presenceTtlMs: 45_000,\n presenceHeartbeatMs: 15_000,\n heartbeatMs: 30_000,\n});\n\n/** The most messages a channel may retain. */\nexport const MAX_HISTORY = 10_000;\n\n/**\n * Validates and fills in the limits.\n * @param given The limits the app set.\n * @returns The complete limits.\n * @throws RangeError for a limit that is not a positive integer (a history of 0 is allowed).\n */\nexport function resolveLimits(given: Partial<HubLimits> | undefined): HubLimits {\n const out: Record<string, number> = { ...DEFAULT_HUB_LIMITS };\n for (const [name, value] of Object.entries(given ?? {})) {\n if (!(name in DEFAULT_HUB_LIMITS)) throw new TypeError(`unknown limit \"${name}\".`);\n if (value === undefined) continue;\n const floor = name === \"history\" ? 0 : 1;\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < floor)\n throw new RangeError(\n `limits.${name} must be ${floor === 0 ? \"a non-negative\" : \"a positive\"} integer.`,\n );\n out[name] = value;\n }\n if ((out[\"history\"] as number) > MAX_HISTORY)\n throw new RangeError(`limits.history must be at most ${MAX_HISTORY}.`);\n if ((out[\"presenceTtlMs\"] as number) <= (out[\"presenceHeartbeatMs\"] as number))\n throw new RangeError(\"limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs.\");\n return out as unknown as HubLimits;\n}\n\n/** A token bucket: `rate` tokens per second up to `burst`. */\nexport class TokenBucket {\n #tokens: number;\n #at: number;\n readonly #rate: number;\n readonly #burst: number;\n\n constructor(rate: number, burst: number, now: number) {\n this.#rate = rate;\n this.#burst = burst;\n this.#tokens = burst;\n this.#at = now;\n }\n\n /** Takes one token; false when the bucket is empty. */\n take(now: number): boolean {\n const elapsed = Math.max(0, now - this.#at);\n this.#at = now;\n this.#tokens = Math.min(this.#burst, this.#tokens + (elapsed * this.#rate) / 1000);\n if (this.#tokens < 1) return false;\n this.#tokens -= 1;\n return true;\n }\n}\n\n/** Counts events inside a sliding window; `hit` returns true once the count reaches the limit. */\nexport class WindowCounter {\n readonly #times: number[] = [];\n readonly #limit: number;\n readonly #windowMs: number;\n\n constructor(limit: number, windowMs: number) {\n this.#limit = limit;\n this.#windowMs = windowMs;\n }\n\n hit(now: number): boolean {\n this.#times.push(now);\n const cutoff = now - this.#windowMs;\n while (this.#times.length > 0 && (this.#times[0] as number) <= cutoff) this.#times.shift();\n return this.#times.length >= this.#limit;\n }\n}\n"],"mappings":"AA4CA,MAAaA,EAAgC,OAAO,OAAO,CACzD,gBAAiB,MACjB,yBAA0B,GAC1B,yBAA0B,IAC1B,mBAAoB,IACpB,cAAe,KACf,mBAAoB,GACpB,gBAAiB,IACjB,iBAAkB,QAClB,mBAAoB,GACpB,aAAc,GACd,0BAA2B,IAC3B,iBAAkB,IAClB,QAAS,IACT,aAAc,IACd,iBAAkB,IAClB,cAAe,KACf,oBAAqB,KACrB,YAAa,GACf,CAAC,EAGYC,EAAc,IAQ3B,SAAgBC,EAAc,EAAkD,CAC9E,IAAMC,EAA8B,CAAE,GAAGH,CAAmB,EAC5D,IAAK,GAAM,CAACI,EAAMC,KAAU,OAAO,QAAQC,GAAS,CAAC,CAAC,EAAG,CACvD,GAAI,EAAEF,KAAQJ,GAAqB,MAAU,UAAU,kBAAkBI,EAAK,GAAG,EACjF,GAAIC,IAAU,IAAA,GAAW,SACzB,IAAME,EAAQH,IAAS,UAAY,EAAI,EACvC,GAAI,OAAOC,GAAU,UAAY,CAAC,OAAO,UAAUA,CAAK,GAAKA,EAAQE,EACnE,MAAU,WACR,UAAUH,EAAK,WAAWG,IAAU,EAAI,iBAAmB,aAAa,UAC1E,EACF,EAAIH,GAAQC,CACd,CACA,GAAKF,EAAIK,QAAA,IACP,MAAU,WAAW,kCAAkCP,EAAY,EAAE,EACvE,GAAKE,EAAIM,eAAgCN,EAAIO,oBAC3C,MAAU,WAAW,sEAAsE,EAC7F,OAAOP,CACT,CAGA,IAAaQ,EAAb,KAAyB,CACvB,GACA,GACA,GACA,GAEA,YAAY,EAAc,EAAe,EAAa,CACpD,KAAKC,GAAQC,EACb,KAAKC,GAASC,EACd,KAAKC,GAAUD,EACf,KAAKE,GAAMC,CACb,CAGA,KAAK,EAAsB,CACzB,IAAMC,EAAU,KAAK,IAAI,EAAGD,EAAM,KAAKD,EAAG,EAK1C,MAJA,MAAKA,GAAMC,EACX,KAAKF,GAAU,KAAK,IAAI,KAAKF,GAAQ,KAAKE,GAAWG,EAAU,KAAKP,GAAS,GAAI,EAC7E,KAAKI,GAAU,EAAU,IAC7B,OAAKA,GACE,GACT,CACF,EAGaI,EAAb,KAA2B,CACzB,GAA4B,CAAC,EAC7B,GACA,GAEA,YAAY,EAAe,EAAkB,CAC3C,KAAKC,GAASC,EACd,KAAKC,GAAYC,CACnB,CAEA,IAAI,EAAsB,CACxB,KAAKC,GAAO,KAAKP,CAAG,EACpB,IAAMQ,EAASR,EAAM,KAAKK,GAC1B,KAAO,KAAKE,GAAO,OAAS,GAAM,KAAKA,GAAO,IAAiBC,GAAQ,KAAKD,GAAO,MAAM,EACzF,OAAO,KAAKA,GAAO,QAAU,KAAKJ,EACpC,CACF,E"}
1
+ {"version":3,"file":"limits.js","names":["DEFAULT_HUB_LIMITS","MAX_HISTORY","resolveLimits","out","name","value","given","floor","\"history\"","\"presenceTtlMs\"","\"presenceHeartbeatMs\"","TokenBucket","#rate","rate","#burst","burst","#tokens","#at","now","elapsed","WindowCounter","#limit","limit","#windowMs","windowMs","#times","cutoff"],"sources":["../../../src/server/limits.ts"],"sourcesContent":["/**\n * The limits of a hub. Every one is bounded, so a hub cannot be made to hold unbounded state (see the realtime protocol reference).\n * @since 1.0.0\n */\nexport interface HubLimits {\n /** Largest frame, in UTF-8 bytes, either way. Default 65,536. */\n readonly maxMessageBytes: number;\n /** Channels one connection may subscribe to. Default 64. */\n readonly maxChannelsPerConnection: number;\n /** Subscribers one channel may have on this server. Default 50,000. */\n readonly maxSubscribersPerChannel: number;\n /** Presence members one channel may have, across servers. Default 1,000. */\n readonly maxPresenceMembers: number;\n /** Largest presence state, in bytes of JSON. Default 1,024. */\n readonly maxStateBytes: number;\n /** Requests one connection may have in flight. Default 32. */\n readonly maxPendingRequests: number;\n /** Frames one connection may have waiting to be handled. Default 256. */\n readonly maxQueuedFrames: number;\n /** Bytes the transport may hold unsent before the connection is closed as a slow consumer. Default 1 MiB. */\n readonly maxBufferedBytes: number;\n /**\n * Publishes per second per connection. Default 20. It is also the refill rate of the bucket that `sub` and `unsub` frames spend\n * from (burst: the larger of `publishBurst` and `maxChannelsPerConnection`), so subscription churn is limited like publishing.\n */\n readonly publishesPerSecond: number;\n /** Largest burst of publishes per connection. Default 40. */\n readonly publishBurst: number;\n /** Publishes per second per channel on this server. Default 500. */\n readonly channelPublishesPerSecond: number;\n /** Milliseconds a request may run. Default 10,000. */\n readonly requestTimeoutMs: number;\n /** Messages kept per channel unless a rule says otherwise. Default 100. */\n readonly history: number;\n /** Milliseconds a message is kept. Default 300,000. */\n readonly historyTtlMs: number;\n /** Milliseconds an idempotency key is remembered. Default 60,000. */\n readonly idempotencyTtlMs: number;\n /** Milliseconds a presence member lives without a heartbeat. Default 45,000. */\n readonly presenceTtlMs: number;\n /** Milliseconds between the heartbeats a client is asked to send (`ready.hb`). Default 30,000. */\n readonly heartbeatMs: number;\n /** Milliseconds between presence heartbeats (and reaping). Default 15,000. */\n readonly presenceHeartbeatMs: number;\n}\n\n/** The defaults of {@link HubLimits}. @since 1.0.0 */\nexport const DEFAULT_HUB_LIMITS: HubLimits = Object.freeze({\n maxMessageBytes: 65_536,\n maxChannelsPerConnection: 64,\n maxSubscribersPerChannel: 50_000,\n maxPresenceMembers: 1_000,\n maxStateBytes: 1_024,\n maxPendingRequests: 32,\n maxQueuedFrames: 256,\n maxBufferedBytes: 1_048_576,\n publishesPerSecond: 20,\n publishBurst: 40,\n channelPublishesPerSecond: 500,\n requestTimeoutMs: 10_000,\n history: 100,\n historyTtlMs: 300_000,\n idempotencyTtlMs: 60_000,\n presenceTtlMs: 45_000,\n presenceHeartbeatMs: 15_000,\n heartbeatMs: 30_000,\n});\n\n/** The most messages a channel may retain. */\nexport const MAX_HISTORY = 10_000;\n\n/**\n * Validates and fills in the limits.\n * @param given The limits the app set.\n * @returns The complete limits.\n * @throws RangeError for a limit that is not a positive integer (a history of 0 is allowed).\n */\nexport function resolveLimits(given: Partial<HubLimits> | undefined): HubLimits {\n const out: Record<string, number> = { ...DEFAULT_HUB_LIMITS };\n for (const [name, value] of Object.entries(given ?? {})) {\n if (!(name in DEFAULT_HUB_LIMITS))\n throw new TypeError(__DEV__ ? `unknown limit \"${name}\".` : \"REALTIME_LIMIT_2\");\n if (value === undefined) continue;\n const floor = name === \"history\" ? 0 : 1;\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < floor)\n throw new RangeError(\n __DEV__\n ? `limits.${name} must be ${floor === 0 ? \"a non-negative\" : \"a positive\"} integer.`\n : \"REALTIME_LIMIT_4\",\n );\n out[name] = value;\n }\n if ((out[\"history\"] as number) > MAX_HISTORY)\n throw new RangeError(\n __DEV__ ? `limits.history must be at most ${MAX_HISTORY}.` : \"REALTIME_LIMIT_1\",\n );\n if ((out[\"presenceTtlMs\"] as number) <= (out[\"presenceHeartbeatMs\"] as number))\n throw new RangeError(\n __DEV__\n ? \"limits.presenceTtlMs must be larger than limits.presenceHeartbeatMs.\"\n : \"REALTIME_LIMIT_3\",\n );\n return out as unknown as HubLimits;\n}\n\n/** A token bucket: `rate` tokens per second up to `burst`. */\nexport class TokenBucket {\n #tokens: number;\n #at: number;\n readonly #rate: number;\n readonly #burst: number;\n\n constructor(rate: number, burst: number, now: number) {\n this.#rate = rate;\n this.#burst = burst;\n this.#tokens = burst;\n this.#at = now;\n }\n\n /** Takes one token; false when the bucket is empty. */\n take(now: number): boolean {\n const elapsed = Math.max(0, now - this.#at);\n this.#at = now;\n this.#tokens = Math.min(this.#burst, this.#tokens + (elapsed * this.#rate) / 1000);\n if (this.#tokens < 1) return false;\n this.#tokens -= 1;\n return true;\n }\n}\n\n/** Counts events inside a sliding window; `hit` returns true once the count reaches the limit. */\nexport class WindowCounter {\n readonly #times: number[] = [];\n readonly #limit: number;\n readonly #windowMs: number;\n\n constructor(limit: number, windowMs: number) {\n this.#limit = limit;\n this.#windowMs = windowMs;\n }\n\n hit(now: number): boolean {\n this.#times.push(now);\n const cutoff = now - this.#windowMs;\n while (this.#times.length > 0 && (this.#times[0] as number) <= cutoff) this.#times.shift();\n return this.#times.length >= this.#limit;\n }\n}\n"],"mappings":"AA+CA,MAAaA,EAAgC,OAAO,OAAO,CACzD,gBAAiB,MACjB,yBAA0B,GAC1B,yBAA0B,IAC1B,mBAAoB,IACpB,cAAe,KACf,mBAAoB,GACpB,gBAAiB,IACjB,iBAAkB,QAClB,mBAAoB,GACpB,aAAc,GACd,0BAA2B,IAC3B,iBAAkB,IAClB,QAAS,IACT,aAAc,IACd,iBAAkB,IAClB,cAAe,KACf,oBAAqB,KACrB,YAAa,GACf,CAAC,EAGYC,EAAc,IAQ3B,SAAgBC,EAAc,EAAkD,CAC9E,IAAMC,EAA8B,CAAE,GAAGH,CAAmB,EAC5D,IAAK,GAAM,CAACI,EAAMC,KAAU,OAAO,QAAQC,GAAS,CAAC,CAAC,EAAG,CACvD,GAAI,EAAEF,KAAQJ,GACZ,MAAU,UAAiD,kBAAkB,EAC/E,GAAIK,IAAU,IAAA,GAAW,SACzB,IAAME,EAAQH,IAAS,UAAY,EAAI,EACvC,GAAI,OAAOC,GAAU,UAAY,CAAC,OAAO,UAAUA,CAAK,GAAKA,EAAQE,EACnE,MAAU,WAGJ,kBACN,EACF,EAAIH,GAAQC,CACd,CACA,GAAKF,EAAIK,QAAA,IACP,MAAU,WACqD,kBAC/D,EACF,GAAKL,EAAIM,eAAgCN,EAAIO,oBAC3C,MAAU,WAGJ,kBACN,EACF,OAAOP,CACT,CAGA,IAAaQ,EAAb,KAAyB,CACvB,GACA,GACA,GACA,GAEA,YAAY,EAAc,EAAe,EAAa,CACpD,KAAKC,GAAQC,EACb,KAAKC,GAASC,EACd,KAAKC,GAAUD,EACf,KAAKE,GAAMC,CACb,CAGA,KAAK,EAAsB,CACzB,IAAMC,EAAU,KAAK,IAAI,EAAGD,EAAM,KAAKD,EAAG,EAK1C,MAJA,MAAKA,GAAMC,EACX,KAAKF,GAAU,KAAK,IAAI,KAAKF,GAAQ,KAAKE,GAAWG,EAAU,KAAKP,GAAS,GAAI,EAC7E,KAAKI,GAAU,EAAU,IAC7B,OAAKA,GACE,GACT,CACF,EAGaI,EAAb,KAA2B,CACzB,GAA4B,CAAC,EAC7B,GACA,GAEA,YAAY,EAAe,EAAkB,CAC3C,KAAKC,GAASC,EACd,KAAKC,GAAYC,CACnB,CAEA,IAAI,EAAsB,CACxB,KAAKC,GAAO,KAAKP,CAAG,EACpB,IAAMQ,EAASR,EAAM,KAAKK,GAC1B,KAAO,KAAKE,GAAO,OAAS,GAAM,KAAKA,GAAO,IAAiBC,GAAQ,KAAKD,GAAO,MAAM,EACzF,OAAO,KAAKA,GAAO,QAAU,KAAKJ,EACpC,CACF,E"}
@@ -1,2 +1,2 @@
1
- const e=/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/,t=/^\{\.\.\.([A-Za-z_][A-Za-z0-9_]*)\}$/,n=/^[A-Za-z0-9_.@/-]+$/;function r(r,i){if(typeof r!=`string`||r.length===0||r.length>128)throw TypeError(`the channel pattern "${String(r)}" must be 1..128 characters.`);let a=r.split(`:`),o=/* @__PURE__ */ new Set,s=a.map((i,s)=>{let c=t.exec(i);if(c!==null){if(s!==a.length-1)throw TypeError(`the channel pattern "${r}": {...name} must be the last segment.`);let e=c[1];if(o.has(e))throw TypeError(`the channel pattern "${r}" repeats "${e}".`);return o.add(e),{kind:`rest`,name:e}}let l=e.exec(i);if(l!==null){let e=l[1];if(o.has(e))throw TypeError(`the channel pattern "${r}" repeats "${e}".`);return o.add(e),{kind:`param`,name:e}}if(!n.test(i)||i.startsWith(`$`))throw TypeError(`the channel pattern "${r}" has an invalid segment "${i}".`);return{kind:`literal`,text:i}});if(i.history!==void 0&&(!Number.isInteger(i.history)||i.history<0))throw RangeError(`the rule for "${r}": history must be a non-negative integer.`);return{pattern:r,parts:s,rule:i}}function i(e,t){let n=t.split(`:`);if(n.includes(``))return;let{parts:r}=e;if(r[r.length-1]?.kind===`rest`?n.length<r.length:n.length!==r.length)return;let i={};for(let e=0;e<r.length;e+=1){let t=r[e],a=n[e];if(t.kind===`rest`)return i[t.name]=n.slice(e).join(`:`),i;if(t.kind===`literal`){if(a!==t.text)return}else i[t.name]=a}return i}export{r as compileRule,i as matchRule};
1
+ const e=/^\{([A-Za-z_][A-Za-z0-9_]*)\}$/,t=/^\{\.\.\.([A-Za-z_][A-Za-z0-9_]*)\}$/,n=/^[A-Za-z0-9_.@/-]+$/;function r(r,i){if(typeof r!=`string`||r.length===0||r.length>128)throw TypeError(`REALTIME_PATTERN_1`);let a=r.split(`:`),o=/* @__PURE__ */ new Set,s=a.map((r,i)=>{let s=t.exec(r);if(s!==null){if(i!==a.length-1)throw TypeError(`REALTIME_PATTERN_3`);let e=s[1];if(o.has(e))throw TypeError(`REALTIME_PATTERN_2`);return o.add(e),{kind:`rest`,name:e}}let c=e.exec(r);if(c!==null){let e=c[1];if(o.has(e))throw TypeError(`REALTIME_PATTERN_2`);return o.add(e),{kind:`param`,name:e}}if(!n.test(r)||r.startsWith(`$`))throw TypeError(`REALTIME_PATTERN_4`);return{kind:`literal`,text:r}});if(i.history!==void 0&&(!Number.isInteger(i.history)||i.history<0))throw RangeError(`REALTIME_RULE`);return{pattern:r,parts:s,rule:i}}function i(e,t){let n=t.split(`:`);if(n.includes(``))return;let{parts:r}=e;if(r[r.length-1]?.kind===`rest`?n.length<r.length:n.length!==r.length)return;let i={};for(let e=0;e<r.length;e+=1){let t=r[e],a=n[e];if(t.kind===`rest`)return i[t.name]=n.slice(e).join(`:`),i;if(t.kind===`literal`){if(a!==t.text)return}else i[t.name]=a}return i}export{r as compileRule,i as matchRule};
2
2
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":["PARAM","REST","LITERAL","compileRule","pattern","segments","seen","parts","rest","segment","at","name","param","rule","matchRule","channel","compiled","params","part"],"sources":["../../../src/server/rules.ts"],"sourcesContent":["import type { Json } from \"../protocol\";\n\n/** Who a connection is. `user` identifies a person; other claims are the app's. @since 1.0.0 */\nexport interface Identity {\n readonly user?: string;\n readonly [claim: string]: unknown;\n}\n\n/** What a rule predicate is asked. @since 1.0.0 */\nexport interface RuleContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly op: \"subscribe\" | \"publish\" | \"presence\";\n}\n\n/** `true`, `false`, or a predicate that decides per connection. @since 1.0.0 */\nexport type Permission<Params extends Readonly<Record<string, string>> = Record<string, string>> =\n | boolean\n | ((context: RuleContext<Params>) => boolean | Promise<boolean>);\n\n/** What a request handler is given. @since 1.0.0 */\nexport interface RequestContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly event: string;\n readonly data: Json;\n /** Aborted when the connection closes, the request times out or the hub closes. */\n readonly signal: AbortSignal;\n}\n\n/** Answers a request. The returned value is the reply (JSON). @since 1.0.0 */\nexport type RequestHandler<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> = (context: RequestContext<Params>) => Json | undefined | Promise<Json | undefined>;\n\n/**\n * What a channel pattern allows. Every permission defaults to `false`: a channel nobody wrote a rule for is closed.\n * @since 1.0.0\n */\nexport interface ChannelRule<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly subscribe?: Permission<Params>;\n readonly publish?: Permission<Params>;\n readonly presence?: Permission<Params>;\n /** Messages kept for resume (default: the hub's `history` limit; 0 turns resume off for these channels). */\n readonly history?: number;\n /** Request handlers by event name. */\n readonly handlers?: Readonly<Record<string, RequestHandler<Params>>>;\n}\n\ntype Part =\n | { readonly kind: \"literal\"; readonly text: string }\n | { readonly kind: \"param\"; readonly name: string }\n | { readonly kind: \"rest\"; readonly name: string };\n\n/** A compiled rule. @internal */\nexport interface CompiledRule {\n readonly pattern: string;\n readonly parts: readonly Part[];\n readonly rule: ChannelRule;\n}\n\nconst PARAM = /^\\{([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst REST = /^\\{\\.\\.\\.([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst LITERAL = /^[A-Za-z0-9_.@/-]+$/;\n\n/**\n * Compiles a channel pattern: `:`-separated segments that are literals, `{name}` (one segment) or a final `{...name}` (one or\n * more segments).\n * @throws TypeError for an invalid pattern.\n */\nexport function compileRule(pattern: string, rule: ChannelRule): CompiledRule {\n if (typeof pattern !== \"string\" || pattern.length === 0 || pattern.length > 128)\n throw new TypeError(`the channel pattern \"${String(pattern)}\" must be 1..128 characters.`);\n const segments = pattern.split(\":\");\n const seen = new Set<string>();\n const parts = segments.map((segment, at): Part => {\n const rest = REST.exec(segment);\n if (rest !== null) {\n if (at !== segments.length - 1)\n throw new TypeError(\n `the channel pattern \"${pattern}\": {...name} must be the last segment.`,\n );\n const name = rest[1] as string;\n if (seen.has(name))\n throw new TypeError(`the channel pattern \"${pattern}\" repeats \"${name}\".`);\n seen.add(name);\n return { kind: \"rest\", name };\n }\n const param = PARAM.exec(segment);\n if (param !== null) {\n const name = param[1] as string;\n if (seen.has(name))\n throw new TypeError(`the channel pattern \"${pattern}\" repeats \"${name}\".`);\n seen.add(name);\n return { kind: \"param\", name };\n }\n if (!LITERAL.test(segment) || segment.startsWith(\"$\"))\n throw new TypeError(`the channel pattern \"${pattern}\" has an invalid segment \"${segment}\".`);\n return { kind: \"literal\", text: segment };\n });\n if (rule.history !== undefined && (!Number.isInteger(rule.history) || rule.history < 0))\n throw new RangeError(`the rule for \"${pattern}\": history must be a non-negative integer.`);\n return { pattern, parts, rule };\n}\n\n/** The params a name captures against a rule, or undefined when it does not match. */\nexport function matchRule(\n compiled: CompiledRule,\n channel: string,\n): Record<string, string> | undefined {\n const segments = channel.split(\":\");\n if (segments.includes(\"\")) return undefined;\n const { parts } = compiled;\n const last = parts[parts.length - 1];\n if (last?.kind === \"rest\" ? segments.length < parts.length : segments.length !== parts.length)\n return undefined;\n const params: Record<string, string> = {};\n for (let at = 0; at < parts.length; at += 1) {\n const part = parts[at] as Part;\n const segment = segments[at] as string;\n if (part.kind === \"rest\") {\n params[part.name] = segments.slice(at).join(\":\");\n return params;\n }\n if (part.kind === \"literal\") {\n if (segment !== part.text) return undefined;\n } else {\n params[part.name] = segment;\n }\n }\n return params;\n}\n"],"mappings":"AAqEA,MAAMA,EAAQ,iCACRC,EAAO,uCACPC,EAAU,sBAOhB,SAAgBC,EAAY,EAAiB,EAAiC,CAC5E,GAAI,OAAOC,GAAY,UAAYA,EAAQ,SAAW,GAAKA,EAAQ,OAAS,IAC1E,MAAU,UAAU,wBAAwB,OAAOA,CAAO,EAAE,6BAA6B,EAC3F,IAAMC,EAAWD,EAAQ,MAAM,GAAG,EAC5BE,kBAAO,IAAI,IACXC,EAAQF,EAAS,KAAK,EAAS,IAAa,CAChD,IAAMG,EAAOP,EAAK,KAAKQ,CAAO,EAC9B,GAAID,IAAS,KAAM,CACjB,GAAIE,IAAOL,EAAS,OAAS,EAC3B,MAAU,UACR,wBAAwBD,EAAQ,uCAClC,EACF,IAAMO,EAAOH,EAAK,GAClB,GAAIF,EAAK,IAAIK,CAAI,EACf,MAAU,UAAU,wBAAwBP,EAAQ,aAAaO,EAAK,GAAG,EAE3E,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,OAAQ,MAAK,CAC9B,CACA,IAAMC,EAAQZ,EAAM,KAAKS,CAAO,EAChC,GAAIG,IAAU,KAAM,CAClB,IAAMD,EAAOC,EAAM,GACnB,GAAIN,EAAK,IAAIK,CAAI,EACf,MAAU,UAAU,wBAAwBP,EAAQ,aAAaO,EAAK,GAAG,EAE3E,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,QAAS,MAAK,CAC/B,CACA,GAAI,CAACT,EAAQ,KAAKO,CAAO,GAAKA,EAAQ,WAAW,GAAG,EAClD,MAAU,UAAU,wBAAwBL,EAAQ,4BAA4BK,EAAQ,GAAG,EAC7F,MAAO,CAAE,KAAM,UAAW,KAAMA,CAAQ,CAC1C,CAAC,EACD,GAAII,EAAK,UAAY,IAAA,KAAc,CAAC,OAAO,UAAUA,EAAK,OAAO,GAAKA,EAAK,QAAU,GACnF,MAAU,WAAW,iBAAiBT,EAAQ,2CAA2C,EAC3F,MAAO,CAAE,UAAS,QAAO,MAAK,CAChC,CAGA,SAAgBU,EACd,EACA,EACoC,CACpC,IAAMT,EAAWU,EAAQ,MAAM,GAAG,EAClC,GAAIV,EAAS,SAAS,EAAE,EAAG,OAC3B,GAAM,CAAE,SAAUW,EAElB,GADaT,EAAMA,EAAM,OAAS,EAC1B,EAAE,OAAS,OAASF,EAAS,OAASE,EAAM,OAASF,EAAS,SAAWE,EAAM,OACrF,OACF,IAAMU,EAAiC,CAAC,EACxC,IAAK,IAAIP,EAAK,EAAGA,EAAKH,EAAM,OAAQ,GAAM,EAAG,CAC3C,IAAMW,EAAOX,EAAMG,GACbD,EAAUJ,EAASK,GACzB,GAAIQ,EAAK,OAAS,OAEhB,MADA,GAAOA,EAAK,MAAQb,EAAS,MAAMK,CAAE,CAAC,CAAC,KAAK,GAAG,EACxCO,EAET,GAAIC,EAAK,OAAS,UACZ,IAAAT,IAAYS,EAAK,KAAM,MAAO,KAElC,GAAOA,EAAK,MAAQT,CAExB,CACA,OAAOQ,CACT,C"}
1
+ {"version":3,"file":"rules.js","names":["PARAM","REST","LITERAL","compileRule","pattern","segments","seen","parts","rest","segment","at","name","param","rule","matchRule","channel","compiled","params","part"],"sources":["../../../src/server/rules.ts"],"sourcesContent":["import type { Json } from \"../protocol\";\n\n/** Who a connection is. `user` identifies a person; other claims are the app's. @since 1.0.0 */\nexport interface Identity {\n readonly user?: string;\n readonly [claim: string]: unknown;\n}\n\n/** What a rule predicate is asked. @since 1.0.0 */\nexport interface RuleContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly op: \"subscribe\" | \"publish\" | \"presence\";\n}\n\n/** `true`, `false`, or a predicate that decides per connection. @since 1.0.0 */\nexport type Permission<Params extends Readonly<Record<string, string>> = Record<string, string>> =\n | boolean\n | ((context: RuleContext<Params>) => boolean | Promise<boolean>);\n\n/** What a request handler is given. @since 1.0.0 */\nexport interface RequestContext<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly channel: string;\n readonly params: Params;\n readonly identity: Identity;\n readonly event: string;\n readonly data: Json;\n /** Aborted when the connection closes, the request times out or the hub closes. */\n readonly signal: AbortSignal;\n}\n\n/** Answers a request. The returned value is the reply (JSON). @since 1.0.0 */\nexport type RequestHandler<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> = (context: RequestContext<Params>) => Json | undefined | Promise<Json | undefined>;\n\n/**\n * What a channel pattern allows. Every permission defaults to `false`: a channel nobody wrote a rule for is closed.\n * @since 1.0.0\n */\nexport interface ChannelRule<\n Params extends Readonly<Record<string, string>> = Record<string, string>,\n> {\n readonly subscribe?: Permission<Params>;\n readonly publish?: Permission<Params>;\n readonly presence?: Permission<Params>;\n /** Messages kept for resume (default: the hub's `history` limit; 0 turns resume off for these channels). */\n readonly history?: number;\n /** Request handlers by event name. */\n readonly handlers?: Readonly<Record<string, RequestHandler<Params>>>;\n}\n\ntype Part =\n | { readonly kind: \"literal\"; readonly text: string }\n | { readonly kind: \"param\"; readonly name: string }\n | { readonly kind: \"rest\"; readonly name: string };\n\n/** A compiled rule. @internal */\nexport interface CompiledRule {\n readonly pattern: string;\n readonly parts: readonly Part[];\n readonly rule: ChannelRule;\n}\n\nconst PARAM = /^\\{([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst REST = /^\\{\\.\\.\\.([A-Za-z_][A-Za-z0-9_]*)\\}$/;\nconst LITERAL = /^[A-Za-z0-9_.@/-]+$/;\n\n/**\n * Compiles a channel pattern: `:`-separated segments that are literals, `{name}` (one segment) or a final `{...name}` (one or\n * more segments).\n * @throws TypeError for an invalid pattern.\n */\nexport function compileRule(pattern: string, rule: ChannelRule): CompiledRule {\n if (typeof pattern !== \"string\" || pattern.length === 0 || pattern.length > 128)\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${String(pattern)}\" must be 1..128 characters.`\n : \"REALTIME_PATTERN_1\",\n );\n const segments = pattern.split(\":\");\n const seen = new Set<string>();\n const parts = segments.map((segment, at): Part => {\n const rest = REST.exec(segment);\n if (rest !== null) {\n if (at !== segments.length - 1)\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${pattern}\": {...name} must be the last segment.`\n : \"REALTIME_PATTERN_3\",\n );\n const name = rest[1] as string;\n if (seen.has(name))\n throw new TypeError(\n __DEV__ ? `the channel pattern \"${pattern}\" repeats \"${name}\".` : \"REALTIME_PATTERN_2\",\n );\n seen.add(name);\n return { kind: \"rest\", name };\n }\n const param = PARAM.exec(segment);\n if (param !== null) {\n const name = param[1] as string;\n if (seen.has(name))\n throw new TypeError(\n __DEV__ ? `the channel pattern \"${pattern}\" repeats \"${name}\".` : \"REALTIME_PATTERN_2\",\n );\n seen.add(name);\n return { kind: \"param\", name };\n }\n if (!LITERAL.test(segment) || segment.startsWith(\"$\"))\n throw new TypeError(\n __DEV__\n ? `the channel pattern \"${pattern}\" has an invalid segment \"${segment}\".`\n : \"REALTIME_PATTERN_4\",\n );\n return { kind: \"literal\", text: segment };\n });\n if (rule.history !== undefined && (!Number.isInteger(rule.history) || rule.history < 0))\n throw new RangeError(\n __DEV__\n ? `the rule for \"${pattern}\": history must be a non-negative integer.`\n : \"REALTIME_RULE\",\n );\n return { pattern, parts, rule };\n}\n\n/** The params a name captures against a rule, or undefined when it does not match. */\nexport function matchRule(\n compiled: CompiledRule,\n channel: string,\n): Record<string, string> | undefined {\n const segments = channel.split(\":\");\n if (segments.includes(\"\")) return undefined;\n const { parts } = compiled;\n const last = parts[parts.length - 1];\n if (last?.kind === \"rest\" ? segments.length < parts.length : segments.length !== parts.length)\n return undefined;\n const params: Record<string, string> = {};\n for (let at = 0; at < parts.length; at += 1) {\n const part = parts[at] as Part;\n const segment = segments[at] as string;\n if (part.kind === \"rest\") {\n params[part.name] = segments.slice(at).join(\":\");\n return params;\n }\n if (part.kind === \"literal\") {\n if (segment !== part.text) return undefined;\n } else {\n params[part.name] = segment;\n }\n }\n return params;\n}\n"],"mappings":"AAqEA,MAAMA,EAAQ,iCACRC,EAAO,uCACPC,EAAU,sBAOhB,SAAgBC,EAAY,EAAiB,EAAiC,CAC5E,GAAI,OAAOC,GAAY,UAAYA,EAAQ,SAAW,GAAKA,EAAQ,OAAS,IAC1E,MAAU,UAGJ,oBACN,EACF,IAAMC,EAAWD,EAAQ,MAAM,GAAG,EAC5BE,kBAAO,IAAI,IACXC,EAAQF,EAAS,KAAK,EAAS,IAAa,CAChD,IAAMG,EAAOP,EAAK,KAAKQ,CAAO,EAC9B,GAAID,IAAS,KAAM,CACjB,GAAIE,IAAOL,EAAS,OAAS,EAC3B,MAAU,UAGJ,oBACN,EACF,IAAMM,EAAOH,EAAK,GAClB,GAAIF,EAAK,IAAIK,CAAI,EACf,MAAU,UAC0D,oBACpE,EAEF,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,OAAQ,MAAK,CAC9B,CACA,IAAMC,EAAQZ,EAAM,KAAKS,CAAO,EAChC,GAAIG,IAAU,KAAM,CAClB,IAAMD,EAAOC,EAAM,GACnB,GAAIN,EAAK,IAAIK,CAAI,EACf,MAAU,UAC0D,oBACpE,EAEF,OADAL,EAAK,IAAIK,CAAI,EACN,CAAE,KAAM,QAAS,MAAK,CAC/B,CACA,GAAI,CAACT,EAAQ,KAAKO,CAAO,GAAKA,EAAQ,WAAW,GAAG,EAClD,MAAU,UAGJ,oBACN,EACF,MAAO,CAAE,KAAM,UAAW,KAAMA,CAAQ,CAC1C,CAAC,EACD,GAAII,EAAK,UAAY,IAAA,KAAc,CAAC,OAAO,UAAUA,EAAK,OAAO,GAAKA,EAAK,QAAU,GACnF,MAAU,WAGJ,eACN,EACF,MAAO,CAAE,UAAS,QAAO,MAAK,CAChC,CAGA,SAAgBC,EACd,EACA,EACoC,CACpC,IAAMT,EAAWU,EAAQ,MAAM,GAAG,EAClC,GAAIV,EAAS,SAAS,EAAE,EAAG,OAC3B,GAAM,CAAE,SAAUW,EAElB,GADaT,EAAMA,EAAM,OAAS,EAC1B,EAAE,OAAS,OAASF,EAAS,OAASE,EAAM,OAASF,EAAS,SAAWE,EAAM,OACrF,OACF,IAAMU,EAAiC,CAAC,EACxC,IAAK,IAAIP,EAAK,EAAGA,EAAKH,EAAM,OAAQ,GAAM,EAAG,CAC3C,IAAMW,EAAOX,EAAMG,GACbD,EAAUJ,EAASK,GACzB,GAAIQ,EAAK,OAAS,OAEhB,MADA,GAAOA,EAAK,MAAQb,EAAS,MAAMK,CAAE,CAAC,CAAC,KAAK,GAAG,EACxCO,EAET,GAAIC,EAAK,OAAS,UACZ,IAAAT,IAAYS,EAAK,KAAM,MAAO,KAElC,GAAOA,EAAK,MAAQT,CAExB,CACA,OAAOQ,CACT,C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-js/realtime",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Typed channels, presence and resumable delivery over WebSocket or server-sent events",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,18 +77,19 @@
77
77
  "url": "https://github.com/shadowjs-dev/realtime/issues"
78
78
  },
79
79
  "peerDependencies": {
80
- "@shadow-js/core": "^1.0.0"
80
+ "@shadow-js/core": "^1.0.2"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@biomejs/biome": "2.5.15",
84
84
  "@resvg/resvg-wasm": "2.6.2",
85
- "@shadow-js/core": "^1.0.0",
86
- "@shadow-js/dusk": "^1.0.0",
85
+ "@shadow-js/core": "^1.0.2",
86
+ "@shadow-js/dusk": "^1.0.2",
87
87
  "@shadow-js/i18n": "*",
88
88
  "@shadow-js/router": "*",
89
- "@shadow-js/testing": "^1.0.0",
89
+ "@shadow-js/testing": "^1.0.2",
90
90
  "@testing-library/dom": "10.4.2",
91
91
  "@types/node": "^24.0.0",
92
+ "esbuild": "0.27.0",
92
93
  "fast-check": "^4.10.2",
93
94
  "happy-dom": "^21.0.3",
94
95
  "satori": "0.43.0",