@dejadex/web 0.0.0-stage → 0.1.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 (113) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +214 -2
  3. package/dist/errors.d.ts +8 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +9 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/geometry/homography.d.ts +75 -0
  8. package/dist/geometry/homography.d.ts.map +1 -0
  9. package/dist/geometry/homography.js +146 -0
  10. package/dist/geometry/homography.js.map +1 -0
  11. package/dist/identify/assets.d.ts +39 -0
  12. package/dist/identify/assets.d.ts.map +1 -0
  13. package/dist/identify/assets.js +167 -0
  14. package/dist/identify/assets.js.map +1 -0
  15. package/dist/identify/decode.d.ts +6 -0
  16. package/dist/identify/decode.d.ts.map +1 -0
  17. package/dist/identify/decode.js +53 -0
  18. package/dist/identify/decode.js.map +1 -0
  19. package/dist/identify/engine.d.ts +32 -0
  20. package/dist/identify/engine.d.ts.map +1 -0
  21. package/dist/identify/engine.js +127 -0
  22. package/dist/identify/engine.js.map +1 -0
  23. package/dist/identify/identifier.d.ts +81 -0
  24. package/dist/identify/identifier.d.ts.map +1 -0
  25. package/dist/identify/identifier.js +327 -0
  26. package/dist/identify/identifier.js.map +1 -0
  27. package/dist/identify/manifest.d.ts +60 -0
  28. package/dist/identify/manifest.d.ts.map +1 -0
  29. package/dist/identify/manifest.js +211 -0
  30. package/dist/identify/manifest.js.map +1 -0
  31. package/dist/identify/preprocess.d.ts +52 -0
  32. package/dist/identify/preprocess.d.ts.map +1 -0
  33. package/dist/identify/preprocess.js +195 -0
  34. package/dist/identify/preprocess.js.map +1 -0
  35. package/dist/identify/router.d.ts +60 -0
  36. package/dist/identify/router.d.ts.map +1 -0
  37. package/dist/identify/router.js +83 -0
  38. package/dist/identify/router.js.map +1 -0
  39. package/dist/identify/runtime.d.ts +69 -0
  40. package/dist/identify/runtime.d.ts.map +1 -0
  41. package/dist/identify/runtime.js +140 -0
  42. package/dist/identify/runtime.js.map +1 -0
  43. package/dist/identify/search.d.ts +35 -0
  44. package/dist/identify/search.d.ts.map +1 -0
  45. package/dist/identify/search.js +99 -0
  46. package/dist/identify/search.js.map +1 -0
  47. package/dist/identify/server.d.ts +60 -0
  48. package/dist/identify/server.d.ts.map +1 -0
  49. package/dist/identify/server.js +149 -0
  50. package/dist/identify/server.js.map +1 -0
  51. package/dist/identify/shard.d.ts +31 -0
  52. package/dist/identify/shard.d.ts.map +1 -0
  53. package/dist/identify/shard.js +146 -0
  54. package/dist/identify/shard.js.map +1 -0
  55. package/dist/identify/types.d.ts +107 -0
  56. package/dist/identify/types.d.ts.map +1 -0
  57. package/dist/identify/types.js +7 -0
  58. package/dist/identify/types.js.map +1 -0
  59. package/dist/identify/worker.d.ts +2 -0
  60. package/dist/identify/worker.d.ts.map +1 -0
  61. package/dist/identify/worker.js +13 -0
  62. package/dist/identify/worker.js.map +1 -0
  63. package/dist/identify/workerProtocol.d.ts +54 -0
  64. package/dist/identify/workerProtocol.d.ts.map +1 -0
  65. package/dist/identify/workerProtocol.js +113 -0
  66. package/dist/identify/workerProtocol.js.map +1 -0
  67. package/dist/index.d.ts +20 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +16 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/lease/verify.d.ts +69 -0
  72. package/dist/lease/verify.d.ts.map +1 -0
  73. package/dist/lease/verify.js +235 -0
  74. package/dist/lease/verify.js.map +1 -0
  75. package/dist/scanner.d.ts +10 -0
  76. package/dist/scanner.d.ts.map +1 -0
  77. package/dist/scanner.js +58 -0
  78. package/dist/scanner.js.map +1 -0
  79. package/dist/types.d.ts +180 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/usage/meter.d.ts +83 -0
  84. package/dist/usage/meter.d.ts.map +1 -0
  85. package/dist/usage/meter.js +111 -0
  86. package/dist/usage/meter.js.map +1 -0
  87. package/dist/usage/store.d.ts +52 -0
  88. package/dist/usage/store.d.ts.map +1 -0
  89. package/dist/usage/store.js +198 -0
  90. package/dist/usage/store.js.map +1 -0
  91. package/package.json +64 -4
  92. package/src/errors.ts +24 -0
  93. package/src/geometry/homography.ts +178 -0
  94. package/src/identify/assets.ts +189 -0
  95. package/src/identify/decode.ts +54 -0
  96. package/src/identify/engine.ts +167 -0
  97. package/src/identify/identifier.ts +419 -0
  98. package/src/identify/manifest.ts +240 -0
  99. package/src/identify/preprocess.ts +227 -0
  100. package/src/identify/router.ts +115 -0
  101. package/src/identify/runtime.ts +197 -0
  102. package/src/identify/search.ts +115 -0
  103. package/src/identify/server.ts +167 -0
  104. package/src/identify/shard.ts +154 -0
  105. package/src/identify/types.ts +122 -0
  106. package/src/identify/worker.ts +13 -0
  107. package/src/identify/workerProtocol.ts +136 -0
  108. package/src/index.ts +71 -0
  109. package/src/lease/verify.ts +252 -0
  110. package/src/scanner.ts +56 -0
  111. package/src/types.ts +196 -0
  112. package/src/usage/meter.ts +165 -0
  113. package/src/usage/store.ts +245 -0
@@ -0,0 +1,111 @@
1
+ import { randomId } from './store.js';
2
+ /** Reports per batch handed to the host. */
3
+ export const MAX_BATCH_REPORTS = 500;
4
+ /** Sessions whose seen cards are remembered at once; the oldest is forgotten first. */
5
+ const MAX_SESSIONS = 32;
6
+ export class UsageMeter {
7
+ store;
8
+ now;
9
+ newId;
10
+ /** session → card ids already charged (by either side) in it. */
11
+ seen = new Map();
12
+ /** lease id → cards charged under it on this page (device + service). */
13
+ used = new Map();
14
+ chain = Promise.resolve();
15
+ constructor(options) {
16
+ this.store = options.store;
17
+ this.now = options.now ?? (() => Date.now());
18
+ this.newId = options.newId ?? (() => (typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : randomId()));
19
+ }
20
+ /** Cards charged under `leaseId` on this page so far. */
21
+ usedUnder(leaseId) {
22
+ return this.used.get(leaseId) ?? 0;
23
+ }
24
+ /** Whether the lease's allowance is used up. */
25
+ exhausted(lease) {
26
+ return this.usedUnder(lease.jti) >= lease.allowance;
27
+ }
28
+ /** Tallies one call's answers; resolves once the tally is stored. */
29
+ record(input) {
30
+ return this.serial(async () => {
31
+ const charged = [];
32
+ let serverCharged = 0;
33
+ if (input.purpose !== 'retry') {
34
+ const seen = this.sessionSet(input.session);
35
+ for (const r of input.results) {
36
+ const top = r.candidates[0];
37
+ if (r.kind !== 'card' || !r.confident || !top || seen.has(top.cardId))
38
+ continue;
39
+ seen.add(top.cardId);
40
+ if (r.identificationId !== undefined)
41
+ serverCharged++;
42
+ else
43
+ charged.push(top.cardId);
44
+ }
45
+ }
46
+ const leaseId = input.lease.jti;
47
+ this.used.set(leaseId, this.usedUnder(leaseId) + serverCharged + charged.length);
48
+ await this.store.tally(leaseId, input.session, charged, 1, this.now());
49
+ return { charged, serverCharged };
50
+ });
51
+ }
52
+ /**
53
+ * Seals the open tallies and hands the queued reports to `deliver` in
54
+ * batches of at most 500, oldest first. Each batch is removed once `deliver`
55
+ * resolves; a rejection stops the drain, leaves that batch queued, and is
56
+ * rethrown.
57
+ */
58
+ async drain(deliver) {
59
+ // Sealing goes through the record queue, so a tally being written is not split.
60
+ await this.serial(() => this.store.seal(this.newId));
61
+ const deviceId = await this.store.deviceId();
62
+ let delivered = 0;
63
+ for (;;) {
64
+ const reports = await this.store.peek(MAX_BATCH_REPORTS);
65
+ if (reports.length === 0)
66
+ break;
67
+ await deliver({ deviceId, reports });
68
+ await this.store.remove(reports.map((r) => r.reportId));
69
+ await this.store.markReported(reports);
70
+ delivered += reports.length;
71
+ }
72
+ return { delivered, pending: await this.store.size() };
73
+ }
74
+ pending() {
75
+ return this.store.size();
76
+ }
77
+ /**
78
+ * Makes sure the lease gets at least one report, even if nothing is ever
79
+ * identified under it: the service renews a lease only once the previous
80
+ * one has reported (an empty report counts).
81
+ */
82
+ touch(leaseId, session) {
83
+ return this.serial(() => this.store.tally(leaseId, session, [], 0, this.now()));
84
+ }
85
+ /** What the host's lease request carries about the lease being renewed. */
86
+ async leaseRequest(previousLeaseId) {
87
+ return {
88
+ previousLeaseId,
89
+ usageReportedThrough: previousLeaseId === null ? null : await this.store.reportedThrough(previousLeaseId),
90
+ };
91
+ }
92
+ sessionSet(session) {
93
+ let set = this.seen.get(session);
94
+ if (!set) {
95
+ set = new Set();
96
+ this.seen.set(session, set);
97
+ if (this.seen.size > MAX_SESSIONS) {
98
+ const oldest = this.seen.keys().next().value;
99
+ if (oldest !== undefined)
100
+ this.seen.delete(oldest);
101
+ }
102
+ }
103
+ return set;
104
+ }
105
+ serial(task) {
106
+ const run = this.chain.then(task, task);
107
+ this.chain = run.catch(() => undefined);
108
+ return run;
109
+ }
110
+ }
111
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../../src/usage/meter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The device's usage queue: what was counted waits here, across reloads,
3
+ * until the host has delivered it. IndexedDB in the browser; an in-memory
4
+ * store for tests and for runtimes without IndexedDB (where usage then lives
5
+ * only as long as the page).
6
+ *
7
+ * Two stages:
8
+ * - a **tally** per (lease, session) collects the cards first identified in
9
+ * that session and the number of calls, as answers come in;
10
+ * - `seal` turns every open tally into a `UsageReport` with the lease's next
11
+ * `seq` and a fresh `reportId`, atomically. A sealed report never changes,
12
+ * so delivering it twice is harmless (the service dedupes by `reportId`).
13
+ *
14
+ * Database `dejadex-usage`, version 1:
15
+ * tallies key `<leaseId>\n<sessionId>` → Tally
16
+ * reports keyPath `report.reportId`, index `order` → { order, report }
17
+ * meta `deviceId`, `order`, `seq:<leaseId>`, `reported:<leaseId>`
18
+ */
19
+ import type { UsageReport } from '../types.js';
20
+ export interface Tally {
21
+ readonly leaseId: string;
22
+ readonly sessionId: string;
23
+ /** Distinct, in first-seen order. */
24
+ readonly cardIds: readonly string[];
25
+ readonly calls: number;
26
+ /** Device clock (ms) of the latest addition. */
27
+ readonly at: number;
28
+ }
29
+ export interface UsageStore {
30
+ /** A random per-install id (at least 128 bits), created on first use. */
31
+ deviceId(): Promise<string>;
32
+ /** Adds cards (deduplicated) and calls to the open tally of (lease, session). */
33
+ tally(leaseId: string, sessionId: string, cardIds: readonly string[], calls: number, at: number): Promise<void>;
34
+ /** Seals every open tally into a queued report; returns how many were sealed. */
35
+ seal(newReportId: () => string): Promise<number>;
36
+ /** The oldest `limit` queued reports, in queue order. */
37
+ peek(limit: number): Promise<readonly UsageReport[]>;
38
+ remove(reportIds: readonly string[]): Promise<void>;
39
+ /** Records that these reports were delivered: each lease's highest delivered `seq`. */
40
+ markReported(reports: readonly UsageReport[]): Promise<void>;
41
+ /** The highest delivered `seq` of a lease, or null when none was. */
42
+ reportedThrough(leaseId: string): Promise<number | null>;
43
+ /** Queued reports plus open tallies. */
44
+ size(): Promise<number>;
45
+ }
46
+ export declare function randomId(bytes?: number): string;
47
+ export declare function memoryUsageStore(): UsageStore;
48
+ /** The IndexedDB store. `factory` defaults to the global `indexedDB`. */
49
+ export declare function indexedDbUsageStore(factory?: IDBFactory): Promise<UsageStore>;
50
+ /** IndexedDB where it opens, else memory (private windows can refuse it). */
51
+ export declare function defaultUsageStore(): Promise<UsageStore>;
52
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,yEAAyE;IACzE,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3B,iFAAiF;IACjF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/G,iFAAiF;IACjF,IAAI,CAAC,WAAW,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAChD,yDAAyD;IACzD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAA;IACpD,MAAM,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,uFAAuF;IACvF,YAAY,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,qEAAqE;IACrE,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxD,wCAAwC;IACxC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CACxB;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAK,GAAG,MAAM,CAM3C;AAqBD,wBAAgB,gBAAgB,IAAI,UAAU,CA0C7C;AAyBD,yEAAyE;AACzE,wBAAsB,mBAAmB,CAAC,OAAO,GAAE,UAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2F9F;AAED,6EAA6E;AAC7E,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAO7D"}
@@ -0,0 +1,198 @@
1
+ export function randomId(bytes = 16) {
2
+ const b = new Uint8Array(bytes);
3
+ crypto.getRandomValues(b);
4
+ let out = '';
5
+ for (const x of b)
6
+ out += x.toString(16).padStart(2, '0');
7
+ return out;
8
+ }
9
+ const tallyKey = (leaseId, sessionId) => `${leaseId}\n${sessionId}`;
10
+ function merge(prev, leaseId, sessionId, cardIds, calls, at) {
11
+ const cards = [...(prev?.cardIds ?? [])];
12
+ for (const id of cardIds)
13
+ if (!cards.includes(id))
14
+ cards.push(id);
15
+ return { leaseId, sessionId, cardIds: cards, calls: (prev?.calls ?? 0) + calls, at };
16
+ }
17
+ const toReport = (t, seq, reportId) => ({
18
+ reportId,
19
+ leaseId: t.leaseId,
20
+ seq,
21
+ sessionId: t.sessionId,
22
+ distinctCards: t.cardIds.length,
23
+ cardIds: [...t.cardIds],
24
+ calls: t.calls,
25
+ at: t.at,
26
+ });
27
+ export function memoryUsageStore() {
28
+ let device = null;
29
+ const seq = new Map();
30
+ const tallies = new Map();
31
+ const queue = [];
32
+ const reported = new Map();
33
+ return {
34
+ async deviceId() {
35
+ device ??= randomId();
36
+ return device;
37
+ },
38
+ async tally(leaseId, sessionId, cardIds, calls, at) {
39
+ const key = tallyKey(leaseId, sessionId);
40
+ tallies.set(key, merge(tallies.get(key), leaseId, sessionId, cardIds, calls, at));
41
+ },
42
+ async seal(newReportId) {
43
+ const open = [...tallies.values()];
44
+ tallies.clear();
45
+ for (const t of open) {
46
+ const next = (seq.get(t.leaseId) ?? 0) + 1;
47
+ seq.set(t.leaseId, next);
48
+ queue.push(toReport(t, next, newReportId()));
49
+ }
50
+ return open.length;
51
+ },
52
+ async peek(limit) {
53
+ return queue.slice(0, limit);
54
+ },
55
+ async remove(ids) {
56
+ const drop = new Set(ids);
57
+ for (let i = queue.length - 1; i >= 0; i--)
58
+ if (drop.has(queue[i].reportId))
59
+ queue.splice(i, 1);
60
+ },
61
+ async markReported(reports) {
62
+ for (const r of reports)
63
+ reported.set(r.leaseId, Math.max(reported.get(r.leaseId) ?? 0, r.seq));
64
+ },
65
+ async reportedThrough(leaseId) {
66
+ return reported.get(leaseId) ?? null;
67
+ },
68
+ async size() {
69
+ return queue.length + tallies.size;
70
+ },
71
+ };
72
+ }
73
+ const DB_NAME = 'dejadex-usage';
74
+ const DB_VERSION = 1;
75
+ function request(req) {
76
+ return new Promise((resolve, reject) => {
77
+ req.onsuccess = () => resolve(req.result);
78
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'));
79
+ });
80
+ }
81
+ function done(tx) {
82
+ return new Promise((resolve, reject) => {
83
+ tx.oncomplete = () => resolve();
84
+ tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'));
85
+ tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed'));
86
+ });
87
+ }
88
+ /** The IndexedDB store. `factory` defaults to the global `indexedDB`. */
89
+ export async function indexedDbUsageStore(factory = indexedDB) {
90
+ const open = factory.open(DB_NAME, DB_VERSION);
91
+ open.onupgradeneeded = () => {
92
+ const db = open.result;
93
+ if (!db.objectStoreNames.contains('tallies'))
94
+ db.createObjectStore('tallies');
95
+ if (!db.objectStoreNames.contains('reports')) {
96
+ db.createObjectStore('reports', { keyPath: 'report.reportId' }).createIndex('order', 'order', { unique: true });
97
+ }
98
+ if (!db.objectStoreNames.contains('meta'))
99
+ db.createObjectStore('meta');
100
+ };
101
+ const db = await request(open);
102
+ return {
103
+ async deviceId() {
104
+ const tx = db.transaction('meta', 'readwrite');
105
+ const meta = tx.objectStore('meta');
106
+ let id = (await request(meta.get('deviceId')));
107
+ if (typeof id !== 'string') {
108
+ id = randomId();
109
+ meta.put(id, 'deviceId');
110
+ }
111
+ await done(tx);
112
+ return id;
113
+ },
114
+ async tally(leaseId, sessionId, cardIds, calls, at) {
115
+ const tx = db.transaction('tallies', 'readwrite');
116
+ const store = tx.objectStore('tallies');
117
+ const key = tallyKey(leaseId, sessionId);
118
+ const prev = (await request(store.get(key)));
119
+ store.put(merge(prev, leaseId, sessionId, cardIds, calls, at), key);
120
+ await done(tx);
121
+ },
122
+ async seal(newReportId) {
123
+ const tx = db.transaction(['tallies', 'reports', 'meta'], 'readwrite');
124
+ const tallies = tx.objectStore('tallies');
125
+ const reports = tx.objectStore('reports');
126
+ const meta = tx.objectStore('meta');
127
+ const open = (await request(tallies.getAll()));
128
+ let order = (await request(meta.get('order'))) ?? 0;
129
+ const seqs = new Map();
130
+ for (const t of open) {
131
+ const key = `seq:${t.leaseId}`;
132
+ let last = seqs.get(key);
133
+ if (last === undefined)
134
+ last = (await request(meta.get(key))) ?? 0;
135
+ const next = last + 1;
136
+ seqs.set(key, next);
137
+ reports.put({ order: ++order, report: toReport(t, next, newReportId()) });
138
+ }
139
+ for (const [key, value] of seqs)
140
+ meta.put(value, key);
141
+ meta.put(order, 'order');
142
+ tallies.clear();
143
+ await done(tx);
144
+ return open.length;
145
+ },
146
+ async peek(limit) {
147
+ const tx = db.transaction('reports', 'readonly');
148
+ const rows = (await request(tx.objectStore('reports').index('order').getAll(null, limit)));
149
+ await done(tx);
150
+ return rows.map((r) => r.report);
151
+ },
152
+ async remove(ids) {
153
+ const tx = db.transaction('reports', 'readwrite');
154
+ const store = tx.objectStore('reports');
155
+ for (const id of ids)
156
+ store.delete(id);
157
+ await done(tx);
158
+ },
159
+ async markReported(reports) {
160
+ const tx = db.transaction('meta', 'readwrite');
161
+ const meta = tx.objectStore('meta');
162
+ const top = new Map();
163
+ for (const r of reports)
164
+ top.set(r.leaseId, Math.max(top.get(r.leaseId) ?? 0, r.seq));
165
+ for (const [leaseId, seq] of top) {
166
+ const key = `reported:${leaseId}`;
167
+ const prev = (await request(meta.get(key)));
168
+ if (prev === undefined || seq > prev)
169
+ meta.put(seq, key);
170
+ }
171
+ await done(tx);
172
+ },
173
+ async reportedThrough(leaseId) {
174
+ const tx = db.transaction('meta', 'readonly');
175
+ const v = (await request(tx.objectStore('meta').get(`reported:${leaseId}`)));
176
+ await done(tx);
177
+ return v ?? null;
178
+ },
179
+ async size() {
180
+ const tx = db.transaction(['reports', 'tallies'], 'readonly');
181
+ const [a, b] = await Promise.all([request(tx.objectStore('reports').count()), request(tx.objectStore('tallies').count())]);
182
+ await done(tx);
183
+ return a + b;
184
+ },
185
+ };
186
+ }
187
+ /** IndexedDB where it opens, else memory (private windows can refuse it). */
188
+ export async function defaultUsageStore() {
189
+ try {
190
+ if (typeof indexedDB !== 'undefined')
191
+ return await indexedDbUsageStore();
192
+ }
193
+ catch {
194
+ // fall through to memory
195
+ }
196
+ return memoryUsageStore();
197
+ }
198
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@dejadex/web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Framework-free browser SDK: on-device card detection, tracking and identification",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://dejadex.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dejadexhq/DejaDex.git",
10
+ "directory": "sdk/web"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./identify-worker": {
21
+ "types": "./dist/identify/worker.d.ts",
22
+ "import": "./dist/identify/worker.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "!src/**/*.test.ts",
29
+ "!src/**/*.testutil.ts",
30
+ "!src/**/__fixtures__",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "sideEffects": [
34
+ "./dist/identify/worker.js"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "prepack": "npm run build",
39
+ "typecheck": "tsc --noEmit",
40
+ "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "assets:dev": "node scripts/dev-assets.mjs",
44
+ "measure:identify": "npm run build && node scripts/measure-identify.mjs"
45
+ },
46
+ "engines": {
47
+ "node": ">=22"
48
+ },
49
+ "publishConfig": {
50
+ "registry": "https://registry.npmjs.org/",
51
+ "access": "public"
52
+ },
53
+ "devDependencies": {
54
+ "@eslint/js": "^9.39.5",
55
+ "eslint": "^9.39.5",
56
+ "fake-indexeddb": "6.2.6",
57
+ "globals": "^15.15.0",
58
+ "typescript": "^5.9.3",
59
+ "typescript-eslint": "^8.65.0",
60
+ "vitest": "^5.0.5"
61
+ },
62
+ "dependencies": {
63
+ "@noble/ed25519": "3.2.0",
64
+ "onnxruntime-web": "1.30.0"
65
+ }
66
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** Machine-readable reasons the scanner refuses or stops. Stable; hosts branch on them. */
2
+ export type ScannerErrorCode =
3
+ | 'INVALID_OPTIONS'
4
+ | 'NOT_IMPLEMENTED'
5
+ | 'LEASE_INVALID'
6
+ | 'LEASE_EXPIRED'
7
+ | 'ALLOWANCE_EXHAUSTED'
8
+ | 'ORIGIN_NOT_ALLOWED'
9
+ | 'DOMAIN_NOT_ALLOWED'
10
+ | 'UNSUPPORTED_DEVICE'
11
+ | 'MODEL_INTEGRITY'
12
+ | 'NETWORK'
13
+
14
+ export class ScannerError extends Error {
15
+ override readonly name = 'ScannerError'
16
+
17
+ constructor(
18
+ readonly code: ScannerErrorCode,
19
+ message: string,
20
+ options?: ErrorOptions,
21
+ ) {
22
+ super(message, options)
23
+ }
24
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Planar homographies for AR labels: the projective map from a card's own
3
+ * unit rectangle to the four corners the detector found on screen, and its
4
+ * CSS `matrix3d` form, so a host page can lay a DOM element on the card's
5
+ * plane with nothing but `transform`.
6
+ *
7
+ * Conventions, fixed for every consumer:
8
+ *
9
+ * - Card space is the unit square: (0,0) top-left, (1,0) top-right, (1,1)
10
+ * bottom-right, (0,1) bottom-left of the card as printed (portrait).
11
+ * - Screen space is CSS pixels of the element the corners were measured in,
12
+ * y pointing down.
13
+ * - A `Quad` lists the screen corners in that same order: TL, TR, BR, BL.
14
+ * - A `Homography` is a 3×3 matrix in ROW-MAJOR order, normalised so the
15
+ * bottom-right entry is 1. It maps (u, v, 1) to (x·w, y·w, w).
16
+ *
17
+ * The square-to-quad solution is the closed form from P. Heckbert,
18
+ * "Fundamentals of Texture Mapping and Image Warping" (1989), §2.2.3: no
19
+ * linear solve, exact for any convex quad, and cheap enough to run per track
20
+ * per frame.
21
+ */
22
+
23
+ export interface Point {
24
+ readonly x: number
25
+ readonly y: number
26
+ }
27
+
28
+ /** Screen corners in card order: top-left, top-right, bottom-right, bottom-left. */
29
+ export type Quad = readonly [Point, Point, Point, Point]
30
+
31
+ /** Row-major 3×3 matrix, `[a, b, c, d, e, f, g, h, i]` with i = 1 once normalised. */
32
+ export type Homography = readonly [number, number, number, number, number, number, number, number, number]
33
+
34
+ export const IDENTITY: Homography = [1, 0, 0, 0, 1, 0, 0, 0, 1]
35
+
36
+ /** The unit card rectangle's corners, in `Quad` order. */
37
+ export const UNIT_QUAD: Quad = [
38
+ { x: 0, y: 0 },
39
+ { x: 1, y: 0 },
40
+ { x: 1, y: 1 },
41
+ { x: 0, y: 1 },
42
+ ]
43
+
44
+ /** Relative tolerance below which a quad or a matrix counts as degenerate. */
45
+ const DEGENERATE_EPS = 1e-12
46
+
47
+ const isFiniteQuad = (q: Quad): boolean => q.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))
48
+
49
+ /**
50
+ * The homography that maps the unit card rectangle onto `quad`.
51
+ *
52
+ * Throws `RangeError` when no proper homography exists: a non-finite corner,
53
+ * three collinear corners (zero area), or a quad that is not convex — a
54
+ * concave or self-crossing quad would need the projective plane to fold, and
55
+ * the label would be drawn through the horizon.
56
+ */
57
+ export function unitSquareToQuad(quad: Quad): Homography {
58
+ if (!isFiniteQuad(quad)) throw new RangeError('quad has a non-finite corner')
59
+ const [p0, p1, p2, p3] = quad
60
+
61
+ const dx1 = p1.x - p2.x
62
+ const dx2 = p3.x - p2.x
63
+ const dy1 = p1.y - p2.y
64
+ const dy2 = p3.y - p2.y
65
+ const sx = p0.x - p1.x + p2.x - p3.x
66
+ const sy = p0.y - p1.y + p2.y - p3.y
67
+
68
+ const span = Math.max(
69
+ ...quad.map((p) => Math.abs(p.x - p0.x)),
70
+ ...quad.map((p) => Math.abs(p.y - p0.y)),
71
+ )
72
+ const den = dx1 * dy2 - dx2 * dy1
73
+ if (span === 0 || Math.abs(den) <= DEGENERATE_EPS * span * span) {
74
+ throw new RangeError('quad is degenerate (zero area or collinear corners)')
75
+ }
76
+
77
+ const g = (sx * dy2 - dx2 * sy) / den
78
+ const h = (dx1 * sy - sx * dy1) / den
79
+
80
+ // w at each corner: (0,0) → 1, (1,0) → 1+g, (1,1) → 1+g+h, (0,1) → 1+h.
81
+ // All must share a sign, or the quad is not the image of a convex square.
82
+ if (1 + g <= DEGENERATE_EPS || 1 + h <= DEGENERATE_EPS || 1 + g + h <= DEGENERATE_EPS) {
83
+ throw new RangeError('quad is not convex')
84
+ }
85
+
86
+ return [
87
+ p1.x - p0.x + g * p1.x,
88
+ p3.x - p0.x + h * p3.x,
89
+ p0.x,
90
+ p1.y - p0.y + g * p1.y,
91
+ p3.y - p0.y + h * p3.y,
92
+ p0.y,
93
+ g,
94
+ h,
95
+ 1,
96
+ ]
97
+ }
98
+
99
+ /** Maps a point through `m`. Throws `RangeError` for a point on the vanishing line. */
100
+ export function applyHomography(m: Homography, p: Point): Point {
101
+ const [a, b, c, d, e, f, g, h, i] = m
102
+ const w = g * p.x + h * p.y + i
103
+ if (Math.abs(w) <= DEGENERATE_EPS) throw new RangeError('point maps to infinity')
104
+ return { x: (a * p.x + b * p.y + c) / w, y: (d * p.x + e * p.y + f) / w }
105
+ }
106
+
107
+ /** `a · b` (apply `b` first, then `a`), normalised. */
108
+ export function multiplyHomography(a: Homography, b: Homography): Homography {
109
+ const r = (row: number, col: number): number =>
110
+ a[row * 3]! * b[col]! + a[row * 3 + 1]! * b[3 + col]! + a[row * 3 + 2]! * b[6 + col]!
111
+ return normalise([r(0, 0), r(0, 1), r(0, 2), r(1, 0), r(1, 1), r(1, 2), r(2, 0), r(2, 1), r(2, 2)])
112
+ }
113
+
114
+ /** The inverse map, normalised. Throws `RangeError` for a singular matrix. */
115
+ export function invertHomography(m: Homography): Homography {
116
+ const [a, b, c, d, e, f, g, h, i] = m
117
+ const A = e * i - f * h
118
+ const B = -(d * i - f * g)
119
+ const C = d * h - e * g
120
+ const det = a * A + b * B + c * C
121
+ const scale = Math.max(...m.map(Math.abs))
122
+ if (!Number.isFinite(det) || Math.abs(det) <= DEGENERATE_EPS * scale * scale * scale) {
123
+ throw new RangeError('homography is singular')
124
+ }
125
+ // Adjugate (transposed cofactors); the 1/det factor cancels in normalise().
126
+ return normalise([A, -(b * i - c * h), b * f - c * e, B, a * i - c * g, -(a * f - c * d), C, -(a * h - b * g), a * e - b * d])
127
+ }
128
+
129
+ /** Scales `m` so its bottom-right entry is 1. */
130
+ export function normalise(m: readonly number[]): Homography {
131
+ const k = m[8]!
132
+ if (Math.abs(k) <= DEGENERATE_EPS) throw new RangeError('homography cannot be normalised (h33 = 0)')
133
+ return m.map((v) => v / k) as unknown as Homography
134
+ }
135
+
136
+ export interface ElementSize {
137
+ /** The label element's layout width in CSS pixels (its `offsetWidth`). */
138
+ readonly width: number
139
+ /** The label element's layout height in CSS pixels (its `offsetHeight`). */
140
+ readonly height: number
141
+ }
142
+
143
+ /**
144
+ * The CSS `matrix3d(...)` that draws an element of `size` onto the quad that
145
+ * `m` maps the unit card to.
146
+ *
147
+ * The element must be absolutely positioned at the origin of the coordinate
148
+ * space the corners were measured in, with `transform-origin: 0 0`. Its own
149
+ * box (0..width, 0..height) is first scaled to the unit square, then sent
150
+ * through `m`. The 3×3 homography becomes the 4×4
151
+ *
152
+ * | a b 0 c |
153
+ * | d e 0 f |
154
+ * | 0 0 1 0 |
155
+ * | g h 0 i |
156
+ *
157
+ * which leaves z alone, and CSS takes it in COLUMN-major order.
158
+ */
159
+ export function toCssMatrix3d(m: Homography, size: ElementSize = { width: 1, height: 1 }): string {
160
+ if (!(size.width > 0) || !(size.height > 0)) throw new RangeError('element size must be positive')
161
+ const scaled = multiplyHomography(m, [1 / size.width, 0, 0, 0, 1 / size.height, 0, 0, 0, 1])
162
+ const [a, b, c, d, e, f, g, h, i] = scaled
163
+ const columnMajor = [a, d, 0, g, b, e, 0, h, 0, 0, 1, 0, c, f, 0, i]
164
+ return `matrix3d(${columnMajor.map(formatCssNumber).join(', ')})`
165
+ }
166
+
167
+ /** Fixed precision, no `-0`, no exponent: what every engine parses identically. */
168
+ function formatCssNumber(v: number): string {
169
+ if (!Number.isFinite(v)) throw new RangeError('non-finite matrix entry')
170
+ const fixed = Number(v.toFixed(12))
171
+ if (fixed === 0) return '0'
172
+ return fixed.toFixed(12).replace(/\.?0+$/, '')
173
+ }
174
+
175
+ /** Convenience for the common case: corners in, CSS transform out. */
176
+ export function quadToCssMatrix3d(quad: Quad, size?: ElementSize): string {
177
+ return toCssMatrix3d(unitSquareToQuad(quad), size)
178
+ }