@mailwoman/tiger 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/constants.ts +12 -12
  2. package/county.ts +5 -2
  3. package/files.ts +4 -1
  4. package/geoid.ts +11 -9
  5. package/out/constants.d.ts +5 -5
  6. package/out/constants.d.ts.map +1 -1
  7. package/out/constants.js +0 -5
  8. package/out/constants.js.map +1 -1
  9. package/out/county.d.ts +5 -2
  10. package/out/county.d.ts.map +1 -1
  11. package/out/county.js +3 -2
  12. package/out/county.js.map +1 -1
  13. package/out/files.d.ts +1 -1
  14. package/out/files.d.ts.map +1 -1
  15. package/out/files.js +0 -1
  16. package/out/files.js.map +1 -1
  17. package/out/geoid.d.ts +14 -16
  18. package/out/geoid.d.ts.map +1 -1
  19. package/out/geoid.js +1 -2
  20. package/out/geoid.js.map +1 -1
  21. package/out/sdk/fetch.d.ts +18 -6
  22. package/out/sdk/fetch.d.ts.map +1 -1
  23. package/out/sdk/fetch.js +11 -5
  24. package/out/sdk/fetch.js.map +1 -1
  25. package/out/sdk/redistricting.d.ts +18 -6
  26. package/out/sdk/redistricting.d.ts.map +1 -1
  27. package/out/sdk/redistricting.js +12 -6
  28. package/out/sdk/redistricting.js.map +1 -1
  29. package/out/sdk/schema.d.ts +14 -7
  30. package/out/sdk/schema.d.ts.map +1 -1
  31. package/out/sdk/schema.js +4 -0
  32. package/out/sdk/schema.js.map +1 -1
  33. package/out/sdk/state/TIGERTractEntity.d.ts +1 -1
  34. package/out/sdk/state/TIGERTractEntity.d.ts.map +1 -1
  35. package/out/sdk/state/TIGERTractEntity.js +1 -1
  36. package/out/sdk/state/TIGERTractEntity.js.map +1 -1
  37. package/out/sdk/state/block-operations.d.ts +1 -1
  38. package/out/sdk/state/block-operations.d.ts.map +1 -1
  39. package/out/sdk/state/block-operations.js +0 -1
  40. package/out/sdk/state/block-operations.js.map +1 -1
  41. package/out/state.d.ts +343 -113
  42. package/out/state.d.ts.map +1 -1
  43. package/out/state.js +174 -57
  44. package/out/state.js.map +1 -1
  45. package/out/tabulation-block.d.ts +3 -3
  46. package/out/tabulation-block.d.ts.map +1 -1
  47. package/out/tabulation-block.js +1 -3
  48. package/out/tabulation-block.js.map +1 -1
  49. package/out/tools/race-dots-map.d.ts +30 -10
  50. package/out/tools/race-dots-map.d.ts.map +1 -1
  51. package/out/tools/race-dots-map.js +6 -2
  52. package/out/tools/race-dots-map.js.map +1 -1
  53. package/out/tools/race-dots.d.ts +24 -8
  54. package/out/tools/race-dots.d.ts.map +1 -1
  55. package/out/tools/race-dots.js +15 -5
  56. package/out/tools/race-dots.js.map +1 -1
  57. package/out/tools/serve-range.d.ts +12 -4
  58. package/out/tools/serve-range.d.ts.map +1 -1
  59. package/out/tools/serve-range.js.map +1 -1
  60. package/out/tract.d.ts +2 -2
  61. package/out/tract.d.ts.map +1 -1
  62. package/out/tract.js +1 -2
  63. package/out/tract.js.map +1 -1
  64. package/out/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +3 -3
  66. package/sdk/fetch.ts +36 -11
  67. package/sdk/redistricting.ts +39 -12
  68. package/sdk/schema.ts +21 -7
  69. package/sdk/state/TIGERTractEntity.ts +1 -1
  70. package/sdk/state/block-operations.ts +7 -6
  71. package/state.ts +175 -57
  72. package/tabulation-block.ts +3 -3
  73. package/tools/race-dots-map.ts +35 -11
  74. package/tools/race-dots.ts +52 -12
  75. package/tools/serve-range.ts +16 -4
  76. package/tract.ts +2 -2
package/state.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { GeoFeature, MultiPolygonLiteral } from "@mailwoman/spatial"
8
8
 
9
- import { GeoIDPart } from "./geoid.ts"
9
+ import type { GeoIDPart } from "./geoid.ts"
10
10
  import type { TIGERTabulatedBlockProperties } from "./tabulation-block.ts"
11
11
 
12
12
  //#region FIPS Codes
@@ -21,157 +21,259 @@ import type { TIGERTabulatedBlockProperties } from "./tabulation-block.ts"
21
21
  * @see {@linkcode AdminLevel1Code} for a combined set of states and territories.
22
22
  */
23
23
  export const FIPSStateCode = {
24
- /** @title Alabama */
24
+ /**
25
+ * @title Alabama
26
+ */
25
27
  AL: "01",
26
28
 
27
- /** @title Alaska */
29
+ /**
30
+ * @title Alaska
31
+ */
28
32
  AK: "02",
29
33
 
30
- /** @title Arizona */
34
+ /**
35
+ * @title Arizona
36
+ */
31
37
  AZ: "04",
32
38
 
33
- /** @title Arkansas */
39
+ /**
40
+ * @title Arkansas
41
+ */
34
42
  AR: "05",
35
43
 
36
- /** @title California */
44
+ /**
45
+ * @title California
46
+ */
37
47
  CA: "06",
38
48
 
39
- /** @title Colorado */
49
+ /**
50
+ * @title Colorado
51
+ */
40
52
  CO: "08",
41
53
 
42
- /** @title Connecticut */
54
+ /**
55
+ * @title Connecticut
56
+ */
43
57
  CT: "09",
44
58
 
45
- /** @title Delaware */
59
+ /**
60
+ * @title Delaware
61
+ */
46
62
  DE: "10",
47
63
 
48
- /** @title District Of Columbia */
64
+ /**
65
+ * @title District Of Columbia
66
+ */
49
67
  DC: "11",
50
68
 
51
- /** @title Florida */
69
+ /**
70
+ * @title Florida
71
+ */
52
72
  FL: "12",
53
73
 
54
- /** @title Georgia */
74
+ /**
75
+ * @title Georgia
76
+ */
55
77
  GA: "13",
56
78
 
57
- /** @title Hawaii */
79
+ /**
80
+ * @title Hawaii
81
+ */
58
82
  HI: "15",
59
83
 
60
- /** @title Idaho */
84
+ /**
85
+ * @title Idaho
86
+ */
61
87
  ID: "16",
62
88
 
63
- /** @title Illinois */
89
+ /**
90
+ * @title Illinois
91
+ */
64
92
  IL: "17",
65
93
 
66
- /** @title Indiana */
94
+ /**
95
+ * @title Indiana
96
+ */
67
97
  IN: "18",
68
98
 
69
- /** @title Iowa */
99
+ /**
100
+ * @title Iowa
101
+ */
70
102
  IA: "19",
71
103
 
72
- /** @title Kansas */
104
+ /**
105
+ * @title Kansas
106
+ */
73
107
  KS: "20",
74
108
 
75
- /** @title Kentucky */
109
+ /**
110
+ * @title Kentucky
111
+ */
76
112
  KY: "21",
77
113
 
78
- /** @title Louisiana */
114
+ /**
115
+ * @title Louisiana
116
+ */
79
117
  LA: "22",
80
118
 
81
- /** @title Maine */
119
+ /**
120
+ * @title Maine
121
+ */
82
122
  ME: "23",
83
123
 
84
- /** @title Maryland */
124
+ /**
125
+ * @title Maryland
126
+ */
85
127
  MD: "24",
86
128
 
87
- /** @title Massachusetts */
129
+ /**
130
+ * @title Massachusetts
131
+ */
88
132
  MA: "25",
89
133
 
90
- /** @title Michigan */
134
+ /**
135
+ * @title Michigan
136
+ */
91
137
  MI: "26",
92
138
 
93
- /** @title Minnesota */
139
+ /**
140
+ * @title Minnesota
141
+ */
94
142
  MN: "27",
95
143
 
96
- /** @title Mississippi */
144
+ /**
145
+ * @title Mississippi
146
+ */
97
147
  MS: "28",
98
148
 
99
- /** @title Missouri */
149
+ /**
150
+ * @title Missouri
151
+ */
100
152
  MO: "29",
101
153
 
102
- /** @title Montana */
154
+ /**
155
+ * @title Montana
156
+ */
103
157
  MT: "30",
104
158
 
105
- /** @title Nebraska */
159
+ /**
160
+ * @title Nebraska
161
+ */
106
162
  NE: "31",
107
163
 
108
- /** @title Nevada */
164
+ /**
165
+ * @title Nevada
166
+ */
109
167
  NV: "32",
110
168
 
111
- /** @title New Hampshire */
169
+ /**
170
+ * @title New Hampshire
171
+ */
112
172
  NH: "33",
113
173
 
114
- /** @title New Jersey */
174
+ /**
175
+ * @title New Jersey
176
+ */
115
177
  NJ: "34",
116
178
 
117
- /** @title New Mexico */
179
+ /**
180
+ * @title New Mexico
181
+ */
118
182
  NM: "35",
119
183
 
120
- /** @title New York */
184
+ /**
185
+ * @title New York
186
+ */
121
187
  NY: "36",
122
188
 
123
- /** @title North Carolina */
189
+ /**
190
+ * @title North Carolina
191
+ */
124
192
  NC: "37",
125
193
 
126
- /** @title North Dakota */
194
+ /**
195
+ * @title North Dakota
196
+ */
127
197
  ND: "38",
128
198
 
129
- /** @title Ohio */
199
+ /**
200
+ * @title Ohio
201
+ */
130
202
  OH: "39",
131
203
 
132
- /** @title Oklahoma */
204
+ /**
205
+ * @title Oklahoma
206
+ */
133
207
  OK: "40",
134
208
 
135
- /** @title Oregon */
209
+ /**
210
+ * @title Oregon
211
+ */
136
212
  OR: "41",
137
213
 
138
- /** @title Pennsylvania */
214
+ /**
215
+ * @title Pennsylvania
216
+ */
139
217
  PA: "42",
140
218
 
141
- /** @title Rhode Island */
219
+ /**
220
+ * @title Rhode Island
221
+ */
142
222
  RI: "44",
143
223
 
144
- /** @title South Carolina */
224
+ /**
225
+ * @title South Carolina
226
+ */
145
227
  SC: "45",
146
228
 
147
- /** @title South Dakota */
229
+ /**
230
+ * @title South Dakota
231
+ */
148
232
  SD: "46",
149
233
 
150
- /** @title Tennessee */
234
+ /**
235
+ * @title Tennessee
236
+ */
151
237
  TN: "47",
152
238
 
153
- /** @title Texas */
239
+ /**
240
+ * @title Texas
241
+ */
154
242
  TX: "48",
155
243
 
156
- /** @title Utah */
244
+ /**
245
+ * @title Utah
246
+ */
157
247
  UT: "49",
158
248
 
159
- /** @title Vermont */
249
+ /**
250
+ * @title Vermont
251
+ */
160
252
  VT: "50",
161
253
 
162
- /** @title Virginia */
254
+ /**
255
+ * @title Virginia
256
+ */
163
257
  VA: "51",
164
258
 
165
- /** @title Washington */
259
+ /**
260
+ * @title Washington
261
+ */
166
262
  WA: "53",
167
263
 
168
- /** @title West Virginia */
264
+ /**
265
+ * @title West Virginia
266
+ */
169
267
  WV: "54",
170
268
 
171
- /** @title Wisconsin */
269
+ /**
270
+ * @title Wisconsin
271
+ */
172
272
  WI: "55",
173
273
 
174
- /** @title Wyoming */
274
+ /**
275
+ * @title Wyoming
276
+ */
175
277
  WY: "56",
176
278
  } as const
177
279
 
@@ -194,19 +296,29 @@ export const FIPSTerritoryCode = {
194
296
  */
195
297
  JA: "74",
196
298
 
197
- /** @title American Samoa */
299
+ /**
300
+ * @title American Samoa
301
+ */
198
302
  AS: "60",
199
303
 
200
- /** @title Guam */
304
+ /**
305
+ * @title Guam
306
+ */
201
307
  GU: "66",
202
308
 
203
- /** @title Northern Mariana Islands */
309
+ /**
310
+ * @title Northern Mariana Islands
311
+ */
204
312
  MP: "69",
205
313
 
206
- /** @title Puerto Rico */
314
+ /**
315
+ * @title Puerto Rico
316
+ */
207
317
  PR: "72",
208
318
 
209
- /** @title Virgin Islands */
319
+ /**
320
+ * @title Virgin Islands
321
+ */
210
322
  VI: "78",
211
323
  } as const
212
324
 
@@ -344,6 +456,9 @@ export const StateAbbreviation = {
344
456
 
345
457
  export type StateAbbreviation = (typeof StateAbbreviation)[keyof typeof StateAbbreviation]
346
458
 
459
+ /**
460
+ * Postal abbreviations for US territories, kept apart from the states so callers can include or exclude them.
461
+ */
347
462
  export const TerritoryAbbreviation = {
348
463
  "American Samoa": "AS",
349
464
  "Johnston Atoll": "JA",
@@ -364,6 +479,9 @@ export type TerritoryAbbreviation = (typeof TerritoryAbbreviation)[keyof typeof
364
479
  */
365
480
  export type AdminLevel1Abbreviation = StateAbbreviation | TerritoryAbbreviation
366
481
 
482
+ /**
483
+ * Every admin-level-1 postal abbreviation — the fifty states, DC, and the territories together.
484
+ */
367
485
  export const AdminLevel1Abbreviation = {
368
486
  ...StateAbbreviation,
369
487
  ...TerritoryAbbreviation,
@@ -4,10 +4,10 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  */
6
6
 
7
- import { type MultiPolygonLiteral } from "@mailwoman/spatial"
7
+ import type { MultiPolygonLiteral } from "@mailwoman/spatial"
8
8
 
9
- import { TIGERProperty, type TIGERPropertyRecord } from "./constants.ts"
10
- import { GeoIDPart, type FIPSBlockGeoID, type ParsedGeoIDBlockLevel } from "./geoid.ts"
9
+ import type { TIGERProperty, TIGERPropertyRecord } from "./constants.ts"
10
+ import type { GeoIDPart, FIPSBlockGeoID, ParsedGeoIDBlockLevel } from "./geoid.ts"
11
11
 
12
12
  /**
13
13
  * The properties of a tabulated block from the TIGER/Line dataset.
@@ -19,25 +19,43 @@
19
19
 
20
20
  import { writeFileSync } from "node:fs"
21
21
 
22
- /** Options for {@linkcode raceDotsMap}. */
22
+ /**
23
+ * Options for {@linkcode raceDotsMap}.
24
+ */
23
25
  export interface RaceDotsMapOptions {
24
- /** Dots tileset URL the page reads client-side. Default `http://localhost:8899/race-dots-oc.pmtiles`. */
26
+ /**
27
+ * Dots tileset URL the page reads client-side. Default `http://localhost:8899/race-dots-oc.pmtiles`.
28
+ */
25
29
  pmtilesURL?: string
26
- /** Output HTML path. Default `/tmp/race-dots-oc.html`. */
30
+ /**
31
+ * Output HTML path. Default `/tmp/race-dots-oc.html`.
32
+ */
27
33
  out?: string
28
- /** People represented by one dot (title/legend copy only). Default 5. */
34
+ /**
35
+ * People represented by one dot (title/legend copy only). Default 5.
36
+ */
29
37
  per?: number
30
- /** Page title. Default derives from `per`. */
38
+ /**
39
+ * Page title. Default derives from `per`.
40
+ */
31
41
  title?: string
32
- /** Initial map center longitude. Default -117.83 (Orange County, CA). */
42
+ /**
43
+ * Initial map center longitude. Default -117.83 (Orange County, CA).
44
+ */
33
45
  lng?: number
34
- /** Initial map center latitude. Default 33.68. */
46
+ /**
47
+ * Initial map center latitude. Default 33.68.
48
+ */
35
49
  lat?: number
36
- /** Initial map zoom. Default 9.4. */
50
+ /**
51
+ * Initial map zoom. Default 9.4.
52
+ */
37
53
  zoom?: number
38
54
  }
39
55
 
40
- /** Result of {@linkcode raceDotsMap}. */
56
+ /**
57
+ * Result of {@linkcode raceDotsMap}.
58
+ */
41
59
  export interface RaceDotsMapResult {
42
60
  outPath: string
43
61
  pmtilesURL: string
@@ -53,7 +71,9 @@ const BASEMAP_TILEJSON_URL = "https://tiles.sister.software/basemap-v4.json"
53
71
  const GLYPHS_URL = "https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf"
54
72
  const SPRITE_URL = "https://protomaps.github.io/basemaps-assets/sprites/v4/light"
55
73
 
56
- // Cooper Center "Racial Dot Map" palette, by `pl_block`/P2 category.
74
+ /**
75
+ * Cooper Center "Racial Dot Map" palette, by `pl_block`/P2 category.
76
+ */
57
77
  const CATEGORY_COLOR: Record<string, string> = {
58
78
  white: "#1f78b4", // blue
59
79
  black: "#33a02c", // green
@@ -64,6 +84,7 @@ const CATEGORY_COLOR: Record<string, string> = {
64
84
  other: "#b15928", // brown
65
85
  multi: "#9e9e9e", // grey
66
86
  }
87
+
67
88
  const CATEGORY_LABEL: Record<string, string> = {
68
89
  white: "White (non-Hispanic)",
69
90
  black: "Black (non-Hispanic)",
@@ -75,7 +96,9 @@ const CATEGORY_LABEL: Record<string, string> = {
75
96
  multi: "Two or more races",
76
97
  }
77
98
 
78
- /** Race-dots MapLibre page writer — see the module doc. */
99
+ /**
100
+ * Race-dots MapLibre page writer — see the module doc.
101
+ */
79
102
  export async function raceDotsMap(
80
103
  options: RaceDotsMapOptions = {},
81
104
  report?: (line: string) => void
@@ -97,6 +120,7 @@ export async function raceDotsMap(
97
120
  for (const [cat, color] of Object.entries(CATEGORY_COLOR)) {
98
121
  colorMatch.push(cat, color)
99
122
  }
123
+
100
124
  colorMatch.push("#9e9e9e")
101
125
 
102
126
  const style = {
@@ -26,32 +26,54 @@ import { DatabaseSync } from "node:sqlite"
26
26
 
27
27
  import { dataRootPath } from "@mailwoman/core/utils"
28
28
 
29
- /** Options for {@linkcode raceDots}. */
29
+ /**
30
+ * Attempts to place a dot inside its polygon by rejection sampling before giving up on it.
31
+ */
32
+ const MAX_PLACEMENT_TRIES = 60
33
+
34
+ /**
35
+ * Options for {@linkcode raceDots}.
36
+ */
30
37
  export interface RaceDotsOptions {
31
- /** TIGER SQLite DB (`tabblock20` ⋈ `pl_block`). Default `$MAILWOMAN_DATA_ROOT/tiger/tiger-oc.db`. */
38
+ /**
39
+ * TIGER SQLite DB (`tabblock20` ⋈ `pl_block`). Default `$MAILWOMAN_DATA_ROOT/tiger/tiger-oc.db`.
40
+ */
32
41
  db?: string
33
- /** Output NDJSON path. Default `/tmp/race-dots.ndjson`. */
42
+ /**
43
+ * Output NDJSON path. Default `/tmp/race-dots.ndjson`.
44
+ */
34
45
  out?: string
35
- /** People represented by one dot. Default 10. */
46
+ /**
47
+ * People represented by one dot. Default 10.
48
+ */
36
49
  per?: number
37
- /** Tippecanoe layer name. Default `dots`. */
50
+ /**
51
+ * Tippecanoe layer name. Default `dots`.
52
+ */
38
53
  layer?: string
39
54
  }
40
55
 
41
- /** Result of {@linkcode raceDots}. */
56
+ /**
57
+ * Result of {@linkcode raceDots}.
58
+ */
42
59
  export interface RaceDotsResult {
43
60
  outPath: string
44
61
  dots: number
45
62
  skipped: number
46
63
  blocks: number
47
- /** Dots emitted per P2 category. */
64
+ /**
65
+ * Dots emitted per P2 category.
66
+ */
48
67
  totals: Record<string, number>
49
68
  }
50
69
 
51
- // The eight P2 categories (columns in pl_block) that partition each block's population.
70
+ /**
71
+ * The eight P2 categories (columns in pl_block) that partition each block's population.
72
+ */
52
73
  const CATEGORIES = ["hispanic", "white", "black", "asian", "aian", "nhpi", "other", "multi"] as const
53
74
 
54
75
  type Ring = number[][]
76
+
55
77
  type PolygonCoords = Ring[]
56
78
 
57
79
  function bbox(rings: PolygonCoords): [number, number, number, number] {
@@ -81,7 +103,9 @@ function bbox(rings: PolygonCoords): [number, number, number, number] {
81
103
  return [minX, minY, maxX, maxY]
82
104
  }
83
105
 
84
- /** Race-by-dot-density NDJSON builder — see the module doc. */
106
+ /**
107
+ * Race-by-dot-density NDJSON builder — see the module doc.
108
+ */
85
109
  export async function raceDots(
86
110
  options: RaceDotsOptions = {},
87
111
  report?: (line: string) => void
@@ -103,15 +127,18 @@ export async function raceDots(
103
127
  while (pick < polys.length - 1 && (r -= areas[pick]!) > 0) {
104
128
  pick++
105
129
  }
130
+
106
131
  const poly = polys[pick]!
132
+
107
133
  const polyFeature = {
108
134
  type: "Feature" as const,
109
135
  geometry: { type: "Polygon" as const, coordinates: poly },
110
136
  properties: {},
111
137
  }
138
+
112
139
  const [minX, minY, maxX, maxY] = bbox(poly)
113
140
 
114
- for (let tries = 0; tries < 60; tries++) {
141
+ for (let tries = 0; tries < MAX_PLACEMENT_TRIES; tries++) {
115
142
  const x = minX + Math.random() * (maxX - minX)
116
143
  const y = minY + Math.random() * (maxY - minY)
117
144
  const pt = { type: "Feature" as const, geometry: { type: "Point" as const, coordinates: [x, y] }, properties: {} }
@@ -123,6 +150,7 @@ export async function raceDots(
123
150
  }
124
151
 
125
152
  const db = new DatabaseSync(DB, { readOnly: true })
153
+
126
154
  const rows = db
127
155
  .prepare(
128
156
  `SELECT b.geometry AS geometry, ${CATEGORIES.map((c) => `p.${c} AS ${c}`).join(", ")}
@@ -130,10 +158,12 @@ export async function raceDots(
130
158
  WHERE p.pop_total > 0`
131
159
  )
132
160
  .all() as Array<{ geometry: string } & Record<(typeof CATEGORIES)[number], number>>
161
+
133
162
  db.close()
134
163
 
135
164
  const out = createWriteStream(OUT)
136
165
  const totals = new Map<string, number>()
166
+
137
167
  let dots = 0,
138
168
  skipped = 0
139
169
 
@@ -145,13 +175,16 @@ export async function raceDots(
145
175
  } catch {
146
176
  continue
147
177
  }
178
+
148
179
  const polys: PolygonCoords[] =
149
180
  geom.type === "Polygon" ? [geom.coordinates as PolygonCoords] : (geom.coordinates as PolygonCoords[])
181
+
150
182
  const areas = polys.map((p) => {
151
183
  const [a, b, c, d] = bbox(p)
152
184
 
153
185
  return Math.max((c - a) * (d - b), 1e-12)
154
186
  })
187
+
155
188
  const totalArea = areas.reduce((s, a) => s + a, 0)
156
189
 
157
190
  for (const cat of CATEGORIES) {
@@ -166,8 +199,10 @@ export async function raceDots(
166
199
 
167
200
  if (!pt) {
168
201
  skipped++
202
+
169
203
  continue
170
204
  }
205
+
171
206
  out.write(
172
207
  JSON.stringify({
173
208
  type: "Feature",
@@ -176,6 +211,7 @@ export async function raceDots(
176
211
  geometry: { type: "Point", coordinates: [Math.round(pt[0] * 1e5) / 1e5, Math.round(pt[1] * 1e5) / 1e5] },
177
212
  }) + "\n"
178
213
  )
214
+
179
215
  dots++
180
216
  totals.set(cat, (totals.get(cat) ?? 0) + 1)
181
217
  }
@@ -183,10 +219,14 @@ export async function raceDots(
183
219
  }
184
220
 
185
221
  out.end()
186
- await new Promise<void>((resolve) => out.on("finish", () => resolve()))
222
+
223
+ await new Promise<void>((resolve) => {
224
+ out.on("finish", () => resolve())
225
+ })
226
+
187
227
  report?.(`[done] ${dots} dots from ${rows.length} blocks (1 dot ≈ ${PER} people); ${skipped} skipped`)
188
228
 
189
- for (const [cat, n] of [...totals.entries()].sort((a, b) => b[1] - a[1])) {
229
+ for (const [cat, n] of [...totals.entries()].toSorted((a, b) => b[1] - a[1])) {
190
230
  report?.(` ${n.toString().padStart(7)} ${cat}`)
191
231
  }
192
232
 
@@ -15,15 +15,23 @@ import { createReadStream, statSync, type Stats } from "node:fs"
15
15
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
16
16
  import { extname, join, normalize } from "node:path"
17
17
 
18
- /** Options for {@linkcode serveWithRangeSupport}. */
18
+ /**
19
+ * Options for {@linkcode serveWithRangeSupport}.
20
+ */
19
21
  export interface ServeRangeOptions {
20
- /** Directory to serve. Default `/tmp`. */
22
+ /**
23
+ * Directory to serve. Default `/tmp`.
24
+ */
21
25
  dir?: string
22
- /** Port to listen on. Default 8899. */
26
+ /**
27
+ * Port to listen on. Default 8899.
28
+ */
23
29
  port?: number
24
30
  }
25
31
 
26
- /** A running range-capable static server, resolved once listening. */
32
+ /**
33
+ * A running range-capable static server, resolved once listening.
34
+ */
27
35
  export interface RangeServer {
28
36
  dir: string
29
37
  port: number
@@ -62,6 +70,7 @@ export function serveWithRangeSupport(
62
70
 
63
71
  return res.end("not found")
64
72
  }
73
+
65
74
  const type = TYPES[extname(path)] || "application/octet-stream"
66
75
  const base = { "Content-Type": type, "Accept-Ranges": "bytes", "Access-Control-Allow-Origin": "*" }
67
76
  const range = req.headers.range
@@ -70,11 +79,13 @@ export function serveWithRangeSupport(
70
79
  if (m) {
71
80
  const start = Number(m[1])
72
81
  const end = m[2] ? Number(m[2]) : st.size - 1
82
+
73
83
  res.writeHead(206, {
74
84
  ...base,
75
85
  "Content-Range": `bytes ${start}-${end}/${st.size}`,
76
86
  "Content-Length": end - start + 1,
77
87
  })
88
+
78
89
  createReadStream(path, { start, end }).pipe(res)
79
90
  } else {
80
91
  res.writeHead(200, { ...base, "Content-Length": st.size })
@@ -84,6 +95,7 @@ export function serveWithRangeSupport(
84
95
 
85
96
  return new Promise<RangeServer>((resolve, reject) => {
86
97
  server.once("error", reject)
98
+
87
99
  server.listen(port, () => {
88
100
  report?.(`range server: ${dir} on http://localhost:${port}`)
89
101
  resolve({ dir, port, server, close: () => server.close() })