@azphalt/web-handoff 0.2.0 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -39,6 +39,61 @@ export interface HandoffPackage {
39
39
  };
40
40
  };
41
41
  }
42
+ /**
43
+ * What a host has done with a package — the `spec/state-reporting.md` § 1 vocabulary, as far as a
44
+ * storefront needs it.
45
+ */
46
+ export type InventoryState = "downloaded" | "installed" | "active" | "failed" | "removed";
47
+ export interface InventoryEntry {
48
+ id: string;
49
+ version: string;
50
+ state: InventoryState;
51
+ /** ISO-8601 instant this was recorded. */
52
+ at?: string;
53
+ }
54
+ /**
55
+ * The store's record of what it has handed to a host.
56
+ *
57
+ * A host telling the store what it holds is the better answer, and the Android store app gets one
58
+ * (`spec/state-reporting.md` § 3). A web page has no host to ask: it fires `azphalt://install` and
59
+ * learns nothing. Without keeping its own books every card says *Get*, including the ones the user
60
+ * just installed, and there is no way to filter what you already have.
61
+ *
62
+ * Never throws. Storage can be disabled or full, and neither is worth failing a browse over — what is
63
+ * lost is nicer labels.
64
+ */
65
+ export declare function loadInventory(): Record<string, InventoryEntry>;
66
+ /** Persist `entries`, replacing what was there. Silent no-op where there is nowhere to put it. */
67
+ export declare function saveInventory(entries: Record<string, InventoryEntry>): void;
68
+ /**
69
+ * Record a successful handoff.
70
+ *
71
+ * Recorded as `installed` rather than `downloaded`, and the distinction is worth stating: the store
72
+ * did **not** observe an install. It observed that the user pressed Install and that a host claimed
73
+ * the link, which is the last thing a web page can observe. `downloaded` means "wanted and not yet
74
+ * had", which describes that moment worse than `installed` does — the user's next question is "did
75
+ * that work", and "not yet had" answers it wrongly in the common case.
76
+ */
77
+ export declare function recordInstalled(inventory: Record<string, InventoryEntry>, id: string, version: string, at?: string): Record<string, InventoryEntry>;
78
+ /**
79
+ * Forget a locally-recorded install.
80
+ *
81
+ * The store's record is a guess (see `recordInstalled`), and a guess with no way to correct it is
82
+ * just a wrong answer that persists. A handoff can succeed — a host claimed the link — and the
83
+ * install still not happen, and on a web visit nothing will ever contradict that. This is how the
84
+ * user says so.
85
+ *
86
+ * Deletes the entry rather than marking it `removed`: `removed` is a *host's* statement that a
87
+ * package was uninstalled, which is a different claim from the store admitting it never knew.
88
+ */
89
+ export declare function forgetInstall(inventory: Record<string, InventoryEntry>, id: string): Record<string, InventoryEntry>;
90
+ /**
91
+ * Whether the viewer has this package.
92
+ *
93
+ * `removed` is kept rather than deleted so a reinstall can be offered, so "has it" is a question
94
+ * about the state and not about the key being present.
95
+ */
96
+ export declare function isHeld(inventory: Record<string, InventoryEntry>, id: string): boolean;
42
97
  /**
43
98
  * The link a storefront emits for a package.
44
99
  *
package/dist/index.js CHANGED
@@ -1,5 +1,88 @@
1
1
  /** How long to wait before concluding nothing claimed the link. See `attemptHandoff`. */
2
2
  const HANDOFF_WAIT_MS = 1_400;
3
+ /**
4
+ * Versioned and per-origin. A self-hosted storefront keeps its own record rather than inheriting the
5
+ * flagship's, and neither can read the other's.
6
+ */
7
+ const INVENTORY_KEY = "azphalt.inventory.v1";
8
+ /**
9
+ * The store's record of what it has handed to a host.
10
+ *
11
+ * A host telling the store what it holds is the better answer, and the Android store app gets one
12
+ * (`spec/state-reporting.md` § 3). A web page has no host to ask: it fires `azphalt://install` and
13
+ * learns nothing. Without keeping its own books every card says *Get*, including the ones the user
14
+ * just installed, and there is no way to filter what you already have.
15
+ *
16
+ * Never throws. Storage can be disabled or full, and neither is worth failing a browse over — what is
17
+ * lost is nicer labels.
18
+ */
19
+ export function loadInventory() {
20
+ try {
21
+ const raw = window.localStorage.getItem(INVENTORY_KEY);
22
+ if (!raw)
23
+ return {};
24
+ const parsed = JSON.parse(raw);
25
+ const out = {};
26
+ for (const e of parsed.entries ?? [])
27
+ if (e?.id)
28
+ out[e.id] = e;
29
+ return out;
30
+ }
31
+ catch {
32
+ // Corrupt storage, or storage switched off. Forgetting is the correct failure here.
33
+ return {};
34
+ }
35
+ }
36
+ /** Persist `entries`, replacing what was there. Silent no-op where there is nowhere to put it. */
37
+ export function saveInventory(entries) {
38
+ try {
39
+ window.localStorage.setItem(INVENTORY_KEY, JSON.stringify({ entries: Object.values(entries) }));
40
+ }
41
+ catch {
42
+ /* quota, or storage disabled */
43
+ }
44
+ }
45
+ /**
46
+ * Record a successful handoff.
47
+ *
48
+ * Recorded as `installed` rather than `downloaded`, and the distinction is worth stating: the store
49
+ * did **not** observe an install. It observed that the user pressed Install and that a host claimed
50
+ * the link, which is the last thing a web page can observe. `downloaded` means "wanted and not yet
51
+ * had", which describes that moment worse than `installed` does — the user's next question is "did
52
+ * that work", and "not yet had" answers it wrongly in the common case.
53
+ */
54
+ export function recordInstalled(inventory, id, version, at) {
55
+ const next = { ...inventory, [id]: { id, version, state: "installed", ...(at ? { at } : {}) } };
56
+ saveInventory(next);
57
+ return next;
58
+ }
59
+ /**
60
+ * Forget a locally-recorded install.
61
+ *
62
+ * The store's record is a guess (see `recordInstalled`), and a guess with no way to correct it is
63
+ * just a wrong answer that persists. A handoff can succeed — a host claimed the link — and the
64
+ * install still not happen, and on a web visit nothing will ever contradict that. This is how the
65
+ * user says so.
66
+ *
67
+ * Deletes the entry rather than marking it `removed`: `removed` is a *host's* statement that a
68
+ * package was uninstalled, which is a different claim from the store admitting it never knew.
69
+ */
70
+ export function forgetInstall(inventory, id) {
71
+ const next = { ...inventory };
72
+ delete next[id];
73
+ saveInventory(next);
74
+ return next;
75
+ }
76
+ /**
77
+ * Whether the viewer has this package.
78
+ *
79
+ * `removed` is kept rather than deleted so a reinstall can be offered, so "has it" is a question
80
+ * about the state and not about the key being present.
81
+ */
82
+ export function isHeld(inventory, id) {
83
+ const s = inventory[id]?.state;
84
+ return s !== undefined && s !== "removed";
85
+ }
3
86
  /**
4
87
  * The link a storefront emits for a package.
5
88
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azphalt/web-handoff",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },