[{"data":1,"prerenderedAt":5434},["ShallowReactive",2],{"docs-en-\u002Fdocs\u002Fstream\u002Fprotocol":3},{"id":4,"title":5,"body":6,"description":5427,"extension":5428,"meta":5429,"navigation":4750,"path":5430,"seo":5431,"stem":5432,"__hash__":5433},"docs_en\u002Fdocs\u002Fstream\u002Fprotocol.md","Protocol reference",{"type":7,"value":8,"toc":5370},"minimark",[9,18,38,44,60,63,68,78,85,99,106,155,172,176,182,193,209,211,217,595,706,745,764,778,813,832,835,864,893,906,927,929,935,1013,1016,1101,1150,1156,1169,1262,1265,1331,1334,1339,1364,1389,1418,1421,1552,1569,1584,1595,1623,1640,1644,1661,1673,1679,1689,1728,1753,1757,1768,1783,1790,1802,1813,1824,1836,1838,1842,1856,1862,1869,1873,1876,1885,1895,1901,1909,1950,1961,1968,1970,1977,1980,1987,2290,2298,2399,2409,2413,2437,2447,2458,2501,2554,2558,2579,2596,2599,2642,2649,2659,2666,2679,2685,2692,2697,2711,2737,2751,2755,2765,2768,2770,2774,2787,2843,2849,2987,2991,2996,3147,3153,3155,3163,3246,3255,3315,3321,3332,3337,3380,3385,3403,3427,3429,3433,3600,3613,3649,3653,3671,3675,3683,3697,3703,3733,3737,3743,3764,3777,3780,3814,3820,3826,3881,3894,3911,3917,3931,4003,4006,4042,4056,4065,4080,4091,4100,4105,4148,4169,4204,4207,4211,4442,4448,4459,4537,4547,4555,4564,4566,4575,4583,4594,4605,4607,4611,4618,4621,4625,4628,4693,4695,4699,5210,5212,5216,5353,5366],[10,11,13,17],"h1",{"id":12},"v1-the-pelyr-open-ais-protocol",[14,15,16],"code",{},"\u002Fv1"," — the Pelyr OPEN-AIS protocol",[19,20,21,24,25,29,30,37],"p",{},[14,22,23],{},"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream"," delivers AIS vessel reports as a live\nstream. This is the ",[26,27,28],"strong",{},"native"," protocol. Anyone migrating an existing\naisstream.io client will find a compatible replica at\n",[31,32,34],"a",{"href":33},"\u002Fen\u002Fdocs\u002Fstream\u002Faisstream",[14,35,36],{},"\u002Fv0"," — it exists for the migration, not for new clients.",[19,39,40,43],{},[26,41,42],{},"Read section 11 before you build against this page."," What this service\ncarries and what it does not is the yes\u002Fno question for an integration, and no\nprotocol detail below changes the answer.",[19,45,46,47,50,51,54,55,59],{},"Every frame ",[26,48,49],{},"the service sends"," on this page is ",[26,52,53],{},"generated byte for byte,\nthe way it actually goes out",". Those examples are not hand-written — they come\nfrom the code: an automated test builds each frame with the same code that\nbuilds it in production, and compares it character by character with what\nstands here. If a line drifts, the test turns red. The frames a ",[56,57,58],"em",{},"client"," sends,\nand the example client in section 12, are hand-written illustrations.",[61,62],"hr",{},[64,65,67],"h2",{"id":66},"_1-connecting","1 · Connecting",[69,70,75],"pre",{"className":71,"code":73,"language":74},[72],"language-text","wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\n","text",[14,76,73],{"__ignoreMap":77},"",[19,79,80,81,84],{},"The key travels ",[26,82,83],{},"one of two ways",", never in the URL:",[69,86,90],{"className":87,"code":88,"language":89,"meta":77,"style":77},"language-http shiki shiki-themes github-light github-dark","Authorization: Bearer pk_...\n","http",[14,91,92],{"__ignoreMap":77},[93,94,97],"span",{"class":95,"line":96},"line",1,[93,98,88],{},[19,100,101,102,105],{},"or, for browsers — the JS ",[14,103,104],{},"WebSocket"," API has no way to set custom headers:",[69,107,111],{"className":108,"code":109,"language":110,"meta":77,"style":77},"language-js shiki shiki-themes github-light github-dark","new WebSocket(\"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\",\n              [\"pelyr.v1\", \"pelyr-key.\" + key])\n","js",[14,112,113,134],{"__ignoreMap":77},[93,114,115,119,123,127,131],{"class":95,"line":96},[93,116,118],{"class":117},"szBVR","new",[93,120,122],{"class":121},"sScJk"," WebSocket",[93,124,126],{"class":125},"sVt8B","(",[93,128,130],{"class":129},"sZZnC","\"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\"",[93,132,133],{"class":125},",\n",[93,135,137,140,143,146,149,152],{"class":95,"line":136},2,[93,138,139],{"class":125},"              [",[93,141,142],{"class":129},"\"pelyr.v1\"",[93,144,145],{"class":125},", ",[93,147,148],{"class":129},"\"pelyr-key.\"",[93,150,151],{"class":117}," +",[93,153,154],{"class":125}," key])\n",[156,157,158],"blockquote",{},[19,159,160,163,164,167,168,171],{},[26,161,162],{},"A key in the query string is rejected",", with HTTP 400 and the code\n",[14,165,166],{},"key_in_query"," — not accepted quietly. Query strings end up in proxy logs\nand travel onward through the ",[14,169,170],{},"Referer"," header. A key that has ever sat\nthere is burned, and no one notices.",[64,173,175],{"id":174},"_2-the-flow-in-one-picture","2 · The flow, in one picture",[69,177,180],{"className":178,"code":179,"language":74},[72],"Client                                   Server\n  │  ─── WebSocket upgrade (+ key) ────────▶\n  │  ◀────────────────── welcome ──────────   immediately, unprompted\n  │\n  │  ─── subscribe ────────────────────────▶   within 5 s, otherwise Close 4003\n  │  ◀────────────────── subscribed ───────\n  │\n  │  ◀────────────────── position ─────────    continuously from here on\n  │  ◀────────────────── position ─────────\n  │  ◀────────────────── heartbeat ────────    every 20 s, even in silence\n  │\n  │  ─── ping ─────────────────────────────▶   optional\n  │  ◀────────────────── pong ─────────────\n",[14,181,179],{"__ignoreMap":77},[19,183,184,185,188,189,192],{},"The ",[14,186,187],{},"welcome"," frame arrives unprompted and first. It carries everything a\nclient would otherwise have to guess — limits, its own scope, the deadline\nfor the first ",[14,190,191],{},"subscribe",", and the attribution duties of the sources.",[19,194,195,201,202,204,205,208],{},[26,196,197,200],{},[14,198,199],{},"ping"," is capped at one answer per second."," A ",[14,203,199],{}," that arrives less\nthan a second after the previous one goes ",[26,206,207],{},"unanswered",".",[61,210],{},[64,212,214,215],{"id":213},"_3-welcome","3 · ",[14,216,187],{},[69,218,222],{"className":219,"code":220,"language":221,"meta":77,"style":77},"language-json shiki shiki-themes github-light github-dark","{\"type\":\"welcome\",\"protocol\":\"pelyr.v1\",\"server_time\":\"2026-08-14T15:04:05Z\",\"subscribe_deadline_ms\":5000,\"bbox_fields\":[\"west\",\"south\",\"east\",\"north\"],\"key\":{\"prefix\":\"pk_\",\"scope\":\"collector\"},\"limits\":{\"subscriptions\":4,\"bbox_per_subscription\":50,\"bbox_per_connection\":80,\"mmsi_per_subscription\":500,\"output_bytes_per_second\":0,\"quota_bytes_per_month\":0,\"quota_bytes_used\":6442450944,\"quota_resets_at\":\"2026-09-01T00:00:00Z\"},\"sources\":[{\"id\":\"0\",\"license\":\"NOASSERTION\",\"source\":\"\",\"attribution\":\"\"},{\"id\":\"1\",\"license\":\"LicenseRef-Pelyr-1.1\",\"source\":\"Pelyr\",\"attribution\":\"AIS data from Pelyr (pelyr.com), Pelyr Data Licence 1.1\"},{\"id\":\"100\",\"license\":\"CC-BY-4.0\",\"source\":\"Digitraffic Finnland\",\"attribution\":\"Traffic data from Fintraffic \u002F digitraffic.fi, licence CC BY 4.0\"},{\"id\":\"101\",\"license\":\"NLOD\",\"source\":\"BarentsWatch Live AIS\",\"attribution\":\"Data delivered by BarentsWatch\"}],\"notes\":[\"msg_type for source 'digitraffic-fi' is assigned by the feed (1 = position, 5 = static) and is not the original AIS message type.\"],\"docs\":\"https:\u002F\u002Fpelyr.com\u002Fstream-api\u002F\",\"terms\":\"https:\u002F\u002Fpelyr.com\u002Fstream-terms\u002F\"}\n","json",[14,223,224],{"__ignoreMap":77},[93,225,226,229,233,236,239,242,245,247,249,251,254,256,259,261,264,266,269,271,274,277,280,282,285,287,290,292,295,298,301,304,307,309,312,314,317,319,322,325,328,330,333,335,338,340,343,345,348,350,353,355,358,360,363,365,368,370,373,375,378,380,383,385,387,389,392,394,397,399,402,404,407,409,412,415,418,420,423,425,428,430,433,435,438,440,443,445,448,450,452,455,457,459,462,464,466,468,471,473,475,477,480,482,484,486,489,491,493,495,498,500,502,504,507,509,511,513,516,518,520,522,525,527,529,531,534,536,538,540,543,545,547,549,552,554,556,558,561,564,567,569,572,574,577,579,582,584,587,589,592],{"class":95,"line":96},[93,227,228],{"class":125},"{",[93,230,232],{"class":231},"sj4cs","\"type\"",[93,234,235],{"class":125},":",[93,237,238],{"class":129},"\"welcome\"",[93,240,241],{"class":125},",",[93,243,244],{"class":231},"\"protocol\"",[93,246,235],{"class":125},[93,248,142],{"class":129},[93,250,241],{"class":125},[93,252,253],{"class":231},"\"server_time\"",[93,255,235],{"class":125},[93,257,258],{"class":129},"\"2026-08-14T15:04:05Z\"",[93,260,241],{"class":125},[93,262,263],{"class":231},"\"subscribe_deadline_ms\"",[93,265,235],{"class":125},[93,267,268],{"class":231},"5000",[93,270,241],{"class":125},[93,272,273],{"class":231},"\"bbox_fields\"",[93,275,276],{"class":125},":[",[93,278,279],{"class":129},"\"west\"",[93,281,241],{"class":125},[93,283,284],{"class":129},"\"south\"",[93,286,241],{"class":125},[93,288,289],{"class":129},"\"east\"",[93,291,241],{"class":125},[93,293,294],{"class":129},"\"north\"",[93,296,297],{"class":125},"],",[93,299,300],{"class":231},"\"key\"",[93,302,303],{"class":125},":{",[93,305,306],{"class":231},"\"prefix\"",[93,308,235],{"class":125},[93,310,311],{"class":129},"\"pk_\"",[93,313,241],{"class":125},[93,315,316],{"class":231},"\"scope\"",[93,318,235],{"class":125},[93,320,321],{"class":129},"\"collector\"",[93,323,324],{"class":125},"},",[93,326,327],{"class":231},"\"limits\"",[93,329,303],{"class":125},[93,331,332],{"class":231},"\"subscriptions\"",[93,334,235],{"class":125},[93,336,337],{"class":231},"4",[93,339,241],{"class":125},[93,341,342],{"class":231},"\"bbox_per_subscription\"",[93,344,235],{"class":125},[93,346,347],{"class":231},"50",[93,349,241],{"class":125},[93,351,352],{"class":231},"\"bbox_per_connection\"",[93,354,235],{"class":125},[93,356,357],{"class":231},"80",[93,359,241],{"class":125},[93,361,362],{"class":231},"\"mmsi_per_subscription\"",[93,364,235],{"class":125},[93,366,367],{"class":231},"500",[93,369,241],{"class":125},[93,371,372],{"class":231},"\"output_bytes_per_second\"",[93,374,235],{"class":125},[93,376,377],{"class":231},"0",[93,379,241],{"class":125},[93,381,382],{"class":231},"\"quota_bytes_per_month\"",[93,384,235],{"class":125},[93,386,377],{"class":231},[93,388,241],{"class":125},[93,390,391],{"class":231},"\"quota_bytes_used\"",[93,393,235],{"class":125},[93,395,396],{"class":231},"6442450944",[93,398,241],{"class":125},[93,400,401],{"class":231},"\"quota_resets_at\"",[93,403,235],{"class":125},[93,405,406],{"class":129},"\"2026-09-01T00:00:00Z\"",[93,408,324],{"class":125},[93,410,411],{"class":231},"\"sources\"",[93,413,414],{"class":125},":[{",[93,416,417],{"class":231},"\"id\"",[93,419,235],{"class":125},[93,421,422],{"class":129},"\"0\"",[93,424,241],{"class":125},[93,426,427],{"class":231},"\"license\"",[93,429,235],{"class":125},[93,431,432],{"class":129},"\"NOASSERTION\"",[93,434,241],{"class":125},[93,436,437],{"class":231},"\"source\"",[93,439,235],{"class":125},[93,441,442],{"class":129},"\"\"",[93,444,241],{"class":125},[93,446,447],{"class":231},"\"attribution\"",[93,449,235],{"class":125},[93,451,442],{"class":129},[93,453,454],{"class":125},"},{",[93,456,417],{"class":231},[93,458,235],{"class":125},[93,460,461],{"class":129},"\"1\"",[93,463,241],{"class":125},[93,465,427],{"class":231},[93,467,235],{"class":125},[93,469,470],{"class":129},"\"LicenseRef-Pelyr-1.1\"",[93,472,241],{"class":125},[93,474,437],{"class":231},[93,476,235],{"class":125},[93,478,479],{"class":129},"\"Pelyr\"",[93,481,241],{"class":125},[93,483,447],{"class":231},[93,485,235],{"class":125},[93,487,488],{"class":129},"\"AIS data from Pelyr (pelyr.com), Pelyr Data Licence 1.1\"",[93,490,454],{"class":125},[93,492,417],{"class":231},[93,494,235],{"class":125},[93,496,497],{"class":129},"\"100\"",[93,499,241],{"class":125},[93,501,427],{"class":231},[93,503,235],{"class":125},[93,505,506],{"class":129},"\"CC-BY-4.0\"",[93,508,241],{"class":125},[93,510,437],{"class":231},[93,512,235],{"class":125},[93,514,515],{"class":129},"\"Digitraffic Finnland\"",[93,517,241],{"class":125},[93,519,447],{"class":231},[93,521,235],{"class":125},[93,523,524],{"class":129},"\"Traffic data from Fintraffic \u002F digitraffic.fi, licence CC BY 4.0\"",[93,526,454],{"class":125},[93,528,417],{"class":231},[93,530,235],{"class":125},[93,532,533],{"class":129},"\"101\"",[93,535,241],{"class":125},[93,537,427],{"class":231},[93,539,235],{"class":125},[93,541,542],{"class":129},"\"NLOD\"",[93,544,241],{"class":125},[93,546,437],{"class":231},[93,548,235],{"class":125},[93,550,551],{"class":129},"\"BarentsWatch Live AIS\"",[93,553,241],{"class":125},[93,555,447],{"class":231},[93,557,235],{"class":125},[93,559,560],{"class":129},"\"Data delivered by BarentsWatch\"",[93,562,563],{"class":125},"}],",[93,565,566],{"class":231},"\"notes\"",[93,568,276],{"class":125},[93,570,571],{"class":129},"\"msg_type for source 'digitraffic-fi' is assigned by the feed (1 = position, 5 = static) and is not the original AIS message type.\"",[93,573,297],{"class":125},[93,575,576],{"class":231},"\"docs\"",[93,578,235],{"class":125},[93,580,581],{"class":129},"\"https:\u002F\u002Fpelyr.com\u002Fstream-api\u002F\"",[93,583,241],{"class":125},[93,585,586],{"class":231},"\"terms\"",[93,588,235],{"class":125},[93,590,591],{"class":129},"\"https:\u002F\u002Fpelyr.com\u002Fstream-terms\u002F\"",[93,593,594],{"class":125},"}\n",[596,597,598,611],"table",{},[599,600,601],"thead",{},[602,603,604,608],"tr",{},[605,606,607],"th",{},"Field",[605,609,610],{},"Meaning",[612,613,614,632,642,660,677,689],"tbody",{},[602,615,616,622],{},[617,618,619],"td",{},[14,620,621],{},"subscribe_deadline_ms",[617,623,624,625,627,628,631],{},"Deadline for the first ",[14,626,191],{},". Let it pass and Close ",[26,629,630],{},"4003"," follows",[602,633,634,639],{},[617,635,636],{},[14,637,638],{},"bbox_fields",[617,640,641],{},"The edge names, spelled out. See section 5",[602,643,644,649],{},[617,645,646],{},[14,647,648],{},"key.scope",[617,650,651,652,655,656,659],{},"What this connection may use: ",[14,653,654],{},"collector"," or ",[14,657,658],{},"collector_receiver",". It is the scope in force at the moment the connection opened — the account's own grant, or a platform-wide grant that covers every account. It can be widened while you are connected, and this field is not updated when that happens; see section 4",[602,661,662,667],{},[617,663,664],{},[14,665,666],{},"limits.*",[617,668,669,670,676],{},"The limits of this connection, see section 9. ",[26,671,672,673,675],{},"A ",[14,674,377],{}," means no limit"," — not \"zero\"",[602,678,679,684],{},[617,680,681],{},[14,682,683],{},"sources[]",[617,685,686],{},[26,687,688],{},"The attribution duty, machine-readable — per id, not per source",[602,690,691,696],{},[617,692,693],{},[14,694,695],{},"notes",[617,697,698,699,705],{},"Operational remarks, present only when there is something to say. See ",[56,700,701,704],{},[14,702,703],{},"notes[]"," — read this before you debug"," in section 4",[19,707,708,715,716,718,719,722,723,726,727,730,731,733,734,737,738,726,741,744],{},[26,709,710,711,714],{},"Read the numbers in ",[14,712,713],{},"limits",", do not hard-code them",", and read a ",[14,717,377],{}," as ",[56,720,721],{},"no\nlimit"," rather than as zero. Both ",[14,724,725],{},"output_bytes_per_second"," and\n",[14,728,729],{},"quota_bytes_per_month"," are ",[14,732,377],{}," today: there is ",[26,735,736],{},"no output rate limit and no\nmonthly volume limit",", and none is planned. The ",[14,739,740],{},"quota_bytes_used",[14,742,743],{},"quota_resets_at"," counters still run, so you can see your own consumption —\nnothing is enforced against it.",[19,746,747,748,753,754,757,758,761,762,208],{},"Every data frame carries an ",[26,749,750],{},[14,751,752],{},"id"," in its ",[14,755,756],{},"license"," field, not the licence\ntext itself — resolve it against ",[14,759,760],{},"sources[].id",". The long credit text lives\nonly in ",[14,763,187],{},[19,765,766,767,770,771,774,775,777],{},"The list names ",[26,768,769],{},"ids, not licences."," Every feed from 100 upward gets its own\nid and its own entry — ",[14,772,773],{},"source"," names that one feed, even when its licence\ntext happens to match another feed's word for word. Two feeds share an id\nonly when they run under one collective licence; ",[14,776,773],{}," then names that\ncollective, not either individual feed. So a frame does tell you which feed\ndelivered it, whenever that feed has an id of its own — and tells you only the\nobligation when it runs under a collective entry.",[19,779,780,781,784,785,787,788,792,793,796,797,802,803,806,807,809,810,208],{},"Two ids are reserved and appear in ",[26,782,783],{},"every"," ",[14,786,187],{},", ahead of any\nfeed-specific entry: ",[26,789,790],{},[14,791,377],{}," — no licence on file for that message's source,\nnothing to attribute —\n",[14,794,795],{},"{\"id\":\"0\",\"license\":\"NOASSERTION\",\"source\":\"\",\"attribution\":\"\"}","; and\n",[26,798,799],{},[14,800,801],{},"1"," — the data runs under Pelyr's own licence —\n",[14,804,805],{},"{\"id\":\"1\",\"license\":\"LicenseRef-Pelyr-1.1\",\"source\":\"Pelyr\",\"attribution\":\"AIS data from Pelyr (pelyr.com), Pelyr Data Licence 1.1\"}",".\nA client built only against a hand-picked example is unprepared for either —\n",[14,808,377],{}," in particular carries an empty ",[14,811,812],{},"attribution",[19,814,815,816,819,820,823,824,826,827,208],{},"The identifier ",[14,817,818],{},"LicenseRef-Pelyr-1.1"," covers data Pelyr obtains directly, as\ndistinct from data it obtains from third-party feeds. Its entry is ",[26,821,822],{},"always","\npresent in ",[14,825,683],{},", even on a connection where no such message arrives.\nThat licence is more restrictive than the open-data licences of the other\nentries: it permits commercial use and requires attribution, but does not\npermit the data to be passed on. The text is at\n",[31,828,829],{"href":829,"rel":830},"https:\u002F\u002Fpelyr.com\u002Fpelyr-data-license",[831],"nofollow",[19,833,834],{},"The identifier carries its version, and that is deliberate — section 8.2 of the\nlicence ties data to the version it was delivered under, and this field is the\nonly place that version reaches you. A new version of the licence gets a new\nidentifier; this one is never reinterpreted.",[19,836,837,840,841,145,843,145,845,145,847,849,850,852,853,855,856,859,860,863],{},[26,838,839],{},"The shape, so you can type it against something."," Every entry has exactly\nfour fields — ",[14,842,752],{},[14,844,756],{},[14,846,773],{},[14,848,812],{}," — always in that order\nand always strings. ",[14,851,773],{}," and ",[14,854,812],{}," can be the ",[26,857,858],{},"empty string",";\nnone of the four is ever ",[14,861,862],{},"null"," and none is ever absent. Entries are ordered by\nid, numerically ascending.",[19,865,866,869,870,872,873,875,876,878,879,881,882,885,886,889,890,892],{},[26,867,868],{},"The id is a decimal string and will stay a string",", never a number. ",[14,871,422],{},"\nand ",[14,874,461],{}," are reserved and appear in every ",[14,877,187],{},"; feeds are numbered from\n",[14,880,497],{}," upward, and the band ",[14,883,884],{},"2","–",[14,887,888],{},"99"," is reserved and unused. A data frame's\n",[14,891,756],{}," is always one of these ids.",[19,894,895,905],{},[26,896,897,898,145,900,852,902,904],{},"The id is the identifier; ",[14,899,756],{},[14,901,773],{},[14,903,812],{}," are display\nstrings."," Do not parse them, do not match on them, do not switch on them —\nthey are free text and may be reworded at any time. Everything you decide, you\ndecide on the id.",[19,907,908,784,914,916,917,919,920,922,923,926],{},[26,909,910,911,913],{},"An unknown ",[14,912,756],{}," id means reconnect.",[14,915,683],{}," is fixed for the\nlifetime of the connection — built once in ",[14,918,187],{},", never refreshed while\nit stays open, and connections can stand for days. If a data frame's\n",[14,921,756],{}," id is not among your own ",[14,924,925],{},"welcome.sources[]",", the source list has\nchanged since you connected: treat the attribution as unknown, and reconnect\nto pick up the current one.",[61,928],{},[64,930,932,933],{"id":931},"_4-subscribe","4 · ",[14,934,191],{},[69,936,938],{"className":219,"code":937,"language":221,"meta":77,"style":77},"{\"type\":\"subscribe\",\"id\":\"ostsee\",\"bbox\":[{\"west\":18,\"south\":59,\"east\":26,\"north\":66}],\"fields\":\"position\"}\n",[14,939,940],{"__ignoreMap":77},[93,941,942,944,946,948,951,953,955,957,960,962,965,967,969,971,974,976,978,980,983,985,987,989,992,994,996,998,1001,1003,1006,1008,1011],{"class":95,"line":96},[93,943,228],{"class":125},[93,945,232],{"class":231},[93,947,235],{"class":125},[93,949,950],{"class":129},"\"subscribe\"",[93,952,241],{"class":125},[93,954,417],{"class":231},[93,956,235],{"class":125},[93,958,959],{"class":129},"\"ostsee\"",[93,961,241],{"class":125},[93,963,964],{"class":231},"\"bbox\"",[93,966,414],{"class":125},[93,968,279],{"class":231},[93,970,235],{"class":125},[93,972,973],{"class":231},"18",[93,975,241],{"class":125},[93,977,284],{"class":231},[93,979,235],{"class":125},[93,981,982],{"class":231},"59",[93,984,241],{"class":125},[93,986,289],{"class":231},[93,988,235],{"class":125},[93,990,991],{"class":231},"26",[93,993,241],{"class":125},[93,995,294],{"class":231},[93,997,235],{"class":125},[93,999,1000],{"class":231},"66",[93,1002,563],{"class":125},[93,1004,1005],{"class":231},"\"fields\"",[93,1007,235],{"class":125},[93,1009,1010],{"class":129},"\"position\"",[93,1012,594],{"class":125},[19,1014,1015],{},"Response:",[69,1017,1019],{"className":219,"code":1018,"language":221,"meta":77,"style":77},"{\"type\":\"subscribed\",\"id\":\"ostsee\",\"effective\":{\"bbox\":[{\"west\":18,\"south\":59,\"east\":26,\"north\":66}],\"fields\":\"position\",\"mmsi_count\":0}}\n",[14,1020,1021],{"__ignoreMap":77},[93,1022,1023,1025,1027,1029,1032,1034,1036,1038,1040,1042,1045,1047,1049,1051,1053,1055,1057,1059,1061,1063,1065,1067,1069,1071,1073,1075,1077,1079,1081,1083,1085,1087,1089,1091,1094,1096,1098],{"class":95,"line":96},[93,1024,228],{"class":125},[93,1026,232],{"class":231},[93,1028,235],{"class":125},[93,1030,1031],{"class":129},"\"subscribed\"",[93,1033,241],{"class":125},[93,1035,417],{"class":231},[93,1037,235],{"class":125},[93,1039,959],{"class":129},[93,1041,241],{"class":125},[93,1043,1044],{"class":231},"\"effective\"",[93,1046,303],{"class":125},[93,1048,964],{"class":231},[93,1050,414],{"class":125},[93,1052,279],{"class":231},[93,1054,235],{"class":125},[93,1056,973],{"class":231},[93,1058,241],{"class":125},[93,1060,284],{"class":231},[93,1062,235],{"class":125},[93,1064,982],{"class":231},[93,1066,241],{"class":125},[93,1068,289],{"class":231},[93,1070,235],{"class":125},[93,1072,991],{"class":231},[93,1074,241],{"class":125},[93,1076,294],{"class":231},[93,1078,235],{"class":125},[93,1080,1000],{"class":231},[93,1082,563],{"class":125},[93,1084,1005],{"class":231},[93,1086,235],{"class":125},[93,1088,1010],{"class":129},[93,1090,241],{"class":125},[93,1092,1093],{"class":231},"\"mmsi_count\"",[93,1095,235],{"class":125},[93,1097,377],{"class":231},[93,1099,1100],{"class":125},"}}\n",[596,1102,1103,1111],{},[599,1104,1105],{},[602,1106,1107,1109],{},[605,1108,607],{},[605,1110,610],{},[612,1112,1113,1127,1137],{},[602,1114,1115,1119],{},[617,1116,1117],{},[14,1118,752],{},[617,1120,184,1121,1123,1124,1126],{},[14,1122,752],{}," from your ",[14,1125,191],{},", echoed back",[602,1128,1129,1134],{},[617,1130,1131],{},[14,1132,1133],{},"effective",[617,1135,1136],{},"The selection that actually applies — see below",[602,1138,1139,1143],{},[617,1140,1141],{},[14,1142,695],{},[617,1144,1145,1146],{},"Present only when there is something to say. See ",[56,1147,1148,704],{},[14,1149,703],{},[1151,1152,1154,704],"h3",{"id":1153},"notes-read-this-before-you-debug",[14,1155,703],{},[19,1157,184,1158,1161,1162,1164,1165,1168],{},[14,1159,1160],{},"subscribed"," frame may carry a ",[14,1163,695],{}," array. ",[26,1166,1167],{},"It is the only place where\nthe service tells you that it accepted your subscription but will not do what\nyou meant."," There is no error, because nothing was invalid — the subscription\nsimply cannot deliver what the combination of fields implies.",[69,1170,1172],{"className":219,"code":1171,"language":221,"meta":77,"style":77},"{\"type\":\"subscribed\",\"id\":\"static\",\"effective\":{\"bbox\":[{\"west\":18,\"south\":59,\"east\":26,\"north\":66}],\"fields\":\"full\",\"mmsi_count\":0},\"notes\":[\"include_positionless has no effect while a bbox is set\"]}\n",[14,1173,1174],{"__ignoreMap":77},[93,1175,1176,1178,1180,1182,1184,1186,1188,1190,1193,1195,1197,1199,1201,1203,1205,1207,1209,1211,1213,1215,1217,1219,1221,1223,1225,1227,1229,1231,1233,1235,1237,1239,1242,1244,1246,1248,1250,1252,1254,1256,1259],{"class":95,"line":96},[93,1177,228],{"class":125},[93,1179,232],{"class":231},[93,1181,235],{"class":125},[93,1183,1031],{"class":129},[93,1185,241],{"class":125},[93,1187,417],{"class":231},[93,1189,235],{"class":125},[93,1191,1192],{"class":129},"\"static\"",[93,1194,241],{"class":125},[93,1196,1044],{"class":231},[93,1198,303],{"class":125},[93,1200,964],{"class":231},[93,1202,414],{"class":125},[93,1204,279],{"class":231},[93,1206,235],{"class":125},[93,1208,973],{"class":231},[93,1210,241],{"class":125},[93,1212,284],{"class":231},[93,1214,235],{"class":125},[93,1216,982],{"class":231},[93,1218,241],{"class":125},[93,1220,289],{"class":231},[93,1222,235],{"class":125},[93,1224,991],{"class":231},[93,1226,241],{"class":125},[93,1228,294],{"class":231},[93,1230,235],{"class":125},[93,1232,1000],{"class":231},[93,1234,563],{"class":125},[93,1236,1005],{"class":231},[93,1238,235],{"class":125},[93,1240,1241],{"class":129},"\"full\"",[93,1243,241],{"class":125},[93,1245,1093],{"class":231},[93,1247,235],{"class":125},[93,1249,377],{"class":231},[93,1251,324],{"class":125},[93,1253,566],{"class":231},[93,1255,276],{"class":125},[93,1257,1258],{"class":129},"\"include_positionless has no effect while a bbox is set\"",[93,1260,1261],{"class":125},"]}\n",[19,1263,1264],{},"The notes you can currently receive:",[596,1266,1267,1277],{},[599,1268,1269],{},[602,1270,1271,1274],{},[605,1272,1273],{},"Note",[605,1275,1276],{},"What it means",[612,1278,1279,1292,1305,1318],{},[602,1280,1281,1286],{},[617,1282,1283],{},[14,1284,1285],{},"include_positionless has no effect while a bbox is set",[617,1287,1288,1289],{},"A message without coordinates cannot satisfy a bounding box. Types 5 and 24 will never arrive on this subscription. Use a second subscription with no ",[14,1290,1291],{},"bbox",[602,1293,1294,1299],{},[617,1295,1296],{},[14,1297,1298],{},"bbox[0] crosses the antimeridian",[617,1300,1301,1304],{},[14,1302,1303],{},"west > east"," is read as the statement \"across the 180th meridian\", not as a mistake — see section 5. Because a swapped coordinate pair looks exactly like one, this note says so explicitly instead of leaving you to find half of Siberia in your data",[602,1306,1307,1312],{},[617,1308,1309],{},[14,1310,1311],{},"unknown field 'x' was ignored",[617,1313,1314,1315,1317],{},"A field ",[14,1316,191],{}," doesn't recognise is accepted and reported, not silently dropped",[602,1319,1320,1325],{},[617,1321,1322],{},[14,1323,1324],{},"unknown field 'resume' was ignored; there is no resumption, a reconnect starts from now",[617,1326,1327,1328,1330],{},"Special case: the ",[14,1329,752],{}," format looks like it invites resumption. It doesn't exist",[19,1332,1333],{},"The list grows. Treat an unknown note as informational and log it.",[19,1335,184,1336,1338],{},[14,1337,187],{}," frame carries the same field for operational remarks — its own\nexample in section 3 already shows one in use.",[19,1340,1341,1349,1350,1352,1353,1356,1357,1359,1360,1363],{},[26,1342,1343,1345,1346,208],{},[14,1344,703],{}," is not ",[14,1347,1348],{},"notice"," The two answer the same complaint — \"my\nsubscription delivers nothing\" — from opposite sides. A note rides on the\n",[14,1351,1160],{}," frame and is about ",[26,1354,1355],{},"your filter as the server read it",": it is\nsettled the moment the subscription is accepted and never changes afterwards. A\n",[14,1358,1348],{}," is a frame of its own and is about ",[26,1361,1362],{},"the data",": what the stream is,\nor is not, doing for a subscription that has already been accepted. Section 9\nlists the notice codes.",[19,1365,1366,1371,1372,655,1374,1377,1378,1381,1382,1384,1385,1388],{},[26,1367,1368,1370],{},[14,1369,1133],{}," is the selection that actually applies, not the one that was\nrequested."," A typo in ",[14,1373,1291],{},[14,1375,1376],{},"msg_types"," shows up here in the ",[26,1379,1380],{},"first","\nframe back, instead of as data that simply never arrives. ",[14,1383,1133],{}," does\nnot name a source selection — which sources reach this connection is not a\nper-subscription choice, see ",[56,1386,1387],{},"Every subscribe field"," below.",[19,1390,1391,1399,1400,1403,1404,1406,1407,1410,1411,1414,1415,1417],{},[26,1392,1393,1395,1396,1398],{},[14,1394,1376],{}," appears in ",[14,1397,1133],{}," as soon as a type filter applies"," —\nsorted ascending, e.g. ",[14,1401,1402],{},"\"msg_types\":[1,2,3,18,19]",". Without a filter the\nfield is absent entirely; listing all 27 numbers just to say \"no\nrestriction\" would cost bytes on every ",[14,1405,191],{}," for no information. A\ntypo within the valid range — ",[14,1408,1409],{},"15"," instead of ",[14,1412,1413],{},"5"," — shows up in the\n",[26,1416,1380],{}," frame back instead of as data that simply never arrives.",[1151,1419,1387],{"id":1420},"every-subscribe-field",[596,1422,1423,1434],{},[599,1424,1425],{},[602,1426,1427,1429,1432],{},[605,1428,607],{},[605,1430,1431],{},"Default",[605,1433,610],{},[612,1435,1436,1454,1472,1486,1504,1522,1537],{},[602,1437,1438,1442,1445],{},[617,1439,1440],{},[14,1441,752],{},[617,1443,1444],{},"—",[617,1446,1447,1448,784,1450,1453],{},"Name of the subscription. A second subscribe with the same ",[14,1449,752],{},[26,1451,1452],{},"replaces"," the previous one",[602,1455,1456,1460,1463],{},[617,1457,1458],{},[14,1459,1291],{},[617,1461,1462],{},"all",[617,1464,1465,1466,1468,1469,1471],{},"Up to ",[26,1467,347],{}," boxes, ORed together. At most ",[26,1470,357],{}," across all subscriptions combined",[602,1473,1474,1479,1481],{},[617,1475,1476],{},[14,1477,1478],{},"mmsi",[617,1480,1462],{},[617,1482,1465,1483,1485],{},[26,1484,367],{}," MMSI",[602,1487,1488,1492,1494],{},[617,1489,1490],{},[14,1491,1376],{},[617,1493,1462],{},[617,1495,1496,1497,1503],{},"Integer AIS message ids, 1 to 27. ",[26,1498,1499,1500],{},"The typical vessel set is ",[14,1501,1502],{},"[1, 2, 3, 18, 19]"," — without it, base stations (4), SAR aircraft (9) and aids to navigation (21) would reach a client that renders everything as a ship. None of those three currently reach this stream, but the filter is the contract, not the current feed set",[602,1505,1506,1511,1515],{},[617,1507,1508],{},[14,1509,1510],{},"fields",[617,1512,1513],{},[14,1514,1241],{},[617,1516,1517,655,1519,1521],{},[14,1518,1241],{},[14,1520,1010],{},", see section 6",[602,1523,1524,1529,1534],{},[617,1525,1526],{},[14,1527,1528],{},"include_positionless",[617,1530,1531],{},[14,1532,1533],{},"false",[617,1535,1536],{},"Also messages without a coordinate (static data, names)",[602,1538,1539,1544,1546],{},[617,1540,1541],{},[14,1542,1543],{},"min_interval_ms",[617,1545,1444],{},[617,1547,1548,1551],{},[26,1549,1550],{},"Rejected."," See section 10",[19,1553,1554,1555,1558,1559,1561,1562,1565,1566,1388],{},"There is no field to choose a source. Which sources reach a connection is\ndecided entirely by the granted scope, and ",[14,1556,1557],{},"welcome.key.scope"," tells you what\nit was when the connection opened — read it rather than requesting\noptimistically. A ",[14,1560,191],{}," that still sends a ",[14,1563,1564],{},"sources"," field is accepted\nunchanged; the field is simply unrecognised, see ",[56,1567,1568],{},"Unknown fields are reported,\nnot rejected",[19,1570,1571,1572,1575,1576,1578,1579,1581,1582,208],{},"The scope can be ",[26,1573,1574],{},"widened"," while you are connected — the operator can open\nthe receiver stream to every account at once. That takes effect within a\nminute, without a close and without a new frame: ",[14,1577,1557],{}," still\nshows the older, narrower value, and additional messages simply start\narriving. Nothing you have to handle, and nothing breaks — but do not treat\nthe value from the ",[14,1580,187],{}," as a guarantee that the stream will stay that\nnarrow. If you need the current scope, reconnect and read the new ",[14,1583,187],{},[19,1585,1586,1587,1590,1591,1594],{},"A scope that is ",[26,1588,1589],{},"taken away"," is the other direction, and that one does close\nthe connection: Close ",[26,1592,1593],{},"4002",", see the close code table.",[19,1596,1597,1598,1601,1602,1604,1605,1607,1608,1611,1612,1615,1616,1619,1620,208],{},"Several filters within one subscription are ",[26,1599,1600],{},"AND","ed together: ",[14,1603,1291],{}," +\n",[14,1606,1376],{}," means \"in this box ",[26,1609,1610],{},"and"," of this type\". Several boxes among\nthemselves are ",[26,1613,1614],{},"OR","ed. Up to ",[26,1617,1618],{},"4 subscriptions"," per connection, and those\nin turn are ORed — a message that matches two subscriptions still arrives\nonly ",[26,1621,1622],{},"once",[19,1624,1625,1626,726,1629,1632,1633,1635,1636,1639],{},"Two limits apply to boxes: 50 per subscription and 80 across all\nsubscriptions combined (",[14,1627,1628],{},"limits.bbox_per_subscription",[14,1630,1631],{},"limits.bbox_per_connection"," in the ",[14,1634,187],{},"). ",[26,1637,1638],{},"The 80 is the one that\nbinds"," — plan your boxes against it, not against the 50. Section 9 has the\nrules for staying inside it while changing a filter.",[1151,1641,1643],{"id":1642},"unknown-fields-are-reported-not-rejected","Unknown fields are reported, not rejected",[19,1645,672,1646,1648,1649,1652,1653,1655,1656,1658,1659,208],{},[14,1647,191],{}," that includes a field this server doesn't recognise is still\n",[26,1650,1651],{},"accepted",". The ",[14,1654,1160],{}," frame names the field in\n",[14,1657,695],{},": ",[14,1660,1311],{},[19,1662,1663,1666,1667,1669,1670,1672],{},[14,1664,1665],{},"resume"," in particular gets a longer note — ",[14,1668,1324],{}," — because the\nframe ",[14,1671,752],{}," practically invites the field. There is no resumption.",[1151,1674,184,1676,1678],{"id":1675},"the-subscribed-frame-is-not-a-guarantee",[14,1677,1160],{}," frame is not a guarantee",[19,1680,1681,1682,1684,1685,1688],{},"In normal operation the confirmation arrives within milliseconds, and it names\nthe ",[14,1683,752],{}," you sent. ",[26,1686,1687],{},"It is not promised, and there is no latency you may rely\non."," Two cases to build for:",[1690,1691,1692,1714],"ul",{},[1693,1694,1695,1707,1708,1710,1711,1713],"li",{},[26,1696,1697,1698,1700,1701,1704,1705,208],{},"A rejected ",[14,1699,191],{}," gets an ",[14,1702,1703],{},"error",", not a ",[14,1706,1160],{}," Too soon, an\ninvalid box, too many boxes, a fifth subscription: each answers with an\n",[14,1709,1703],{}," carrying the ",[14,1712,752],{},". The previous subscription stays in place.",[1693,1715,1716,1719,1720,1723,1724,1727],{},[26,1717,1718],{},"Under back-pressure a confirmation can be lost."," Confirmations, errors,\nnotices and ",[14,1721,1722],{},"pong"," share one bounded queue. If it is full, the frame is\ndropped — while the subscription ",[56,1725,1726],{},"is"," active. The loss is counted on our\nside, but nothing reaches you.",[19,1729,1730,1731,1734,1735,1737,1738,1740,1741,1743,1744,1746,1747,1749,1750,1752],{},"So: ",[26,1732,1733],{},"keep a re-arm timer."," If no ",[14,1736,1160],{}," and no ",[14,1739,1703],{}," for an ",[14,1742,752],{},"\narrives within a few seconds, send the ",[14,1745,191],{}," again — it is idempotent, a\nknown ",[14,1748,752],{}," replaces. Do not read the absence of a confirmation as \"rejected\",\nand do not read its presence as the only proof the filter is live: the\n",[14,1751,1133],{}," block inside it is that proof.",[1151,1754,1756],{"id":1755},"overlapping-subscriptions-deliver-once","Overlapping subscriptions deliver once",[19,1758,1759,1760,1763,1764,1767],{},"Two subscriptions on the same connection that both match a message still\nproduce exactly ",[26,1761,1762],{},"one"," frame — the base rule stated above, under ",[56,1765,1766],{},"Every\nsubscribe field",". The same holds for an overlap between two boxes inside a\nsingle subscription.",[19,1769,1770,1771,1773,1774,1776,1777,1779,1780,1782],{},"What that rule doesn't say is which ",[14,1772,1510],{}," the frame is built with. The\nframe is built once per connection, from the ",[26,1775,1380],{}," subscription that\nmatches, in the order you created them. If two overlapping subscriptions ask\nfor different ",[14,1778,1510],{},", the first one wins for the whole overlap — even\nthough the ",[14,1781,1160],{}," frame confirmed each subscription its own value. If\nyou need both profiles reliably, use two connections.",[1151,1784,1786,1789],{"id":1785},"unsubscribe-ends-one-subscription",[14,1787,1788],{},"unsubscribe"," ends one subscription",[19,1791,1792,1795,1796,1798,1799,1801],{},[14,1793,1794],{},"{\"type\":\"unsubscribe\",\"id\":\"…\"}"," removes the subscription carrying that ",[14,1797,752],{},".\nOnly ",[14,1800,752],{}," is read; anything else in the frame is ignored.",[19,1803,1804,1807,1808,1810,1811,208],{},[26,1805,1806],{},"Nothing comes back."," There is no confirmation frame, and an ",[14,1809,752],{}," that does\nnot exist is not an error either — the frame is then simply a no-op. What does\nchange is your budget: the boxes of the removed subscription stop counting\nagainst the 80 per connection, and its slot is free for a new ",[14,1812,752],{},[19,1814,1815,1820,1821,1823],{},[26,1816,1817,1819],{},[14,1818,1788],{}," spends nothing from the subscribe allowance"," (section 9) and\nis never rate-limited — send as many as you like, whenever you like. It also\ncannot put you back before the deadline for the first ",[14,1822,191],{},": a connection that has subscribed once has met that deadline for\ngood. Removing the last subscription leaves the connection open and silent,\nheartbeats included.",[19,1825,1826,1827,1829,1830,784,1833,1835],{},"You will rarely need it. A ",[14,1828,191],{}," with a ",[26,1831,1832],{},"known",[14,1834,752],{}," replaces that\nsubscription atomically, which is the cheaper way to change a filter.",[61,1837],{},[1151,1839,1841],{"id":1840},"after-a-change-frames-from-the-previous-filter-may-still-arrive","After a change, frames from the previous filter may still arrive",[19,1843,1844,1845,1848,1849,1851,1852,1855],{},"Replacing or removing a subscription swaps the ",[26,1846,1847],{},"filter",". It does not empty\nwhat is already queued for you, so for a short moment after the ",[14,1850,1160],{},"\nframe you can still receive vessels that were selected under the ",[56,1853,1854],{},"previous","\nfilter.",[19,1857,1858,1859],{},"The common case is a map that panned: the user moves the viewport, you replace\nthe dynamic subscription, and a handful of vessels outside the new box still\nland. ",[26,1860,1861],{},"That is not a fault on either side.",[19,1863,1864,1865,1868],{},"Emptying the buffer instead would be the worse answer: it would also throw away\nthe frames that are valid under ",[56,1866,1867],{},"both"," filters.",[1151,1870,1872],{"id":1871},"subscription-patterns-that-work","Subscription patterns that work",[19,1874,1875],{},"Three things that are hard to derive and expensive to get wrong.",[19,1877,1878,1884],{},[26,1879,1880,1881,1883],{},"Keep fixed areas and per-user areas under separate ",[14,1882,752],{},"s."," A user changing\ntheir viewport then re-sends only the small dynamic subscription instead of your\nwhole always-on set. With one combined subscription, every viewport change\ncosts the entire filter.",[19,1886,1887,1894],{},[26,1888,1889,1890,1893],{},"When merging boxes to fit the limits, merge by smallest ",[56,1891,1892],{},"added"," covered\narea — not by smallest absolute union."," The absolute union makes visibly worse\nchoices: it will merge two distant small boxes rather than a small box that\nalready sits inside a large one, and the subscription then pays for the empty\ngap between them.",[19,1896,1897,1900],{},[26,1898,1899],{},"Order your subscribes by what your users notice first"," — positions before\nnames. The allowance covers four frames without waiting (section 9), so a\nnormal initial set costs one round trip; beyond that, order still matters.",[64,1902,1904,1905,1908],{"id":1903},"_5-bounding-boxes-have-named-edges","5 · Bounding boxes have ",[26,1906,1907],{},"named"," edges",[69,1910,1912],{"className":219,"code":1911,"language":221,"meta":77,"style":77},"{\"west\":18,\"south\":59,\"east\":26,\"north\":66}\n",[14,1913,1914],{"__ignoreMap":77},[93,1915,1916,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944,1946,1948],{"class":95,"line":96},[93,1917,228],{"class":125},[93,1919,279],{"class":231},[93,1921,235],{"class":125},[93,1923,973],{"class":231},[93,1925,241],{"class":125},[93,1927,284],{"class":231},[93,1929,235],{"class":125},[93,1931,982],{"class":231},[93,1933,241],{"class":125},[93,1935,289],{"class":231},[93,1937,235],{"class":125},[93,1939,991],{"class":231},[93,1941,241],{"class":125},[93,1943,294],{"class":231},[93,1945,235],{"class":125},[93,1947,1000],{"class":231},[93,1949,594],{"class":125},[19,1951,1952,1954,1955,1958,1959,208],{},[14,1953,1303],{}," is ",[26,1956,1957],{},"not an error"," — it is the statement \"across the 180th\nmeridian\". Because a swapped pair looks identical, you still get a note in\n",[14,1960,695],{},[19,1962,1963,1964,1967],{},"Limits: latitude −90…90, longitude −180…180. ",[14,1965,1966],{},"south > north"," is an error.",[61,1969],{},[64,1971,1973,1974,1976],{"id":1972},"_6-data-frames-fields-formats-and-freshness","6 · Data frames: ",[14,1975,1510],{},", formats and freshness",[19,1978,1979],{},"Measured against a real contract record:",[19,1981,1982],{},[26,1983,1984,1986],{},[14,1985,1241],{}," (default) — 556 bytes.",[69,1988,1990],{"className":219,"code":1989,"language":221,"meta":77,"style":77},"{\"type\":\"position\",\"id\":\"1755183845123-0\",\"license\":\"100\",\"data\":{\"schema\":1,\"ingest_ts\":\"2026-08-13T10:00:00.500Z\",\"rx_ts\":\"2026-08-13T10:00:00Z\",\"mmsi\":230123456,\"msg_type\":1,\"lat\":60.161234,\"lon\":24.934567,\"h3_r10\":\"8a1126d33c37fff\",\"sog\":12.3,\"cog\":45.6,\"heading\":47,\"nav_status\":0,\"rot\":-2.7,\"pos_accuracy\":true,\"shipname\":null,\"shiptype\":null,\"imo\":null,\"callsign\":null,\"destination\":null,\"draught\":null,\"eta\":null,\"dim_a\":null,\"dim_b\":null,\"dim_c\":null,\"dim_d\":null,\"length\":null,\"beam\":null,\"dedupe_key\":\"d13eee4b5299487084b80cd5720e6ac9d5d6aa4a\"}}\n",[14,1991,1992],{"__ignoreMap":77},[93,1993,1994,1996,1998,2000,2002,2004,2006,2008,2011,2013,2015,2017,2019,2021,2024,2026,2029,2031,2033,2035,2038,2040,2043,2045,2048,2050,2053,2055,2058,2060,2063,2065,2068,2070,2072,2074,2077,2079,2082,2084,2087,2089,2092,2094,2097,2099,2102,2104,2107,2109,2112,2114,2117,2119,2122,2124,2127,2129,2132,2134,2137,2139,2141,2143,2146,2148,2151,2153,2156,2158,2161,2163,2166,2168,2170,2172,2175,2177,2179,2181,2184,2186,2188,2190,2193,2195,2197,2199,2202,2204,2206,2208,2211,2213,2215,2217,2220,2222,2224,2226,2229,2231,2233,2235,2238,2240,2242,2244,2247,2249,2251,2253,2256,2258,2260,2262,2265,2267,2269,2271,2274,2276,2278,2280,2283,2285,2288],{"class":95,"line":96},[93,1995,228],{"class":125},[93,1997,232],{"class":231},[93,1999,235],{"class":125},[93,2001,1010],{"class":129},[93,2003,241],{"class":125},[93,2005,417],{"class":231},[93,2007,235],{"class":125},[93,2009,2010],{"class":129},"\"1755183845123-0\"",[93,2012,241],{"class":125},[93,2014,427],{"class":231},[93,2016,235],{"class":125},[93,2018,497],{"class":129},[93,2020,241],{"class":125},[93,2022,2023],{"class":231},"\"data\"",[93,2025,303],{"class":125},[93,2027,2028],{"class":231},"\"schema\"",[93,2030,235],{"class":125},[93,2032,801],{"class":231},[93,2034,241],{"class":125},[93,2036,2037],{"class":231},"\"ingest_ts\"",[93,2039,235],{"class":125},[93,2041,2042],{"class":129},"\"2026-08-13T10:00:00.500Z\"",[93,2044,241],{"class":125},[93,2046,2047],{"class":231},"\"rx_ts\"",[93,2049,235],{"class":125},[93,2051,2052],{"class":129},"\"2026-08-13T10:00:00Z\"",[93,2054,241],{"class":125},[93,2056,2057],{"class":231},"\"mmsi\"",[93,2059,235],{"class":125},[93,2061,2062],{"class":231},"230123456",[93,2064,241],{"class":125},[93,2066,2067],{"class":231},"\"msg_type\"",[93,2069,235],{"class":125},[93,2071,801],{"class":231},[93,2073,241],{"class":125},[93,2075,2076],{"class":231},"\"lat\"",[93,2078,235],{"class":125},[93,2080,2081],{"class":231},"60.161234",[93,2083,241],{"class":125},[93,2085,2086],{"class":231},"\"lon\"",[93,2088,235],{"class":125},[93,2090,2091],{"class":231},"24.934567",[93,2093,241],{"class":125},[93,2095,2096],{"class":231},"\"h3_r10\"",[93,2098,235],{"class":125},[93,2100,2101],{"class":129},"\"8a1126d33c37fff\"",[93,2103,241],{"class":125},[93,2105,2106],{"class":231},"\"sog\"",[93,2108,235],{"class":125},[93,2110,2111],{"class":231},"12.3",[93,2113,241],{"class":125},[93,2115,2116],{"class":231},"\"cog\"",[93,2118,235],{"class":125},[93,2120,2121],{"class":231},"45.6",[93,2123,241],{"class":125},[93,2125,2126],{"class":231},"\"heading\"",[93,2128,235],{"class":125},[93,2130,2131],{"class":231},"47",[93,2133,241],{"class":125},[93,2135,2136],{"class":231},"\"nav_status\"",[93,2138,235],{"class":125},[93,2140,377],{"class":231},[93,2142,241],{"class":125},[93,2144,2145],{"class":231},"\"rot\"",[93,2147,235],{"class":125},[93,2149,2150],{"class":231},"-2.7",[93,2152,241],{"class":125},[93,2154,2155],{"class":231},"\"pos_accuracy\"",[93,2157,235],{"class":125},[93,2159,2160],{"class":231},"true",[93,2162,241],{"class":125},[93,2164,2165],{"class":231},"\"shipname\"",[93,2167,235],{"class":125},[93,2169,862],{"class":231},[93,2171,241],{"class":125},[93,2173,2174],{"class":231},"\"shiptype\"",[93,2176,235],{"class":125},[93,2178,862],{"class":231},[93,2180,241],{"class":125},[93,2182,2183],{"class":231},"\"imo\"",[93,2185,235],{"class":125},[93,2187,862],{"class":231},[93,2189,241],{"class":125},[93,2191,2192],{"class":231},"\"callsign\"",[93,2194,235],{"class":125},[93,2196,862],{"class":231},[93,2198,241],{"class":125},[93,2200,2201],{"class":231},"\"destination\"",[93,2203,235],{"class":125},[93,2205,862],{"class":231},[93,2207,241],{"class":125},[93,2209,2210],{"class":231},"\"draught\"",[93,2212,235],{"class":125},[93,2214,862],{"class":231},[93,2216,241],{"class":125},[93,2218,2219],{"class":231},"\"eta\"",[93,2221,235],{"class":125},[93,2223,862],{"class":231},[93,2225,241],{"class":125},[93,2227,2228],{"class":231},"\"dim_a\"",[93,2230,235],{"class":125},[93,2232,862],{"class":231},[93,2234,241],{"class":125},[93,2236,2237],{"class":231},"\"dim_b\"",[93,2239,235],{"class":125},[93,2241,862],{"class":231},[93,2243,241],{"class":125},[93,2245,2246],{"class":231},"\"dim_c\"",[93,2248,235],{"class":125},[93,2250,862],{"class":231},[93,2252,241],{"class":125},[93,2254,2255],{"class":231},"\"dim_d\"",[93,2257,235],{"class":125},[93,2259,862],{"class":231},[93,2261,241],{"class":125},[93,2263,2264],{"class":231},"\"length\"",[93,2266,235],{"class":125},[93,2268,862],{"class":231},[93,2270,241],{"class":125},[93,2272,2273],{"class":231},"\"beam\"",[93,2275,235],{"class":125},[93,2277,862],{"class":231},[93,2279,241],{"class":125},[93,2281,2282],{"class":231},"\"dedupe_key\"",[93,2284,235],{"class":125},[93,2286,2287],{"class":129},"\"d13eee4b5299487084b80cd5720e6ac9d5d6aa4a\"",[93,2289,1100],{"class":125},[19,2291,2292,2297],{},[26,2293,2294,2296],{},[14,2295,1010],{}," — 195 bytes."," Eight fields:",[69,2299,2301],{"className":219,"code":2300,"language":221,"meta":77,"style":77},"{\"type\":\"position\",\"id\":\"1755183845123-0\",\"license\":\"100\",\"data\":{\"mmsi\":230123456,\"msg_type\":1,\"lat\":60.161234,\"lon\":24.934567,\"sog\":12.3,\"cog\":45.6,\"heading\":47,\"rx_ts\":\"2026-08-13T10:00:00Z\"}}\n",[14,2302,2303],{"__ignoreMap":77},[93,2304,2305,2307,2309,2311,2313,2315,2317,2319,2321,2323,2325,2327,2329,2331,2333,2335,2337,2339,2341,2343,2345,2347,2349,2351,2353,2355,2357,2359,2361,2363,2365,2367,2369,2371,2373,2375,2377,2379,2381,2383,2385,2387,2389,2391,2393,2395,2397],{"class":95,"line":96},[93,2306,228],{"class":125},[93,2308,232],{"class":231},[93,2310,235],{"class":125},[93,2312,1010],{"class":129},[93,2314,241],{"class":125},[93,2316,417],{"class":231},[93,2318,235],{"class":125},[93,2320,2010],{"class":129},[93,2322,241],{"class":125},[93,2324,427],{"class":231},[93,2326,235],{"class":125},[93,2328,497],{"class":129},[93,2330,241],{"class":125},[93,2332,2023],{"class":231},[93,2334,303],{"class":125},[93,2336,2057],{"class":231},[93,2338,235],{"class":125},[93,2340,2062],{"class":231},[93,2342,241],{"class":125},[93,2344,2067],{"class":231},[93,2346,235],{"class":125},[93,2348,801],{"class":231},[93,2350,241],{"class":125},[93,2352,2076],{"class":231},[93,2354,235],{"class":125},[93,2356,2081],{"class":231},[93,2358,241],{"class":125},[93,2360,2086],{"class":231},[93,2362,235],{"class":125},[93,2364,2091],{"class":231},[93,2366,241],{"class":125},[93,2368,2106],{"class":231},[93,2370,235],{"class":125},[93,2372,2111],{"class":231},[93,2374,241],{"class":125},[93,2376,2116],{"class":231},[93,2378,235],{"class":125},[93,2380,2121],{"class":231},[93,2382,241],{"class":125},[93,2384,2126],{"class":231},[93,2386,235],{"class":125},[93,2388,2131],{"class":231},[93,2390,241],{"class":125},[93,2392,2047],{"class":231},[93,2394,235],{"class":125},[93,2396,2052],{"class":129},[93,2398,1100],{"class":125},[19,2400,2401,2402,2404,2405,2408],{},"If a field is missing, it stands as ",[14,2403,862],{}," rather than being left out\nentirely: the profile has a ",[26,2406,2407],{},"fixed"," field list, and whoever expects eight\nfields gets eight.",[1151,2410,2412],{"id":2411},"field-formats-that-will-surprise-you","Field formats that will surprise you",[19,2414,2415,2424,2425,2428,2429,2432,2433,2436],{},[26,2416,2417,1954,2420,2423],{},[14,2418,2419],{},"eta",[14,2421,2422],{},"MM-DDTHH:MM",", or null."," No year, no offset — the AIS message\ncarries neither. It is ",[26,2426,2427],{},"not ISO 8601",", and ",[14,2430,2431],{},"Date.parse"," returns ",[14,2434,2435],{},"NaN"," on it.\nParse it by hand against the current year, and expect the value to be in the\npast when a voyage crosses a year boundary.",[19,2438,2439,2440,2442,2443,2446],{},"The AIS not-available sentinels are mapped to null before they reach you:\nmonth 0, day 0 and a malformed value all become ",[14,2441,862],{},". One case survives:\na valid month and day with hour 24 or minute 60 arrives as ",[14,2444,2445],{},"12-16T00:00",". If\nmidnight matters to you, treat it as \"date known, time unknown\".",[19,2448,2449],{},[26,2450,2451,852,2454,2457],{},[14,2452,2453],{},"rx_ts",[14,2455,2456],{},"ingest_ts"," are different clocks at different precision.",[596,2459,2460,2471],{},[599,2461,2462],{},[602,2463,2464,2466,2469],{},[605,2465,607],{},[605,2467,2468],{},"Format",[605,2470,610],{},[612,2472,2473,2487],{},[602,2474,2475,2479,2484],{},[617,2476,2477],{},[14,2478,2453],{},[617,2480,2481],{},[14,2482,2483],{},"YYYY-MM-DDTHH:mm:ssZ",[617,2485,2486],{},"When the upstream source saw the message. Second precision",[602,2488,2489,2493,2498],{},[617,2490,2491],{},[14,2492,2456],{},[617,2494,2495],{},[14,2496,2497],{},"YYYY-MM-DDTHH:mm:ss.SSSZ",[617,2499,2500],{},"When we accepted it. Millisecond precision",[19,2502,2503,2509,2510,2512,2513,2515,2516,2518,2519,2522,2523,2526,2527,852,2529,2532,2533,2535,2536,852,2538,2540,2541,2543,2544,2546,2547,2549,2550,2553],{},[26,2504,2505,2506,2508],{},"Use ",[14,2507,2453],{}," for freshness and for merging with another source."," It is the\nonly one that describes the event rather than our handling of it. When two\nrecords tie on ",[14,2511,2453],{},", break the tie on ",[14,2514,2456],{},". The frame ",[14,2517,752],{}," does\n",[26,2520,2521],{},"not"," help beyond that: it orders frames only ",[26,2524,2525],{},"within one source",". A key\nscoped for both ",[14,2528,654],{},[14,2530,2531],{},"receiver"," sees both mixed on one\nconnection, and each source hands out its ",[14,2534,752],{}," values independently — at low\nthroughput, ",[14,2537,654],{},[14,2539,2531],{}," can even produce the identical ",[14,2542,752],{},"\nstring for two different frames. Do not use ",[14,2545,752],{}," as a cross-source tie-break; we don't have one to\noffer. Comparing our ",[14,2548,2453],{}," against another provider's ",[56,2551,2552],{},"receipt"," time biases\nthe merge toward that provider — put both feeds on their own event\ntimestamps.",[1151,2555,2557],{"id":2556},"vessel-dimensions","Vessel dimensions",[19,2559,2560,2561,2564,2565,2568,2569,2572,2573,852,2576,208],{},"Carried in ",[14,2562,2563],{},"fields:\"full\""," only, as six fields: ",[14,2566,2567],{},"dim_a"," to ",[14,2570,2571],{},"dim_d"," plus\n",[14,2574,2575],{},"length",[14,2577,2578],{},"beam",[19,2580,2581,2582,145,2585,2588,2589,852,2591,2593,2594,208],{},"AIS does not transmit a length. It transmits four distances from the position\nsensor — to the bow, the stern, port and starboard — and ",[14,2583,2584],{},"length = dim_a + dim_b",[14,2586,2587],{},"beam = dim_c + dim_d",". You get both forms, because not every feed\nknows the four offsets: some deliver the finished dimensions only. A feed that\nhas the offsets fills all six fields; a feed that has only the dimensions fills\n",[14,2590,2575],{},[14,2592,2578],{}," and leaves the four offsets ",[14,2595,862],{},[19,2597,2598],{},"Three things to build for:",[1690,2600,2601,2610,2625],{},[1693,2602,2603,2609],{},[26,2604,2605,2606,2608],{},"All six are ",[14,2607,862],{}," unless the frame is static data"," (types 5, 19 and 24\npart B). A position report has no dimensions to carry, so a bbox-scoped\nsubscription will almost never see them — you need the static route from\nsection 7.",[1693,2611,2612,2618,2619,2622,2623,208],{},[26,2613,2614,2615,2617],{},"A single ",[14,2616,377],{}," is a real value",", not a missing one: the sensor sits on that\nedge. Only the whole group ",[14,2620,2621],{},"dim_a = dim_b = dim_c = dim_d = 0"," means \"not\navailable\", and then all six fields are ",[14,2624,862],{},[1693,2626,2627,784,2630,852,2632,2635,2636,852,2639,2641],{},[26,2628,2629],{},"The ranges differ.",[14,2631,2567],{},[14,2633,2634],{},"dim_b"," go to 511, ",[14,2637,2638],{},"dim_c",[14,2640,2571],{},"\nonly to 63 — AIS encodes the pairs with nine and with six bits.",[1151,2643,2645,2646,2648],{"id":2644},"freshness-belongs-on-rx_ts-not-on-the-arrival-rate","Freshness belongs on ",[14,2647,2453],{},", not on the arrival rate",[19,2650,2651,2652,2655,2656,2658],{},"A client that hides ships after ",[56,2653,2654],{},"n"," seconds without a frame must use ",[14,2657,2453],{},"\nfor that, not the time since the last frame it received. The arrival rate\nvaries: feeds have gaps, and a ship does not report at an even pace. A ship\nthat hasn't sent a frame in a while has therefore not disappeared.",[1151,2660,2662,2665],{"id":2661},"dedupe_key-and-what-happens-when-two-feeds-see-the-same-vessel",[14,2663,2664],{},"dedupe_key",", and what happens when two feeds see the same vessel",[19,2667,2668,2670,2671,2674,2675,2678],{},[14,2669,2664],{}," is in ",[14,2672,2673],{},"data",", in the ",[14,2676,2677],{},"full"," profile only. It is a short checksum\nover the report itself:",[69,2680,2683],{"className":2681,"code":2682,"language":74},[72],"SHA-1( mmsi | rx_ts | lat,lon,sog,cog )\n",[14,2684,2682],{"__ignoreMap":77},[19,2686,2687,2688,2691],{},"Six values, joined in that order, with the numbers in the contract's canonical\nform. ",[26,2689,2690],{},"Every frame carries the key in this one form",", whatever the frame came\nfrom. Two frames with the same key describe the same report; that is all the\nfield claims.",[19,2693,2694],{},[26,2695,2696],{},"Now the question that decides your merge logic: do two feeds carrying the same\nvessel report reach you as one frame or two?",[19,2698,2699,2702,2703,2706,2707,2710],{},[26,2700,2701],{},"One, when the content is identical."," If the same content arrives again for\nthe same MMSI within 60 seconds — same position to four decimal places, same\n",[14,2704,2705],{},"heading",", same ",[14,2708,2709],{},"nav_status"," — the repeat is not sent. The suppression happens\nbefore your subscriptions are consulted at all, so it applies whichever profile\nor protocol you use. With two overlapping feeds this is the common case, and it\nis why the same vessel does not arrive twice.",[19,2712,2713,2716,2717,2719,2720,2722,2723,784,2726,2728,2729,2731,2732,2734,2735,208],{},[26,2714,2715],{},"Two, when anything differs."," A different ",[14,2718,2453],{},", a position that moved in\nthe fifth decimal place, a changed ",[14,2721,2705],{},": that is a new report, it is\ndelivered, and it carries a ",[26,2724,2725],{},"different",[14,2727,2664],{},". So ",[14,2730,2664],{}," is not\na per-vessel identity and not a cross-feed join key — for \"same vessel\", use\n",[14,2733,1478],{},"; for \"which report is newer\", use ",[14,2736,2453],{},[19,2738,2739,2740,2743,2744,2746,2747,2750],{},"One consequence worth planning for: ",[26,2741,2742],{},"a vessel that is not moving reports less\noften than it transmits."," A ship at anchor sending the identical position\nevery few seconds reaches you at most once per 60-second window. Do not read a\ngap in its frames as a lost connection — that is what ",[14,2745,2453],{}," and the\nheartbeat's ",[14,2748,2749],{},"feed"," field are for.",[1151,2752,2754],{"id":2753},"after-an-upstream-outage-the-gap-stays-a-gap","After an upstream outage, the gap stays a gap",[19,2756,2757,2758,2761,2762,2764],{},"An upstream outage of more than a minute is not replayed once the feed\nrecovers: the backlog is ",[26,2759,2760],{},"discarded"," rather than delivered late as if it\nwere live. You will see a gap in the stream, with ",[14,2763,2453],{}," on the frames\nafter it jumping forward to match.",[19,2766,2767],{},"This is deliberate. A backlog replayed at full speed is indistinguishable\nfrom live traffic at the client, and it would place every vessel at a\nposition it already left. A gap is honest instead — the next frame for each\nvessel carries where it actually is now.",[61,2769],{},[64,2771,2773],{"id":2772},"_7-getting-vessel-names-static-data","7 · Getting vessel names: static data",[19,2775,2776,2777,2780,2781,235],{},"Names, call signs, destinations and ETAs travel in AIS types 5 and 24. Those\nmessages carry ",[26,2778,2779],{},"no position",", and a message without coordinates cannot\nsatisfy a bounding box. They therefore arrive only through a subscription that\nhas ",[26,2782,2783,2784,2786],{},"no ",[14,2785,1291],{}," at all",[69,2788,2790],{"className":219,"code":2789,"language":221,"meta":77,"style":77},"{\"type\":\"subscribe\",\"id\":\"static\",\"msg_types\":[5,24],\"include_positionless\":true,\"fields\":\"full\"}\n",[14,2791,2792],{"__ignoreMap":77},[93,2793,2794,2796,2798,2800,2802,2804,2806,2808,2810,2812,2815,2817,2819,2821,2824,2826,2829,2831,2833,2835,2837,2839,2841],{"class":95,"line":96},[93,2795,228],{"class":125},[93,2797,232],{"class":231},[93,2799,235],{"class":125},[93,2801,950],{"class":129},[93,2803,241],{"class":125},[93,2805,417],{"class":231},[93,2807,235],{"class":125},[93,2809,1192],{"class":129},[93,2811,241],{"class":125},[93,2813,2814],{"class":231},"\"msg_types\"",[93,2816,276],{"class":125},[93,2818,1413],{"class":231},[93,2820,241],{"class":125},[93,2822,2823],{"class":231},"24",[93,2825,297],{"class":125},[93,2827,2828],{"class":231},"\"include_positionless\"",[93,2830,235],{"class":125},[93,2832,2160],{"class":231},[93,2834,241],{"class":125},[93,2836,1005],{"class":231},[93,2838,235],{"class":125},[93,2840,1241],{"class":129},[93,2842,594],{"class":125},[19,2844,2845,2846,2848],{},"Join it to positions by ",[14,2847,1478],{},". Five things to know before you turn it on:",[2850,2851,2852,2942,2948,2961,2981],"ol",{},[1693,2853,2854,2857,2858,2861,2862,2865,2866,2868,2869,2935,2938,2939,2941],{},[26,2855,2856],{},"Unscoped, it is global."," There is no way to scope static data\n",[56,2859,2860],{},"geographically"," — but you can scope it ",[26,2863,2864],{},"by vessel",": add ",[14,2867,1478],{}," to the\nsame subscription and you get static data only for the vessels you name, up\nto 500 per subscription. That is the recommended shape once you know which\nvessels you care about — the subscription above with one field added:",[69,2870,2872],{"className":219,"code":2871,"language":221,"meta":77,"style":77},"{\"type\":\"subscribe\",\"id\":\"static\",\"mmsi\":[230123456,230108610],\"msg_types\":[5,24],\"include_positionless\":true,\"fields\":\"full\"}\n",[14,2873,2874],{"__ignoreMap":77},[93,2875,2876,2878,2880,2882,2884,2886,2888,2890,2892,2894,2896,2898,2900,2902,2905,2907,2909,2911,2913,2915,2917,2919,2921,2923,2925,2927,2929,2931,2933],{"class":95,"line":96},[93,2877,228],{"class":125},[93,2879,232],{"class":231},[93,2881,235],{"class":125},[93,2883,950],{"class":129},[93,2885,241],{"class":125},[93,2887,417],{"class":231},[93,2889,235],{"class":125},[93,2891,1192],{"class":129},[93,2893,241],{"class":125},[93,2895,2057],{"class":231},[93,2897,276],{"class":125},[93,2899,2062],{"class":231},[93,2901,241],{"class":125},[93,2903,2904],{"class":231},"230108610",[93,2906,297],{"class":125},[93,2908,2814],{"class":231},[93,2910,276],{"class":125},[93,2912,1413],{"class":231},[93,2914,241],{"class":125},[93,2916,2823],{"class":231},[93,2918,297],{"class":125},[93,2920,2828],{"class":231},[93,2922,235],{"class":125},[93,2924,2160],{"class":231},[93,2926,241],{"class":125},[93,2928,1005],{"class":231},[93,2930,235],{"class":125},[93,2932,1241],{"class":129},[93,2934,594],{"class":125},[2936,2937],"br",{},"Left unscoped, static messages are a substantial fraction of the stream —\nplan for the same order of magnitude as your position traffic, not a\ntrickle. Size your consumer for that, scope it by ",[14,2940,1478],{},", or leave the\nsubscription off.",[1693,2943,2944,2947],{},[26,2945,2946],{},"Bound your pending store."," A client that keeps static records for MMSIs\nit has never seen a position for will accumulate the world's fleet. Apply\nstatic only to vessels already in your cache and accept that a name may take\none repeat cycle to appear — that is minutes, not seconds, so do not treat a\nstill-nameless vessel as an error.",[1693,2949,2950,2953,2954,2957,2958,2960],{},[26,2951,2952],{},"The 500 MMSI cap is per subscription, and there is no connection-wide MMSI\nlimit."," Four subscriptions can therefore name up to 2,000 vessels between\nthem. If your nameless population is larger than one subscription holds,\n",[26,2955,2956],{},"rotate the slots"," rather than keeping a fixed list: replace the same\nsubscription ",[14,2959,752],{}," with the next batch. A vessel whose static messages never\nreach the network would otherwise hold a slot forever.",[1693,2962,2963,2966,2967,2970,2971,852,2974,2977,2978],{},[26,2964,2965],{},"Type 24 arrives in two parts, and they are separate messages."," Part A\ncarries ",[14,2968,2969],{},"shipname",". Part B carries ",[14,2972,2973],{},"shiptype",[14,2975,2976],{},"callsign",".\n",[26,2979,2980],{},"Merge your stored static state field by field. A client\nthat replaces it will clobber the name when part B arrives.",[1693,2982,2983,2986],{},[26,2984,2985],{},"It costs nothing against the 80-box budget"," — the limit counts boxes,\nand this subscription has none (section 9).",[1151,2988,2990],{"id":2989},"which-fields-are-populated-for-which-type","Which fields are populated for which type",[19,2992,2993,2995],{},[14,2994,2563],{}," is not a field list. It is the contract payload passed through.",[596,2997,2998,3036],{},[599,2999,3000],{},[602,3001,3002,3005,3008,3011,3018,3033],{},[605,3003,3004],{},"Type",[605,3006,3007],{},"What it is",[605,3009,3010],{},"Position",[605,3012,3013,3015,3016],{},[14,3014,2969],{}," \u002F ",[14,3017,2973],{},[605,3019,3020,3015,3022,3015,3025,3015,3028,3015,3031],{},[14,3021,2976],{},[14,3023,3024],{},"imo",[14,3026,3027],{},"destination",[14,3029,3030],{},"draught",[14,3032,2419],{},[605,3034,3035],{},"dimensions",[612,3037,3038,3055,3070,3091,3107,3126],{},[602,3039,3040,3043,3046,3049,3051,3053],{},[617,3041,3042],{},"1, 2, 3",[617,3044,3045],{},"Class A position",[617,3047,3048],{},"yes",[617,3050,862],{},[617,3052,862],{},[617,3054,862],{},[602,3056,3057,3059,3062,3064,3066,3068],{},[617,3058,973],{},[617,3060,3061],{},"Class B position",[617,3063,3048],{},[617,3065,862],{},[617,3067,862],{},[617,3069,862],{},[602,3071,3072,3075,3078,3080,3085,3087],{},[617,3073,3074],{},"19",[617,3076,3077],{},"Class B extended position",[617,3079,3048],{},[617,3081,3082],{},[26,3083,3084],{},"set",[617,3086,862],{},[617,3088,3089],{},[26,3090,3084],{},[602,3092,3093,3095,3098,3101,3103,3105],{},[617,3094,1413],{},[617,3096,3097],{},"Class A static and voyage",[617,3099,3100],{},"no",[617,3102,3084],{},[617,3104,3084],{},[617,3106,3084],{},[602,3108,3109,3112,3115,3117,3122,3124],{},[617,3110,3111],{},"24 part A",[617,3113,3114],{},"Class B static",[617,3116,3100],{},[617,3118,3119,3121],{},[14,3120,2969],{}," only",[617,3123,862],{},[617,3125,862],{},[602,3127,3128,3131,3133,3135,3139,3143],{},[617,3129,3130],{},"24 part B",[617,3132,3114],{},[617,3134,3100],{},[617,3136,3137,3121],{},[14,3138,2973],{},[617,3140,3141,3121],{},[14,3142,2976],{},[617,3144,3145],{},[26,3146,3084],{},[19,3148,3149,3150,3152],{},"Type 19 is the exception worth knowing: it carries a name ",[26,3151,1610],{}," a position in\nthe same frame.",[61,3154],{},[64,3156,3158,3159,3162],{"id":3157},"_8-heartbeat-every-20-seconds-even-in-silence","8 · ",[14,3160,3161],{},"heartbeat"," — every 20 seconds, even in silence",[69,3164,3166],{"className":219,"code":3165,"language":221,"meta":77,"style":77},"{\"type\":\"heartbeat\",\"at\":\"2026-08-14T15:04:25Z\",\"sent\":1482,\"dropped\":0,\"lag_ms\":340,\"feed\":\"ok\",\"quota_bytes_used\":6442450944,\"quota_resets_at\":\"2026-09-01T00:00:00Z\"}\n",[14,3167,3168],{"__ignoreMap":77},[93,3169,3170,3172,3174,3176,3179,3181,3184,3186,3189,3191,3194,3196,3199,3201,3204,3206,3208,3210,3213,3215,3218,3220,3223,3225,3228,3230,3232,3234,3236,3238,3240,3242,3244],{"class":95,"line":96},[93,3171,228],{"class":125},[93,3173,232],{"class":231},[93,3175,235],{"class":125},[93,3177,3178],{"class":129},"\"heartbeat\"",[93,3180,241],{"class":125},[93,3182,3183],{"class":231},"\"at\"",[93,3185,235],{"class":125},[93,3187,3188],{"class":129},"\"2026-08-14T15:04:25Z\"",[93,3190,241],{"class":125},[93,3192,3193],{"class":231},"\"sent\"",[93,3195,235],{"class":125},[93,3197,3198],{"class":231},"1482",[93,3200,241],{"class":125},[93,3202,3203],{"class":231},"\"dropped\"",[93,3205,235],{"class":125},[93,3207,377],{"class":231},[93,3209,241],{"class":125},[93,3211,3212],{"class":231},"\"lag_ms\"",[93,3214,235],{"class":125},[93,3216,3217],{"class":231},"340",[93,3219,241],{"class":125},[93,3221,3222],{"class":231},"\"feed\"",[93,3224,235],{"class":125},[93,3226,3227],{"class":129},"\"ok\"",[93,3229,241],{"class":125},[93,3231,391],{"class":231},[93,3233,235],{"class":125},[93,3235,396],{"class":231},[93,3237,241],{"class":125},[93,3239,401],{"class":231},[93,3241,235],{"class":125},[93,3243,406],{"class":129},[93,3245,594],{"class":125},[19,3247,3248,3251,3252,3254],{},[26,3249,3250],{},"An operating requirement, not a convenience."," A WS ping also runs, but it\nis invisible in the browser JS API — which makes the ",[14,3253,3161],{}," the only\nthing a browser can use to tell \"the line is up, nothing is just happening\nright now\" from \"the line is dead\".",[596,3256,3257,3265],{},[599,3258,3259],{},[602,3260,3261,3263],{},[605,3262,607],{},[605,3264,610],{},[612,3266,3267,3287,3297],{},[602,3268,3269,3278],{},[617,3270,3271,3274,3275],{},[14,3272,3273],{},"sent"," · ",[14,3276,3277],{},"dropped",[617,3279,3280,3281],{},"Counters for this connection. ",[26,3282,3283,3286],{},[14,3284,3285],{},"dropped > 0"," means: you are losing data",[602,3288,3289,3294],{},[617,3290,3291],{},[14,3292,3293],{},"lag_ms",[617,3295,3296],{},"Distance to the source. Permanently three digits is normal, four is not",[602,3298,3299,3303],{},[617,3300,3301],{},[14,3302,2749],{},[617,3304,3305,655,3307,3310,3311,3314],{},[14,3306,3227],{},[14,3308,3309],{},"\"stalled\""," — ",[26,3312,3313],{},"whether anything at all is currently reaching this service."," See below",[1151,3316,3318,3320],{"id":3317},"feed-answers-the-one-question-you-cannot-answer-yourself",[14,3319,2749],{}," answers the one question you cannot answer yourself",[19,3322,3323,3324,3327,3328,3331],{},"A subscription that was accepted and delivers nothing has two possible causes,\nand from the outside they look identical: ",[26,3325,3326],{},"nothing is moving inside your\nfilter",", or ",[26,3329,3330],{},"nothing is reaching this service at all",". The heartbeat keeps\narriving either way, so the connection looks healthy in both cases.",[19,3333,3334,3336],{},[14,3335,2749],{}," separates them, in every heartbeat:",[596,3338,3339,3351],{},[599,3340,3341],{},[602,3342,3343,3346,3348],{},[605,3344,3345],{},"Value",[605,3347,610],{},[605,3349,3350],{},"What to do",[612,3352,3353,3365],{},[602,3354,3355,3359,3362],{},[617,3356,3357],{},[14,3358,3227],{},[617,3360,3361],{},"Messages are reaching the service",[617,3363,3364],{},"If you get nothing, it is your filter. Widen it, or accept the silence",[602,3366,3367,3371,3374],{},[617,3368,3369],{},[14,3370,3309],{},[617,3372,3373],{},"Nothing has reached the service for more than 60 seconds",[617,3375,3376,3379],{},[26,3377,3378],{},"Not your filter."," Keep the connection open; delivery resumes without any action from you",[19,3381,3382,3384],{},[14,3383,3309],{}," is also what you get from a service that has not yet received its\nfirst message — after a restart, for instance. For a client that is receiving\nnothing, the two are the same event, and in both the cause is not its filter.",[19,3386,3387,3390,3391,3393,3394,3397,3398,3400,3401,208],{},[26,3388,3389],{},"This is a statement about the service, not about your subscription."," It\ncarries no ",[14,3392,752],{},". A subscription with no matches while the feed is healthy is\nthe other case, and it has its own frame: ",[14,3395,3396],{},"notice{empty_subscription}"," in\nsection 9. Do not build a watchdog on the arrival rate alone — build it on\n",[14,3399,2749],{},", and treat an unknown value as ",[14,3402,3227],{},[19,3404,3405,3410,1954,3412,3415,3416,3419,3420,3423,3424,3426],{},[26,3406,3407,3409],{},[14,3408,3277],{}," counts differently from the threshold for Close 4008.",[14,3411,3277],{},[26,3413,3414],{},"cumulative since the connection began"," and spans both kinds\nof loss (ring buffer and output limit). The advance warning\n",[14,3417,3418],{},"notice{slow_consumer_warning}"," and the 4008 decision, by contrast, count\nonly ring-buffer losses, and ",[26,3421,3422],{},"per 60-second window",". Comparing ",[14,3425,3277],{},"\nagainst the threshold of 1,000 named there is therefore misleading.",[61,3428],{},[64,3430,3432],{"id":3431},"_9-limits-and-what-happens-when-theyre-exceeded","9 · Limits, and what happens when they're exceeded",[596,3434,3435,3447],{},[599,3436,3437],{},[602,3438,3439,3442,3444],{},[605,3440,3441],{},"Limit",[605,3443,3345],{},[605,3445,3446],{},"On exceeding it",[612,3448,3449,3470,3483,3496,3509,3528,3552,3566,3582],{},[602,3450,3451,3454,3456],{},[617,3452,3453],{},"Subscriptions per connection",[617,3455,337],{},[617,3457,3458,784,3460,784,3463,3465,3466,3469],{},[14,3459,1703],{},[14,3461,3462],{},"subscription_too_large",[26,3464,1610],{}," Close ",[26,3467,3468],{},"4014",", on the first attempt",[602,3471,3472,3475,3477],{},[617,3473,3474],{},"Boxes per subscription",[617,3476,347],{},[617,3478,3479,784,3481],{},[14,3480,1703],{},[14,3482,3462],{},[602,3484,3485,3488,3490],{},[617,3486,3487],{},"Boxes per connection (all subscriptions)",[617,3489,357],{},[617,3491,3492,784,3494],{},[14,3493,1703],{},[14,3495,3462],{},[602,3497,3498,3501,3503],{},[617,3499,3500],{},"MMSI per subscription",[617,3502,367],{},[617,3504,3505,784,3507],{},[14,3506,1703],{},[14,3508,3462],{},[602,3510,3511,3518,3523],{},[617,3512,3513,3514,784,3516],{},"Deadline for the ",[26,3515,1380],{},[14,3517,191],{},[617,3519,3520,3521],{},"5 s, as ",[14,3522,621],{},[617,3524,3525,3526],{},"Close ",[26,3527,630],{},[602,3529,3530,3535,3541],{},[617,3531,3532,3534],{},[14,3533,191],{}," frames",[617,3536,3537,3540],{},[26,3538,3539],{},"4 without waiting",", then one per 5 s",[617,3542,3543,784,3545,3548,3549],{},[14,3544,1703],{},[14,3546,3547],{},"subscribe_rate_limited","; the fifth too-early frame in a row: Close ",[26,3550,3551],{},"4006",[602,3553,3554,3557,3561],{},[617,3555,3556],{},"Connections per account",[617,3558,3559],{},[26,3560,884],{},[617,3562,3525,3563],{},[26,3564,3565],{},"4005",[602,3567,3568,3571,3574],{},[617,3569,3570],{},"Too-slow consumer",[617,3572,3573],{},">1,000 losses in 60 s",[617,3575,3576,3578,3579],{},[14,3577,3418],{},", then Close ",[26,3580,3581],{},"4008",[602,3583,3584,3587,3592],{},[617,3585,3586],{},"Output rate",[617,3588,3589],{},[14,3590,3591],{},"limits.output_bytes_per_second",[617,3593,3594,3578,3597],{},[14,3595,3596],{},"notice{throttled}",[26,3598,3599],{},"4009",[19,3601,3602,3605,3606,3608,3609,3612],{},[26,3603,3604],{},"An oversized subscribe is rejected whole. Nothing is trimmed."," Fifty-one\nboxes, 501 MMSI, a box sum over 80: the frame is refused with\n",[14,3607,3462],{},", no part of it is applied, and your ",[26,3610,3611],{},"previous\nsubscription stays active and unchanged",". We would rather refuse than deliver\na selection you did not ask for.",[19,3614,3615,3618,3619,3622,3623,3625,3626,3628,3629,3631,3632,784,3634,3636,3637,3639,3640,3642,3643,3645,3646,3648],{},[26,3616,3617],{},"Principle: the first violation is a frame, the repeated one is a close.","\nA typo doesn't cost a connection — but ",[26,3620,3621],{},"never retry a rejected frame\nverbatim",": a second consecutive invalid ",[14,3624,191],{}," ends the connection with\nClose ",[26,3627,3468],{},". Fix the frame or drop it; a blind retry loop is the one client\nbehaviour this rule punishes. One row is the exception: a ",[14,3630,191],{}," with\na ",[26,3633,118],{},[14,3635,752],{}," while four subscriptions are already active is answered with\n",[14,3638,3462],{}," and Close ",[26,3641,3468],{}," straight away. Release one with\n",[14,3644,1788],{}," first, or reuse an existing ",[14,3647,752],{}," — that replaces instead of\nadding.",[1151,3650,3652],{"id":3651},"working-within-the-subscription-limits","Working within the subscription limits",[19,3654,3655,3656,3658,3659,3661,3662,3664,3665,3667,3668,3670],{},"A subscribe with an ",[14,3657,752],{}," that already exists ",[26,3660,1452],{}," that subscription\natomically — no ",[14,3663,1788],{}," needed — and its old boxes therefore do ",[26,3666,2521],{},"\ncount twice against the 80-box budget. A subscription with no ",[14,3669,1291],{}," costs\nnothing against it at all.",[1151,3672,3674],{"id":3673},"the-subscribe-allowance-is-a-bucket-not-a-stopwatch","The subscribe allowance is a bucket, not a stopwatch",[19,3676,3677],{},[26,3678,3679,3680,3682],{},"A fresh connection may send four ",[14,3681,191],{}," frames with no waiting at all.",[19,3684,3685,3686,3689,3690,3692,3693,3696],{},"After that the allowance refills at ",[26,3687,3688],{},"one frame per five seconds",", up to four\nin hand. The long-run limit is unchanged: a client that keeps changing filters\nstill gets one accepted ",[14,3691,191],{}," per five seconds. What changed is ",[56,3694,3695],{},"when"," it\nmay spend them — saved-up allowance can be spent at once.",[19,3698,3699,3700,3702],{},"Only an ",[26,3701,1651],{}," subscribe spends from the allowance. A subscribe rejected\nfor arriving with an empty bucket costs nothing, so a retry does not push your\nown deadline further out.",[19,3704,3705,3708,3709,3712,3713,3715,3716,3719,3720,3722,3723,3725,3726,3729,3730,3732],{},[26,3706,3707],{},"One consequence to plan for."," The rate estimate behind\n",[14,3710,3711],{},"effective.estimated_rate_per_s"," is measured against recent traffic, and it is\ncomputed at most once per five seconds per connection — the same interval, for\nthe same reason. A burst of four subscriptions therefore gets that estimate for the\n",[26,3714,1380],{}," of them; for the rest, ",[14,3717,3718],{},"estimated_rate_per_s"," is simply absent from\nthe ",[14,3721,1160],{}," frame, and neither ",[14,3724,3396],{}," nor\n",[14,3727,3728],{},"notice{feed_stalled}"," fires. If you want the coverage notice for every\nsubscription, space your subscribes five seconds apart. If you want your map\nfilled quickly, burst them — and read ",[14,3731,2749],{}," in the heartbeat (section 8) for\nthe answer the notices would have given you about the feed itself.",[1151,3734,3736],{"id":3735},"when-you-fall-behind","When you fall behind",[19,3738,3739,3740,3742],{},"Your connection has ",[26,3741,1762],{}," send buffer, shared by all your subscriptions. When\nyou cannot keep up with it, we drop — and we choose what to drop so that it\ncosts you as little as possible:",[1690,3744,3745,3751,3757],{},[1693,3746,672,3747,3750],{},[26,3748,3749],{},"superseded position"," for a vessel that already has one waiting is\noverwritten. You lose the older position, not the newer one.",[1693,3752,672,3753,3756],{},[26,3754,3755],{},"static frame never displaces a waiting position."," That position would be\ngone, not merely stale.",[1693,3758,3759,3760,3763],{},"Beyond that, the ",[26,3761,3762],{},"oldest"," frame goes.",[19,3765,3766,3767,3769,3770,3772,3773,3776],{},"Either way, the loss adds to ",[14,3768,3277],{}," in the next ",[14,3771,3161],{},". ",[26,3774,3775],{},"More than\n1,000 losses within 60 seconds ends the connection with Close 4008",". The\nwindow is a tumbling window that starts with the connection, not with a\nsubscription — resubscribing or adding a subscription does not reset it.",[19,3778,3779],{},"Two consequences worth planning for:",[1690,3781,3782,3792],{},[1693,3783,3784,3791],{},[26,3785,3786,852,3788,3790],{},[14,3787,3273],{},[14,3789,3277],{}," are cumulative for the life of the connection."," A\nresubscribe resets nothing; a reconnect resets everything, because it is a\nnew session. Emit deltas and guard against the counter appearing to go\nbackwards across a reconnect.",[1693,3793,3794,3799,3800,3806,3807,3809,3810,3813],{},[26,3795,3796,3797,208],{},"A connection can be closed for falling behind before it has ever received a\nheartbeat reporting a non-zero ",[14,3798,3277],{}," Losses are detected far more often\nthan the heartbeat is sent, and once a consumer is behind, 1,000 losses are\nreached in seconds, not minutes. ",[26,3801,3802,3803,3805],{},"Do not use ",[14,3804,3277],{}," as your early-warning\nsignal"," — use ",[14,3808,3418],{}," below, which is sent for\nexactly this reason and does not wait for the next heartbeat. The two levers\nare ",[14,3811,3812],{},"fields:\"position\""," and a narrower filter. The rate that actually reaches\nyou depends on that filter, so measure it once on a throw-away connection\nrather than assuming a number.",[1151,3815,184,3817,3819],{"id":3816},"the-error-frame",[14,3818,1703],{}," frame",[19,3821,3822,3823,3825],{},"An ",[14,3824,1703],{}," names what the service refused, and whether the connection survives\nit:",[69,3827,3829],{"className":219,"code":3828,"language":221,"meta":77,"style":77},"{\"type\":\"error\",\"code\":\"invalid_subscription\",\"message\":\"bbox[0]: south (70) is north of north (60)\",\"id\":\"ostsee\",\"fatal\":false}\n",[14,3830,3831],{"__ignoreMap":77},[93,3832,3833,3835,3837,3839,3842,3844,3847,3849,3852,3854,3857,3859,3862,3864,3866,3868,3870,3872,3875,3877,3879],{"class":95,"line":96},[93,3834,228],{"class":125},[93,3836,232],{"class":231},[93,3838,235],{"class":125},[93,3840,3841],{"class":129},"\"error\"",[93,3843,241],{"class":125},[93,3845,3846],{"class":231},"\"code\"",[93,3848,235],{"class":125},[93,3850,3851],{"class":129},"\"invalid_subscription\"",[93,3853,241],{"class":125},[93,3855,3856],{"class":231},"\"message\"",[93,3858,235],{"class":125},[93,3860,3861],{"class":129},"\"bbox[0]: south (70) is north of north (60)\"",[93,3863,241],{"class":125},[93,3865,417],{"class":231},[93,3867,235],{"class":125},[93,3869,959],{"class":129},[93,3871,241],{"class":125},[93,3873,3874],{"class":231},"\"fatal\"",[93,3876,235],{"class":125},[93,3878,1533],{"class":231},[93,3880,594],{"class":125},[19,3882,3883,3886,3887,3889,3890,3893],{},[14,3884,3885],{},"fatal"," says whether a close follows immediately — otherwise the client\nwould have to guess it from the code, and the codes carry both meanings. A\nrejected ",[14,3888,191],{}," costs the ",[26,3891,3892],{},"change",", not the connection: the previous\nsubscription stays in place.",[19,3895,3896,3898,3899,3901,3902,3904,3905,3907,3908,3910],{},[14,3897,752],{}," is present when the error belongs to a subscription ",[26,3900,1610],{}," that\nsubscription had an ",[14,3903,752],{},". Do not branch on its presence to decide scope: the\n80-box and 4-subscription limits are connection-wide but carry the ",[14,3906,752],{}," of the\nsubscribe that tripped them, and a subscribe sent without an ",[14,3909,752],{}," produces\nsubscription errors without one.",[1151,3912,3914,3916],{"id":3913},"notice-codes",[14,3915,1348],{}," codes",[19,3918,3919,3920,3922,3923,3926,3927,3930],{},"Advisory, not an error — a ",[14,3921,1348],{}," frame never closes the connection by\nitself. Two of the four are early warnings for a close that follows only if\nthe underlying condition persists (",[14,3924,3925],{},"slow_consumer_warning"," before Close\n4008, ",[14,3928,3929],{},"throttled"," before Close 4009, both already in the table above); the\nother two are never followed by a close at all.",[596,3932,3933,3948],{},[599,3934,3935],{},[602,3936,3937,3940,3945],{},[605,3938,3939],{},"Code",[605,3941,3942,3943],{},"Carries ",[14,3944,752],{},[605,3946,3947],{},"When",[612,3949,3950,3962,3981,3992],{},[602,3951,3952,3957,3959],{},[617,3953,3954],{},[14,3955,3956],{},"empty_subscription",[617,3958,3048],{},[617,3960,3961],{},"Sent once per accepted subscribe, when nothing in the recent traffic fell inside your filter. Widen the bounding box, or drop it once, to see what is flowing",[602,3963,3964,3969,3971],{},[617,3965,3966],{},[14,3967,3968],{},"feed_stalled",[617,3970,3048],{},[617,3972,3973,3974,3977,3978,3980],{},"Sent once per accepted subscribe, when nothing has reached the service recently. The ",[26,3975,3976],{},"condition is service-wide","; the ",[14,3979,752],{}," only says which subscribe surfaced it. Not a coverage limit — do not widen your filter in response",[602,3982,3983,3987,3989],{},[617,3984,3985],{},[14,3986,3925],{},[617,3988,3100],{},[617,3990,3991],{},"Half the loss threshold is reached, i.e. 500 losses in the window. The connection closes past the full one, at the 1,001st",[602,3993,3994,3998,4000],{},[617,3995,3996],{},[14,3997,3929],{},[617,3999,3100],{},[617,4001,4002],{},"The output rate limit is dropping frames for this connection",[19,4004,4005],{},"The list grows. Ignore an unknown code rather than failing on it.",[69,4007,4009],{"className":219,"code":4008,"language":221,"meta":77,"style":77},"{\"type\":\"notice\",\"code\":\"throttled\",\"message\":\"output rate limit reached: frames are being dropped for this connection. Narrow your subscription or use 'fields':'position'. The heartbeat field 'dropped' counts every lost frame.\"}\n",[14,4010,4011],{"__ignoreMap":77},[93,4012,4013,4015,4017,4019,4022,4024,4026,4028,4031,4033,4035,4037,4040],{"class":95,"line":96},[93,4014,228],{"class":125},[93,4016,232],{"class":231},[93,4018,235],{"class":125},[93,4020,4021],{"class":129},"\"notice\"",[93,4023,241],{"class":125},[93,4025,3846],{"class":231},[93,4027,235],{"class":125},[93,4029,4030],{"class":129},"\"throttled\"",[93,4032,241],{"class":125},[93,4034,3856],{"class":231},[93,4036,235],{"class":125},[93,4038,4039],{"class":129},"\"output rate limit reached: frames are being dropped for this connection. Narrow your subscription or use 'fields':'position'. The heartbeat field 'dropped' counts every lost frame.\"",[93,4041,594],{"class":125},[19,4043,4044,3310,4048,4051,4052,4055],{},[26,4045,4046],{},[14,4047,3396],{},[56,4049,4050],{},"the coverage notice"," — is the one a new\nintegration is most likely to meet. The subscribe was accepted, the feed is\nhealthy, and nothing in the recent traffic fell inside the filter: the most\ncommon disappointment on this service is not an error but ",[26,4053,4054],{},"silence",", and this\nframe is what turns that silence into an answer. It arrives once per accepted\nsubscribe, is not repeated while that subscription stands, and closes nothing —\ndata starts flowing the moment a matching message arrives. What to do with it:\nwiden the bounding box, or drop it once, to see what is currently flowing.",[19,4057,4058,4061,4062,4064],{},[26,4059,4060],{},"If the box is not yours to choose"," — a user's map viewport, say — there is\nnothing to widen, and that is fine: the notice is then a statement about the\narea, not a task. Show the empty map, and use ",[14,4063,2749],{}," in the heartbeat\n(section 8) to tell your users the difference between \"nothing here\" and\n\"nothing anywhere\". The\nservice withholds the notice rather than guessing when it cannot yet judge —\nshortly after a restart, and while the upstream feed is stalled, which is what\nthe next notice is for.",[19,4066,4067,4070,4071,4073,4074,4076,4077,4079],{},[26,4068,4069],{},"The two can never both apply to one subscribe."," They come from the same\nmeasurement of recent traffic, and it reports one of three states: usable and\nyour filter matched nothing → ",[14,4072,3956],{},"; too old to mean anything →\n",[14,4075,3968],{},"; not yet usable → neither, and you get silence. So\n",[14,4078,3956],{}," is structurally unreachable while nothing is flowing, and\na recovery cannot produce both.",[19,4081,4082,4086,4087,4090],{},[26,4083,4084],{},[14,4085,3728],{}," is the counterpart to the coverage notice, not a\nreplacement for it: the upstream feed itself has delivered nothing for more\nthan 60 seconds — that is not a coverage limit, and the client should\ntherefore not narrow or widen its filter because of it. It fires only when\na subscription is created ",[26,4088,4089],{},"while"," the stall is already under way, so it does\nnot cover the duration of a stall — only that one moment.",[19,4092,4093,4094,4099],{},"For a stall that begins after you subscribed, read ",[26,4095,4096,4098],{},[14,4097,2749],{}," in the heartbeat","\n(section 8). That is the signal built for exactly this case, it arrives every\n20 seconds, and it needs no new subscription to reach you.",[156,4101,4102],{},[19,4103,4104],{},"The upstream feed has not delivered recently. Not a coverage limit — do not\nwiden your filter in response.",[69,4106,4108],{"className":219,"code":4107,"language":221,"meta":77,"style":77},"{\"type\":\"notice\",\"code\":\"feed_stalled\",\"id\":\"ostsee\",\"message\":\"No messages have reached this service recently. This is not a coverage limit and not a problem with your filter: the upstream feed is not currently delivering. Keep the connection open; delivery resumes without any action from you.\"}\n",[14,4109,4110],{"__ignoreMap":77},[93,4111,4112,4114,4116,4118,4120,4122,4124,4126,4129,4131,4133,4135,4137,4139,4141,4143,4146],{"class":95,"line":96},[93,4113,228],{"class":125},[93,4115,232],{"class":231},[93,4117,235],{"class":125},[93,4119,4021],{"class":129},[93,4121,241],{"class":125},[93,4123,3846],{"class":231},[93,4125,235],{"class":125},[93,4127,4128],{"class":129},"\"feed_stalled\"",[93,4130,241],{"class":125},[93,4132,417],{"class":231},[93,4134,235],{"class":125},[93,4136,959],{"class":129},[93,4138,241],{"class":125},[93,4140,3856],{"class":231},[93,4142,235],{"class":125},[93,4144,4145],{"class":129},"\"No messages have reached this service recently. This is not a coverage limit and not a problem with your filter: the upstream feed is not currently delivering. Keep the connection open; delivery resumes without any action from you.\"",[93,4147,594],{"class":125},[19,4149,4150,4154,4155,4157,4158,4161,4162,4164,4165,4168],{},[26,4151,4152],{},[14,4153,3418],{}," is the advance warning before Close\n",[26,4156,3581],{},": it arrives at ",[26,4159,4160],{},"half"," the loss threshold (500 of 1,000 losses in\nthe 60-second window), at most once per window. ",[14,4163,3277],{}," alone doesn't work\nas an early warning: in exactly the window where the heartbeat would be\nreporting the losses, it no longer arrives in time. This notice goes out\nindependently of the heartbeat cadence, as soon as the halfway point is\nreached, leaving you the larger half of the window to react. The number in\n",[14,4166,4167],{},"message"," is the count at the moment it fired, so depending on the loss rate\nit can already be well above 500 — as in the following example with 650.",[69,4170,4172],{"className":219,"code":4171,"language":221,"meta":77,"style":77},"{\"type\":\"notice\",\"code\":\"slow_consumer_warning\",\"message\":\"dropping frames: 650 lost in this window; the connection closes at 1000 in this window (a separate, per-window count from the heartbeat's cumulative 'dropped'). Narrow your subscription or use 'fields':'position'.\"}\n",[14,4173,4174],{"__ignoreMap":77},[93,4175,4176,4178,4180,4182,4184,4186,4188,4190,4193,4195,4197,4199,4202],{"class":95,"line":96},[93,4177,228],{"class":125},[93,4179,232],{"class":231},[93,4181,235],{"class":125},[93,4183,4021],{"class":129},[93,4185,241],{"class":125},[93,4187,3846],{"class":231},[93,4189,235],{"class":125},[93,4191,4192],{"class":129},"\"slow_consumer_warning\"",[93,4194,241],{"class":125},[93,4196,3856],{"class":231},[93,4198,235],{"class":125},[93,4200,4201],{"class":129},"\"dropping frames: 650 lost in this window; the connection closes at 1000 in this window (a separate, per-window count from the heartbeat's cumulative 'dropped'). Narrow your subscription or use 'fields':'position'.\"",[93,4203,594],{"class":125},[19,4205,4206],{},"The wording in that frame names the threshold, not the exact trigger: 1,000\nlosses in the window still leave the connection standing, the 1,001st ends it.",[1151,4208,4210],{"id":4209},"all-close-codes","All close codes",[596,4212,4213,4224],{},[599,4214,4215],{},[602,4216,4217,4219,4222],{},[605,4218,3939],{},[605,4220,4221],{},"Name",[605,4223,610],{},[612,4225,4226,4241,4256,4276,4290,4305,4319,4335,4353,4367,4381,4396,4414,4429],{},[602,4227,4228,4233,4238],{},[617,4229,4230],{},[26,4231,4232],{},"4000",[617,4234,4235],{},[14,4236,4237],{},"protocol_error",[617,4239,4240],{},"Frame was not a valid JSON object, or the type is unknown",[602,4242,4243,4248,4253],{},[617,4244,4245],{},[26,4246,4247],{},"4001",[617,4249,4250],{},[14,4251,4252],{},"unauthorized",[617,4254,4255],{},"Reserved — not sent by this service today. A missing or rejected key is answered with HTTP 401 before the upgrade completes, never with this close code",[602,4257,4258,4262,4267],{},[617,4259,4260],{},[26,4261,1593],{},[617,4263,4264],{},[14,4265,4266],{},"forbidden_scope",[617,4268,4269,4270,4273,4274],{},"The granted scope was ",[26,4271,4272],{},"narrowed"," while the connection was open and at least one subscription was active. Only a withdrawal closes: a scope that is widened mid-connection is applied silently, without a close and without a new frame. Reconnect to see the new scope in the ",[14,4275,187],{},[602,4277,4278,4282,4287],{},[617,4279,4280],{},[26,4281,630],{},[617,4283,4284],{},[14,4285,4286],{},"subscribe_timeout",[617,4288,4289],{},"No subscribe within the deadline from the welcome frame",[602,4291,4292,4297,4302],{},[617,4293,4294],{},[26,4295,4296],{},"4004",[617,4298,4299],{},[14,4300,4301],{},"invalid_subscription",[617,4303,4304],{},"Subscription invalid repeatedly",[602,4306,4307,4311,4316],{},[617,4308,4309],{},[26,4310,3565],{},[617,4312,4313],{},[14,4314,4315],{},"too_many_connections",[617,4317,4318],{},"Too many concurrent connections — this account's own limit or the service's overall limit; the close reason names which",[602,4320,4321,4325,4329],{},[617,4322,4323],{},[26,4324,3551],{},[617,4326,4327],{},[14,4328,3547],{},[617,4330,4331,4332],{},"Five subscribe frames in a row that each arrived with an empty allowance — see ",[56,4333,4334],{},"The subscribe allowance is a bucket",[602,4336,4337,4342,4347],{},[617,4338,4339],{},[26,4340,4341],{},"4007",[617,4343,4344],{},[14,4345,4346],{},"idle_timeout",[617,4348,4349,4350,4352],{},"No WebSocket-level pong within 45 seconds — the transport pong, not the JSON ",[14,4351,1722],{}," frame. Browsers answer it automatically",[602,4354,4355,4359,4364],{},[617,4356,4357],{},[26,4358,3581],{},[617,4360,4361],{},[14,4362,4363],{},"slow_consumer",[617,4365,4366],{},"The client reads too slowly; the reason names the number of dropped messages",[602,4368,4369,4373,4378],{},[617,4370,4371],{},[26,4372,3599],{},[617,4374,4375],{},[14,4376,4377],{},"rate_limited",[617,4379,4380],{},"The output byte rate limit was exceeded for 60 seconds",[602,4382,4383,4388,4393],{},[617,4384,4385],{},[26,4386,4387],{},"4011",[617,4389,4390],{},[14,4391,4392],{},"key_revoked",[617,4394,4395],{},"Key revoked — within 60 seconds, even on an open connection",[602,4397,4398,4403,4408],{},[617,4399,4400],{},[26,4401,4402],{},"4012",[617,4404,4405],{},[14,4406,4407],{},"server_restart",[617,4409,4410,4411],{},"Redeploy. Reason: ",[14,4412,4413],{},"redeploy; reconnect after 5-30s jitter",[602,4415,4416,4421,4426],{},[617,4417,4418],{},[26,4419,4420],{},"4013",[617,4422,4423],{},[14,4424,4425],{},"upstream_unavailable",[617,4427,4428],{},"Reserved — not sent by this service today. The key store being unreachable during authentication is answered with HTTP 503 before the upgrade completes, never with this close code",[602,4430,4431,4435,4439],{},[617,4432,4433],{},[26,4434,3468],{},[617,4436,4437],{},[14,4438,3462],{},[617,4440,4441],{},"Too many bounding boxes or MMSI for one subscription, too many bounding boxes across the connection, or too many subscriptions on the connection",[19,4443,4444,4445,4447],{},"Every fatal close is preceded by an ",[14,4446,1703],{}," frame with the same code —\nsome client libraries swallow the close reason, and the user would then see\nonly a number.",[19,4449,4450,852,4452,4454,4455,4458],{},[26,4451,4247],{},[26,4453,4420],{}," exist as reserved identifiers but are not sent by this\nservice today: a key that is missing, unknown, or cannot be verified, and a\nkey store that is temporarily unreachable, are both decided ",[26,4456,4457],{},"before"," the\nWebSocket upgrade — as an HTTP status with a JSON body, not as a close code.\nThe same is true for one of the rejections below: a key in the query string.\nIt is answered as HTTP, and the connection never becomes a WebSocket at all:",[596,4460,4461,4472],{},[599,4462,4463],{},[602,4464,4465,4468,4470],{},[605,4466,4467],{},"HTTP status",[605,4469,3939],{},[605,4471,3947],{},[612,4473,4474,4486,4498,4514,4526],{},[602,4475,4476,4479,4483],{},[617,4477,4478],{},"400",[617,4480,4481],{},[14,4482,166],{},[617,4484,4485],{},"The API key was placed in the query string instead of a header",[602,4487,4488,4491,4495],{},[617,4489,4490],{},"401",[617,4492,4493],{},[14,4494,4252],{},[617,4496,4497],{},"The key is missing, unknown, or rejected",[602,4499,4500,4502,4507],{},[617,4501,4490],{},[617,4503,4504],{},[14,4505,4506],{},"stream_capability_missing",[617,4508,4509,4510,4513],{},"The key is valid but was not sold for the stream. It grants access to the HTTPS API only; ask for the ",[14,4511,4512],{},"stream"," capability to be added to it",[602,4515,4516,4519,4523],{},[617,4517,4518],{},"503",[617,4520,4521],{},[14,4522,4315],{},[617,4524,4525],{},"The service is at its overall connection limit",[602,4527,4528,4530,4534],{},[617,4529,4518],{},[617,4531,4532],{},[14,4533,4425],{},[617,4535,4536],{},"The key store is temporarily unreachable",[19,4538,4539,4540,4543,4544,4546],{},"The remaining one — the service's overall connection limit — is the exception\nin this table: it can arrive ",[26,4541,4542],{},"either way",". The limit is checked twice, once\nbefore the upgrade and once more when the finished connection is registered,\nbecause another client can take the last free slot between those two moments.\nThe usual answer is the 503 above; a connection that loses that race is\naccepted and then ended with Close ",[26,4545,3565],{},", whose reason names the service\nlimit rather than the account limit. Handle both.",[19,4548,4549,4551,4552,4554],{},[26,4550,4402],{}," carries the reason ",[14,4553,4413],{},". The\njitter is meant seriously: every client of this service reconnecting in the\nsame second is a self-inflicted load attack.",[19,4556,4557,4559,4560,4563],{},[26,4558,4387],{}," arrives within 60 seconds of a revocation — even on an ",[26,4561,4562],{},"open","\nconnection. A withdrawn key does not stay valid until the next time a\nconnection is established.",[61,4565],{},[64,4567,4569,4570,1954,4572],{"id":4568},"_10-min_interval_ms-is-rejected","10 · ",[14,4571,1543],{},[26,4573,4574],{},"rejected",[19,4576,672,4577,4579,4580,4582],{},[14,4578,191],{}," carrying this field (other than ",[14,4581,377],{},") gets an error, not a\nsilent no-op. This is a deliberate departure from the original design, and\nthe reason lies in the architecture:",[19,4584,4585,4586,4589,4590,4593],{},"A frame is prepared once and shared by everyone it goes to, and by the time it\nreaches your connection it is no longer attributable to one of your\nsubscriptions. Throttling the whole ",[26,4587,4588],{},"connection"," instead would be easy — but\nas soon as a connection carries two subscriptions and only one of them asked\nfor it, we would be discarding messages belonging to the ",[26,4591,4592],{},"other"," one. That is\nexactly silent throttling, the one thing this protocol is built against.",[19,4595,4596,4597,4599,4600,145,4602,208],{},"That leaves two honest answers: honour the promise, or don't make it. What\nthe field was meant to achieve is usually better served by a narrower\nselection — ",[14,4598,1478],{},", a smaller ",[14,4601,1291],{},[14,4603,4604],{},"fields: \"position\"",[61,4606],{},[64,4608,4610],{"id":4609},"_11-what-this-service-carries-and-what-it-does-not","11 · What this service carries, and what it does not",[19,4612,4613,4614,4617],{},"Coverage is broad but uneven. Global coverage is ",[26,4615,4616],{},"sampled"," — roughly one\nposition per vessel per minute. Nordic waters are continuous.",[19,4619,4620],{},"This is stated this plainly because no protocol changes it. Whoever learns\nthe limit beforehand stays for what is covered; whoever only discovers it\nafter integrating leaves.",[1151,4622,4624],{"id":4623},"deliberate-non-features","Deliberate non-features",[19,4626,4627],{},"Each of these has been asked for and answered. They are listed together so you\ndo not design against a capability that will not arrive.",[1690,4629,4630,4641,4660,4672,4679,4687],{},[1693,4631,4632,4637,4638,4640],{},[26,4633,4634,4635,208],{},"No resumption by frame ",[14,4636,752],{}," There is no history buffer and a reconnect\nstarts from now. Reconcile with ",[14,4639,2453],{}," instead, and keep your vessel cache\nacross the reconnect.",[1693,4642,4643,4646,4647,4650,4651,4656,4657,4659],{},[26,4644,4645],{},"No per-vessel state on the server."," This service holds no last-known state\nfor a vessel, which is why two things do not exist and will not: ",[26,4648,4649],{},"inline\nstatic enrichment"," (names attached to position frames) and ",[26,4652,4653,4655],{},[14,4654,1291],{}," on a\nstatic subscription"," (filtering positionless messages by a remembered\nposition). Scope static data by ",[14,4658,1478],{}," instead — see section 7.",[1693,4661,4662,4668,4669,4671],{},[26,4663,4664,4665,208],{},"No ",[14,4666,4667],{},"permessage-deflate"," A deflate stream is stateful per connection.\n",[14,4670,3812],{}," is the alternative and is 2.85× more effective than\ncompression would be.",[1693,4673,4674,4678],{},[26,4675,4664,4676,208],{},[14,4677,1543],{}," See section 10 for why a per-subscription promise\ncannot be kept here.",[1693,4680,4681,784,4684,4686],{},[26,4682,4683],{},"No per-source capability or coverage table.",[14,4685,925],{}," names the\nsources that reach your connection; which message types and which regions\neach one carries is not published.",[1693,4688,4689,4692],{},[26,4690,4691],{},"No message types 4, 9 or 21."," Base stations, SAR aircraft and aids to\nnavigation do not currently reach this stream. Filter for them anyway if you\ncare: the filter is the contract, not the current feed set.",[61,4694],{},[64,4696,4698],{"id":4697},"_12-a-complete-client","12 · A complete client",[69,4700,4702],{"className":108,"code":4701,"language":110,"meta":77,"style":77},"const ws = new WebSocket(\"wss:\u002F\u002Fstream.pelyr.com\u002Fv1\u002Fstream\",\n                         [\"pelyr.v1\", \"pelyr-key.\" + KEY]);\n\nws.onmessage = (ev) => {\n  const frame = JSON.parse(ev.data);\n  switch (frame.type) {\n    case \"welcome\":\n      \u002F\u002F Attribution is mandatory, and belongs where the user actually sees it.\n      showAttribution(frame.sources.map(s => s.attribution));\n      ws.send(JSON.stringify({\n        type: \"subscribe\",\n        id: \"ostsee\",\n        bbox: [{ west: 18, south: 59, east: 26, north: 66 }],\n        fields: \"position\",\n      }));\n      break;\n\n    case \"subscribed\":\n      \u002F\u002F effective, not what was requested — this is where a gap shows up.\n      console.log(\"active:\", frame.effective);\n      break;\n\n    case \"position\":\n      drawShip(frame.data);\n      break;\n\n    case \"heartbeat\":\n      if (frame.dropped > 0) console.warn(\"data loss:\", frame.dropped);\n      break;\n\n    case \"error\":\n      console.error(frame.code, frame.message);\n      break;\n  }\n};\n\n\u002F\u002F 4012 means redeploy — reconnect with jitter, not immediately.\nws.onclose = (ev) => {\n  const waitTime = ev.code === 4012 ? 5000 + Math.random() * 25000 : 1000;\n  setTimeout(reconnect, waitTime);\n};\n",[14,4703,4704,4726,4745,4752,4779,4800,4809,4821,4828,4851,4873,4883,4893,4919,4929,4935,4944,4949,4959,4965,4982,4989,4994,5004,5013,5020,5025,5035,5064,5071,5076,5086,5096,5103,5109,5115,5120,5126,5146,5196,5205],{"__ignoreMap":77},[93,4705,4706,4709,4712,4715,4718,4720,4722,4724],{"class":95,"line":96},[93,4707,4708],{"class":117},"const",[93,4710,4711],{"class":231}," ws",[93,4713,4714],{"class":117}," =",[93,4716,4717],{"class":117}," new",[93,4719,122],{"class":121},[93,4721,126],{"class":125},[93,4723,130],{"class":129},[93,4725,133],{"class":125},[93,4727,4728,4731,4733,4735,4737,4739,4742],{"class":95,"line":136},[93,4729,4730],{"class":125},"                         [",[93,4732,142],{"class":129},[93,4734,145],{"class":125},[93,4736,148],{"class":129},[93,4738,151],{"class":117},[93,4740,4741],{"class":231}," KEY",[93,4743,4744],{"class":125},"]);\n",[93,4746,4748],{"class":95,"line":4747},3,[93,4749,4751],{"emptyLinePlaceholder":4750},true,"\n",[93,4753,4755,4758,4761,4763,4766,4770,4773,4776],{"class":95,"line":4754},4,[93,4756,4757],{"class":125},"ws.",[93,4759,4760],{"class":121},"onmessage",[93,4762,4714],{"class":117},[93,4764,4765],{"class":125}," (",[93,4767,4769],{"class":4768},"s4XuR","ev",[93,4771,4772],{"class":125},") ",[93,4774,4775],{"class":117},"=>",[93,4777,4778],{"class":125}," {\n",[93,4780,4782,4785,4787,4789,4792,4794,4797],{"class":95,"line":4781},5,[93,4783,4784],{"class":117},"  const",[93,4786,3819],{"class":231},[93,4788,4714],{"class":117},[93,4790,4791],{"class":231}," JSON",[93,4793,208],{"class":125},[93,4795,4796],{"class":121},"parse",[93,4798,4799],{"class":125},"(ev.data);\n",[93,4801,4803,4806],{"class":95,"line":4802},6,[93,4804,4805],{"class":117},"  switch",[93,4807,4808],{"class":125}," (frame.type) {\n",[93,4810,4812,4815,4818],{"class":95,"line":4811},7,[93,4813,4814],{"class":117},"    case",[93,4816,4817],{"class":129}," \"welcome\"",[93,4819,4820],{"class":125},":\n",[93,4822,4824],{"class":95,"line":4823},8,[93,4825,4827],{"class":4826},"sJ8bj","      \u002F\u002F Attribution is mandatory, and belongs where the user actually sees it.\n",[93,4829,4831,4834,4837,4840,4842,4845,4848],{"class":95,"line":4830},9,[93,4832,4833],{"class":121},"      showAttribution",[93,4835,4836],{"class":125},"(frame.sources.",[93,4838,4839],{"class":121},"map",[93,4841,126],{"class":125},[93,4843,4844],{"class":4768},"s",[93,4846,4847],{"class":117}," =>",[93,4849,4850],{"class":125}," s.attribution));\n",[93,4852,4854,4857,4860,4862,4865,4867,4870],{"class":95,"line":4853},10,[93,4855,4856],{"class":125},"      ws.",[93,4858,4859],{"class":121},"send",[93,4861,126],{"class":125},[93,4863,4864],{"class":231},"JSON",[93,4866,208],{"class":125},[93,4868,4869],{"class":121},"stringify",[93,4871,4872],{"class":125},"({\n",[93,4874,4876,4879,4881],{"class":95,"line":4875},11,[93,4877,4878],{"class":125},"        type: ",[93,4880,950],{"class":129},[93,4882,133],{"class":125},[93,4884,4886,4889,4891],{"class":95,"line":4885},12,[93,4887,4888],{"class":125},"        id: ",[93,4890,959],{"class":129},[93,4892,133],{"class":125},[93,4894,4896,4899,4901,4904,4906,4909,4911,4914,4916],{"class":95,"line":4895},13,[93,4897,4898],{"class":125},"        bbox: [{ west: ",[93,4900,973],{"class":231},[93,4902,4903],{"class":125},", south: ",[93,4905,982],{"class":231},[93,4907,4908],{"class":125},", east: ",[93,4910,991],{"class":231},[93,4912,4913],{"class":125},", north: ",[93,4915,1000],{"class":231},[93,4917,4918],{"class":125}," }],\n",[93,4920,4922,4925,4927],{"class":95,"line":4921},14,[93,4923,4924],{"class":125},"        fields: ",[93,4926,1010],{"class":129},[93,4928,133],{"class":125},[93,4930,4932],{"class":95,"line":4931},15,[93,4933,4934],{"class":125},"      }));\n",[93,4936,4938,4941],{"class":95,"line":4937},16,[93,4939,4940],{"class":117},"      break",[93,4942,4943],{"class":125},";\n",[93,4945,4947],{"class":95,"line":4946},17,[93,4948,4751],{"emptyLinePlaceholder":4750},[93,4950,4952,4954,4957],{"class":95,"line":4951},18,[93,4953,4814],{"class":117},[93,4955,4956],{"class":129}," \"subscribed\"",[93,4958,4820],{"class":125},[93,4960,4962],{"class":95,"line":4961},19,[93,4963,4964],{"class":4826},"      \u002F\u002F effective, not what was requested — this is where a gap shows up.\n",[93,4966,4968,4971,4974,4976,4979],{"class":95,"line":4967},20,[93,4969,4970],{"class":125},"      console.",[93,4972,4973],{"class":121},"log",[93,4975,126],{"class":125},[93,4977,4978],{"class":129},"\"active:\"",[93,4980,4981],{"class":125},", frame.effective);\n",[93,4983,4985,4987],{"class":95,"line":4984},21,[93,4986,4940],{"class":117},[93,4988,4943],{"class":125},[93,4990,4992],{"class":95,"line":4991},22,[93,4993,4751],{"emptyLinePlaceholder":4750},[93,4995,4997,4999,5002],{"class":95,"line":4996},23,[93,4998,4814],{"class":117},[93,5000,5001],{"class":129}," \"position\"",[93,5003,4820],{"class":125},[93,5005,5007,5010],{"class":95,"line":5006},24,[93,5008,5009],{"class":121},"      drawShip",[93,5011,5012],{"class":125},"(frame.data);\n",[93,5014,5016,5018],{"class":95,"line":5015},25,[93,5017,4940],{"class":117},[93,5019,4943],{"class":125},[93,5021,5023],{"class":95,"line":5022},26,[93,5024,4751],{"emptyLinePlaceholder":4750},[93,5026,5028,5030,5033],{"class":95,"line":5027},27,[93,5029,4814],{"class":117},[93,5031,5032],{"class":129}," \"heartbeat\"",[93,5034,4820],{"class":125},[93,5036,5038,5041,5044,5047,5050,5053,5056,5058,5061],{"class":95,"line":5037},28,[93,5039,5040],{"class":117},"      if",[93,5042,5043],{"class":125}," (frame.dropped ",[93,5045,5046],{"class":117},">",[93,5048,5049],{"class":231}," 0",[93,5051,5052],{"class":125},") console.",[93,5054,5055],{"class":121},"warn",[93,5057,126],{"class":125},[93,5059,5060],{"class":129},"\"data loss:\"",[93,5062,5063],{"class":125},", frame.dropped);\n",[93,5065,5067,5069],{"class":95,"line":5066},29,[93,5068,4940],{"class":117},[93,5070,4943],{"class":125},[93,5072,5074],{"class":95,"line":5073},30,[93,5075,4751],{"emptyLinePlaceholder":4750},[93,5077,5079,5081,5084],{"class":95,"line":5078},31,[93,5080,4814],{"class":117},[93,5082,5083],{"class":129}," \"error\"",[93,5085,4820],{"class":125},[93,5087,5089,5091,5093],{"class":95,"line":5088},32,[93,5090,4970],{"class":125},[93,5092,1703],{"class":121},[93,5094,5095],{"class":125},"(frame.code, frame.message);\n",[93,5097,5099,5101],{"class":95,"line":5098},33,[93,5100,4940],{"class":117},[93,5102,4943],{"class":125},[93,5104,5106],{"class":95,"line":5105},34,[93,5107,5108],{"class":125},"  }\n",[93,5110,5112],{"class":95,"line":5111},35,[93,5113,5114],{"class":125},"};\n",[93,5116,5118],{"class":95,"line":5117},36,[93,5119,4751],{"emptyLinePlaceholder":4750},[93,5121,5123],{"class":95,"line":5122},37,[93,5124,5125],{"class":4826},"\u002F\u002F 4012 means redeploy — reconnect with jitter, not immediately.\n",[93,5127,5129,5131,5134,5136,5138,5140,5142,5144],{"class":95,"line":5128},38,[93,5130,4757],{"class":125},[93,5132,5133],{"class":121},"onclose",[93,5135,4714],{"class":117},[93,5137,4765],{"class":125},[93,5139,4769],{"class":4768},[93,5141,4772],{"class":125},[93,5143,4775],{"class":117},[93,5145,4778],{"class":125},[93,5147,5149,5151,5154,5156,5159,5162,5165,5168,5171,5173,5176,5179,5182,5185,5188,5191,5194],{"class":95,"line":5148},39,[93,5150,4784],{"class":117},[93,5152,5153],{"class":231}," waitTime",[93,5155,4714],{"class":117},[93,5157,5158],{"class":125}," ev.code ",[93,5160,5161],{"class":117},"===",[93,5163,5164],{"class":231}," 4012",[93,5166,5167],{"class":117}," ?",[93,5169,5170],{"class":231}," 5000",[93,5172,151],{"class":117},[93,5174,5175],{"class":125}," Math.",[93,5177,5178],{"class":121},"random",[93,5180,5181],{"class":125},"() ",[93,5183,5184],{"class":117},"*",[93,5186,5187],{"class":231}," 25000",[93,5189,5190],{"class":117}," :",[93,5192,5193],{"class":231}," 1000",[93,5195,4943],{"class":125},[93,5197,5199,5202],{"class":95,"line":5198},40,[93,5200,5201],{"class":121},"  setTimeout",[93,5203,5204],{"class":125},"(reconnect, waitTime);\n",[93,5206,5208],{"class":95,"line":5207},41,[93,5209,5114],{"class":125},[61,5211],{},[64,5213,5215],{"id":5214},"appendix-frame-types-at-a-glance","Appendix · Frame types at a glance",[596,5217,5218,5229],{},[599,5219,5220],{},[602,5221,5222,5224,5227],{},[605,5223,3004],{},[605,5225,5226],{},"Direction",[605,5228,3947],{},[612,5230,5231,5243,5255,5269,5288,5300,5311,5325,5336],{},[602,5232,5233,5237,5240],{},[617,5234,5235],{},[14,5236,187],{},[617,5238,5239],{},"← Server",[617,5241,5242],{},"Once, immediately after the upgrade",[602,5244,5245,5249,5252],{},[617,5246,5247],{},[14,5248,191],{},[617,5250,5251],{},"→ Client",[617,5253,5254],{},"Within 5 s, any time after that",[602,5256,5257,5261,5263],{},[617,5258,5259],{},[14,5260,1788],{},[617,5262,5251],{},[617,5264,5265,5266,5268],{},"Ends a subscription by its ",[14,5267,752],{},"; unanswered, see section 4",[602,5270,5271,5275,5277],{},[617,5272,5273],{},[14,5274,1160],{},[617,5276,5239],{},[617,5278,5279,5280,784,5282,5284,5285,5287],{},"For every ",[26,5281,1651],{},[14,5283,191],{},"; a rejected one gets an ",[14,5286,1703],{},", and under back-pressure the frame can be lost — see section 4",[602,5289,5290,5295,5297],{},[617,5291,5292],{},[14,5293,5294],{},"position",[617,5296,5239],{},[617,5298,5299],{},"Continuously",[602,5301,5302,5306,5308],{},[617,5303,5304],{},[14,5305,3161],{},[617,5307,5239],{},[617,5309,5310],{},"Every 20 s",[602,5312,5313,5317,5319],{},[617,5314,5315],{},[14,5316,1703],{},[617,5318,5239],{},[617,5320,5321,5322,5324],{},"On violations, ",[14,5323,3885],{}," says whether a close follows",[602,5326,5327,5331,5333],{},[617,5328,5329],{},[14,5330,1348],{},[617,5332,5239],{},[617,5334,5335],{},"Advisories without an error",[602,5337,5338,5344,5347],{},[617,5339,5340,3015,5342],{},[14,5341,199],{},[14,5343,1722],{},[617,5345,5346],{},"→ \u002F ←",[617,5348,5349,5350,5352],{},"Optional, for proxies that hide WS pings; max. 1 ",[14,5351,1722],{}," per second, see section 2",[19,5354,5355,5356,5358,5359,5362,5363,5365],{},"All frames are text frames, UTF-8, ",[26,5357,1762],{}," JSON object per frame, and\n",[14,5360,5361],{},"type"," always comes ",[26,5364,1380],{}," — a client can read the type without parsing\nthe whole frame.",[5367,5368,5369],"style",{},"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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":77,"searchDepth":136,"depth":136,"links":5371},[5372,5373,5374,5376,5390,5392,5402,5405,5410,5420,5422,5425,5426],{"id":66,"depth":136,"text":67},{"id":174,"depth":136,"text":175},{"id":213,"depth":136,"text":5375},"3 · welcome",{"id":931,"depth":136,"text":5377,"children":5378},"4 · subscribe",[5379,5381,5382,5383,5385,5386,5388,5389],{"id":1153,"depth":4747,"text":5380},"notes[] — read this before you debug",{"id":1420,"depth":4747,"text":1387},{"id":1642,"depth":4747,"text":1643},{"id":1675,"depth":4747,"text":5384},"The subscribed frame is not a guarantee",{"id":1755,"depth":4747,"text":1756},{"id":1785,"depth":4747,"text":5387},"unsubscribe ends one subscription",{"id":1840,"depth":4747,"text":1841},{"id":1871,"depth":4747,"text":1872},{"id":1903,"depth":136,"text":5391},"5 · Bounding boxes have named edges",{"id":1972,"depth":136,"text":5393,"children":5394},"6 · Data frames: fields, formats and freshness",[5395,5396,5397,5399,5401],{"id":2411,"depth":4747,"text":2412},{"id":2556,"depth":4747,"text":2557},{"id":2644,"depth":4747,"text":5398},"Freshness belongs on rx_ts, not on the arrival rate",{"id":2661,"depth":4747,"text":5400},"dedupe_key, and what happens when two feeds see the same vessel",{"id":2753,"depth":4747,"text":2754},{"id":2772,"depth":136,"text":2773,"children":5403},[5404],{"id":2989,"depth":4747,"text":2990},{"id":3157,"depth":136,"text":5406,"children":5407},"8 · heartbeat — every 20 seconds, even in silence",[5408],{"id":3317,"depth":4747,"text":5409},"feed answers the one question you cannot answer yourself",{"id":3431,"depth":136,"text":3432,"children":5411},[5412,5413,5414,5415,5417,5419],{"id":3651,"depth":4747,"text":3652},{"id":3673,"depth":4747,"text":3674},{"id":3735,"depth":4747,"text":3736},{"id":3816,"depth":4747,"text":5416},"The error frame",{"id":3913,"depth":4747,"text":5418},"notice codes",{"id":4209,"depth":4747,"text":4210},{"id":4568,"depth":136,"text":5421},"10 · min_interval_ms is rejected",{"id":4609,"depth":136,"text":4610,"children":5423},[5424],{"id":4623,"depth":4747,"text":4624},{"id":4697,"depth":136,"text":4698},{"id":5214,"depth":136,"text":5215},"Every frame, every field, all fifteen close codes of the Pelyr OPEN-AIS v1 stream.","md",{},"\u002Fdocs\u002Fstream\u002Fprotocol",{"title":5,"description":5427},"docs\u002Fstream\u002Fprotocol","MiDpgE-Mmwg6moAJw0U6rNGGxyJydhUSnvob_D6XGoc",1789034411682]