@dejadex/web 0.0.0-stage → 0.1.1
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/CHANGELOG.md +35 -0
- package/README.md +222 -2
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/geometry/homography.d.ts +75 -0
- package/dist/geometry/homography.d.ts.map +1 -0
- package/dist/geometry/homography.js +146 -0
- package/dist/geometry/homography.js.map +1 -0
- package/dist/identify/assets.d.ts +39 -0
- package/dist/identify/assets.d.ts.map +1 -0
- package/dist/identify/assets.js +167 -0
- package/dist/identify/assets.js.map +1 -0
- package/dist/identify/decode.d.ts +6 -0
- package/dist/identify/decode.d.ts.map +1 -0
- package/dist/identify/decode.js +53 -0
- package/dist/identify/decode.js.map +1 -0
- package/dist/identify/engine.d.ts +32 -0
- package/dist/identify/engine.d.ts.map +1 -0
- package/dist/identify/engine.js +127 -0
- package/dist/identify/engine.js.map +1 -0
- package/dist/identify/identifier.d.ts +91 -0
- package/dist/identify/identifier.d.ts.map +1 -0
- package/dist/identify/identifier.js +332 -0
- package/dist/identify/identifier.js.map +1 -0
- package/dist/identify/manifest.d.ts +60 -0
- package/dist/identify/manifest.d.ts.map +1 -0
- package/dist/identify/manifest.js +211 -0
- package/dist/identify/manifest.js.map +1 -0
- package/dist/identify/preprocess.d.ts +52 -0
- package/dist/identify/preprocess.d.ts.map +1 -0
- package/dist/identify/preprocess.js +195 -0
- package/dist/identify/preprocess.js.map +1 -0
- package/dist/identify/router.d.ts +60 -0
- package/dist/identify/router.d.ts.map +1 -0
- package/dist/identify/router.js +83 -0
- package/dist/identify/router.js.map +1 -0
- package/dist/identify/runtime.d.ts +69 -0
- package/dist/identify/runtime.d.ts.map +1 -0
- package/dist/identify/runtime.js +140 -0
- package/dist/identify/runtime.js.map +1 -0
- package/dist/identify/search.d.ts +35 -0
- package/dist/identify/search.d.ts.map +1 -0
- package/dist/identify/search.js +99 -0
- package/dist/identify/search.js.map +1 -0
- package/dist/identify/server.d.ts +60 -0
- package/dist/identify/server.d.ts.map +1 -0
- package/dist/identify/server.js +149 -0
- package/dist/identify/server.js.map +1 -0
- package/dist/identify/shard.d.ts +31 -0
- package/dist/identify/shard.d.ts.map +1 -0
- package/dist/identify/shard.js +146 -0
- package/dist/identify/shard.js.map +1 -0
- package/dist/identify/types.d.ts +107 -0
- package/dist/identify/types.d.ts.map +1 -0
- package/dist/identify/types.js +7 -0
- package/dist/identify/types.js.map +1 -0
- package/dist/identify/worker.d.ts +2 -0
- package/dist/identify/worker.d.ts.map +1 -0
- package/dist/identify/worker.js +13 -0
- package/dist/identify/worker.js.map +1 -0
- package/dist/identify/workerProtocol.d.ts +54 -0
- package/dist/identify/workerProtocol.d.ts.map +1 -0
- package/dist/identify/workerProtocol.js +113 -0
- package/dist/identify/workerProtocol.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lease/verify.d.ts +69 -0
- package/dist/lease/verify.d.ts.map +1 -0
- package/dist/lease/verify.js +235 -0
- package/dist/lease/verify.js.map +1 -0
- package/dist/scanner.d.ts +10 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +58 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +180 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/usage/meter.d.ts +83 -0
- package/dist/usage/meter.d.ts.map +1 -0
- package/dist/usage/meter.js +111 -0
- package/dist/usage/meter.js.map +1 -0
- package/dist/usage/store.d.ts +40 -0
- package/dist/usage/store.d.ts.map +1 -0
- package/dist/usage/store.js +248 -0
- package/dist/usage/store.js.map +1 -0
- package/package.json +64 -4
- package/src/errors.ts +24 -0
- package/src/geometry/homography.ts +178 -0
- package/src/identify/assets.ts +189 -0
- package/src/identify/decode.ts +54 -0
- package/src/identify/engine.ts +167 -0
- package/src/identify/identifier.ts +431 -0
- package/src/identify/manifest.ts +240 -0
- package/src/identify/preprocess.ts +227 -0
- package/src/identify/router.ts +115 -0
- package/src/identify/runtime.ts +197 -0
- package/src/identify/search.ts +115 -0
- package/src/identify/server.ts +167 -0
- package/src/identify/shard.ts +154 -0
- package/src/identify/types.ts +122 -0
- package/src/identify/worker.ts +13 -0
- package/src/identify/workerProtocol.ts +136 -0
- package/src/index.ts +71 -0
- package/src/lease/verify.ts +252 -0
- package/src/scanner.ts +56 -0
- package/src/types.ts +196 -0
- package/src/usage/meter.ts +165 -0
- package/src/usage/store.ts +282 -0
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
* The connection is never held against the host: on `versionchange` (the
|
|
20
|
+
* host's `indexedDB.deleteDatabase('dejadex-usage')` on sign-out, or a newer
|
|
21
|
+
* page upgrading it) the store closes itself, so the delete proceeds. A closed
|
|
22
|
+
* store rejects every later call with a `ScannerError`; nothing is dropped
|
|
23
|
+
* without the caller hearing of it.
|
|
24
|
+
*/
|
|
25
|
+
import { ScannerError } from '../errors.js'
|
|
26
|
+
import type { UsageReport } from '../types.js'
|
|
27
|
+
|
|
28
|
+
export interface Tally {
|
|
29
|
+
readonly leaseId: string
|
|
30
|
+
readonly sessionId: string
|
|
31
|
+
/** Distinct, in first-seen order. */
|
|
32
|
+
readonly cardIds: readonly string[]
|
|
33
|
+
readonly calls: number
|
|
34
|
+
/** Device clock (ms) of the latest addition. */
|
|
35
|
+
readonly at: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface UsageStore {
|
|
39
|
+
/** A random per-install id (at least 128 bits), created on first use. */
|
|
40
|
+
deviceId(): Promise<string>
|
|
41
|
+
/** Adds cards (deduplicated) and calls to the open tally of (lease, session). */
|
|
42
|
+
tally(leaseId: string, sessionId: string, cardIds: readonly string[], calls: number, at: number): Promise<void>
|
|
43
|
+
/** Seals every open tally into a queued report; returns how many were sealed. */
|
|
44
|
+
seal(newReportId: () => string): Promise<number>
|
|
45
|
+
/** The oldest `limit` queued reports, in queue order. */
|
|
46
|
+
peek(limit: number): Promise<readonly UsageReport[]>
|
|
47
|
+
remove(reportIds: readonly string[]): Promise<void>
|
|
48
|
+
/** Records that these reports were delivered: each lease's highest delivered `seq`. */
|
|
49
|
+
markReported(reports: readonly UsageReport[]): Promise<void>
|
|
50
|
+
/** The highest delivered `seq` of a lease, or null when none was. */
|
|
51
|
+
reportedThrough(leaseId: string): Promise<number | null>
|
|
52
|
+
/** Queued reports plus open tallies. */
|
|
53
|
+
size(): Promise<number>
|
|
54
|
+
/**
|
|
55
|
+
* Releases what the store holds open (the IndexedDB connection). Later calls
|
|
56
|
+
* reject with `ScannerError` `INVALID_OPTIONS`. Optional for stores a host
|
|
57
|
+
* implements; the identifier closes only a store it opened itself.
|
|
58
|
+
*/
|
|
59
|
+
close?(): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function randomId(bytes = 16): string {
|
|
63
|
+
const b = new Uint8Array(bytes)
|
|
64
|
+
crypto.getRandomValues(b)
|
|
65
|
+
let out = ''
|
|
66
|
+
for (const x of b) out += x.toString(16).padStart(2, '0')
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tallyKey = (leaseId: string, sessionId: string): string => `${leaseId}\n${sessionId}`
|
|
71
|
+
|
|
72
|
+
function merge(prev: Tally | undefined, leaseId: string, sessionId: string, cardIds: readonly string[], calls: number, at: number): Tally {
|
|
73
|
+
const cards = [...(prev?.cardIds ?? [])]
|
|
74
|
+
for (const id of cardIds) if (!cards.includes(id)) cards.push(id)
|
|
75
|
+
return { leaseId, sessionId, cardIds: cards, calls: (prev?.calls ?? 0) + calls, at }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const toReport = (t: Tally, seq: number, reportId: string): UsageReport => ({
|
|
79
|
+
reportId,
|
|
80
|
+
leaseId: t.leaseId,
|
|
81
|
+
seq,
|
|
82
|
+
sessionId: t.sessionId,
|
|
83
|
+
distinctCards: t.cardIds.length,
|
|
84
|
+
cardIds: [...t.cardIds],
|
|
85
|
+
calls: t.calls,
|
|
86
|
+
at: t.at,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export function memoryUsageStore(): UsageStore {
|
|
90
|
+
let device: string | null = null
|
|
91
|
+
const seq = new Map<string, number>()
|
|
92
|
+
const tallies = new Map<string, Tally>()
|
|
93
|
+
const queue: UsageReport[] = []
|
|
94
|
+
const reported = new Map<string, number>()
|
|
95
|
+
return {
|
|
96
|
+
async deviceId() {
|
|
97
|
+
device ??= randomId()
|
|
98
|
+
return device
|
|
99
|
+
},
|
|
100
|
+
async tally(leaseId, sessionId, cardIds, calls, at) {
|
|
101
|
+
const key = tallyKey(leaseId, sessionId)
|
|
102
|
+
tallies.set(key, merge(tallies.get(key), leaseId, sessionId, cardIds, calls, at))
|
|
103
|
+
},
|
|
104
|
+
async seal(newReportId) {
|
|
105
|
+
const open = [...tallies.values()]
|
|
106
|
+
tallies.clear()
|
|
107
|
+
for (const t of open) {
|
|
108
|
+
const next = (seq.get(t.leaseId) ?? 0) + 1
|
|
109
|
+
seq.set(t.leaseId, next)
|
|
110
|
+
queue.push(toReport(t, next, newReportId()))
|
|
111
|
+
}
|
|
112
|
+
return open.length
|
|
113
|
+
},
|
|
114
|
+
async peek(limit) {
|
|
115
|
+
return queue.slice(0, limit)
|
|
116
|
+
},
|
|
117
|
+
async remove(ids) {
|
|
118
|
+
const drop = new Set(ids)
|
|
119
|
+
for (let i = queue.length - 1; i >= 0; i--) if (drop.has((queue[i] as UsageReport).reportId)) queue.splice(i, 1)
|
|
120
|
+
},
|
|
121
|
+
async markReported(reports) {
|
|
122
|
+
for (const r of reports) reported.set(r.leaseId, Math.max(reported.get(r.leaseId) ?? 0, r.seq))
|
|
123
|
+
},
|
|
124
|
+
async reportedThrough(leaseId) {
|
|
125
|
+
return reported.get(leaseId) ?? null
|
|
126
|
+
},
|
|
127
|
+
async size() {
|
|
128
|
+
return queue.length + tallies.size
|
|
129
|
+
},
|
|
130
|
+
/** Nothing to release: memory lives as long as the page. */
|
|
131
|
+
close() {},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DB_NAME = 'dejadex-usage'
|
|
136
|
+
const DB_VERSION = 1
|
|
137
|
+
|
|
138
|
+
function request<T>(req: IDBRequest<T>): Promise<T> {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
req.onsuccess = () => resolve(req.result)
|
|
141
|
+
req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'))
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function done(tx: IDBTransaction): Promise<void> {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
tx.oncomplete = () => resolve()
|
|
148
|
+
tx.onabort = () => reject(tx.error ?? new Error('IndexedDB transaction aborted'))
|
|
149
|
+
tx.onerror = () => reject(tx.error ?? new Error('IndexedDB transaction failed'))
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface QueuedRow {
|
|
154
|
+
readonly order: number
|
|
155
|
+
readonly report: UsageReport
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The IndexedDB store. `factory` defaults to the global `indexedDB`. */
|
|
159
|
+
export async function indexedDbUsageStore(factory: IDBFactory = indexedDB): Promise<UsageStore> {
|
|
160
|
+
const open = factory.open(DB_NAME, DB_VERSION)
|
|
161
|
+
open.onupgradeneeded = () => {
|
|
162
|
+
const db = open.result
|
|
163
|
+
if (!db.objectStoreNames.contains('tallies')) db.createObjectStore('tallies')
|
|
164
|
+
if (!db.objectStoreNames.contains('reports')) {
|
|
165
|
+
db.createObjectStore('reports', { keyPath: 'report.reportId' }).createIndex('order', 'order', { unique: true })
|
|
166
|
+
}
|
|
167
|
+
if (!db.objectStoreNames.contains('meta')) db.createObjectStore('meta')
|
|
168
|
+
}
|
|
169
|
+
const db = await request(open)
|
|
170
|
+
|
|
171
|
+
/** Why the connection is gone, once it is. */
|
|
172
|
+
let closed: string | null = null
|
|
173
|
+
const shut = (why: string) => {
|
|
174
|
+
if (closed !== null) return
|
|
175
|
+
closed = why
|
|
176
|
+
db.close()
|
|
177
|
+
}
|
|
178
|
+
// Another party wants the database (a host's deleteDatabase, a newer
|
|
179
|
+
// version): let go, or its request is blocked for the life of the document.
|
|
180
|
+
db.onversionchange = () => shut('it was deleted or upgraded elsewhere')
|
|
181
|
+
// The browser closed it (storage cleared, the disk went away).
|
|
182
|
+
db.onclose = () => {
|
|
183
|
+
closed ??= 'the browser closed it'
|
|
184
|
+
}
|
|
185
|
+
const transaction = (names: string | string[], mode: IDBTransactionMode): IDBTransaction => {
|
|
186
|
+
if (closed !== null) throw new ScannerError('INVALID_OPTIONS', `the usage store is closed: ${closed}`)
|
|
187
|
+
return db.transaction(names, mode)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
async deviceId() {
|
|
192
|
+
const tx = transaction('meta', 'readwrite')
|
|
193
|
+
const meta = tx.objectStore('meta')
|
|
194
|
+
let id = (await request(meta.get('deviceId'))) as unknown
|
|
195
|
+
if (typeof id !== 'string') {
|
|
196
|
+
id = randomId()
|
|
197
|
+
meta.put(id, 'deviceId')
|
|
198
|
+
}
|
|
199
|
+
await done(tx)
|
|
200
|
+
return id as string
|
|
201
|
+
},
|
|
202
|
+
async tally(leaseId, sessionId, cardIds, calls, at) {
|
|
203
|
+
const tx = transaction('tallies', 'readwrite')
|
|
204
|
+
const store = tx.objectStore('tallies')
|
|
205
|
+
const key = tallyKey(leaseId, sessionId)
|
|
206
|
+
const prev = (await request(store.get(key))) as Tally | undefined
|
|
207
|
+
store.put(merge(prev, leaseId, sessionId, cardIds, calls, at), key)
|
|
208
|
+
await done(tx)
|
|
209
|
+
},
|
|
210
|
+
async seal(newReportId) {
|
|
211
|
+
const tx = transaction(['tallies', 'reports', 'meta'], 'readwrite')
|
|
212
|
+
const tallies = tx.objectStore('tallies')
|
|
213
|
+
const reports = tx.objectStore('reports')
|
|
214
|
+
const meta = tx.objectStore('meta')
|
|
215
|
+
const open = (await request(tallies.getAll())) as Tally[]
|
|
216
|
+
let order = ((await request(meta.get('order'))) as number | undefined) ?? 0
|
|
217
|
+
const seqs = new Map<string, number>()
|
|
218
|
+
for (const t of open) {
|
|
219
|
+
const key = `seq:${t.leaseId}`
|
|
220
|
+
let last = seqs.get(key)
|
|
221
|
+
if (last === undefined) last = ((await request(meta.get(key))) as number | undefined) ?? 0
|
|
222
|
+
const next = last + 1
|
|
223
|
+
seqs.set(key, next)
|
|
224
|
+
reports.put({ order: ++order, report: toReport(t, next, newReportId()) } satisfies QueuedRow)
|
|
225
|
+
}
|
|
226
|
+
for (const [key, value] of seqs) meta.put(value, key)
|
|
227
|
+
meta.put(order, 'order')
|
|
228
|
+
tallies.clear()
|
|
229
|
+
await done(tx)
|
|
230
|
+
return open.length
|
|
231
|
+
},
|
|
232
|
+
async peek(limit) {
|
|
233
|
+
const tx = transaction('reports', 'readonly')
|
|
234
|
+
const rows = (await request(tx.objectStore('reports').index('order').getAll(null, limit))) as QueuedRow[]
|
|
235
|
+
await done(tx)
|
|
236
|
+
return rows.map((r) => r.report)
|
|
237
|
+
},
|
|
238
|
+
async remove(ids) {
|
|
239
|
+
const tx = transaction('reports', 'readwrite')
|
|
240
|
+
const store = tx.objectStore('reports')
|
|
241
|
+
for (const id of ids) store.delete(id)
|
|
242
|
+
await done(tx)
|
|
243
|
+
},
|
|
244
|
+
async markReported(reports) {
|
|
245
|
+
const tx = transaction('meta', 'readwrite')
|
|
246
|
+
const meta = tx.objectStore('meta')
|
|
247
|
+
const top = new Map<string, number>()
|
|
248
|
+
for (const r of reports) top.set(r.leaseId, Math.max(top.get(r.leaseId) ?? 0, r.seq))
|
|
249
|
+
for (const [leaseId, seq] of top) {
|
|
250
|
+
const key = `reported:${leaseId}`
|
|
251
|
+
const prev = (await request(meta.get(key))) as number | undefined
|
|
252
|
+
if (prev === undefined || seq > prev) meta.put(seq, key)
|
|
253
|
+
}
|
|
254
|
+
await done(tx)
|
|
255
|
+
},
|
|
256
|
+
async reportedThrough(leaseId) {
|
|
257
|
+
const tx = transaction('meta', 'readonly')
|
|
258
|
+
const v = (await request(tx.objectStore('meta').get(`reported:${leaseId}`))) as number | undefined
|
|
259
|
+
await done(tx)
|
|
260
|
+
return v ?? null
|
|
261
|
+
},
|
|
262
|
+
async size() {
|
|
263
|
+
const tx = transaction(['reports', 'tallies'], 'readonly')
|
|
264
|
+
const [a, b] = await Promise.all([request(tx.objectStore('reports').count()), request(tx.objectStore('tallies').count())])
|
|
265
|
+
await done(tx)
|
|
266
|
+
return a + b
|
|
267
|
+
},
|
|
268
|
+
close() {
|
|
269
|
+
shut('close() was called')
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** IndexedDB where it opens, else memory (private windows can refuse it). */
|
|
275
|
+
export async function defaultUsageStore(): Promise<UsageStore> {
|
|
276
|
+
try {
|
|
277
|
+
if (typeof indexedDB !== 'undefined') return await indexedDbUsageStore()
|
|
278
|
+
} catch {
|
|
279
|
+
// fall through to memory
|
|
280
|
+
}
|
|
281
|
+
return memoryUsageStore()
|
|
282
|
+
}
|