[{"data":1,"prerenderedAt":1905},["ShallowReactive",2],{"navigation":3,"-guide-sync":83,"-guide-sync-surround":1902},[4,46],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Guide","\u002Fguide","1.guide\u002F1.index",[9,11,16,21,26,31,36,41],{"title":5,"path":6,"stem":7,"icon":10},"ph:book-open-duotone",{"title":12,"path":13,"stem":14,"icon":15},"Hooks","\u002Fguide\u002Fhooks","1.guide\u002F2.hooks","material-symbols-light:data-object",{"title":17,"path":18,"stem":19,"icon":20},"Peer","\u002Fguide\u002Fpeer","1.guide\u002F3.peer","mynaui:api",{"title":22,"path":23,"stem":24,"icon":25},"Message","\u002Fguide\u002Fmessage","1.guide\u002F4.message","solar:letter-line-duotone",{"title":27,"path":28,"stem":29,"icon":30},"Pub \u002F Sub","\u002Fguide\u002Fpubsub","1.guide\u002F5.pubsub","simple-icons:googlepubsub",{"title":32,"path":33,"stem":34,"icon":35},"Sync Backplane","\u002Fguide\u002Fsync","1.guide\u002F6.sync","tabler:refresh",{"title":37,"path":38,"stem":39,"icon":40},"Resolver API","\u002Fguide\u002Fresolver","1.guide\u002F7.resolver","tabler:route",{"title":42,"path":43,"stem":44,"icon":45},"WebSocket Proxy","\u002Fguide\u002Fproxy","1.guide\u002F8.proxy","tabler:arrows-exchange",{"title":47,"path":48,"stem":49,"children":50,"icon":52},"Adapters","\u002Fadapters","2.adapters\u002F1.index",[51,53,58,63,68,73,78],{"title":47,"path":48,"stem":49,"icon":52},"emojione-monotone:electric-plug",{"title":54,"path":55,"stem":56,"icon":57},"Bun","\u002Fadapters\u002Fbun","2.adapters\u002Fbun","simple-icons:bun",{"title":59,"path":60,"stem":61,"icon":62},"Bunny","\u002Fadapters\u002Fbunny","2.adapters\u002Fbunny","mdi:rabbit",{"title":64,"path":65,"stem":66,"icon":67},"Cloudflare","\u002Fadapters\u002Fcloudflare","2.adapters\u002Fcloudflare","devicon-plain:cloudflareworkers",{"title":69,"path":70,"stem":71,"icon":72},"Deno","\u002Fadapters\u002Fdeno","2.adapters\u002Fdeno","teenyicons:deno-solid",{"title":74,"path":75,"stem":76,"icon":77},"Node.js","\u002Fadapters\u002Fnode","2.adapters\u002Fnode","akar-icons:node-fill",{"title":79,"path":80,"stem":81,"icon":82},"SSE","\u002Fadapters\u002Fsse","2.adapters\u002Fsse","clarity:two-way-arrows-line",{"id":84,"title":32,"body":85,"description":147,"extension":1897,"meta":1898,"navigation":1899,"path":33,"seo":1900,"stem":34,"__hash__":1901},"content\u002F1.guide\u002F6.sync.md",{"type":86,"value":87,"toc":1886,"icon":35},"minimark",[88,104,126,141,269,280,291,296,303,352,464,473,477,591,607,620,624,631,650,820,835,840,846,878,1067,1076,1107,1124,1130,1165,1184,1360,1378,1384,1417,1471,1475,1494,1527,1539,1550,1574,1822,1843,1848,1882],[89,90,91],"important",{},[92,93,94,95,99,100,103],"p",{},"\nExperimental! The ",[96,97,98],"code",{},"sync"," option and ",[96,101,102],{},"crossws\u002Fsync"," API may change between 0.x versions.",[92,105,106,107,111,112,116,117,120,121,125],{},"crossws ",[108,109,110],"a",{"href":28},"pub\u002Fsub"," is in-memory and ",[113,114,115],"strong",{},"local to one instance",". When you run more than one instance — multiple regions, processes, or replicas behind a load balancer — a ",[96,118,119],{},"peer.publish(\"chat\", ...)"," only reaches the peers connected to ",[122,123,124],"em",{},"that"," instance. A subscriber connected elsewhere never sees it.",[92,127,128,129,132,133,136,137,140],{},"A ",[113,130,131],{},"sync adapter"," bridges that gap: it relays published messages between instances over a shared backplane (e.g. Redis pub\u002Fsub) and fans inbound messages out to local subscribers. You don't need to change your hooks — ",[96,134,135],{},"subscribe"," and ",[96,138,139],{},"publish"," keep working, now across the cluster.",[142,143,148],"pre",{"className":144,"code":145,"language":146,"meta":147,"style":147},"language-js shiki shiki-themes github-light github-dark github-dark","import nodeAdapter from \"crossws\u002Fadapters\u002Fnode\";\nimport { redis } from \"crossws\u002Fsync\";\nimport Redis from \"ioredis\";\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: redis({ client: new Redis(), channel: \"my-app\" }),\n});\n","js","",[96,149,150,173,188,203,210,230,236,263],{"__ignoreMap":147},[151,152,155,159,163,166,170],"span",{"class":153,"line":154},"line",1,[151,156,158],{"class":157},"so5gQ","import",[151,160,162],{"class":161},"slsVL"," nodeAdapter ",[151,164,165],{"class":157},"from",[151,167,169],{"class":168},"sfrk1"," \"crossws\u002Fadapters\u002Fnode\"",[151,171,172],{"class":161},";\n",[151,174,176,178,181,183,186],{"class":153,"line":175},2,[151,177,158],{"class":157},[151,179,180],{"class":161}," { redis } ",[151,182,165],{"class":157},[151,184,185],{"class":168}," \"crossws\u002Fsync\"",[151,187,172],{"class":161},[151,189,191,193,196,198,201],{"class":153,"line":190},3,[151,192,158],{"class":157},[151,194,195],{"class":161}," Redis ",[151,197,165],{"class":157},[151,199,200],{"class":168}," \"ioredis\"",[151,202,172],{"class":161},[151,204,206],{"class":153,"line":205},4,[151,207,209],{"emptyLinePlaceholder":208},true,"\n",[151,211,213,216,220,223,227],{"class":153,"line":212},5,[151,214,215],{"class":157},"const",[151,217,219],{"class":218},"suiK_"," ws",[151,221,222],{"class":157}," =",[151,224,226],{"class":225},"shcOC"," nodeAdapter",[151,228,229],{"class":161},"({\n",[151,231,233],{"class":153,"line":232},6,[151,234,235],{"class":161},"  hooks,\n",[151,237,239,242,245,248,251,254,257,260],{"class":153,"line":238},7,[151,240,241],{"class":161},"  sync: ",[151,243,244],{"class":225},"redis",[151,246,247],{"class":161},"({ client: ",[151,249,250],{"class":157},"new",[151,252,253],{"class":225}," Redis",[151,255,256],{"class":161},"(), channel: ",[151,258,259],{"class":168},"\"my-app\"",[151,261,262],{"class":161}," }),\n",[151,264,266],{"class":153,"line":265},8,[151,267,268],{"class":161},"});\n",[92,270,271,272,275,276,279],{},"Each driver takes a single options object. The ",[96,273,274],{},"channel"," name is ",[113,277,278],{},"required"," — it scopes the cluster, so unrelated servers don't silently bridge into each other.",[92,281,282,283,286,287,290],{},"crossws subscribes to the backplane automatically. On shutdown, call ",[96,284,285],{},"await ws.close()"," — it closes every connected peer and tears down the backplane in one step (leaving any Redis\u002FPostgres client you passed in connected; its lifecycle stays yours). Any server you created (e.g. the ",[96,288,289],{},"http.Server",") is also yours to close.",[292,293,295],"h2",{"id":294},"delivery-semantics","Delivery semantics",[92,297,298,299,302],{},"The cross-instance relay is ",[113,300,301],{},"best-effort and fire-and-forget"," — design your topics to tolerate that:",[304,305,306,313,324,338],"ul",{},[307,308,309,312],"li",{},[113,310,311],{},"At-most-once and unordered."," A relayed message reaches remote subscribers at most once, and ordering across instances is not guaranteed. If you need to detect a gap, carry your own sequence\u002Fversion in the payload rather than assuming a reliable stream.",[307,314,315,318,319,323],{},[113,316,317],{},"No replay or buffering."," Messages published while an instance is disconnected from the backplane — or before it finishes subscribing on startup — are not queued; that instance simply misses them (see the ioredis vs node-redis ",[108,320,322],{"href":321},"#redis-client-channel-connector","reconnect note",").",[307,325,326,329,330,333,334,337],{},[113,327,328],{},"Local delivery is independent."," ",[96,331,332],{},"peer.publish()"," always reaches ",[122,335,336],{},"local"," subscribers synchronously, even when the backplane is down. Only the cross-instance relay is best-effort.",[307,339,340,347,348,351],{},[113,341,342,343,346],{},"Failures never throw into your ",[96,344,345],{},"publish()","."," A backplane error is isolated so it can't crash the process. Pass ",[96,349,350],{},"onError"," to observe a degraded backplane (for logging, metrics, or alerting):",[142,353,355],{"className":144,"code":354,"language":146,"meta":147,"style":147},"const ws = nodeAdapter({\n  hooks,\n  sync: redis({ client: new Redis(), channel: \"my-app\" }),\n  onError(error, { stage }) {\n    \u002F\u002F stage: \"subscribe\" (initial connect) | \"publish\" (relay out) | \"delivery\" (fan-in)\n    metrics.increment(`crossws.sync.error.${stage}`);\n    console.error(\"[crossws] sync error\", stage, error);\n  },\n});\n",[96,356,357,369,373,391,412,418,439,454,459],{"__ignoreMap":147},[151,358,359,361,363,365,367],{"class":153,"line":154},[151,360,215],{"class":157},[151,362,219],{"class":218},[151,364,222],{"class":157},[151,366,226],{"class":225},[151,368,229],{"class":161},[151,370,371],{"class":153,"line":175},[151,372,235],{"class":161},[151,374,375,377,379,381,383,385,387,389],{"class":153,"line":190},[151,376,241],{"class":161},[151,378,244],{"class":225},[151,380,247],{"class":161},[151,382,250],{"class":157},[151,384,253],{"class":225},[151,386,256],{"class":161},[151,388,259],{"class":168},[151,390,262],{"class":161},[151,392,393,396,399,403,406,409],{"class":153,"line":205},[151,394,395],{"class":225},"  onError",[151,397,398],{"class":161},"(",[151,400,402],{"class":401},"sQHwn","error",[151,404,405],{"class":161},", { ",[151,407,408],{"class":401},"stage",[151,410,411],{"class":161}," }) {\n",[151,413,414],{"class":153,"line":212},[151,415,417],{"class":416},"sCsY4","    \u002F\u002F stage: \"subscribe\" (initial connect) | \"publish\" (relay out) | \"delivery\" (fan-in)\n",[151,419,420,423,426,428,431,433,436],{"class":153,"line":232},[151,421,422],{"class":161},"    metrics.",[151,424,425],{"class":225},"increment",[151,427,398],{"class":161},[151,429,430],{"class":168},"`crossws.sync.error.${",[151,432,408],{"class":161},[151,434,435],{"class":168},"}`",[151,437,438],{"class":161},");\n",[151,440,441,444,446,448,451],{"class":153,"line":238},[151,442,443],{"class":161},"    console.",[151,445,402],{"class":225},[151,447,398],{"class":161},[151,449,450],{"class":168},"\"[crossws] sync error\"",[151,452,453],{"class":161},", stage, error);\n",[151,455,456],{"class":153,"line":265},[151,457,458],{"class":161},"  },\n",[151,460,462],{"class":153,"line":461},9,[151,463,268],{"class":161},[92,465,466,467,469,470,346],{},"Without ",[96,468,350],{},", failures are logged to ",[96,471,472],{},"console.error",[292,474,476],{"id":475},"choosing-a-driver","Choosing a driver",[478,479,480,499],"table",{},[481,482,483],"thead",{},[484,485,486,490,493,496],"tr",{},[487,488,489],"th",{},"Driver",[487,491,492],{},"Backplane",[487,494,495],{},"Reach",[487,497,498],{},"When to use",[500,501,502,520,545,570],"tbody",{},[484,503,504,511,514,517],{},[505,506,507],"td",{},[108,508,509],{"href":321},[96,510,244],{},[505,512,513],{},"Redis pub\u002Fsub",[505,515,516],{},"Multi-region \u002F multi-host",[505,518,519],{},"The general-purpose choice for a real cluster.",[484,521,522,530,540,542],{},[505,523,524],{},[108,525,527],{"href":526},"#pgsql-client-channel-connector",[96,528,529],{},"pgsql",[505,531,532,533,536,537],{},"Postgres ",[96,534,535],{},"LISTEN","\u002F",[96,538,539],{},"NOTIFY",[505,541,516],{},[505,543,544],{},"You already run Postgres and would rather not add Redis (payloads must stay under 8 KB).",[484,546,547,555,561,564],{},[505,548,549],{},[108,550,552],{"href":551},"#cluster-channel",[96,553,554],{},"cluster",[505,556,557,558,560],{},"Node.js ",[96,559,554],{}," IPC",[505,562,563],{},"Single host (forked processes)",[505,565,566,567,569],{},"Multiple processes on one host (Node ",[96,568,554],{}," \u002F PM2) without a network broker.",[484,571,572,580,585,588],{},[505,573,574],{},[108,575,577],{"href":576},"#broadcastchannel-channel",[96,578,579],{},"broadcastChannel",[505,581,582],{},[96,583,584],{},"BroadcastChannel",[505,586,587],{},"Single process (worker threads)",[505,589,590],{},"Local fan-out, tests, in-process worker threads.",[92,592,593,594,596,597,136,599,601,602,136,604,606],{},"All four ship in ",[96,595,102],{}," and have no third-party dependencies — ",[96,598,244],{},[96,600,529],{}," take a client you bring; ",[96,603,554],{},[96,605,579],{}," need nothing.",[608,609,610],"note",{},[92,611,612,613,615,616,346],{},"\nOn ",[113,614,64],{}," the model is different — a single Durable Object is already cluster-global, so a backplane is only relevant for multi-instance sharding or the fallback path. See ",[108,617,619],{"href":618},"\u002Fadapters\u002Fcloudflare#sync-across-instances","Cloudflare → Sync across instances",[292,621,623],{"id":622},"sync-drivers","Sync Drivers",[625,626,628],"h3",{"id":627},"redis-client-channel-connector",[96,629,630],{},"redis({ client, channel, connector? })",[92,632,633,634,136,640,645,646,649],{},"Works out of the box with both ",[108,635,639],{"href":636,"rel":637},"https:\u002F\u002Fgithub.com\u002Fredis\u002Fioredis",[638],"nofollow","ioredis",[108,641,644],{"href":642,"rel":643},"https:\u002F\u002Fgithub.com\u002Fredis\u002Fnode-redis",[638],"node-redis",". The client flavor is auto-detected; pass ",[96,647,648],{},"connector: \"ioredis\" | \"node-redis\""," to override detection.",[651,652,653,724],"CodeGroup",{},[142,654,656],{"className":144,"code":655,"filename":639,"language":146,"meta":147,"style":147},"import { redis } from \"crossws\u002Fsync\";\nimport Redis from \"ioredis\";\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: redis({ client: new Redis(), channel: \"my-app\" }),\n});\n",[96,657,658,670,682,686,698,702,720],{"__ignoreMap":147},[151,659,660,662,664,666,668],{"class":153,"line":154},[151,661,158],{"class":157},[151,663,180],{"class":161},[151,665,165],{"class":157},[151,667,185],{"class":168},[151,669,172],{"class":161},[151,671,672,674,676,678,680],{"class":153,"line":175},[151,673,158],{"class":157},[151,675,195],{"class":161},[151,677,165],{"class":157},[151,679,200],{"class":168},[151,681,172],{"class":161},[151,683,684],{"class":153,"line":190},[151,685,209],{"emptyLinePlaceholder":208},[151,687,688,690,692,694,696],{"class":153,"line":205},[151,689,215],{"class":157},[151,691,219],{"class":218},[151,693,222],{"class":157},[151,695,226],{"class":225},[151,697,229],{"class":161},[151,699,700],{"class":153,"line":212},[151,701,235],{"class":161},[151,703,704,706,708,710,712,714,716,718],{"class":153,"line":232},[151,705,241],{"class":161},[151,707,244],{"class":225},[151,709,247],{"class":161},[151,711,250],{"class":157},[151,713,253],{"class":225},[151,715,256],{"class":161},[151,717,259],{"class":168},[151,719,262],{"class":161},[151,721,722],{"class":153,"line":238},[151,723,268],{"class":161},[142,725,727],{"className":144,"code":726,"filename":644,"language":146,"meta":147,"style":147},"import { redis } from \"crossws\u002Fsync\";\nimport { createClient } from \"redis\";\n\nconst client = await createClient().connect();\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: redis({ client, channel: \"my-app\" }),\n});\n",[96,728,729,741,755,759,783,787,799,803,816],{"__ignoreMap":147},[151,730,731,733,735,737,739],{"class":153,"line":154},[151,732,158],{"class":157},[151,734,180],{"class":161},[151,736,165],{"class":157},[151,738,185],{"class":168},[151,740,172],{"class":161},[151,742,743,745,748,750,753],{"class":153,"line":175},[151,744,158],{"class":157},[151,746,747],{"class":161}," { createClient } ",[151,749,165],{"class":157},[151,751,752],{"class":168}," \"redis\"",[151,754,172],{"class":161},[151,756,757],{"class":153,"line":190},[151,758,209],{"emptyLinePlaceholder":208},[151,760,761,763,766,768,771,774,777,780],{"class":153,"line":205},[151,762,215],{"class":157},[151,764,765],{"class":218}," client",[151,767,222],{"class":157},[151,769,770],{"class":157}," await",[151,772,773],{"class":225}," createClient",[151,775,776],{"class":161},"().",[151,778,779],{"class":225},"connect",[151,781,782],{"class":161},"();\n",[151,784,785],{"class":153,"line":212},[151,786,209],{"emptyLinePlaceholder":208},[151,788,789,791,793,795,797],{"class":153,"line":232},[151,790,215],{"class":157},[151,792,219],{"class":218},[151,794,222],{"class":157},[151,796,226],{"class":225},[151,798,229],{"class":161},[151,800,801],{"class":153,"line":238},[151,802,235],{"class":161},[151,804,805,807,809,812,814],{"class":153,"line":265},[151,806,241],{"class":161},[151,808,244],{"class":225},[151,810,811],{"class":161},"({ client, channel: ",[151,813,259],{"class":168},[151,815,262],{"class":161},[151,817,818],{"class":153,"line":461},[151,819,268],{"class":161},[92,821,822,823,826,827,830,831,834],{},"crossws derives a dedicated ",[96,824,825],{},"SUBSCRIBE"," connection from your client via ",[96,828,829],{},"duplicate()"," (for node-redis it also ",[96,832,833],{},"connect()","s it), so a single connected client is all you pass in.",[608,836,837],{},[92,838,839],{},"\nOn a dropped connection, ioredis automatically re-subscribes its channels; node-redis does not restore subscriptions the same way, so after a transient outage a node-redis-backed instance may stop receiving relayed messages until the client reconnects and re-subscribes. Prefer ioredis if resilience to flaky connections matters.",[625,841,843],{"id":842},"pgsql-client-channel-connector",[96,844,845],{},"pgsql({ client, channel, connector? })",[92,847,848,849,857,858,863,864,867,868,873,874,877],{},"Relays over PostgreSQL ",[108,850,853,536,855],{"href":851,"rel":852},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-notify.html",[638],[96,854,535],{},[96,856,539],{}," — handy for clusters that already run Postgres and would rather not add Redis. Works with both ",[108,859,862],{"href":860,"rel":861},"https:\u002F\u002Fgithub.com\u002Fbrianc\u002Fnode-postgres",[638],"node-postgres"," (",[96,865,866],{},"pg",") and ",[108,869,872],{"href":870,"rel":871},"https:\u002F\u002Fgithub.com\u002Fporsager\u002Fpostgres",[638],"postgres.js","; the flavor is auto-detected, with ",[96,875,876],{},"connector: \"pg\" | \"postgres.js\""," to override.",[651,879,880,981],{},[142,881,883],{"className":144,"code":882,"filename":862,"language":146,"meta":147,"style":147},"import { pgsql } from \"crossws\u002Fsync\";\nimport { Client } from \"pg\";\n\nconst client = new Client();\nawait client.connect();\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: pgsql({ client, channel: \"my-app\" }),\n});\n",[96,884,885,898,912,916,932,944,948,960,964,976],{"__ignoreMap":147},[151,886,887,889,892,894,896],{"class":153,"line":154},[151,888,158],{"class":157},[151,890,891],{"class":161}," { pgsql } ",[151,893,165],{"class":157},[151,895,185],{"class":168},[151,897,172],{"class":161},[151,899,900,902,905,907,910],{"class":153,"line":175},[151,901,158],{"class":157},[151,903,904],{"class":161}," { Client } ",[151,906,165],{"class":157},[151,908,909],{"class":168}," \"pg\"",[151,911,172],{"class":161},[151,913,914],{"class":153,"line":190},[151,915,209],{"emptyLinePlaceholder":208},[151,917,918,920,922,924,927,930],{"class":153,"line":205},[151,919,215],{"class":157},[151,921,765],{"class":218},[151,923,222],{"class":157},[151,925,926],{"class":157}," new",[151,928,929],{"class":225}," Client",[151,931,782],{"class":161},[151,933,934,937,940,942],{"class":153,"line":212},[151,935,936],{"class":157},"await",[151,938,939],{"class":161}," client.",[151,941,779],{"class":225},[151,943,782],{"class":161},[151,945,946],{"class":153,"line":232},[151,947,209],{"emptyLinePlaceholder":208},[151,949,950,952,954,956,958],{"class":153,"line":238},[151,951,215],{"class":157},[151,953,219],{"class":218},[151,955,222],{"class":157},[151,957,226],{"class":225},[151,959,229],{"class":161},[151,961,962],{"class":153,"line":265},[151,963,235],{"class":161},[151,965,966,968,970,972,974],{"class":153,"line":461},[151,967,241],{"class":161},[151,969,529],{"class":225},[151,971,811],{"class":161},[151,973,259],{"class":168},[151,975,262],{"class":161},[151,977,979],{"class":153,"line":978},10,[151,980,268],{"class":161},[142,982,984],{"className":144,"code":983,"filename":872,"language":146,"meta":147,"style":147},"import { pgsql } from \"crossws\u002Fsync\";\nimport postgresjs from \"postgres\";\n\nconst sql = postgresjs();\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: pgsql({ client: sql, channel: \"my-app\" }),\n});\n",[96,985,986,998,1012,1016,1030,1034,1046,1050,1063],{"__ignoreMap":147},[151,987,988,990,992,994,996],{"class":153,"line":154},[151,989,158],{"class":157},[151,991,891],{"class":161},[151,993,165],{"class":157},[151,995,185],{"class":168},[151,997,172],{"class":161},[151,999,1000,1002,1005,1007,1010],{"class":153,"line":175},[151,1001,158],{"class":157},[151,1003,1004],{"class":161}," postgresjs ",[151,1006,165],{"class":157},[151,1008,1009],{"class":168}," \"postgres\"",[151,1011,172],{"class":161},[151,1013,1014],{"class":153,"line":190},[151,1015,209],{"emptyLinePlaceholder":208},[151,1017,1018,1020,1023,1025,1028],{"class":153,"line":205},[151,1019,215],{"class":157},[151,1021,1022],{"class":218}," sql",[151,1024,222],{"class":157},[151,1026,1027],{"class":225}," postgresjs",[151,1029,782],{"class":161},[151,1031,1032],{"class":153,"line":212},[151,1033,209],{"emptyLinePlaceholder":208},[151,1035,1036,1038,1040,1042,1044],{"class":153,"line":232},[151,1037,215],{"class":157},[151,1039,219],{"class":218},[151,1041,222],{"class":157},[151,1043,226],{"class":225},[151,1045,229],{"class":161},[151,1047,1048],{"class":153,"line":238},[151,1049,235],{"class":161},[151,1051,1052,1054,1056,1059,1061],{"class":153,"line":265},[151,1053,241],{"class":161},[151,1055,529],{"class":225},[151,1057,1058],{"class":161},"({ client: sql, channel: ",[151,1060,259],{"class":168},[151,1062,262],{"class":161},[151,1064,1065],{"class":153,"line":461},[151,1066,268],{"class":161},[92,1068,1069,1070,1072,1073,1075],{},"Unlike Redis ",[96,1071,825],{},", Postgres ",[96,1074,535],{}," doesn't block the connection, so no duplicate connection is needed (for node-postgres the same client both listens and notifies; postgres.js reserves its own dedicated connection internally).",[89,1077,1078],{},[92,1079,1080,1081,1084,1085,1088,1089,1092,1093,1095,1096,1098,1099,1102,1103,1106],{},"\nPass a dedicated ",[96,1082,1083],{},"Client",", ",[113,1086,1087],{},"not"," a ",[96,1090,1091],{},"Pool",". Pool connections rotate per query, so a ",[96,1094,535],{}," lands on a backend that's then returned to the pool and notifications never reach a stable listener. A ",[96,1097,1091],{}," is detected and rejected at construction. For the same reason, give this driver its own client rather than sharing one across two ",[96,1100,1101],{},"pgsql()"," instances on the same channel — closing one issues ",[96,1104,1105],{},"UNLISTEN"," and silences the others.",[92,1108,1109,1110,1112,1113,1116,1117,1119,1120,1123],{},"Two transport limits to keep in mind: the ",[96,1111,274],{}," name maps to a Postgres identifier, capped at ",[113,1114,1115],{},"63 bytes"," (rejected at construction if longer), and a ",[96,1118,539],{}," payload is capped at ",[113,1121,1122],{},"8000 bytes"," (base64 inflates binary ~33%), so keep relayed messages small.",[625,1125,1127],{"id":1126},"cluster-channel",[96,1128,1129],{},"cluster({ channel })",[92,1131,1132,1133,1139,1140,1143,1144,1146,1147,1150,1151,1155,1156,1160,1161,346],{},"Relays over Node.js ",[108,1134,1137],{"href":1135,"rel":1136},"https:\u002F\u002Fnodejs.org\u002Fapi\u002Fcluster.html",[638],[96,1138,554],{}," IPC — bridges the ",[113,1141,1142],{},"forked processes on a single host"," (Node ",[96,1145,554],{},", or PM2 ",[96,1148,1149],{},"instances",") without standing up a network broker. This is the gap ",[108,1152,1153],{"href":576},[96,1154,579],{}," leaves: its registry is per-process and silently won't sync across forks. For multiple hosts or regions you still want ",[108,1157,1158],{"href":321},[96,1159,244],{}," or ",[108,1162,1163],{"href":526},[96,1164,529],{},[92,1166,1167,1168,1171,1172,1175,1176,1179,1180,1183],{},"The driver runs in the ",[113,1169,1170],{},"workers","; the ",[113,1173,1174],{},"primary"," needs a one-line relay because cluster workers can't message each other directly — IPC only flows between each worker and the primary, which rebroadcasts. Call ",[96,1177,1178],{},"setupPrimaryCluster()"," once in the primary (it's a no-op in workers, so guarding with ",[96,1181,1182],{},"cluster.isPrimary"," is optional):",[142,1185,1187],{"className":144,"code":1186,"language":146,"meta":147,"style":147},"import cluster from \"node:cluster\";\nimport { availableParallelism } from \"node:os\";\nimport { setupPrimaryCluster, cluster as clusterSync } from \"crossws\u002Fsync\";\n\nif (cluster.isPrimary) {\n  setupPrimaryCluster();\n  for (let i = 0; i \u003C availableParallelism(); i++) cluster.fork();\n} else {\n  const ws = nodeAdapter({\n    hooks,\n    sync: clusterSync({ channel: \"my-app\" }),\n  });\n  \u002F\u002F ... start your server\n}\n",[96,1188,1189,1203,1217,1236,1240,1248,1255,1297,1308,1321,1326,1342,1348,1354],{"__ignoreMap":147},[151,1190,1191,1193,1196,1198,1201],{"class":153,"line":154},[151,1192,158],{"class":157},[151,1194,1195],{"class":161}," cluster ",[151,1197,165],{"class":157},[151,1199,1200],{"class":168}," \"node:cluster\"",[151,1202,172],{"class":161},[151,1204,1205,1207,1210,1212,1215],{"class":153,"line":175},[151,1206,158],{"class":157},[151,1208,1209],{"class":161}," { availableParallelism } ",[151,1211,165],{"class":157},[151,1213,1214],{"class":168}," \"node:os\"",[151,1216,172],{"class":161},[151,1218,1219,1221,1224,1227,1230,1232,1234],{"class":153,"line":190},[151,1220,158],{"class":157},[151,1222,1223],{"class":161}," { setupPrimaryCluster, cluster ",[151,1225,1226],{"class":157},"as",[151,1228,1229],{"class":161}," clusterSync } ",[151,1231,165],{"class":157},[151,1233,185],{"class":168},[151,1235,172],{"class":161},[151,1237,1238],{"class":153,"line":205},[151,1239,209],{"emptyLinePlaceholder":208},[151,1241,1242,1245],{"class":153,"line":212},[151,1243,1244],{"class":157},"if",[151,1246,1247],{"class":161}," (cluster.isPrimary) {\n",[151,1249,1250,1253],{"class":153,"line":232},[151,1251,1252],{"class":225},"  setupPrimaryCluster",[151,1254,782],{"class":161},[151,1256,1257,1260,1262,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295],{"class":153,"line":238},[151,1258,1259],{"class":157},"  for",[151,1261,863],{"class":161},[151,1263,1264],{"class":157},"let",[151,1266,1267],{"class":161}," i ",[151,1269,1270],{"class":157},"=",[151,1272,1273],{"class":218}," 0",[151,1275,1276],{"class":161},"; i ",[151,1278,1279],{"class":157},"\u003C",[151,1281,1282],{"class":225}," availableParallelism",[151,1284,1285],{"class":161},"(); i",[151,1287,1288],{"class":157},"++",[151,1290,1291],{"class":161},") cluster.",[151,1293,1294],{"class":225},"fork",[151,1296,782],{"class":161},[151,1298,1299,1302,1305],{"class":153,"line":265},[151,1300,1301],{"class":161},"} ",[151,1303,1304],{"class":157},"else",[151,1306,1307],{"class":161}," {\n",[151,1309,1310,1313,1315,1317,1319],{"class":153,"line":461},[151,1311,1312],{"class":157},"  const",[151,1314,219],{"class":218},[151,1316,222],{"class":157},[151,1318,226],{"class":225},[151,1320,229],{"class":161},[151,1322,1323],{"class":153,"line":978},[151,1324,1325],{"class":161},"    hooks,\n",[151,1327,1329,1332,1335,1338,1340],{"class":153,"line":1328},11,[151,1330,1331],{"class":161},"    sync: ",[151,1333,1334],{"class":225},"clusterSync",[151,1336,1337],{"class":161},"({ channel: ",[151,1339,259],{"class":168},[151,1341,262],{"class":161},[151,1343,1345],{"class":153,"line":1344},12,[151,1346,1347],{"class":161},"  });\n",[151,1349,1351],{"class":153,"line":1350},13,[151,1352,1353],{"class":416},"  \u002F\u002F ... start your server\n",[151,1355,1357],{"class":153,"line":1356},14,[151,1358,1359],{"class":161},"}\n",[92,1361,1362,1363,1366,1367,1370,1371,1374,1375,1377],{},"Binary payloads are base64-encoded, so default IPC serialization is enough — you don't need ",[96,1364,1365],{},"serialization: \"advanced\"",". Calling ",[96,1368,1369],{},"clusterSync()"," outside a forked worker (no ",[96,1372,1373],{},"process.send",") throws on ",[96,1376,135],{},", surfacing the misconfiguration rather than silently not syncing.",[625,1379,1381],{"id":1380},"broadcastchannel-channel",[96,1382,1383],{},"broadcastChannel({ channel })",[92,1385,1386,1387,1389,1390,1393,1394,1396,1397,1399,1400,1404,1405,1160,1409,1413,1414,1416],{},"Bridges instances that share a ",[96,1388,584],{}," registry. On Node.js, Deno and Bun that registry is scoped to a ",[113,1391,1392],{},"single process"," — it spans the main thread and its worker threads, but ",[113,1395,1087],{}," separate OS processes (e.g. Node ",[96,1398,554],{},"\u002FPM2 forks), which each get an isolated registry and silently won't sync. For forked processes on one host use ",[108,1401,1402],{"href":551},[96,1403,554],{},"; across hosts or regions use ",[108,1406,1407],{"href":321},[96,1408,244],{},[108,1410,1411],{"href":526},[96,1412,529],{},". (Deno Deploy is the exception — its ",[96,1415,584],{}," spans isolates.)",[142,1418,1420],{"className":144,"code":1419,"language":146,"meta":147,"style":147},"import { broadcastChannel } from \"crossws\u002Fsync\";\n\nconst ws = nodeAdapter({\n  hooks,\n  sync: broadcastChannel({ channel: \"my-app\" }),\n});\n",[96,1421,1422,1435,1439,1451,1455,1467],{"__ignoreMap":147},[151,1423,1424,1426,1429,1431,1433],{"class":153,"line":154},[151,1425,158],{"class":157},[151,1427,1428],{"class":161}," { broadcastChannel } ",[151,1430,165],{"class":157},[151,1432,185],{"class":168},[151,1434,172],{"class":161},[151,1436,1437],{"class":153,"line":175},[151,1438,209],{"emptyLinePlaceholder":208},[151,1440,1441,1443,1445,1447,1449],{"class":153,"line":190},[151,1442,215],{"class":157},[151,1444,219],{"class":218},[151,1446,222],{"class":157},[151,1448,226],{"class":225},[151,1450,229],{"class":161},[151,1452,1453],{"class":153,"line":205},[151,1454,235],{"class":161},[151,1456,1457,1459,1461,1463,1465],{"class":153,"line":212},[151,1458,241],{"class":161},[151,1460,579],{"class":225},[151,1462,1337],{"class":161},[151,1464,259],{"class":168},[151,1466,262],{"class":161},[151,1468,1469],{"class":153,"line":232},[151,1470,268],{"class":161},[292,1472,1474],{"id":1473},"writing-a-driver","Writing a driver",[92,1476,1477,1478,1481,1482,1485,1486,1489,1490,1493],{},"Any transport that can fan a message out to your other instances can back a sync driver. A driver is a ",[96,1479,1480],{},"SyncAdapter",": a factory crossws calls ",[113,1483,1484],{},"once per instance",", passing a stable per-instance ",[96,1487,1488],{},"id",". It returns a ",[96,1491,1492],{},"SyncDriver"," with three methods:",[92,1495,1496,1501,1502,1504,1505,1508,1509,1512,1513,1515,1516,1519,1520,1523,1524,346],{},[113,1497,1498],{},[96,1499,1500],{},"publish(msg)"," — called for every local ",[96,1503,332],{}," \u002F ",[96,1506,1507],{},"adapter.publish()",". Relay ",[96,1510,1511],{},"msg"," to the other instances over your backplane. A ",[96,1514,1511],{}," is ",[96,1517,1518],{},"{ namespace, topic, data }",", where ",[96,1521,1522],{},"data"," is a ",[96,1525,1526],{},"string | Uint8Array",[92,1528,1529,1534,1535,1538],{},[113,1530,1531],{},[96,1532,1533],{},"subscribe(deliver)"," — called once on startup. Listen for messages from the other instances and hand each one to ",[96,1536,1537],{},"deliver",", which fans it out to this instance's local subscribers.",[92,1540,1541,329,1546,1549],{},[113,1542,1543],{},[96,1544,1545],{},"close()",[122,1547,1548],{},"(optional)"," — called when the adapter shuts down. Release any connection you opened.",[92,1551,1552,1553,1556,1557,1560,1561,1563,1564,1566,1567,1504,1570,1573],{},"Two things are your responsibility on the wire: ",[113,1554,1555],{},"suppress your own echo"," (most backplanes deliver a publisher its own messages) and ",[113,1558,1559],{},"preserve binary payloads"," (most transports are text-only). Both are handled by a small envelope that stamps the sender ",[96,1562,1488],{}," and base64-encodes binary ",[96,1565,1522],{},". crossws exports the same ",[96,1568,1569],{},"encodeEnvelope",[96,1571,1572],{},"decodeEnvelope"," helpers its built-in drivers use, so you don't have to reimplement them:",[142,1575,1579],{"className":1576,"code":1577,"language":1578,"meta":147,"style":147},"language-ts shiki shiki-themes github-light github-dark github-dark","import { encodeEnvelope, decodeEnvelope } from \"crossws\u002Fsync\";\nimport type { SyncAdapter } from \"crossws\u002Fsync\";\n\nconst mySync: SyncAdapter = ({ id }) => ({\n  subscribe(deliver) {\n    backplane.on(\"message\", (raw) => {\n      const envelope = decodeEnvelope(raw);\n      if (!envelope || envelope.id === id) {\n        return; \u002F\u002F ignore malformed messages and our own echo\n      }\n      deliver(envelope.msg);\n    });\n  },\n  publish(msg) {\n    backplane.send(encodeEnvelope(id, msg)); \u002F\u002F stamps `id` so we can filter it back out\n  },\n  close() {\n    backplane.disconnect(); \u002F\u002F optional: release the connection on shutdown\n  },\n});\n","ts",[96,1580,1581,1594,1610,1614,1643,1655,1681,1697,1722,1733,1738,1746,1751,1755,1766,1784,1789,1798,1812,1817],{"__ignoreMap":147},[151,1582,1583,1585,1588,1590,1592],{"class":153,"line":154},[151,1584,158],{"class":157},[151,1586,1587],{"class":161}," { encodeEnvelope, decodeEnvelope } ",[151,1589,165],{"class":157},[151,1591,185],{"class":168},[151,1593,172],{"class":161},[151,1595,1596,1598,1601,1604,1606,1608],{"class":153,"line":175},[151,1597,158],{"class":157},[151,1599,1600],{"class":157}," type",[151,1602,1603],{"class":161}," { SyncAdapter } ",[151,1605,165],{"class":157},[151,1607,185],{"class":168},[151,1609,172],{"class":161},[151,1611,1612],{"class":153,"line":190},[151,1613,209],{"emptyLinePlaceholder":208},[151,1615,1616,1618,1621,1624,1627,1629,1632,1634,1637,1640],{"class":153,"line":205},[151,1617,215],{"class":157},[151,1619,1620],{"class":225}," mySync",[151,1622,1623],{"class":157},":",[151,1625,1626],{"class":225}," SyncAdapter",[151,1628,222],{"class":157},[151,1630,1631],{"class":161}," ({ ",[151,1633,1488],{"class":401},[151,1635,1636],{"class":161}," }) ",[151,1638,1639],{"class":157},"=>",[151,1641,1642],{"class":161}," ({\n",[151,1644,1645,1648,1650,1652],{"class":153,"line":212},[151,1646,1647],{"class":225},"  subscribe",[151,1649,398],{"class":161},[151,1651,1537],{"class":401},[151,1653,1654],{"class":161},") {\n",[151,1656,1657,1660,1663,1665,1668,1671,1674,1677,1679],{"class":153,"line":232},[151,1658,1659],{"class":161},"    backplane.",[151,1661,1662],{"class":225},"on",[151,1664,398],{"class":161},[151,1666,1667],{"class":168},"\"message\"",[151,1669,1670],{"class":161},", (",[151,1672,1673],{"class":401},"raw",[151,1675,1676],{"class":161},") ",[151,1678,1639],{"class":157},[151,1680,1307],{"class":161},[151,1682,1683,1686,1689,1691,1694],{"class":153,"line":238},[151,1684,1685],{"class":157},"      const",[151,1687,1688],{"class":218}," envelope",[151,1690,222],{"class":157},[151,1692,1693],{"class":225}," decodeEnvelope",[151,1695,1696],{"class":161},"(raw);\n",[151,1698,1699,1702,1704,1707,1710,1713,1716,1719],{"class":153,"line":265},[151,1700,1701],{"class":157},"      if",[151,1703,863],{"class":161},[151,1705,1706],{"class":157},"!",[151,1708,1709],{"class":161},"envelope ",[151,1711,1712],{"class":157},"||",[151,1714,1715],{"class":161}," envelope.id ",[151,1717,1718],{"class":157},"===",[151,1720,1721],{"class":161}," id) {\n",[151,1723,1724,1727,1730],{"class":153,"line":461},[151,1725,1726],{"class":157},"        return",[151,1728,1729],{"class":161},"; ",[151,1731,1732],{"class":416},"\u002F\u002F ignore malformed messages and our own echo\n",[151,1734,1735],{"class":153,"line":978},[151,1736,1737],{"class":161},"      }\n",[151,1739,1740,1743],{"class":153,"line":1328},[151,1741,1742],{"class":225},"      deliver",[151,1744,1745],{"class":161},"(envelope.msg);\n",[151,1747,1748],{"class":153,"line":1344},[151,1749,1750],{"class":161},"    });\n",[151,1752,1753],{"class":153,"line":1350},[151,1754,458],{"class":161},[151,1756,1757,1760,1762,1764],{"class":153,"line":1356},[151,1758,1759],{"class":225},"  publish",[151,1761,398],{"class":161},[151,1763,1511],{"class":401},[151,1765,1654],{"class":161},[151,1767,1769,1771,1774,1776,1778,1781],{"class":153,"line":1768},15,[151,1770,1659],{"class":161},[151,1772,1773],{"class":225},"send",[151,1775,398],{"class":161},[151,1777,1569],{"class":225},[151,1779,1780],{"class":161},"(id, msg)); ",[151,1782,1783],{"class":416},"\u002F\u002F stamps `id` so we can filter it back out\n",[151,1785,1787],{"class":153,"line":1786},16,[151,1788,458],{"class":161},[151,1790,1792,1795],{"class":153,"line":1791},17,[151,1793,1794],{"class":225},"  close",[151,1796,1797],{"class":161},"() {\n",[151,1799,1801,1803,1806,1809],{"class":153,"line":1800},18,[151,1802,1659],{"class":161},[151,1804,1805],{"class":225},"disconnect",[151,1807,1808],{"class":161},"(); ",[151,1810,1811],{"class":416},"\u002F\u002F optional: release the connection on shutdown\n",[151,1813,1815],{"class":153,"line":1814},19,[151,1816,458],{"class":161},[151,1818,1820],{"class":153,"line":1819},20,[151,1821,268],{"class":161},[92,1823,1824,1827,1828,1831,1832,1835,1836,1839,1840,1842],{},[96,1825,1826],{},"encodeEnvelope(id, msg)"," returns a JSON string; ",[96,1829,1830],{},"decodeEnvelope(raw)"," parses it back to ",[96,1833,1834],{},"{ id, msg }"," (or ",[96,1837,1838],{},"undefined"," for anything malformed). Binary ",[96,1841,1522],{}," is base64-encoded inside the envelope and restored on decode.",[92,1844,1845],{},[113,1846,1847],{},"Notes:",[304,1849,1850,1863],{},[307,1851,1852,1855,1856,1858,1859,1862],{},[113,1853,1854],{},"Skip the echo check and every publish is delivered twice locally"," — once from your own loopback, once from ",[96,1857,1537],{},". The ",[96,1860,1861],{},"envelope.id === id"," guard is what prevents it.",[307,1864,1865,1868,1869,1871,1872,1874,1875,1878,1879,1881],{},[113,1866,1867],{},"Reuse the envelope helpers"," unless your transport carries structured data natively. ",[96,1870,584],{},", for example, can pass ",[96,1873,1834],{}," (binary ",[96,1876,1877],{},"Uint8Array"," and all) straight through, so its built-in driver skips the JSON\u002Fbase64 envelope entirely and filters on ",[96,1880,1488],{}," directly.",[1883,1884,1885],"style",{},"html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQHwn, html code.shiki .sQHwn{--shiki-light:#E36209;--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .sCsY4, html code.shiki .sCsY4{--shiki-light:#6A737D;--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":147,"searchDepth":175,"depth":175,"links":1887},[1888,1889,1890,1896],{"id":294,"depth":175,"text":295},{"id":475,"depth":175,"text":476},{"id":622,"depth":175,"text":623,"children":1891},[1892,1893,1894,1895],{"id":627,"depth":190,"text":630},{"id":842,"depth":190,"text":845},{"id":1126,"depth":190,"text":1129},{"id":1380,"depth":190,"text":1383},{"id":1473,"depth":175,"text":1474},"md",{"icon":35},{"icon":35},{"title":32,"description":147},"I89cLFPKnG7TFRLWEFFt6-RtKgl3IWRMczxzSXr4xko",[1903,1904],{"title":27,"path":28,"stem":29,"description":147,"icon":30,"children":-1},{"title":37,"path":38,"stem":39,"description":147,"icon":40,"children":-1},1782852618202]