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