nixamp 0.10.1 → 0.10.2

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.
@@ -100,6 +100,12 @@ export interface Ended {
100
100
  startedAt: number;
101
101
  /** The last heartbeat we saw, which is as close to "ended" as we can know. */
102
102
  endedAt: number;
103
+ /**
104
+ * The codes its channels had, so a server that stops and comes back -- an
105
+ * update restarts the daemon, and the daemon withdraws its listing on the
106
+ * way down -- puts each channel back under the code people were given.
107
+ */
108
+ channelCodes?: Record<string, string>;
103
109
  }
104
110
  /** How long an ended stream is still worth telling a caller about. */
105
111
  export declare const ENDED_TTL_MS: number;
package/dist/directory.js CHANGED
@@ -129,8 +129,14 @@ export class Directory {
129
129
  */
130
130
  seedEnded(items) {
131
131
  for (const item of items) {
132
- if (!this.ended.has(item.id) && !this.items.has(item.id))
132
+ if (!this.ended.has(item.id) && !this.items.has(item.id)) {
133
133
  this.ended.set(item.id, item);
134
+ // Its channels' codes are reserved too, so a server coming back after
135
+ // the directory restarted still finds them its own.
136
+ if (item.channelCodes && Object.keys(item.channelCodes).length > 0) {
137
+ this.channelHistory.set(item.id, { ...(this.channelHistory.get(item.id) ?? {}), ...item.channelCodes });
138
+ }
139
+ }
134
140
  }
135
141
  }
136
142
  constructor(ttl = TTL_MS, now = Date.now,
@@ -186,7 +192,11 @@ export class Directory {
186
192
  // channel that was gone for a heartbeat -- a server restarting puts
187
193
  // its channels back a moment after it announces -- comes back to the
188
194
  // code people were given, not a new one.
189
- { ...(this.channelHistory.get(id) ?? {}), ...(existing?.channelCodes ?? {}) }, id),
195
+ {
196
+ ...(previously && "channelCodes" in previously ? previously.channelCodes ?? {} : {}),
197
+ ...(this.channelHistory.get(id) ?? {}),
198
+ ...(existing?.channelCodes ?? {}),
199
+ }, id),
190
200
  updatedAt: this.now(),
191
201
  // A stream that never stopped keeps its original start. One that did
192
202
  // starts again now, because that is what a caller is being told about.
@@ -204,7 +214,7 @@ export class Directory {
204
214
  if (item !== undefined)
205
215
  this.remember(item);
206
216
  this.items.delete(id);
207
- this.channelHistory.delete(id);
217
+ // The channel history stays with the ended record, and goes when it goes.
208
218
  }
209
219
  list() {
210
220
  this.sweep();
@@ -322,6 +332,9 @@ export class Directory {
322
332
  nowPlaying: item.nowPlaying,
323
333
  startedAt: item.startedAt,
324
334
  endedAt: item.updatedAt,
335
+ // Every code its channels have had, not only the ones on at the end:
336
+ // a restart announces before its channels are back.
337
+ channelCodes: { ...(this.channelHistory.get(item.id) ?? {}), ...item.channelCodes },
325
338
  };
326
339
  this.ended.set(item.id, record);
327
340
  this.mirror?.save(record);
@@ -344,13 +357,13 @@ export class Directory {
344
357
  if (item.updatedAt < cutoff) {
345
358
  this.remember(item);
346
359
  this.items.delete(id);
347
- this.channelHistory.delete(id);
348
360
  }
349
361
  }
350
362
  const forget = this.now() - ENDED_TTL_MS;
351
363
  for (const [id, item] of this.ended) {
352
364
  if (item.endedAt < forget) {
353
365
  this.ended.delete(id);
366
+ this.channelHistory.delete(id);
354
367
  this.mirror?.drop(id);
355
368
  }
356
369
  }
package/dist/durable.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface StoredEnded {
34
34
  nowPlaying: string;
35
35
  startedAt: number;
36
36
  endedAt: number;
37
+ /** Each channel's phone code, by name, so a server coming back keeps them. */
38
+ channelCodes?: Record<string, string>;
37
39
  }
38
40
  export declare class Durable {
39
41
  private readonly db;
package/dist/durable.js CHANGED
@@ -9,6 +9,7 @@ const SCHEMA = `
9
9
  started_at BIGINT NOT NULL,
10
10
  ended_at BIGINT NOT NULL
11
11
  );
12
+ ALTER TABLE ended_streams ADD COLUMN IF NOT EXISTS channel_codes JSONB NOT NULL DEFAULT '{}'::jsonb;
12
13
  CREATE INDEX IF NOT EXISTS ended_streams_code ON ended_streams (code);
13
14
 
14
15
  CREATE TABLE IF NOT EXISTS stream_reminders (
@@ -18,6 +19,26 @@ const SCHEMA = `
18
19
  PRIMARY KEY (code, phone)
19
20
  );
20
21
  `;
22
+ /** A JSONB column, as pg hands it back: an object already, or a string on an odd driver. */
23
+ function codesFrom(value) {
24
+ let parsed = value;
25
+ if (typeof value === "string") {
26
+ try {
27
+ parsed = JSON.parse(value);
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
34
+ return {};
35
+ const codes = {};
36
+ for (const [name, code] of Object.entries(parsed)) {
37
+ if (typeof code === "string" && /^\d{6}$/.test(code))
38
+ codes[name] = code;
39
+ }
40
+ return codes;
41
+ }
21
42
  export class Durable {
22
43
  db;
23
44
  onEvent;
@@ -48,8 +69,8 @@ export class Durable {
48
69
  }
49
70
  async saveEnded(stream) {
50
71
  await this.quietly("an ended stream", () => this.db.query(`INSERT INTO ended_streams
51
- (id, code, name, owner_id, url, now_playing, started_at, ended_at)
52
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
72
+ (id, code, name, owner_id, url, now_playing, started_at, ended_at, channel_codes)
73
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
53
74
  ON CONFLICT (id) DO UPDATE
54
75
  SET code = EXCLUDED.code,
55
76
  name = EXCLUDED.name,
@@ -57,7 +78,8 @@ export class Durable {
57
78
  url = EXCLUDED.url,
58
79
  now_playing = EXCLUDED.now_playing,
59
80
  started_at = EXCLUDED.started_at,
60
- ended_at = EXCLUDED.ended_at`, [
81
+ ended_at = EXCLUDED.ended_at,
82
+ channel_codes = EXCLUDED.channel_codes`, [
61
83
  stream.id,
62
84
  stream.code,
63
85
  stream.name,
@@ -66,6 +88,7 @@ export class Durable {
66
88
  stream.nowPlaying,
67
89
  stream.startedAt,
68
90
  stream.endedAt,
91
+ JSON.stringify(stream.channelCodes ?? {}),
69
92
  ]));
70
93
  }
71
94
  /** A stream that came back, or one old enough to forget. */
@@ -88,6 +111,7 @@ export class Durable {
88
111
  // nothing like a number.
89
112
  startedAt: Number(r["started_at"] ?? 0),
90
113
  endedAt: Number(r["ended_at"] ?? 0),
114
+ channelCodes: codesFrom(r["channel_codes"]),
91
115
  }));
92
116
  }
93
117
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/directory.ts CHANGED
@@ -105,6 +105,12 @@ export interface Ended {
105
105
  startedAt: number;
106
106
  /** The last heartbeat we saw, which is as close to "ended" as we can know. */
107
107
  endedAt: number;
108
+ /**
109
+ * The codes its channels had, so a server that stops and comes back -- an
110
+ * update restarts the daemon, and the daemon withdraws its listing on the
111
+ * way down -- puts each channel back under the code people were given.
112
+ */
113
+ channelCodes?: Record<string, string>;
108
114
  }
109
115
 
110
116
  /** How long an ended stream is still worth telling a caller about. */
@@ -237,7 +243,14 @@ export class Directory {
237
243
  */
238
244
  seedEnded(items: readonly Ended[]): void {
239
245
  for (const item of items) {
240
- if (!this.ended.has(item.id) && !this.items.has(item.id)) this.ended.set(item.id, item);
246
+ if (!this.ended.has(item.id) && !this.items.has(item.id)) {
247
+ this.ended.set(item.id, item);
248
+ // Its channels' codes are reserved too, so a server coming back after
249
+ // the directory restarted still finds them its own.
250
+ if (item.channelCodes && Object.keys(item.channelCodes).length > 0) {
251
+ this.channelHistory.set(item.id, { ...(this.channelHistory.get(item.id) ?? {}), ...item.channelCodes });
252
+ }
253
+ }
241
254
  }
242
255
  }
243
256
 
@@ -297,7 +310,11 @@ export class Directory {
297
310
  // channel that was gone for a heartbeat -- a server restarting puts
298
311
  // its channels back a moment after it announces -- comes back to the
299
312
  // code people were given, not a new one.
300
- { ...(this.channelHistory.get(id) ?? {}), ...(existing?.channelCodes ?? {}) },
313
+ {
314
+ ...(previously && "channelCodes" in previously ? previously.channelCodes ?? {} : {}),
315
+ ...(this.channelHistory.get(id) ?? {}),
316
+ ...(existing?.channelCodes ?? {}),
317
+ },
301
318
  id,
302
319
  ),
303
320
  updatedAt: this.now(),
@@ -316,7 +333,7 @@ export class Directory {
316
333
  const item = this.items.get(id);
317
334
  if (item !== undefined) this.remember(item);
318
335
  this.items.delete(id);
319
- this.channelHistory.delete(id);
336
+ // The channel history stays with the ended record, and goes when it goes.
320
337
  }
321
338
 
322
339
  list(): Listing[] {
@@ -435,6 +452,9 @@ export class Directory {
435
452
  nowPlaying: item.nowPlaying,
436
453
  startedAt: item.startedAt,
437
454
  endedAt: item.updatedAt,
455
+ // Every code its channels have had, not only the ones on at the end:
456
+ // a restart announces before its channels are back.
457
+ channelCodes: { ...(this.channelHistory.get(item.id) ?? {}), ...item.channelCodes },
438
458
  };
439
459
  this.ended.set(item.id, record);
440
460
  this.mirror?.save(record);
@@ -457,13 +477,13 @@ export class Directory {
457
477
  if (item.updatedAt < cutoff) {
458
478
  this.remember(item);
459
479
  this.items.delete(id);
460
- this.channelHistory.delete(id);
461
480
  }
462
481
  }
463
482
  const forget = this.now() - ENDED_TTL_MS;
464
483
  for (const [id, item] of this.ended) {
465
484
  if (item.endedAt < forget) {
466
485
  this.ended.delete(id);
486
+ this.channelHistory.delete(id);
467
487
  this.mirror?.drop(id);
468
488
  }
469
489
  }
package/src/durable.ts CHANGED
@@ -35,6 +35,8 @@ export interface StoredEnded {
35
35
  nowPlaying: string;
36
36
  startedAt: number;
37
37
  endedAt: number;
38
+ /** Each channel's phone code, by name, so a server coming back keeps them. */
39
+ channelCodes?: Record<string, string>;
38
40
  }
39
41
 
40
42
  const SCHEMA = `
@@ -48,6 +50,7 @@ const SCHEMA = `
48
50
  started_at BIGINT NOT NULL,
49
51
  ended_at BIGINT NOT NULL
50
52
  );
53
+ ALTER TABLE ended_streams ADD COLUMN IF NOT EXISTS channel_codes JSONB NOT NULL DEFAULT '{}'::jsonb;
51
54
  CREATE INDEX IF NOT EXISTS ended_streams_code ON ended_streams (code);
52
55
 
53
56
  CREATE TABLE IF NOT EXISTS stream_reminders (
@@ -58,6 +61,24 @@ const SCHEMA = `
58
61
  );
59
62
  `;
60
63
 
64
+ /** A JSONB column, as pg hands it back: an object already, or a string on an odd driver. */
65
+ function codesFrom(value: unknown): Record<string, string> {
66
+ let parsed: unknown = value;
67
+ if (typeof value === "string") {
68
+ try {
69
+ parsed = JSON.parse(value);
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
75
+ const codes: Record<string, string> = {};
76
+ for (const [name, code] of Object.entries(parsed as Record<string, unknown>)) {
77
+ if (typeof code === "string" && /^\d{6}$/.test(code)) codes[name] = code;
78
+ }
79
+ return codes;
80
+ }
81
+
61
82
  export class Durable {
62
83
  private ready: Promise<void> | null = null;
63
84
 
@@ -91,8 +112,8 @@ export class Durable {
91
112
  await this.quietly("an ended stream", () =>
92
113
  this.db.query(
93
114
  `INSERT INTO ended_streams
94
- (id, code, name, owner_id, url, now_playing, started_at, ended_at)
95
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
115
+ (id, code, name, owner_id, url, now_playing, started_at, ended_at, channel_codes)
116
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
96
117
  ON CONFLICT (id) DO UPDATE
97
118
  SET code = EXCLUDED.code,
98
119
  name = EXCLUDED.name,
@@ -100,7 +121,8 @@ export class Durable {
100
121
  url = EXCLUDED.url,
101
122
  now_playing = EXCLUDED.now_playing,
102
123
  started_at = EXCLUDED.started_at,
103
- ended_at = EXCLUDED.ended_at`,
124
+ ended_at = EXCLUDED.ended_at,
125
+ channel_codes = EXCLUDED.channel_codes`,
104
126
  [
105
127
  stream.id,
106
128
  stream.code,
@@ -110,6 +132,7 @@ export class Durable {
110
132
  stream.nowPlaying,
111
133
  stream.startedAt,
112
134
  stream.endedAt,
135
+ JSON.stringify(stream.channelCodes ?? {}),
113
136
  ],
114
137
  ),
115
138
  );
@@ -141,6 +164,7 @@ export class Durable {
141
164
  // nothing like a number.
142
165
  startedAt: Number(r["started_at"] ?? 0),
143
166
  endedAt: Number(r["ended_at"] ?? 0),
167
+ channelCodes: codesFrom(r["channel_codes"]),
144
168
  }));
145
169
  } catch (error) {
146
170
  this.onEvent(` could not read ended streams: ${(error as Error).message}`);
package/web/dist/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1789044172172";
2
+ const CACHE = "nixamp-1789044699569";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/apple-touch-icon.png",