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