partysocket
Advanced tools
Comparing version 0.0.0-66c5e5d to 0.0.0-66d07f5
@@ -1,20 +0,661 @@ | ||
import ReconnectingWebSocket from "./ws"; | ||
class PartySocket extends ReconnectingWebSocket { | ||
constructor(partySocketOptions) { | ||
const { host, room, protocol, query, protocols, ...socketOptions } = partySocketOptions; | ||
const _pk = crypto.randomUUID(); | ||
let url = `${protocol || (host.startsWith("localhost:") || host.startsWith("127.0.0.1:") ? "ws" : "wss")}://${host}/party/${room}`; | ||
if (query) { | ||
url += `?${new URLSearchParams({ ...query, _pk }).toString()}`; | ||
"use strict"; | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
// src/index.ts | ||
var src_exports = {}; | ||
__export(src_exports, { | ||
PartySocket: () => PartySocket, | ||
WebSocket: () => ReconnectingWebSocket, | ||
default: () => PartySocket | ||
}); | ||
module.exports = __toCommonJS(src_exports); | ||
// src/ws.ts | ||
if (!globalThis.EventTarget || !globalThis.Event) { | ||
console.error(` | ||
PartySocket requires a global 'EventTarget' class to be available! | ||
You can polyfill this global by adding this to your code before any partysocket imports: | ||
\`\`\` | ||
import 'partysocket/event-target-polyfill'; | ||
\`\`\` | ||
Please file an issue at https://github.com/partykit/partykit if you're still having trouble. | ||
`); | ||
} | ||
var ErrorEvent = class extends Event { | ||
message; | ||
error; | ||
constructor(error, target) { | ||
super("error", target); | ||
this.message = error.message; | ||
this.error = error; | ||
} | ||
}; | ||
var CloseEvent = class extends Event { | ||
code; | ||
reason; | ||
wasClean = true; | ||
constructor(code = 1e3, reason = "", target) { | ||
super("close", target); | ||
this.code = code; | ||
this.reason = reason; | ||
} | ||
}; | ||
var Events = { | ||
Event, | ||
ErrorEvent, | ||
CloseEvent | ||
}; | ||
function assert(condition, msg) { | ||
if (!condition) { | ||
throw new Error(msg); | ||
} | ||
} | ||
function cloneEventBrowser(e) { | ||
return new e.constructor(e.type, e); | ||
} | ||
function cloneEventNode(e) { | ||
if ("data" in e) { | ||
const evt2 = new MessageEvent(e.type, e); | ||
return evt2; | ||
} | ||
if ("code" in e || "reason" in e) { | ||
const evt2 = new CloseEvent( | ||
// @ts-expect-error we need to fix event/listener types | ||
e.code || 1999, | ||
// @ts-expect-error we need to fix event/listener types | ||
e.reason || "unknown reason", | ||
e | ||
); | ||
return evt2; | ||
} | ||
if ("error" in e) { | ||
const evt2 = new ErrorEvent(e.error, e); | ||
return evt2; | ||
} | ||
const evt = new Event(e.type, e); | ||
return evt; | ||
} | ||
var isNode = typeof process !== "undefined" && typeof process.versions?.node !== "undefined" && typeof document === "undefined"; | ||
var cloneEvent = isNode ? cloneEventNode : cloneEventBrowser; | ||
var DEFAULT = { | ||
maxReconnectionDelay: 1e4, | ||
minReconnectionDelay: 1e3 + Math.random() * 4e3, | ||
minUptime: 5e3, | ||
reconnectionDelayGrowFactor: 1.3, | ||
connectionTimeout: 4e3, | ||
maxRetries: Infinity, | ||
maxEnqueuedMessages: Infinity, | ||
startClosed: false, | ||
debug: false | ||
}; | ||
var didWarnAboutMissingWebSocket = false; | ||
var ReconnectingWebSocket = class _ReconnectingWebSocket extends EventTarget { | ||
_ws; | ||
_retryCount = -1; | ||
_uptimeTimeout; | ||
_connectTimeout; | ||
_shouldReconnect = true; | ||
_connectLock = false; | ||
_binaryType = "blob"; | ||
_closeCalled = false; | ||
_messageQueue = []; | ||
_debugLogger = console.log.bind(console); | ||
_url; | ||
_protocols; | ||
_options; | ||
constructor(url, protocols, options = {}) { | ||
super(); | ||
this._url = url; | ||
this._protocols = protocols; | ||
this._options = options; | ||
if (this._options.startClosed) { | ||
this._shouldReconnect = false; | ||
} | ||
if (this._options.debugLogger) { | ||
this._debugLogger = this._options.debugLogger; | ||
} | ||
this._connect(); | ||
} | ||
static get CONNECTING() { | ||
return 0; | ||
} | ||
static get OPEN() { | ||
return 1; | ||
} | ||
static get CLOSING() { | ||
return 2; | ||
} | ||
static get CLOSED() { | ||
return 3; | ||
} | ||
get CONNECTING() { | ||
return _ReconnectingWebSocket.CONNECTING; | ||
} | ||
get OPEN() { | ||
return _ReconnectingWebSocket.OPEN; | ||
} | ||
get CLOSING() { | ||
return _ReconnectingWebSocket.CLOSING; | ||
} | ||
get CLOSED() { | ||
return _ReconnectingWebSocket.CLOSED; | ||
} | ||
get binaryType() { | ||
return this._ws ? this._ws.binaryType : this._binaryType; | ||
} | ||
set binaryType(value) { | ||
this._binaryType = value; | ||
if (this._ws) { | ||
this._ws.binaryType = value; | ||
} | ||
} | ||
/** | ||
* Returns the number or connection retries | ||
*/ | ||
get retryCount() { | ||
return Math.max(this._retryCount, 0); | ||
} | ||
/** | ||
* The number of bytes of data that have been queued using calls to send() but not yet | ||
* transmitted to the network. This value resets to zero once all queued data has been sent. | ||
* This value does not reset to zero when the connection is closed; if you keep calling send(), | ||
* this will continue to climb. Read only | ||
*/ | ||
get bufferedAmount() { | ||
const bytes = this._messageQueue.reduce((acc, message) => { | ||
if (typeof message === "string") { | ||
acc += message.length; | ||
} else if (message instanceof Blob) { | ||
acc += message.size; | ||
} else { | ||
acc += message.byteLength; | ||
} | ||
return acc; | ||
}, 0); | ||
return bytes + (this._ws ? this._ws.bufferedAmount : 0); | ||
} | ||
/** | ||
* The extensions selected by the server. This is currently only the empty string or a list of | ||
* extensions as negotiated by the connection | ||
*/ | ||
get extensions() { | ||
return this._ws ? this._ws.extensions : ""; | ||
} | ||
/** | ||
* A string indicating the name of the sub-protocol the server selected; | ||
* this will be one of the strings specified in the protocols parameter when creating the | ||
* WebSocket object | ||
*/ | ||
get protocol() { | ||
return this._ws ? this._ws.protocol : ""; | ||
} | ||
/** | ||
* The current state of the connection; this is one of the Ready state constants | ||
*/ | ||
get readyState() { | ||
if (this._ws) { | ||
return this._ws.readyState; | ||
} | ||
return this._options.startClosed ? _ReconnectingWebSocket.CLOSED : _ReconnectingWebSocket.CONNECTING; | ||
} | ||
/** | ||
* The URL as resolved by the constructor | ||
*/ | ||
get url() { | ||
return this._ws ? this._ws.url : ""; | ||
} | ||
/** | ||
* Whether the websocket object is now in reconnectable state | ||
*/ | ||
get shouldReconnect() { | ||
return this._shouldReconnect; | ||
} | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to CLOSED | ||
*/ | ||
onclose = null; | ||
/** | ||
* An event listener to be called when an error occurs | ||
*/ | ||
onerror = null; | ||
/** | ||
* An event listener to be called when a message is received from the server | ||
*/ | ||
onmessage = null; | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to OPEN; | ||
* this indicates that the connection is ready to send and receive data | ||
*/ | ||
onopen = null; | ||
/** | ||
* Closes the WebSocket connection or connection attempt, if any. If the connection is already | ||
* CLOSED, this method does nothing | ||
*/ | ||
close(code = 1e3, reason) { | ||
this._closeCalled = true; | ||
this._shouldReconnect = false; | ||
this._clearTimeouts(); | ||
if (!this._ws) { | ||
this._debug("close enqueued: no ws instance"); | ||
return; | ||
} | ||
if (this._ws.readyState === this.CLOSED) { | ||
this._debug("close: already closed"); | ||
return; | ||
} | ||
this._ws.close(code, reason); | ||
} | ||
/** | ||
* Closes the WebSocket connection or connection attempt and connects again. | ||
* Resets retry counter; | ||
*/ | ||
reconnect(code, reason) { | ||
this._shouldReconnect = true; | ||
this._closeCalled = false; | ||
this._retryCount = -1; | ||
if (!this._ws || this._ws.readyState === this.CLOSED) { | ||
this._connect(); | ||
} else { | ||
url += `?_pk=${_pk}`; | ||
this._disconnect(code, reason); | ||
this._connect(); | ||
} | ||
super(url, protocols, socketOptions); | ||
} | ||
/** | ||
* Enqueue specified data to be transmitted to the server over the WebSocket connection | ||
*/ | ||
send(data) { | ||
if (this._ws && this._ws.readyState === this.OPEN) { | ||
this._debug("send", data); | ||
this._ws.send(data); | ||
} else { | ||
const { maxEnqueuedMessages = DEFAULT.maxEnqueuedMessages } = this._options; | ||
if (this._messageQueue.length < maxEnqueuedMessages) { | ||
this._debug("enqueue", data); | ||
this._messageQueue.push(data); | ||
} | ||
} | ||
} | ||
_debug(...args) { | ||
if (this._options.debug) { | ||
this._debugLogger("RWS>", ...args); | ||
} | ||
} | ||
_getNextDelay() { | ||
const { | ||
reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor, | ||
minReconnectionDelay = DEFAULT.minReconnectionDelay, | ||
maxReconnectionDelay = DEFAULT.maxReconnectionDelay | ||
} = this._options; | ||
let delay = 0; | ||
if (this._retryCount > 0) { | ||
delay = minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1); | ||
if (delay > maxReconnectionDelay) { | ||
delay = maxReconnectionDelay; | ||
} | ||
} | ||
this._debug("next delay", delay); | ||
return delay; | ||
} | ||
_wait() { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, this._getNextDelay()); | ||
}); | ||
} | ||
_getNextProtocols(protocolsProvider) { | ||
if (!protocolsProvider) return Promise.resolve(null); | ||
if (typeof protocolsProvider === "string" || Array.isArray(protocolsProvider)) { | ||
return Promise.resolve(protocolsProvider); | ||
} | ||
if (typeof protocolsProvider === "function") { | ||
const protocols = protocolsProvider(); | ||
if (!protocols) return Promise.resolve(null); | ||
if (typeof protocols === "string" || Array.isArray(protocols)) { | ||
return Promise.resolve(protocols); | ||
} | ||
if (protocols.then) { | ||
return protocols; | ||
} | ||
} | ||
throw Error("Invalid protocols"); | ||
} | ||
_getNextUrl(urlProvider) { | ||
if (typeof urlProvider === "string") { | ||
return Promise.resolve(urlProvider); | ||
} | ||
if (typeof urlProvider === "function") { | ||
const url = urlProvider(); | ||
if (typeof url === "string") { | ||
return Promise.resolve(url); | ||
} | ||
if (url.then) { | ||
return url; | ||
} | ||
} | ||
throw Error("Invalid URL"); | ||
} | ||
_connect() { | ||
if (this._connectLock || !this._shouldReconnect) { | ||
return; | ||
} | ||
this._connectLock = true; | ||
const { | ||
maxRetries = DEFAULT.maxRetries, | ||
connectionTimeout = DEFAULT.connectionTimeout | ||
} = this._options; | ||
if (this._retryCount >= maxRetries) { | ||
this._debug("max retries reached", this._retryCount, ">=", maxRetries); | ||
return; | ||
} | ||
this._retryCount++; | ||
this._debug("connect", this._retryCount); | ||
this._removeListeners(); | ||
this._wait().then( | ||
() => Promise.all([ | ||
this._getNextUrl(this._url), | ||
this._getNextProtocols(this._protocols || null) | ||
]) | ||
).then(([url, protocols]) => { | ||
if (this._closeCalled) { | ||
this._connectLock = false; | ||
return; | ||
} | ||
if (!this._options.WebSocket && typeof WebSocket === "undefined" && !didWarnAboutMissingWebSocket) { | ||
console.error(`\u203C\uFE0F No WebSocket implementation available. You should define options.WebSocket. | ||
For example, if you're using node.js, run \`npm install ws\`, and then in your code: | ||
import PartySocket from 'partysocket'; | ||
import WS from 'ws'; | ||
const partysocket = new PartySocket({ | ||
host: "127.0.0.1:1999", | ||
room: "test-room", | ||
WebSocket: WS | ||
}); | ||
`); | ||
didWarnAboutMissingWebSocket = true; | ||
} | ||
const WS = this._options.WebSocket || WebSocket; | ||
this._debug("connect", { url, protocols }); | ||
this._ws = protocols ? new WS(url, protocols) : new WS(url); | ||
this._ws.binaryType = this._binaryType; | ||
this._connectLock = false; | ||
this._addListeners(); | ||
this._connectTimeout = setTimeout( | ||
() => this._handleTimeout(), | ||
connectionTimeout | ||
); | ||
}).catch((err) => { | ||
this._connectLock = false; | ||
this._handleError(new Events.ErrorEvent(Error(err.message), this)); | ||
}); | ||
} | ||
_handleTimeout() { | ||
this._debug("timeout event"); | ||
this._handleError(new Events.ErrorEvent(Error("TIMEOUT"), this)); | ||
} | ||
_disconnect(code = 1e3, reason) { | ||
this._clearTimeouts(); | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._removeListeners(); | ||
try { | ||
this._ws.close(code, reason); | ||
this._handleClose(new Events.CloseEvent(code, reason, this)); | ||
} catch (error) { | ||
} | ||
} | ||
_acceptOpen() { | ||
this._debug("accept open"); | ||
this._retryCount = 0; | ||
} | ||
_handleOpen = (event) => { | ||
this._debug("open event"); | ||
const { minUptime = DEFAULT.minUptime } = this._options; | ||
clearTimeout(this._connectTimeout); | ||
this._uptimeTimeout = setTimeout(() => this._acceptOpen(), minUptime); | ||
assert(this._ws, "WebSocket is not defined"); | ||
this._ws.binaryType = this._binaryType; | ||
this._messageQueue.forEach((message) => this._ws?.send(message)); | ||
this._messageQueue = []; | ||
if (this.onopen) { | ||
this.onopen(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_handleMessage = (event) => { | ||
this._debug("message event"); | ||
if (this.onmessage) { | ||
this.onmessage(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_handleError = (event) => { | ||
this._debug("error event", event.message); | ||
this._disconnect( | ||
void 0, | ||
event.message === "TIMEOUT" ? "timeout" : void 0 | ||
); | ||
if (this.onerror) { | ||
this.onerror(event); | ||
} | ||
this._debug("exec error listeners"); | ||
this.dispatchEvent(cloneEvent(event)); | ||
this._connect(); | ||
}; | ||
_handleClose = (event) => { | ||
this._debug("close event"); | ||
this._clearTimeouts(); | ||
if (this._shouldReconnect) { | ||
this._connect(); | ||
} | ||
if (this.onclose) { | ||
this.onclose(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_removeListeners() { | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._debug("removeListeners"); | ||
this._ws.removeEventListener("open", this._handleOpen); | ||
this._ws.removeEventListener("close", this._handleClose); | ||
this._ws.removeEventListener("message", this._handleMessage); | ||
this._ws.removeEventListener("error", this._handleError); | ||
} | ||
_addListeners() { | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._debug("addListeners"); | ||
this._ws.addEventListener("open", this._handleOpen); | ||
this._ws.addEventListener("close", this._handleClose); | ||
this._ws.addEventListener("message", this._handleMessage); | ||
this._ws.addEventListener("error", this._handleError); | ||
} | ||
_clearTimeouts() { | ||
clearTimeout(this._connectTimeout); | ||
clearTimeout(this._uptimeTimeout); | ||
} | ||
}; | ||
// src/index.ts | ||
var valueIsNotNil = (keyValuePair) => keyValuePair[1] !== null && keyValuePair[1] !== void 0; | ||
function generateUUID() { | ||
if (typeof crypto !== "undefined" && crypto.randomUUID) { | ||
return crypto.randomUUID(); | ||
} | ||
let d = (/* @__PURE__ */ new Date()).getTime(); | ||
let d2 = typeof performance !== "undefined" && performance.now && performance.now() * 1e3 || 0; | ||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { | ||
let r = Math.random() * 16; | ||
if (d > 0) { | ||
r = (d + r) % 16 | 0; | ||
d = Math.floor(d / 16); | ||
} else { | ||
r = (d2 + r) % 16 | 0; | ||
d2 = Math.floor(d2 / 16); | ||
} | ||
return (c === "x" ? r : r & 3 | 8).toString(16); | ||
}); | ||
} | ||
function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) { | ||
const { | ||
host: rawHost, | ||
path: rawPath, | ||
protocol: rawProtocol, | ||
room, | ||
party, | ||
prefix, | ||
query | ||
} = partySocketOptions; | ||
let host = rawHost.replace(/^(http|https|ws|wss):\/\//, ""); | ||
if (host.endsWith("/")) { | ||
host = host.slice(0, -1); | ||
} | ||
if (rawPath && rawPath.startsWith("/")) { | ||
throw new Error("path must not start with a slash"); | ||
} | ||
const name = party ?? "main"; | ||
const path = rawPath ? `/${rawPath}` : ""; | ||
const protocol = rawProtocol || (host.startsWith("localhost:") || host.startsWith("127.0.0.1:") || host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.") && host.split(".")[1] >= "16" && host.split(".")[1] <= "31" || host.startsWith("[::ffff:7f00:1]:") ? ( | ||
// http / ws | ||
defaultProtocol | ||
) : ( | ||
// https / wss | ||
defaultProtocol + "s" | ||
)); | ||
const baseUrl = `${protocol}://${host}/${prefix || `parties/${name}/${room}`}${path}`; | ||
const makeUrl = (query2 = {}) => `${baseUrl}?${new URLSearchParams([ | ||
...Object.entries(defaultParams), | ||
...Object.entries(query2).filter(valueIsNotNil) | ||
])}`; | ||
const urlProvider = typeof query === "function" ? async () => makeUrl(await query()) : makeUrl(query); | ||
return { | ||
host, | ||
path, | ||
room, | ||
name, | ||
protocol, | ||
partyUrl: baseUrl, | ||
urlProvider | ||
}; | ||
} | ||
var PartySocket = class extends ReconnectingWebSocket { | ||
constructor(partySocketOptions) { | ||
const wsOptions = getWSOptions(partySocketOptions); | ||
super(wsOptions.urlProvider, wsOptions.protocols, wsOptions.socketOptions); | ||
this.partySocketOptions = partySocketOptions; | ||
this.setWSProperties(wsOptions); | ||
} | ||
_pk; | ||
_pkurl; | ||
name; | ||
room; | ||
host; | ||
path; | ||
updateProperties(partySocketOptions) { | ||
const wsOptions = getWSOptions({ | ||
...this.partySocketOptions, | ||
...partySocketOptions, | ||
host: partySocketOptions.host ?? this.host, | ||
room: partySocketOptions.room ?? this.room, | ||
path: partySocketOptions.path ?? this.path | ||
}); | ||
this._url = wsOptions.urlProvider; | ||
this._protocols = wsOptions.protocols; | ||
this._options = wsOptions.socketOptions; | ||
this.setWSProperties(wsOptions); | ||
} | ||
setWSProperties(wsOptions) { | ||
const { _pk, _pkurl, name, room, host, path } = wsOptions; | ||
this._pk = _pk; | ||
this._pkurl = _pkurl; | ||
this.name = name; | ||
this.room = room; | ||
this.host = host; | ||
this.path = path; | ||
} | ||
_pk; | ||
reconnect(code, reason) { | ||
if (!this.room || !this.host) { | ||
throw new Error( | ||
"The room and host must be set before connecting, use `updateProperties` method to set them or pass them to the constructor." | ||
); | ||
} | ||
super.reconnect(code, reason); | ||
} | ||
get id() { | ||
return this._pk; | ||
} | ||
/** | ||
* Exposes the static PartyKit room URL without applying query parameters. | ||
* To access the currently connected WebSocket url, use PartySocket#url. | ||
*/ | ||
get roomUrl() { | ||
return this._pkurl; | ||
} | ||
// a `fetch` method that uses (almost) the same options as `PartySocket` | ||
static async fetch(options, init) { | ||
const party = getPartyInfo(options, "http"); | ||
const url = typeof party.urlProvider === "string" ? party.urlProvider : await party.urlProvider(); | ||
const doFetch = options.fetch ?? fetch; | ||
return doFetch(url, init); | ||
} | ||
}; | ||
function getWSOptions(partySocketOptions) { | ||
const { | ||
id, | ||
host: _host, | ||
path: _path, | ||
party: _party, | ||
room: _room, | ||
protocol: _protocol, | ||
query: _query, | ||
protocols, | ||
...socketOptions | ||
} = partySocketOptions; | ||
const _pk = id || generateUUID(); | ||
const party = getPartyInfo(partySocketOptions, "ws", { _pk }); | ||
return { | ||
_pk, | ||
_pkurl: party.partyUrl, | ||
name: party.name, | ||
room: party.room, | ||
host: party.host, | ||
path: party.path, | ||
protocols, | ||
socketOptions, | ||
urlProvider: party.urlProvider | ||
}; | ||
} | ||
export { | ||
PartySocket as default | ||
}; | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
PartySocket, | ||
WebSocket | ||
}); | ||
/*! | ||
* Reconnecting WebSocket | ||
* by Pedro Ladaria <pedro.ladaria@gmail.com> | ||
* https://github.com/pladaria/reconnecting-websocket | ||
* License MIT | ||
*/ |
@@ -1,19 +0,781 @@ | ||
import PartySocket from "."; | ||
import { useRef, useEffect } from "react"; | ||
function usePartySocket(options) { | ||
const socketRef = useRef( | ||
new PartySocket({ | ||
...options, | ||
startClosed: true | ||
}) | ||
); | ||
useEffect(() => { | ||
socketRef.current.reconnect(); | ||
"use strict"; | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
// src/react.ts | ||
var react_exports = {}; | ||
__export(react_exports, { | ||
default: () => usePartySocket, | ||
usePartySocket: () => usePartySocket, | ||
useWebSocket: () => useWebSocket | ||
}); | ||
module.exports = __toCommonJS(react_exports); | ||
// src/ws.ts | ||
if (!globalThis.EventTarget || !globalThis.Event) { | ||
console.error(` | ||
PartySocket requires a global 'EventTarget' class to be available! | ||
You can polyfill this global by adding this to your code before any partysocket imports: | ||
\`\`\` | ||
import 'partysocket/event-target-polyfill'; | ||
\`\`\` | ||
Please file an issue at https://github.com/partykit/partykit if you're still having trouble. | ||
`); | ||
} | ||
var ErrorEvent = class extends Event { | ||
message; | ||
error; | ||
constructor(error, target) { | ||
super("error", target); | ||
this.message = error.message; | ||
this.error = error; | ||
} | ||
}; | ||
var CloseEvent = class extends Event { | ||
code; | ||
reason; | ||
wasClean = true; | ||
constructor(code = 1e3, reason = "", target) { | ||
super("close", target); | ||
this.code = code; | ||
this.reason = reason; | ||
} | ||
}; | ||
var Events = { | ||
Event, | ||
ErrorEvent, | ||
CloseEvent | ||
}; | ||
function assert(condition, msg) { | ||
if (!condition) { | ||
throw new Error(msg); | ||
} | ||
} | ||
function cloneEventBrowser(e) { | ||
return new e.constructor(e.type, e); | ||
} | ||
function cloneEventNode(e) { | ||
if ("data" in e) { | ||
const evt2 = new MessageEvent(e.type, e); | ||
return evt2; | ||
} | ||
if ("code" in e || "reason" in e) { | ||
const evt2 = new CloseEvent( | ||
// @ts-expect-error we need to fix event/listener types | ||
e.code || 1999, | ||
// @ts-expect-error we need to fix event/listener types | ||
e.reason || "unknown reason", | ||
e | ||
); | ||
return evt2; | ||
} | ||
if ("error" in e) { | ||
const evt2 = new ErrorEvent(e.error, e); | ||
return evt2; | ||
} | ||
const evt = new Event(e.type, e); | ||
return evt; | ||
} | ||
var isNode = typeof process !== "undefined" && typeof process.versions?.node !== "undefined" && typeof document === "undefined"; | ||
var cloneEvent = isNode ? cloneEventNode : cloneEventBrowser; | ||
var DEFAULT = { | ||
maxReconnectionDelay: 1e4, | ||
minReconnectionDelay: 1e3 + Math.random() * 4e3, | ||
minUptime: 5e3, | ||
reconnectionDelayGrowFactor: 1.3, | ||
connectionTimeout: 4e3, | ||
maxRetries: Infinity, | ||
maxEnqueuedMessages: Infinity, | ||
startClosed: false, | ||
debug: false | ||
}; | ||
var didWarnAboutMissingWebSocket = false; | ||
var ReconnectingWebSocket = class _ReconnectingWebSocket extends EventTarget { | ||
_ws; | ||
_retryCount = -1; | ||
_uptimeTimeout; | ||
_connectTimeout; | ||
_shouldReconnect = true; | ||
_connectLock = false; | ||
_binaryType = "blob"; | ||
_closeCalled = false; | ||
_messageQueue = []; | ||
_debugLogger = console.log.bind(console); | ||
_url; | ||
_protocols; | ||
_options; | ||
constructor(url, protocols, options = {}) { | ||
super(); | ||
this._url = url; | ||
this._protocols = protocols; | ||
this._options = options; | ||
if (this._options.startClosed) { | ||
this._shouldReconnect = false; | ||
} | ||
if (this._options.debugLogger) { | ||
this._debugLogger = this._options.debugLogger; | ||
} | ||
this._connect(); | ||
} | ||
static get CONNECTING() { | ||
return 0; | ||
} | ||
static get OPEN() { | ||
return 1; | ||
} | ||
static get CLOSING() { | ||
return 2; | ||
} | ||
static get CLOSED() { | ||
return 3; | ||
} | ||
get CONNECTING() { | ||
return _ReconnectingWebSocket.CONNECTING; | ||
} | ||
get OPEN() { | ||
return _ReconnectingWebSocket.OPEN; | ||
} | ||
get CLOSING() { | ||
return _ReconnectingWebSocket.CLOSING; | ||
} | ||
get CLOSED() { | ||
return _ReconnectingWebSocket.CLOSED; | ||
} | ||
get binaryType() { | ||
return this._ws ? this._ws.binaryType : this._binaryType; | ||
} | ||
set binaryType(value) { | ||
this._binaryType = value; | ||
if (this._ws) { | ||
this._ws.binaryType = value; | ||
} | ||
} | ||
/** | ||
* Returns the number or connection retries | ||
*/ | ||
get retryCount() { | ||
return Math.max(this._retryCount, 0); | ||
} | ||
/** | ||
* The number of bytes of data that have been queued using calls to send() but not yet | ||
* transmitted to the network. This value resets to zero once all queued data has been sent. | ||
* This value does not reset to zero when the connection is closed; if you keep calling send(), | ||
* this will continue to climb. Read only | ||
*/ | ||
get bufferedAmount() { | ||
const bytes = this._messageQueue.reduce((acc, message) => { | ||
if (typeof message === "string") { | ||
acc += message.length; | ||
} else if (message instanceof Blob) { | ||
acc += message.size; | ||
} else { | ||
acc += message.byteLength; | ||
} | ||
return acc; | ||
}, 0); | ||
return bytes + (this._ws ? this._ws.bufferedAmount : 0); | ||
} | ||
/** | ||
* The extensions selected by the server. This is currently only the empty string or a list of | ||
* extensions as negotiated by the connection | ||
*/ | ||
get extensions() { | ||
return this._ws ? this._ws.extensions : ""; | ||
} | ||
/** | ||
* A string indicating the name of the sub-protocol the server selected; | ||
* this will be one of the strings specified in the protocols parameter when creating the | ||
* WebSocket object | ||
*/ | ||
get protocol() { | ||
return this._ws ? this._ws.protocol : ""; | ||
} | ||
/** | ||
* The current state of the connection; this is one of the Ready state constants | ||
*/ | ||
get readyState() { | ||
if (this._ws) { | ||
return this._ws.readyState; | ||
} | ||
return this._options.startClosed ? _ReconnectingWebSocket.CLOSED : _ReconnectingWebSocket.CONNECTING; | ||
} | ||
/** | ||
* The URL as resolved by the constructor | ||
*/ | ||
get url() { | ||
return this._ws ? this._ws.url : ""; | ||
} | ||
/** | ||
* Whether the websocket object is now in reconnectable state | ||
*/ | ||
get shouldReconnect() { | ||
return this._shouldReconnect; | ||
} | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to CLOSED | ||
*/ | ||
onclose = null; | ||
/** | ||
* An event listener to be called when an error occurs | ||
*/ | ||
onerror = null; | ||
/** | ||
* An event listener to be called when a message is received from the server | ||
*/ | ||
onmessage = null; | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to OPEN; | ||
* this indicates that the connection is ready to send and receive data | ||
*/ | ||
onopen = null; | ||
/** | ||
* Closes the WebSocket connection or connection attempt, if any. If the connection is already | ||
* CLOSED, this method does nothing | ||
*/ | ||
close(code = 1e3, reason) { | ||
this._closeCalled = true; | ||
this._shouldReconnect = false; | ||
this._clearTimeouts(); | ||
if (!this._ws) { | ||
this._debug("close enqueued: no ws instance"); | ||
return; | ||
} | ||
if (this._ws.readyState === this.CLOSED) { | ||
this._debug("close: already closed"); | ||
return; | ||
} | ||
this._ws.close(code, reason); | ||
} | ||
/** | ||
* Closes the WebSocket connection or connection attempt and connects again. | ||
* Resets retry counter; | ||
*/ | ||
reconnect(code, reason) { | ||
this._shouldReconnect = true; | ||
this._closeCalled = false; | ||
this._retryCount = -1; | ||
if (!this._ws || this._ws.readyState === this.CLOSED) { | ||
this._connect(); | ||
} else { | ||
this._disconnect(code, reason); | ||
this._connect(); | ||
} | ||
} | ||
/** | ||
* Enqueue specified data to be transmitted to the server over the WebSocket connection | ||
*/ | ||
send(data) { | ||
if (this._ws && this._ws.readyState === this.OPEN) { | ||
this._debug("send", data); | ||
this._ws.send(data); | ||
} else { | ||
const { maxEnqueuedMessages = DEFAULT.maxEnqueuedMessages } = this._options; | ||
if (this._messageQueue.length < maxEnqueuedMessages) { | ||
this._debug("enqueue", data); | ||
this._messageQueue.push(data); | ||
} | ||
} | ||
} | ||
_debug(...args) { | ||
if (this._options.debug) { | ||
this._debugLogger("RWS>", ...args); | ||
} | ||
} | ||
_getNextDelay() { | ||
const { | ||
reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor, | ||
minReconnectionDelay = DEFAULT.minReconnectionDelay, | ||
maxReconnectionDelay = DEFAULT.maxReconnectionDelay | ||
} = this._options; | ||
let delay = 0; | ||
if (this._retryCount > 0) { | ||
delay = minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1); | ||
if (delay > maxReconnectionDelay) { | ||
delay = maxReconnectionDelay; | ||
} | ||
} | ||
this._debug("next delay", delay); | ||
return delay; | ||
} | ||
_wait() { | ||
return new Promise((resolve) => { | ||
setTimeout(resolve, this._getNextDelay()); | ||
}); | ||
} | ||
_getNextProtocols(protocolsProvider) { | ||
if (!protocolsProvider) return Promise.resolve(null); | ||
if (typeof protocolsProvider === "string" || Array.isArray(protocolsProvider)) { | ||
return Promise.resolve(protocolsProvider); | ||
} | ||
if (typeof protocolsProvider === "function") { | ||
const protocols = protocolsProvider(); | ||
if (!protocols) return Promise.resolve(null); | ||
if (typeof protocols === "string" || Array.isArray(protocols)) { | ||
return Promise.resolve(protocols); | ||
} | ||
if (protocols.then) { | ||
return protocols; | ||
} | ||
} | ||
throw Error("Invalid protocols"); | ||
} | ||
_getNextUrl(urlProvider) { | ||
if (typeof urlProvider === "string") { | ||
return Promise.resolve(urlProvider); | ||
} | ||
if (typeof urlProvider === "function") { | ||
const url = urlProvider(); | ||
if (typeof url === "string") { | ||
return Promise.resolve(url); | ||
} | ||
if (url.then) { | ||
return url; | ||
} | ||
} | ||
throw Error("Invalid URL"); | ||
} | ||
_connect() { | ||
if (this._connectLock || !this._shouldReconnect) { | ||
return; | ||
} | ||
this._connectLock = true; | ||
const { | ||
maxRetries = DEFAULT.maxRetries, | ||
connectionTimeout = DEFAULT.connectionTimeout | ||
} = this._options; | ||
if (this._retryCount >= maxRetries) { | ||
this._debug("max retries reached", this._retryCount, ">=", maxRetries); | ||
return; | ||
} | ||
this._retryCount++; | ||
this._debug("connect", this._retryCount); | ||
this._removeListeners(); | ||
this._wait().then( | ||
() => Promise.all([ | ||
this._getNextUrl(this._url), | ||
this._getNextProtocols(this._protocols || null) | ||
]) | ||
).then(([url, protocols]) => { | ||
if (this._closeCalled) { | ||
this._connectLock = false; | ||
return; | ||
} | ||
if (!this._options.WebSocket && typeof WebSocket === "undefined" && !didWarnAboutMissingWebSocket) { | ||
console.error(`\u203C\uFE0F No WebSocket implementation available. You should define options.WebSocket. | ||
For example, if you're using node.js, run \`npm install ws\`, and then in your code: | ||
import PartySocket from 'partysocket'; | ||
import WS from 'ws'; | ||
const partysocket = new PartySocket({ | ||
host: "127.0.0.1:1999", | ||
room: "test-room", | ||
WebSocket: WS | ||
}); | ||
`); | ||
didWarnAboutMissingWebSocket = true; | ||
} | ||
const WS = this._options.WebSocket || WebSocket; | ||
this._debug("connect", { url, protocols }); | ||
this._ws = protocols ? new WS(url, protocols) : new WS(url); | ||
this._ws.binaryType = this._binaryType; | ||
this._connectLock = false; | ||
this._addListeners(); | ||
this._connectTimeout = setTimeout( | ||
() => this._handleTimeout(), | ||
connectionTimeout | ||
); | ||
}).catch((err) => { | ||
this._connectLock = false; | ||
this._handleError(new Events.ErrorEvent(Error(err.message), this)); | ||
}); | ||
} | ||
_handleTimeout() { | ||
this._debug("timeout event"); | ||
this._handleError(new Events.ErrorEvent(Error("TIMEOUT"), this)); | ||
} | ||
_disconnect(code = 1e3, reason) { | ||
this._clearTimeouts(); | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._removeListeners(); | ||
try { | ||
this._ws.close(code, reason); | ||
this._handleClose(new Events.CloseEvent(code, reason, this)); | ||
} catch (error) { | ||
} | ||
} | ||
_acceptOpen() { | ||
this._debug("accept open"); | ||
this._retryCount = 0; | ||
} | ||
_handleOpen = (event) => { | ||
this._debug("open event"); | ||
const { minUptime = DEFAULT.minUptime } = this._options; | ||
clearTimeout(this._connectTimeout); | ||
this._uptimeTimeout = setTimeout(() => this._acceptOpen(), minUptime); | ||
assert(this._ws, "WebSocket is not defined"); | ||
this._ws.binaryType = this._binaryType; | ||
this._messageQueue.forEach((message) => this._ws?.send(message)); | ||
this._messageQueue = []; | ||
if (this.onopen) { | ||
this.onopen(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_handleMessage = (event) => { | ||
this._debug("message event"); | ||
if (this.onmessage) { | ||
this.onmessage(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_handleError = (event) => { | ||
this._debug("error event", event.message); | ||
this._disconnect( | ||
void 0, | ||
event.message === "TIMEOUT" ? "timeout" : void 0 | ||
); | ||
if (this.onerror) { | ||
this.onerror(event); | ||
} | ||
this._debug("exec error listeners"); | ||
this.dispatchEvent(cloneEvent(event)); | ||
this._connect(); | ||
}; | ||
_handleClose = (event) => { | ||
this._debug("close event"); | ||
this._clearTimeouts(); | ||
if (this._shouldReconnect) { | ||
this._connect(); | ||
} | ||
if (this.onclose) { | ||
this.onclose(event); | ||
} | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
_removeListeners() { | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._debug("removeListeners"); | ||
this._ws.removeEventListener("open", this._handleOpen); | ||
this._ws.removeEventListener("close", this._handleClose); | ||
this._ws.removeEventListener("message", this._handleMessage); | ||
this._ws.removeEventListener("error", this._handleError); | ||
} | ||
_addListeners() { | ||
if (!this._ws) { | ||
return; | ||
} | ||
this._debug("addListeners"); | ||
this._ws.addEventListener("open", this._handleOpen); | ||
this._ws.addEventListener("close", this._handleClose); | ||
this._ws.addEventListener("message", this._handleMessage); | ||
this._ws.addEventListener("error", this._handleError); | ||
} | ||
_clearTimeouts() { | ||
clearTimeout(this._connectTimeout); | ||
clearTimeout(this._uptimeTimeout); | ||
} | ||
}; | ||
// src/index.ts | ||
var valueIsNotNil = (keyValuePair) => keyValuePair[1] !== null && keyValuePair[1] !== void 0; | ||
function generateUUID() { | ||
if (typeof crypto !== "undefined" && crypto.randomUUID) { | ||
return crypto.randomUUID(); | ||
} | ||
let d = (/* @__PURE__ */ new Date()).getTime(); | ||
let d2 = typeof performance !== "undefined" && performance.now && performance.now() * 1e3 || 0; | ||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { | ||
let r = Math.random() * 16; | ||
if (d > 0) { | ||
r = (d + r) % 16 | 0; | ||
d = Math.floor(d / 16); | ||
} else { | ||
r = (d2 + r) % 16 | 0; | ||
d2 = Math.floor(d2 / 16); | ||
} | ||
return (c === "x" ? r : r & 3 | 8).toString(16); | ||
}); | ||
} | ||
function getPartyInfo(partySocketOptions, defaultProtocol, defaultParams = {}) { | ||
const { | ||
host: rawHost, | ||
path: rawPath, | ||
protocol: rawProtocol, | ||
room, | ||
party, | ||
prefix, | ||
query | ||
} = partySocketOptions; | ||
let host = rawHost.replace(/^(http|https|ws|wss):\/\//, ""); | ||
if (host.endsWith("/")) { | ||
host = host.slice(0, -1); | ||
} | ||
if (rawPath && rawPath.startsWith("/")) { | ||
throw new Error("path must not start with a slash"); | ||
} | ||
const name = party ?? "main"; | ||
const path = rawPath ? `/${rawPath}` : ""; | ||
const protocol = rawProtocol || (host.startsWith("localhost:") || host.startsWith("127.0.0.1:") || host.startsWith("192.168.") || host.startsWith("10.") || host.startsWith("172.") && host.split(".")[1] >= "16" && host.split(".")[1] <= "31" || host.startsWith("[::ffff:7f00:1]:") ? ( | ||
// http / ws | ||
defaultProtocol | ||
) : ( | ||
// https / wss | ||
defaultProtocol + "s" | ||
)); | ||
const baseUrl = `${protocol}://${host}/${prefix || `parties/${name}/${room}`}${path}`; | ||
const makeUrl = (query2 = {}) => `${baseUrl}?${new URLSearchParams([ | ||
...Object.entries(defaultParams), | ||
...Object.entries(query2).filter(valueIsNotNil) | ||
])}`; | ||
const urlProvider = typeof query === "function" ? async () => makeUrl(await query()) : makeUrl(query); | ||
return { | ||
host, | ||
path, | ||
room, | ||
name, | ||
protocol, | ||
partyUrl: baseUrl, | ||
urlProvider | ||
}; | ||
} | ||
var PartySocket = class extends ReconnectingWebSocket { | ||
constructor(partySocketOptions) { | ||
const wsOptions = getWSOptions(partySocketOptions); | ||
super(wsOptions.urlProvider, wsOptions.protocols, wsOptions.socketOptions); | ||
this.partySocketOptions = partySocketOptions; | ||
this.setWSProperties(wsOptions); | ||
} | ||
_pk; | ||
_pkurl; | ||
name; | ||
room; | ||
host; | ||
path; | ||
updateProperties(partySocketOptions) { | ||
const wsOptions = getWSOptions({ | ||
...this.partySocketOptions, | ||
...partySocketOptions, | ||
host: partySocketOptions.host ?? this.host, | ||
room: partySocketOptions.room ?? this.room, | ||
path: partySocketOptions.path ?? this.path | ||
}); | ||
this._url = wsOptions.urlProvider; | ||
this._protocols = wsOptions.protocols; | ||
this._options = wsOptions.socketOptions; | ||
this.setWSProperties(wsOptions); | ||
} | ||
setWSProperties(wsOptions) { | ||
const { _pk, _pkurl, name, room, host, path } = wsOptions; | ||
this._pk = _pk; | ||
this._pkurl = _pkurl; | ||
this.name = name; | ||
this.room = room; | ||
this.host = host; | ||
this.path = path; | ||
} | ||
reconnect(code, reason) { | ||
if (!this.room || !this.host) { | ||
throw new Error( | ||
"The room and host must be set before connecting, use `updateProperties` method to set them or pass them to the constructor." | ||
); | ||
} | ||
super.reconnect(code, reason); | ||
} | ||
get id() { | ||
return this._pk; | ||
} | ||
/** | ||
* Exposes the static PartyKit room URL without applying query parameters. | ||
* To access the currently connected WebSocket url, use PartySocket#url. | ||
*/ | ||
get roomUrl() { | ||
return this._pkurl; | ||
} | ||
// a `fetch` method that uses (almost) the same options as `PartySocket` | ||
static async fetch(options, init) { | ||
const party = getPartyInfo(options, "http"); | ||
const url = typeof party.urlProvider === "string" ? party.urlProvider : await party.urlProvider(); | ||
const doFetch = options.fetch ?? fetch; | ||
return doFetch(url, init); | ||
} | ||
}; | ||
function getWSOptions(partySocketOptions) { | ||
const { | ||
id, | ||
host: _host, | ||
path: _path, | ||
party: _party, | ||
room: _room, | ||
protocol: _protocol, | ||
query: _query, | ||
protocols, | ||
...socketOptions | ||
} = partySocketOptions; | ||
const _pk = id || generateUUID(); | ||
const party = getPartyInfo(partySocketOptions, "ws", { _pk }); | ||
return { | ||
_pk, | ||
_pkurl: party.partyUrl, | ||
name: party.name, | ||
room: party.room, | ||
host: party.host, | ||
path: party.path, | ||
protocols, | ||
socketOptions, | ||
urlProvider: party.urlProvider | ||
}; | ||
} | ||
// src/use-handlers.ts | ||
var import_react = require("react"); | ||
var useAttachWebSocketEventHandlers = (socket, options) => { | ||
const handlersRef = (0, import_react.useRef)(options); | ||
handlersRef.current = options; | ||
(0, import_react.useEffect)(() => { | ||
const onOpen = (event) => handlersRef.current?.onOpen?.(event); | ||
const onMessage = (event) => handlersRef.current?.onMessage?.(event); | ||
const onClose = (event) => handlersRef.current?.onClose?.(event); | ||
const onError = (event) => handlersRef.current?.onError?.(event); | ||
socket.addEventListener("open", onOpen); | ||
socket.addEventListener("close", onClose); | ||
socket.addEventListener("error", onError); | ||
socket.addEventListener("message", onMessage); | ||
return () => { | ||
socketRef.current.close(); | ||
socket.removeEventListener("open", onOpen); | ||
socket.removeEventListener("close", onClose); | ||
socket.removeEventListener("error", onError); | ||
socket.removeEventListener("message", onMessage); | ||
}; | ||
}, []); | ||
}, [socket]); | ||
}; | ||
// src/use-socket.ts | ||
var import_react2 = require("react"); | ||
var getOptionsThatShouldCauseRestartWhenChanged = (options) => [ | ||
options.startClosed, | ||
options.minUptime, | ||
options.maxRetries, | ||
options.connectionTimeout, | ||
options.maxEnqueuedMessages, | ||
options.maxReconnectionDelay, | ||
options.minReconnectionDelay, | ||
options.reconnectionDelayGrowFactor, | ||
options.debug | ||
]; | ||
function useStableSocket({ | ||
options, | ||
createSocket, | ||
createSocketMemoKey: createOptionsMemoKey | ||
}) { | ||
const shouldReconnect = createOptionsMemoKey(options); | ||
const socketOptions = (0, import_react2.useMemo)(() => { | ||
return options; | ||
}, [shouldReconnect]); | ||
const [socket, setSocket] = (0, import_react2.useState)( | ||
() => ( | ||
// only connect on first mount | ||
createSocket({ ...socketOptions, startClosed: true }) | ||
) | ||
); | ||
const socketInitializedRef = (0, import_react2.useRef)(null); | ||
const createSocketRef = (0, import_react2.useRef)(createSocket); | ||
createSocketRef.current = createSocket; | ||
(0, import_react2.useEffect)(() => { | ||
if (socketInitializedRef.current === socket) { | ||
const newSocket = createSocketRef.current({ | ||
...socketOptions, | ||
// when reconnecting because of options change, we always reconnect | ||
// (startClosed only applies to initial mount) | ||
startClosed: false | ||
}); | ||
setSocket(newSocket); | ||
} else { | ||
if (!socketInitializedRef.current && socketOptions.startClosed !== true) { | ||
socket.reconnect(); | ||
} | ||
socketInitializedRef.current = socket; | ||
return () => { | ||
socket.close(); | ||
}; | ||
} | ||
}, [socket, socketOptions]); | ||
return socket; | ||
} | ||
export { | ||
usePartySocket as default | ||
}; | ||
// src/use-ws.ts | ||
function useWebSocket(url, protocols, options = {}) { | ||
const socket = useStableSocket({ | ||
options, | ||
createSocket: (options2) => new ReconnectingWebSocket(url, protocols, options2), | ||
createSocketMemoKey: (options2) => JSON.stringify([ | ||
// will reconnect if url or protocols are specified as a string. | ||
// if they are functions, the WebSocket will handle reconnection | ||
url, | ||
protocols, | ||
...getOptionsThatShouldCauseRestartWhenChanged(options2) | ||
]) | ||
}); | ||
useAttachWebSocketEventHandlers(socket, options); | ||
return socket; | ||
} | ||
// src/react.ts | ||
function usePartySocket(options) { | ||
const { host, ...otherOptions } = options; | ||
const socket = useStableSocket({ | ||
options: { | ||
host: host || (typeof window !== "undefined" ? window.location.host : "dummy-domain.com"), | ||
...otherOptions | ||
}, | ||
createSocket: (options2) => new PartySocket(options2), | ||
createSocketMemoKey: (options2) => JSON.stringify([ | ||
// NOTE: if query is defined as a function, the socket | ||
// won't reconnect when you change the function identity | ||
options2.query, | ||
options2.id, | ||
options2.host, | ||
options2.room, | ||
options2.party, | ||
options2.path, | ||
options2.protocol, | ||
options2.protocols, | ||
...getOptionsThatShouldCauseRestartWhenChanged(options2) | ||
]) | ||
}); | ||
useAttachWebSocketEventHandlers(socket, options); | ||
return socket; | ||
} | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
usePartySocket, | ||
useWebSocket | ||
}); | ||
/*! | ||
* Reconnecting WebSocket | ||
* by Pedro Ladaria <pedro.ladaria@gmail.com> | ||
* https://github.com/pladaria/reconnecting-websocket | ||
* License MIT | ||
*/ |
255
dist/ws.js
@@ -1,16 +0,40 @@ | ||
/*! | ||
* Reconnecting WebSocket | ||
* by Pedro Ladaria <pedro.ladaria@gmail.com> | ||
* https://github.com/pladaria/reconnecting-websocket | ||
* License MIT | ||
*/ | ||
class Event { | ||
target; | ||
type; | ||
constructor(type, target) { | ||
this.target = target; | ||
this.type = type; | ||
"use strict"; | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
// src/ws.ts | ||
var ws_exports = {}; | ||
__export(ws_exports, { | ||
CloseEvent: () => CloseEvent, | ||
ErrorEvent: () => ErrorEvent, | ||
default: () => ReconnectingWebSocket | ||
}); | ||
module.exports = __toCommonJS(ws_exports); | ||
if (!globalThis.EventTarget || !globalThis.Event) { | ||
console.error(` | ||
PartySocket requires a global 'EventTarget' class to be available! | ||
You can polyfill this global by adding this to your code before any partysocket imports: | ||
\`\`\` | ||
import 'partysocket/event-target-polyfill'; | ||
\`\`\` | ||
Please file an issue at https://github.com/partykit/partykit if you're still having trouble. | ||
`); | ||
} | ||
class ErrorEvent extends Event { | ||
var ErrorEvent = class extends Event { | ||
message; | ||
@@ -23,4 +47,4 @@ error; | ||
} | ||
} | ||
class CloseEvent extends Event { | ||
}; | ||
var CloseEvent = class extends Event { | ||
code; | ||
@@ -34,4 +58,4 @@ reason; | ||
} | ||
} | ||
const Events = { | ||
}; | ||
var Events = { | ||
Event, | ||
@@ -46,6 +70,30 @@ ErrorEvent, | ||
} | ||
function isWebSocket(w) { | ||
return typeof w !== "undefined" && !!w && typeof w === "function" && "CLOSING" in w && w.CLOSING === 2; | ||
function cloneEventBrowser(e) { | ||
return new e.constructor(e.type, e); | ||
} | ||
const DEFAULT = { | ||
function cloneEventNode(e) { | ||
if ("data" in e) { | ||
const evt2 = new MessageEvent(e.type, e); | ||
return evt2; | ||
} | ||
if ("code" in e || "reason" in e) { | ||
const evt2 = new CloseEvent( | ||
// @ts-expect-error we need to fix event/listener types | ||
e.code || 1999, | ||
// @ts-expect-error we need to fix event/listener types | ||
e.reason || "unknown reason", | ||
e | ||
); | ||
return evt2; | ||
} | ||
if ("error" in e) { | ||
const evt2 = new ErrorEvent(e.error, e); | ||
return evt2; | ||
} | ||
const evt = new Event(e.type, e); | ||
return evt; | ||
} | ||
var isNode = typeof process !== "undefined" && typeof process.versions?.node !== "undefined" && typeof document === "undefined"; | ||
var cloneEvent = isNode ? cloneEventNode : cloneEventBrowser; | ||
var DEFAULT = { | ||
maxReconnectionDelay: 1e4, | ||
@@ -61,10 +109,5 @@ minReconnectionDelay: 1e3 + Math.random() * 4e3, | ||
}; | ||
class ReconnectingWebSocket { | ||
var didWarnAboutMissingWebSocket = false; | ||
var ReconnectingWebSocket = class _ReconnectingWebSocket extends EventTarget { | ||
_ws; | ||
_listeners = { | ||
error: [], | ||
message: [], | ||
open: [], | ||
close: [] | ||
}; | ||
_retryCount = -1; | ||
@@ -78,2 +121,3 @@ _uptimeTimeout; | ||
_messageQueue = []; | ||
_debugLogger = console.log.bind(console); | ||
_url; | ||
@@ -83,2 +127,3 @@ _protocols; | ||
constructor(url, protocols, options = {}) { | ||
super(); | ||
this._url = url; | ||
@@ -90,2 +135,5 @@ this._protocols = protocols; | ||
} | ||
if (this._options.debugLogger) { | ||
this._debugLogger = this._options.debugLogger; | ||
} | ||
this._connect(); | ||
@@ -106,12 +154,12 @@ } | ||
get CONNECTING() { | ||
return ReconnectingWebSocket.CONNECTING; | ||
return _ReconnectingWebSocket.CONNECTING; | ||
} | ||
get OPEN() { | ||
return ReconnectingWebSocket.OPEN; | ||
return _ReconnectingWebSocket.OPEN; | ||
} | ||
get CLOSING() { | ||
return ReconnectingWebSocket.CLOSING; | ||
return _ReconnectingWebSocket.CLOSING; | ||
} | ||
get CLOSED() { | ||
return ReconnectingWebSocket.CLOSED; | ||
return _ReconnectingWebSocket.CLOSED; | ||
} | ||
@@ -127,5 +175,14 @@ get binaryType() { | ||
} | ||
/** | ||
* Returns the number or connection retries | ||
*/ | ||
get retryCount() { | ||
return Math.max(this._retryCount, 0); | ||
} | ||
/** | ||
* The number of bytes of data that have been queued using calls to send() but not yet | ||
* transmitted to the network. This value resets to zero once all queued data has been sent. | ||
* This value does not reset to zero when the connection is closed; if you keep calling send(), | ||
* this will continue to climb. Read only | ||
*/ | ||
get bufferedAmount() { | ||
@@ -144,8 +201,20 @@ const bytes = this._messageQueue.reduce((acc, message) => { | ||
} | ||
/** | ||
* The extensions selected by the server. This is currently only the empty string or a list of | ||
* extensions as negotiated by the connection | ||
*/ | ||
get extensions() { | ||
return this._ws ? this._ws.extensions : ""; | ||
} | ||
/** | ||
* A string indicating the name of the sub-protocol the server selected; | ||
* this will be one of the strings specified in the protocols parameter when creating the | ||
* WebSocket object | ||
*/ | ||
get protocol() { | ||
return this._ws ? this._ws.protocol : ""; | ||
} | ||
/** | ||
* The current state of the connection; this is one of the Ready state constants | ||
*/ | ||
get readyState() { | ||
@@ -155,14 +224,37 @@ if (this._ws) { | ||
} | ||
return this._options.startClosed ? ReconnectingWebSocket.CLOSED : ReconnectingWebSocket.CONNECTING; | ||
return this._options.startClosed ? _ReconnectingWebSocket.CLOSED : _ReconnectingWebSocket.CONNECTING; | ||
} | ||
/** | ||
* The URL as resolved by the constructor | ||
*/ | ||
get url() { | ||
return this._ws ? this._ws.url : ""; | ||
} | ||
/** | ||
* Whether the websocket object is now in reconnectable state | ||
*/ | ||
get shouldReconnect() { | ||
return this._shouldReconnect; | ||
} | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to CLOSED | ||
*/ | ||
onclose = null; | ||
/** | ||
* An event listener to be called when an error occurs | ||
*/ | ||
onerror = null; | ||
/** | ||
* An event listener to be called when a message is received from the server | ||
*/ | ||
onmessage = null; | ||
/** | ||
* An event listener to be called when the WebSocket connection's readyState changes to OPEN; | ||
* this indicates that the connection is ready to send and receive data | ||
*/ | ||
onopen = null; | ||
/** | ||
* Closes the WebSocket connection or connection attempt, if any. If the connection is already | ||
* CLOSED, this method does nothing | ||
*/ | ||
close(code = 1e3, reason) { | ||
@@ -182,2 +274,6 @@ this._closeCalled = true; | ||
} | ||
/** | ||
* Closes the WebSocket connection or connection attempt and connects again. | ||
* Resets retry counter; | ||
*/ | ||
reconnect(code, reason) { | ||
@@ -194,2 +290,5 @@ this._shouldReconnect = true; | ||
} | ||
/** | ||
* Enqueue specified data to be transmitted to the server over the WebSocket connection | ||
*/ | ||
send(data) { | ||
@@ -207,26 +306,5 @@ if (this._ws && this._ws.readyState === this.OPEN) { | ||
} | ||
addEventListener(type, listener) { | ||
if (this._listeners[type]) { | ||
this._listeners[type].push(listener); | ||
} | ||
} | ||
dispatchEvent(event) { | ||
const listeners = this._listeners[event.type]; | ||
if (listeners) { | ||
for (const listener of listeners) { | ||
this._callEventListener(event, listener); | ||
} | ||
} | ||
return true; | ||
} | ||
removeEventListener(type, listener) { | ||
if (this._listeners[type]) { | ||
this._listeners[type] = this._listeners[type].filter( | ||
(l) => l !== listener | ||
); | ||
} | ||
} | ||
_debug(...args) { | ||
if (this._options.debug) { | ||
console.log.apply(console, ["RWS>", ...args]); | ||
this._debugLogger("RWS>", ...args); | ||
} | ||
@@ -256,4 +334,3 @@ } | ||
_getNextProtocols(protocolsProvider) { | ||
if (!protocolsProvider) | ||
return Promise.resolve(null); | ||
if (!protocolsProvider) return Promise.resolve(null); | ||
if (typeof protocolsProvider === "string" || Array.isArray(protocolsProvider)) { | ||
@@ -264,4 +341,3 @@ return Promise.resolve(protocolsProvider); | ||
const protocols = protocolsProvider(); | ||
if (!protocols) | ||
return Promise.resolve(null); | ||
if (!protocols) return Promise.resolve(null); | ||
if (typeof protocols === "string" || Array.isArray(protocols)) { | ||
@@ -307,5 +383,2 @@ return Promise.resolve(protocols); | ||
this._removeListeners(); | ||
if (!isWebSocket(WebSocket)) { | ||
throw Error("No valid WebSocket class provided"); | ||
} | ||
this._wait().then( | ||
@@ -321,4 +394,22 @@ () => Promise.all([ | ||
} | ||
if (!this._options.WebSocket && typeof WebSocket === "undefined" && !didWarnAboutMissingWebSocket) { | ||
console.error(`\u203C\uFE0F No WebSocket implementation available. You should define options.WebSocket. | ||
For example, if you're using node.js, run \`npm install ws\`, and then in your code: | ||
import PartySocket from 'partysocket'; | ||
import WS from 'ws'; | ||
const partysocket = new PartySocket({ | ||
host: "127.0.0.1:1999", | ||
room: "test-room", | ||
WebSocket: WS | ||
}); | ||
`); | ||
didWarnAboutMissingWebSocket = true; | ||
} | ||
const WS = this._options.WebSocket || WebSocket; | ||
this._debug("connect", { url, protocols }); | ||
this._ws = protocols ? new WebSocket(url, protocols) : new WebSocket(url); | ||
this._ws = protocols ? new WS(url, protocols) : new WS(url); | ||
this._ws.binaryType = this._binaryType; | ||
@@ -356,9 +447,2 @@ this._connectLock = false; | ||
} | ||
_callEventListener(event, listener) { | ||
if ("handleEvent" in listener) { | ||
listener.handleEvent(event); | ||
} else { | ||
listener(event); | ||
} | ||
} | ||
_handleOpen = (event) => { | ||
@@ -376,5 +460,3 @@ this._debug("open event"); | ||
} | ||
this._listeners.open.forEach( | ||
(listener) => this._callEventListener(event, listener) | ||
); | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
@@ -386,5 +468,3 @@ _handleMessage = (event) => { | ||
} | ||
this._listeners.message.forEach( | ||
(listener) => this._callEventListener(event, listener) | ||
); | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
@@ -401,5 +481,3 @@ _handleError = (event) => { | ||
this._debug("exec error listeners"); | ||
this._listeners.error.forEach( | ||
(listener) => this._callEventListener(event, listener) | ||
); | ||
this.dispatchEvent(cloneEvent(event)); | ||
this._connect(); | ||
@@ -416,5 +494,3 @@ }; | ||
} | ||
this._listeners.close.forEach( | ||
(listener) => this._callEventListener(event, listener) | ||
); | ||
this.dispatchEvent(cloneEvent(event)); | ||
}; | ||
@@ -445,8 +521,13 @@ _removeListeners() { | ||
} | ||
} | ||
export { | ||
}; | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
CloseEvent, | ||
ErrorEvent, | ||
Event, | ||
ReconnectingWebSocket as default | ||
}; | ||
ErrorEvent | ||
}); | ||
/*! | ||
* Reconnecting WebSocket | ||
* by Pedro Ladaria <pedro.ladaria@gmail.com> | ||
* https://github.com/pladaria/reconnecting-websocket | ||
* License MIT | ||
*/ |
@@ -1,15 +0,47 @@ | ||
import type * as RWS from "./ws"; | ||
import ReconnectingWebSocket from "./ws"; | ||
export type PartySocketOptions = Omit<RWS.Options, "WebSocket" | "constructor"> & { | ||
import ReconnectingWebSocket, { Options } from './ws.js'; | ||
type Maybe<T> = T | null | undefined; | ||
type Params = Record<string, Maybe<string>>; | ||
type PartySocketOptions = Omit<Options, "constructor"> & { | ||
id?: string; | ||
host: string; | ||
room: string; | ||
protocol?: string; | ||
room?: string; | ||
party?: string; | ||
prefix?: string; | ||
protocol?: "ws" | "wss"; | ||
protocols?: string[]; | ||
query?: Record<string, string>; | ||
path?: string; | ||
query?: Params | (() => Params | Promise<Params>); | ||
}; | ||
export default class PartySocket extends ReconnectingWebSocket { | ||
type PartyFetchOptions = { | ||
host: string; | ||
room: string; | ||
party?: string; | ||
prefix?: string; | ||
path?: string; | ||
protocol?: "http" | "https"; | ||
query?: Params | (() => Params | Promise<Params>); | ||
fetch?: typeof fetch; | ||
}; | ||
declare class PartySocket extends ReconnectingWebSocket { | ||
readonly partySocketOptions: PartySocketOptions; | ||
_pk: string; | ||
_pkurl: string; | ||
name: string; | ||
room?: string; | ||
host: string; | ||
path: string; | ||
constructor(partySocketOptions: PartySocketOptions); | ||
updateProperties(partySocketOptions: Partial<PartySocketOptions>): void; | ||
private setWSProperties; | ||
reconnect(code?: number | undefined, reason?: string | undefined): void; | ||
get id(): string; | ||
/** | ||
* Exposes the static PartyKit room URL without applying query parameters. | ||
* To access the currently connected WebSocket url, use PartySocket#url. | ||
*/ | ||
get roomUrl(): string; | ||
static fetch(options: PartyFetchOptions, init?: RequestInit): Promise<Response>; | ||
} | ||
//# sourceMappingURL=index.d.ts.map | ||
export { type PartyFetchOptions, PartySocket, type PartySocketOptions, ReconnectingWebSocket as WebSocket, PartySocket as default }; |
{ | ||
"name": "partysocket", | ||
"version": "0.0.0-66c5e5d", | ||
"description": "party hotline", | ||
"main": "dist/index.js", | ||
"version": "0.0.0-66d07f5", | ||
"description": "A better WebSocket that Just Works™", | ||
"homepage": "https://docs.partykit.io/reference/partysocket-api", | ||
"bugs": "https://github.com/partykit/partykit/issues", | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"exports": { | ||
".": "./dist/index.js", | ||
"./ws": "./dist/ws.js", | ||
"./react": "./dist/react.js" | ||
".": { | ||
"types": "./index.d.ts", | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js" | ||
}, | ||
"./ws": { | ||
"types": "./ws.d.ts", | ||
"import": "./dist/ws.mjs", | ||
"require": "./dist/ws.js" | ||
}, | ||
"./react": { | ||
"types": "./react.d.ts", | ||
"import": "./dist/react.mjs", | ||
"require": "./dist/react.js" | ||
}, | ||
"./use-ws": { | ||
"types": "./use-ws.d.ts", | ||
"import": "./dist/use-ws.mjs", | ||
"require": "./dist/use-ws.js" | ||
}, | ||
"./event-target-polyfill": { | ||
"types": "./event-target-polyfill.d.ts", | ||
"import": "./dist/event-target-polyfill.mjs", | ||
"require": "./dist/event-target-polyfill.js" | ||
} | ||
}, | ||
"tsup": { | ||
"entry": [ | ||
"src/index.ts", | ||
"src/react.ts", | ||
"src/ws.ts", | ||
"src/use-ws.ts", | ||
"src/event-target-polyfill.ts" | ||
], | ||
"format": [ | ||
"esm", | ||
"cjs" | ||
], | ||
"dts": true | ||
}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"clean": "rm -rf dist && rm -rf *.d.ts*", | ||
"build": "npm run clean && npx esbuild src/index.ts src/react.ts src/ws.ts --format=esm --outdir=dist && tsc --project tsconfig.extract.json && mv dist/*.d.ts* ." | ||
"clean": "shx rm -rf dist *.d.ts *.d.mts event-target-polyfill.*", | ||
"post-build": "shx mv dist/*.d.ts dist/*.d.mts* . && shx mv dist/event-target-polyfill.* .", | ||
"build": "npm run clean && tsup --external react && npm run post-build", | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"files": [ | ||
"dist/*.js", | ||
"dist", | ||
"*.d.ts", | ||
"*.d.ts.map" | ||
"event-target-polyfill.*" | ||
], | ||
"keywords": [], | ||
"keywords": [ | ||
"websocket", | ||
"client", | ||
"reconnecting", | ||
"reconnection", | ||
"reconnect", | ||
"forever", | ||
"persistent", | ||
"forever", | ||
"automatic" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/partykit/partykit.git", | ||
"directory": "packages/partykit" | ||
}, | ||
"author": "", | ||
"license": "ISC" | ||
"license": "ISC", | ||
"dependencies": { | ||
"event-target-shim": "^6.0.2" | ||
} | ||
} |
@@ -1,3 +0,11 @@ | ||
import type { PartySocketOptions } from "."; | ||
export default function usePartySocket(options: PartySocketOptions): void; | ||
//# sourceMappingURL=react.d.ts.map | ||
import { PartySocket, PartySocketOptions } from './index.js'; | ||
import { E as EventHandlerOptions } from './use-ws-CnrrNZS2.js'; | ||
export { u as useWebSocket } from './use-ws-CnrrNZS2.js'; | ||
import './ws.js'; | ||
type UsePartySocketOptions = Omit<PartySocketOptions, "host"> & EventHandlerOptions & { | ||
host?: string | undefined; | ||
}; | ||
declare function usePartySocket(options: UsePartySocketOptions): PartySocket; | ||
export { usePartySocket as default, usePartySocket }; |
110
README.md
@@ -1,35 +0,26 @@ | ||
- https://github.com/pladaria/reconnecting-websocket/pull/166 Fix: handle error if getNextUrl throws (TODO: add test for this one ) | ||
- https://github.com/pladaria/reconnecting-websocket/pull/132 feat: make protocols updatable | ||
- https://github.com/pladaria/reconnecting-websocket/pull/141 [Fix] Socket doesn't connect again after closing while connecting | ||
# PartySocket | ||
(TODO: more) | ||
_(Forked from the wonderful [reconnecting-websocket](https://github.com/joewalnes/reconnecting-websocket/) project, updated with pending PRs and bugfixes)_ | ||
- https://github.com/pladaria/reconnecting-websocket/pull/163 Support for Dynamic Protocols | ||
- https://github.com/pladaria/reconnecting-websocket/pull/47 reconnecting and reconnectscheduled custom events | ||
A better WebSocket that Just Works™ | ||
# Reconnecting WebSocket | ||
## Install | ||
[![Build Status](https://travis-ci.org/pladaria/reconnecting-websocket.svg?branch=master&v=1)](https://travis-ci.org/pladaria/reconnecting-websocket) | ||
[![Coverage Status](https://coveralls.io/repos/github/pladaria/reconnecting-websocket/badge.svg?branch=master&v=3)](https://coveralls.io/github/pladaria/reconnecting-websocket?branch=master) | ||
```bash | ||
npm install partysocket | ||
``` | ||
WebSocket that will automatically reconnect if the connection is closed. | ||
## Features | ||
- WebSocket API compatible (same interface, Level0 and Level2 event model) | ||
- Reconnects when a connection drops | ||
- Buffers messages when not connected, and sends accumulated messages when open | ||
- Handle connection timeouts | ||
- Allows changing server URL between reconnections | ||
- Fully configurable | ||
- Multi-platform (Web, ServiceWorkers, Node.js, React Native) | ||
- Multi-platform (Web, ServiceWorkers, Node.js, React Native, Cloudflare Workers, Deno, Bun) | ||
- Dependency free (does not depend on Window, DOM or any EventEmitter library) | ||
- Handle connection timeouts | ||
- Allows changing server URL between reconnections | ||
- Buffering. Will send accumulated messages on open | ||
- Multiple builds available (see dist folder) | ||
- Debug mode | ||
- Works everywhere, not just with PartyKit! | ||
## Install | ||
```bash | ||
npm install --save reconnecting-websocket | ||
``` | ||
## Usage | ||
@@ -39,23 +30,47 @@ | ||
So this documentation should be valid: | ||
[MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). | ||
Ping me if you find any problems. Or, even better, write a test for your case and make a pull | ||
request :) | ||
### Simple usage | ||
```javascript | ||
import ReconnectingWebSocket from "reconnecting-websocket"; | ||
import { WebSocket } from "partysocket"; | ||
const rws = new ReconnectingWebSocket("ws://my.site.com"); | ||
const ws = new WebSocket("wss://my.site.com"); | ||
rws.addEventListener("open", () => { | ||
rws.send("hello!"); | ||
ws.addEventListener("open", () => { | ||
ws.send("hello!"); | ||
}); | ||
``` | ||
### Usage with PartyKit | ||
```javascript | ||
import PartySocket from "partysocket"; | ||
// optional: only needed if creating using inside node.js. Run `npm install ws`, and then add: | ||
// import WS from "ws"; | ||
const ws = new PartySocket({ | ||
host: "project.name.partykit.dev", // or localhost:1999 in dev | ||
room: "my-room", | ||
// add an optional id to identify the client, | ||
// if not provided, a random id will be generated | ||
id: "some-connection-id" | ||
// optional: if used from node.js, you need to pass the WebSocket polyfill imported from `ws` | ||
// WebSocket: WS | ||
}); | ||
// optionally, update the properties of the connection | ||
// (e.g. to change the host or room) | ||
ws.updateProperties({ | ||
host: "another-project.username.partykit.dev", | ||
room: "my-new-room" | ||
}); | ||
ws.reconnect(); // make sure to call reconnect() after updating the properties | ||
``` | ||
### Update URL | ||
The `url` parameter will be resolved before connecting, possible types: | ||
The `url` parameter will be resolved before connecting, with possible types: | ||
@@ -67,5 +82,9 @@ - `string` | ||
```javascript | ||
import ReconnectingWebSocket from "reconnecting-websocket"; | ||
import { WebSocket } from "partysocket"; | ||
const urls = ["ws://my.site.com", "ws://your.site.com", "ws://their.site.com"]; | ||
const urls = [ | ||
"wss://my.site.com", | ||
"wss://your.site.com", | ||
"wss://their.site.com" | ||
]; | ||
let urlIndex = 0; | ||
@@ -76,7 +95,7 @@ | ||
const rws = new ReconnectingWebSocket(urlProvider); | ||
const ws = new WebSocket(urlProvider); | ||
``` | ||
```javascript | ||
import ReconnectingWebSocket from "reconnecting-websocket"; | ||
import { WebSocket } from "partysocket"; | ||
@@ -89,3 +108,3 @@ // async url provider | ||
const rws = new ReconnectingWebSocket(urlProvider); | ||
const ws = new WebSocket(urlProvider); | ||
``` | ||
@@ -104,8 +123,9 @@ | ||
```javascript | ||
import ReconnectingWebSocket from 'reconnecting-websocket`; | ||
const rws = new ReconnectingWebSocket('ws://your.site.com', 'your protocol'); | ||
import { WebSocket } from "partysocket"; | ||
const ws = new WebSocket("wss://your.site.com", "your protocol"); | ||
``` | ||
```javascript | ||
import ReconnectingWebSocket from 'reconnecting-websocket`; | ||
import WebSocket from 'partysocket`; | ||
@@ -118,3 +138,3 @@ const protocols = ['p1', 'p2', ['p3.1', 'p3.2']]; | ||
const rws = new ReconnectingWebSocket('ws://your.site.com', protocolsProvider); | ||
const ws = new WebSocket('wss://your.site.com', protocolsProvider); | ||
``` | ||
@@ -127,3 +147,3 @@ | ||
```javascript | ||
import ReconnectingWebSocket from "reconnecting-websocket"; | ||
import { WebSocket } from "partysocket"; | ||
import WS from "ws"; | ||
@@ -134,5 +154,5 @@ | ||
connectionTimeout: 1000, | ||
maxRetries: 10, | ||
maxRetries: 10 | ||
}; | ||
const rws = new ReconnectingWebSocket("ws://my.site.com", [], options); | ||
const ws = new WebSocket("wss://my.site.com", [], options); | ||
``` | ||
@@ -215,8 +235,4 @@ | ||
## Contributing | ||
[Read here](./CONTRIBUTING.md) | ||
## License | ||
MIT |
76
ws.d.ts
@@ -0,1 +1,11 @@ | ||
type TypedEventTarget<EventMap extends object> = { | ||
new (): IntermediateEventTarget<EventMap>; | ||
}; | ||
interface IntermediateEventTarget<EventMap> extends EventTarget { | ||
addEventListener<K extends keyof EventMap>(type: K, callback: (event: EventMap[K] extends Event ? EventMap[K] : never) => EventMap[K] extends Event ? void : never, options?: boolean | AddEventListenerOptions): void; | ||
addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void; | ||
removeEventListener<K extends keyof EventMap>(type: K, callback: (event: EventMap[K] extends Event ? EventMap[K] : never) => EventMap[K] extends Event ? void : never, options?: boolean | AddEventListenerOptions): void; | ||
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void; | ||
} | ||
/*! | ||
@@ -7,8 +17,4 @@ * Reconnecting WebSocket | ||
*/ | ||
export declare class Event { | ||
target: any; | ||
type: string; | ||
constructor(type: string, target: any); | ||
} | ||
export declare class ErrorEvent extends Event { | ||
declare class ErrorEvent extends Event { | ||
message: string; | ||
@@ -18,3 +24,3 @@ error: Error; | ||
} | ||
export declare class CloseEvent extends Event { | ||
declare class CloseEvent extends Event { | ||
code: number; | ||
@@ -25,3 +31,3 @@ reason: string; | ||
} | ||
export interface WebSocketEventMap { | ||
interface WebSocketEventMap { | ||
close: CloseEvent; | ||
@@ -32,17 +38,4 @@ error: ErrorEvent; | ||
} | ||
export interface WebSocketEventListenerMap { | ||
close: (event: CloseEvent) => void | { | ||
handleEvent: (event: CloseEvent) => void; | ||
}; | ||
error: (event: ErrorEvent) => void | { | ||
handleEvent: (event: ErrorEvent) => void; | ||
}; | ||
message: (event: MessageEvent) => void | { | ||
handleEvent: (event: MessageEvent) => void; | ||
}; | ||
open: (event: Event) => void | { | ||
handleEvent: (event: Event) => void; | ||
}; | ||
} | ||
export type Options = { | ||
type Options = { | ||
WebSocket?: any; | ||
maxReconnectionDelay?: number; | ||
@@ -57,15 +50,10 @@ minReconnectionDelay?: number; | ||
debug?: boolean; | ||
debugLogger?: (...args: any[]) => void; | ||
}; | ||
export type UrlProvider = string | (() => string) | (() => Promise<string>); | ||
export type ProtocolsProvider = null | string | string[] | (() => string | string[] | null) | (() => Promise<string | string[] | null>); | ||
export type Message = string | ArrayBuffer | Blob | ArrayBufferView; | ||
export type ListenersMap = { | ||
error: WebSocketEventListenerMap["error"][]; | ||
message: WebSocketEventListenerMap["message"][]; | ||
open: WebSocketEventListenerMap["open"][]; | ||
close: WebSocketEventListenerMap["close"][]; | ||
}; | ||
export default class ReconnectingWebSocket { | ||
type UrlProvider = string | (() => string) | (() => Promise<string>); | ||
type ProtocolsProvider = null | string | string[] | (() => string | string[] | null) | (() => Promise<string | string[] | null>); | ||
type Message = string | ArrayBuffer | Blob | ArrayBufferView; | ||
declare const ReconnectingWebSocket_base: TypedEventTarget<WebSocketEventMap>; | ||
declare class ReconnectingWebSocket extends ReconnectingWebSocket_base { | ||
private _ws; | ||
private _listeners; | ||
private _retryCount; | ||
@@ -79,5 +67,6 @@ private _uptimeTimeout; | ||
private _messageQueue; | ||
private readonly _url; | ||
private readonly _protocols?; | ||
private readonly _options; | ||
private _debugLogger; | ||
protected _url: UrlProvider; | ||
protected _protocols?: ProtocolsProvider; | ||
protected _options: Options; | ||
constructor(url: UrlProvider, protocols?: ProtocolsProvider, options?: Options); | ||
@@ -159,11 +148,2 @@ static get CONNECTING(): number; | ||
send(data: Message): void; | ||
/** | ||
* Register an event handler of a specific event type | ||
*/ | ||
addEventListener<T extends keyof WebSocketEventListenerMap>(type: T, listener: WebSocketEventListenerMap[T]): void; | ||
dispatchEvent(event: Event): boolean; | ||
/** | ||
* Removes an event listener | ||
*/ | ||
removeEventListener<T extends keyof WebSocketEventListenerMap>(type: T, listener: WebSocketEventListenerMap[T]): void; | ||
private _debug; | ||
@@ -178,3 +158,2 @@ private _getNextDelay; | ||
private _acceptOpen; | ||
private _callEventListener; | ||
private _handleOpen; | ||
@@ -188,2 +167,3 @@ private _handleMessage; | ||
} | ||
//# sourceMappingURL=ws.d.ts.map | ||
export { CloseEvent, ErrorEvent, type Message, type Options, type ProtocolsProvider, type UrlProvider, type WebSocketEventMap, ReconnectingWebSocket as default }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
116067
23
3557
2
230
0
1
1
4
+ Addedevent-target-shim@^6.0.2
+ Addedevent-target-shim@6.0.2(transitive)