tigertag 1.1.0 → 1.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.
- package/CHANGELOG.md +174 -3
- package/README.md +169 -42
- package/bin/tigertag.js +56 -18
- package/database/id_aspect.json +5 -0
- package/database/id_brand.json +119 -10
- package/database/id_catalog.json.gz +0 -0
- package/database/id_catalog.meta.json +9 -0
- package/database/id_material.json +489 -489
- package/database/id_type.json +18 -1
- package/database/id_version.json +30 -1
- package/database/last_update.json +1 -1
- package/package.json +11 -2
- package/src/catalog.js +285 -0
- package/src/datadir.js +65 -0
- package/src/db.js +319 -143
- package/src/index.js +14 -1
- package/src/signature.js +8 -8
- package/src/tag.js +209 -10
package/src/db.js
CHANGED
|
@@ -21,15 +21,42 @@
|
|
|
21
21
|
// https://github.com/TigerTag-Project/TigerTag-RFID-Guide/blob/main/LICENSING.md
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* TigerTag reference
|
|
24
|
+
* TigerTag reference tables (id_*.json): loader, precedence, automatic and
|
|
25
|
+
* manual updates.
|
|
26
|
+
*
|
|
27
|
+
* Where each table comes from (per file):
|
|
28
|
+
* 1. a custom folder (`dbPath`) — used EXCLUSIVELY: a missing file is an error,
|
|
29
|
+
* there is no fallback, and no network call happens unless update() is called;
|
|
30
|
+
* 2. otherwise the newest of
|
|
31
|
+
* - the downloaded copy in the data dir (`dataDir`, `TIGERTAG_DATA_DIR`,
|
|
32
|
+
* default: the per-user cache folder shared with the catalogue), and
|
|
33
|
+
* - the copy bundled in the npm package (always present),
|
|
34
|
+
* compared by their last_update.json timestamps.
|
|
35
|
+
*
|
|
36
|
+
* Updates: `new TigerTagDB()` never waits for the network. With autoUpdate on
|
|
37
|
+
* (default) it starts ONE background check per process, at most once per
|
|
38
|
+
* `maxAge` (default 1 day): one request to the TigerTag API (GitHub mirror as
|
|
39
|
+
* fallback), only the changed tables are downloaded into the data dir, ~5 s
|
|
40
|
+
* timeout, errors are swallowed. `await TigerTagDB.open()` does the same check
|
|
41
|
+
* before returning. `await db.update()` forces it. `offline: true` (or
|
|
42
|
+
* `TIGERTAG_OFFLINE=1`) means zero network calls.
|
|
25
43
|
*/
|
|
26
44
|
|
|
27
45
|
const fs = require('fs');
|
|
28
46
|
const path = require('path');
|
|
47
|
+
const { defaultDataDir, resolveOffline, BUNDLED_DIR } = require('./datadir');
|
|
29
48
|
|
|
30
49
|
const _API_BASE = 'https://api.tigertag.io/api:tigertag';
|
|
31
50
|
const _GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/TigerTag-Project/TigerTag-RFID-Guide/main/database';
|
|
32
|
-
const
|
|
51
|
+
const _MANUAL_TIMEOUT = 30000;
|
|
52
|
+
const _AUTO_TIMEOUT = 5000;
|
|
53
|
+
const _RETRY_AFTER_FAILURE_MS = 3600 * 1000; // a failed automatic check is retried after 1 h
|
|
54
|
+
|
|
55
|
+
/** Default interval between two automatic checks: 1 day. */
|
|
56
|
+
const DEFAULT_UPDATE_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
57
|
+
|
|
58
|
+
const _LAST_UPDATE = 'last_update.json';
|
|
59
|
+
const _STATE_FILE = 'db_state.json';
|
|
33
60
|
|
|
34
61
|
// Maps last_update key → [API endpoint, local filename]
|
|
35
62
|
const _DATASETS = {
|
|
@@ -42,202 +69,351 @@ const _DATASETS = {
|
|
|
42
69
|
measure_units: ['measure_unit/get/all', 'id_measure_unit.json'],
|
|
43
70
|
};
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
// Instance property holding each table
|
|
73
|
+
const _PROPS = {
|
|
74
|
+
versions: '_versions', types: '_types', brands: '_brands', filament_diameters: '_diameters',
|
|
75
|
+
filament_materials: '_materials', aspects: '_aspects', measure_units: '_units',
|
|
76
|
+
};
|
|
46
77
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* Tries the live TigerTag API first; falls back to the GitHub mirror.
|
|
50
|
-
* Only downloads files whose timestamp has changed.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} [dbPath] - Folder where JSON files are stored (created if missing).
|
|
53
|
-
* @param {object} [options]
|
|
54
|
-
* @param {boolean} [options.force=false] - Re-download all files even if up to date.
|
|
55
|
-
* @param {boolean} [options.verbose=true] - Print progress to stdout.
|
|
56
|
-
* @returns {Promise<string[]>} List of filenames that were downloaded/updated.
|
|
57
|
-
*/
|
|
58
|
-
async function syncDatabases(dbPath, { force = false, verbose = true } = {}) {
|
|
59
|
-
const resolvedPath = path.resolve(dbPath || _BUNDLED_DB_PATH);
|
|
60
|
-
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
78
|
+
const _BUNDLED_DB_PATH = BUNDLED_DIR;
|
|
79
|
+
const _backgroundChecks = new Map(); // dataDir → Promise (one automatic check per process)
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
function _readJson(file) {
|
|
82
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
83
|
+
}
|
|
63
84
|
|
|
64
|
-
|
|
85
|
+
function _writeJson(file, data) {
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
|
88
|
+
}
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
async function _getJson(url, fetchImpl, timeout) {
|
|
91
|
+
const f = fetchImpl || globalThis.fetch;
|
|
92
|
+
if (typeof f !== 'function') throw new Error('fetch() is not available (Node.js 18+ required)');
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
95
|
+
try {
|
|
96
|
+
const resp = await f(url, { signal: controller.signal });
|
|
97
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
98
|
+
return JSON.parse(await resp.text());
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
77
101
|
}
|
|
102
|
+
}
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
// One request for every table timestamp: TigerTag API first, GitHub mirror as fallback
|
|
105
|
+
async function _remoteLastUpdate(fetchImpl, timeout, log) {
|
|
81
106
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
datasetUrlFn = (endpoint, _filename) => `${_API_BASE}/${endpoint}`;
|
|
86
|
-
_log('[info] source: api');
|
|
107
|
+
const data = await _getJson(`${_API_BASE}/all/last_update`, fetchImpl, timeout);
|
|
108
|
+
log('[info] source: api');
|
|
109
|
+
return { data, source: 'api', url: (endpoint) => `${_API_BASE}/${endpoint}` };
|
|
87
110
|
} catch (exc) {
|
|
88
|
-
|
|
111
|
+
log(`[warn] TigerTag API unreachable (${exc.message}), falling back to GitHub mirror`);
|
|
89
112
|
try {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
datasetUrlFn = (_endpoint, filename) => `${_GITHUB_RAW_BASE}/${filename}`;
|
|
94
|
-
_log('[info] source: github');
|
|
113
|
+
const data = await _getJson(`${_GITHUB_RAW_BASE}/${_LAST_UPDATE}`, fetchImpl, timeout);
|
|
114
|
+
log('[info] source: github');
|
|
115
|
+
return { data, source: 'github', url: (_endpoint, filename) => `${_GITHUB_RAW_BASE}/${filename}` };
|
|
95
116
|
} catch (exc2) {
|
|
96
117
|
throw new Error(
|
|
97
|
-
`Both API and GitHub mirror are unreachable.\nAPI error: ${exc.message}\
|
|
118
|
+
`Both API and GitHub mirror are unreachable.\nAPI error: ${exc.message}\n`
|
|
119
|
+
+ `GitHub error: ${exc2.message}\nCheck your internet connection.`,
|
|
98
120
|
);
|
|
99
121
|
}
|
|
100
122
|
}
|
|
123
|
+
}
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Download the changed tables into `dir`.
|
|
127
|
+
* `current(key, localLastUpdate)` gives the timestamp already available locally
|
|
128
|
+
* (null = nothing), a table is downloaded when the remote one is newer (or `force`).
|
|
129
|
+
* Returns the list of written files (last_update.json included when rewritten).
|
|
130
|
+
*/
|
|
131
|
+
async function _updateTables(dir, { force = false, fetchImpl = null, timeout = _MANUAL_TIMEOUT, log = () => {}, current }) {
|
|
132
|
+
const remote = await _remoteLastUpdate(fetchImpl, timeout, log);
|
|
133
|
+
const lastUpdateFile = path.join(dir, _LAST_UPDATE);
|
|
134
|
+
const local = _readJson(lastUpdateFile) || {};
|
|
109
135
|
const updated = [];
|
|
110
|
-
|
|
111
136
|
for (const [key, [endpoint, filename]] of Object.entries(_DATASETS)) {
|
|
112
|
-
const remoteTs =
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
_log(`[ok] ${filename}: up to date`);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
_log(`[sync] ${filename}: ${localTs} → ${remoteTs}`);
|
|
127
|
-
const controller = new AbortController();
|
|
128
|
-
const timer = setTimeout(() => controller.abort(), _HTTP_TIMEOUT);
|
|
129
|
-
try {
|
|
130
|
-
const resp = await fetch(datasetUrlFn(endpoint, filename), { signal: controller.signal });
|
|
131
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
132
|
-
const data = await resp.json();
|
|
133
|
-
fs.writeFileSync(localFile, JSON.stringify(data, null, 2), 'utf8');
|
|
134
|
-
updated.push(filename);
|
|
135
|
-
} finally {
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
}
|
|
137
|
+
const remoteTs = remote.data[key];
|
|
138
|
+
if (remoteTs == null) { log(`[skip] ${key}: not in last_update payload`); continue; }
|
|
139
|
+
const have = current(key, local);
|
|
140
|
+
if (!force && have != null && have >= remoteTs) { log(`[ok] ${filename}: up to date`); continue; }
|
|
141
|
+
log(`[sync] ${filename}: ${have} → ${remoteTs}`);
|
|
142
|
+
const data = await _getJson(remote.url(endpoint, filename), fetchImpl, timeout);
|
|
143
|
+
if (!Array.isArray(data)) throw new Error(`${filename}: expected a JSON list`);
|
|
144
|
+
_writeJson(path.join(dir, filename), data);
|
|
145
|
+
local[key] = remoteTs;
|
|
146
|
+
updated.push(filename);
|
|
138
147
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!updated.includes('last_update.json')) updated.push('last_update.json');
|
|
148
|
+
if (updated.length) {
|
|
149
|
+
_writeJson(lastUpdateFile, local);
|
|
150
|
+
updated.push(_LAST_UPDATE);
|
|
143
151
|
}
|
|
152
|
+
return { updated, source: remote.source };
|
|
153
|
+
}
|
|
144
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Download or update the reference tables in one folder (e.g. a custom dbPath).
|
|
157
|
+
* Tries the live TigerTag API first; falls back to the GitHub mirror.
|
|
158
|
+
* Only downloads files whose timestamp has changed.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} [dbPath] - Folder where JSON files are stored (created if missing).
|
|
161
|
+
* Default: the data dir (see TigerTagDB).
|
|
162
|
+
* @param {object} [options]
|
|
163
|
+
* @param {boolean} [options.force=false] - Re-download all files even if up to date.
|
|
164
|
+
* @param {boolean} [options.verbose=true] - Print progress to stdout.
|
|
165
|
+
* @param {number} [options.timeout=30000] - Per-request timeout in ms.
|
|
166
|
+
* @param {Function} [options.fetch] - fetch implementation (default: global fetch).
|
|
167
|
+
* @returns {Promise<string[]>} List of filenames that were downloaded/updated.
|
|
168
|
+
* @throws {Error} When both the API and the GitHub mirror are unreachable.
|
|
169
|
+
*/
|
|
170
|
+
async function syncDatabases(dbPath, { force = false, verbose = true, timeout = _MANUAL_TIMEOUT, fetch = null } = {}) {
|
|
171
|
+
const dir = path.resolve(dbPath || defaultDataDir());
|
|
172
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
173
|
+
const log = verbose ? (msg) => console.log(msg) : () => {};
|
|
174
|
+
const { updated } = await _updateTables(dir, {
|
|
175
|
+
force, fetchImpl: fetch, timeout, log,
|
|
176
|
+
current: (key, local) => (fs.existsSync(path.join(dir, _DATASETS[key][1])) ? (local[key] ?? null) : null),
|
|
177
|
+
});
|
|
145
178
|
return updated;
|
|
146
179
|
}
|
|
147
180
|
|
|
148
181
|
/**
|
|
149
182
|
* Loads and exposes TigerTag JSON reference databases.
|
|
150
183
|
*
|
|
151
|
-
* Ships with bundled JSON files so the SDK works offline immediately after install
|
|
152
|
-
*
|
|
184
|
+
* Ships with bundled JSON files so the SDK works offline immediately after install,
|
|
185
|
+
* keeps a fresher downloaded copy in the data dir, and checks for updates once a day.
|
|
153
186
|
*
|
|
154
187
|
* All ID lookups return the full JSON entry object (or null if not found).
|
|
155
188
|
* The JSON files are the single source of truth — no hardcoded ID mappings.
|
|
156
189
|
*
|
|
157
190
|
* @example
|
|
158
|
-
* const db = new TigerTagDB();
|
|
191
|
+
* const db = new TigerTagDB(); // local data, background daily check
|
|
192
|
+
* const db = await TigerTagDB.open(); // waits for the daily check (5 s max)
|
|
193
|
+
* const db = new TigerTagDB({ offline: true }); // never touches the network
|
|
159
194
|
* const mat = db.material(38219);
|
|
160
195
|
* console.log(mat.label); // "PLA"
|
|
161
|
-
* console.log(mat.density); // 1.24
|
|
162
196
|
*/
|
|
163
197
|
class TigerTagDB {
|
|
164
198
|
/**
|
|
199
|
+
* Synchronous and network-free: loads the freshest LOCAL copy of every table.
|
|
200
|
+
* With autoUpdate on, a background daily check is started (see `ready`).
|
|
201
|
+
*
|
|
165
202
|
* @param {object} [options]
|
|
166
|
-
* @param {string}
|
|
167
|
-
*
|
|
168
|
-
* @param {
|
|
203
|
+
* @param {string} [options.dbPath] - Custom folder, used exclusively (missing file → Error,
|
|
204
|
+
* no fallback, no automatic network call).
|
|
205
|
+
* @param {string} [options.dataDir] - Folder for downloaded copies (default: TIGERTAG_DATA_DIR,
|
|
206
|
+
* TIGERTAG_CACHE_DIR, or the per-user cache folder).
|
|
207
|
+
* @param {boolean} [options.offline] - true = zero network calls (default: TIGERTAG_OFFLINE env).
|
|
208
|
+
* @param {boolean} [options.autoUpdate=true] - Automatic daily check (background in the constructor).
|
|
209
|
+
* @param {boolean} [options.autoSync] - Deprecated alias of autoUpdate.
|
|
210
|
+
* @param {number} [options.maxAge=1 day] - Minimum interval between two automatic checks, in ms.
|
|
211
|
+
* @param {boolean} [options.verbose=false] - Log update progress / errors.
|
|
212
|
+
* @param {Function} [options.fetch] - fetch implementation (default: global fetch).
|
|
213
|
+
* @throws {Error} When a custom dbPath is missing a table or a table cannot be parsed.
|
|
169
214
|
*/
|
|
170
|
-
constructor({
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this.
|
|
177
|
-
this.
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
215
|
+
constructor({
|
|
216
|
+
dbPath, dataDir, offline, autoUpdate, autoSync, maxAge = DEFAULT_UPDATE_MAX_AGE_MS,
|
|
217
|
+
verbose = false, fetch = null,
|
|
218
|
+
} = {}) {
|
|
219
|
+
this._custom = dbPath ? path.resolve(dbPath) : null;
|
|
220
|
+
this._path = this._custom || _BUNDLED_DB_PATH; // kept for backward compatibility
|
|
221
|
+
this._dataDir = path.resolve(dataDir || defaultDataDir());
|
|
222
|
+
this._offline = resolveOffline(offline);
|
|
223
|
+
this._autoUpdate = autoUpdate ?? autoSync ?? true;
|
|
224
|
+
this._maxAge = maxAge;
|
|
225
|
+
this._verbose = verbose;
|
|
226
|
+
this._fetch = fetch;
|
|
227
|
+
this._reloadAll();
|
|
228
|
+
/** Resolves (to this instance) when the background check started by the constructor is done. */
|
|
229
|
+
this.ready = Promise.resolve(this);
|
|
230
|
+
if (this._autoUpdate && this._canAutoUpdate()) {
|
|
231
|
+
let p = _backgroundChecks.get(this._dataDir);
|
|
232
|
+
if (!p) {
|
|
233
|
+
p = this._autoCheck();
|
|
234
|
+
_backgroundChecks.set(this._dataDir, p);
|
|
235
|
+
}
|
|
236
|
+
this.ready = p.then(() => { this._reloadAll(); return this; }, () => this);
|
|
237
|
+
}
|
|
182
238
|
}
|
|
183
239
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
240
|
+
/**
|
|
241
|
+
* Create a TigerTagDB and wait for the automatic daily check (at most once per
|
|
242
|
+
* maxAge, ~5 s timeout, never throws) before returning it.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} [options] - Same options as the constructor.
|
|
245
|
+
* @returns {Promise<TigerTagDB>}
|
|
246
|
+
*/
|
|
247
|
+
static async open(options = {}) {
|
|
248
|
+
const db = new TigerTagDB({ ...options, autoUpdate: false });
|
|
249
|
+
db._autoUpdate = options.autoUpdate ?? options.autoSync ?? true;
|
|
250
|
+
if (db._autoUpdate && db._canAutoUpdate()) {
|
|
251
|
+
await db._autoCheck();
|
|
252
|
+
db._reloadAll();
|
|
199
253
|
}
|
|
254
|
+
return db;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_canAutoUpdate() { return !this._offline && !this._custom; }
|
|
258
|
+
|
|
259
|
+
_log(msg) { if (this._verbose) console.log(msg); }
|
|
260
|
+
|
|
261
|
+
// Timestamp already available locally for a table (newest of data dir and bundled copy)
|
|
262
|
+
_currentTs(key, dataLastUpdate) {
|
|
263
|
+
const bundled = (_readJson(path.join(_BUNDLED_DB_PATH, _LAST_UPDATE)) || {})[key] ?? null;
|
|
264
|
+
const file = path.join(this._dataDir, _DATASETS[key][1]);
|
|
265
|
+
const dl = fs.existsSync(file) ? (dataLastUpdate[key] ?? null) : null;
|
|
266
|
+
if (dl == null) return bundled;
|
|
267
|
+
if (bundled == null) return dl;
|
|
268
|
+
return Math.max(dl, bundled);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_state() { return _readJson(path.join(this._dataDir, _STATE_FILE)) || {}; }
|
|
200
272
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
+ missing.map((fn) => ` - ${fn}`).join('\n')
|
|
204
|
-
+ '\n\n Call await db.sync() to download them.\n\n',
|
|
205
|
-
);
|
|
273
|
+
_saveState(patch) {
|
|
274
|
+
try { _writeJson(path.join(this._dataDir, _STATE_FILE), { ...this._state(), ...patch }); } catch { /* read-only */ }
|
|
206
275
|
}
|
|
207
276
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
277
|
+
// Automatic check: throttled by maxAge (1 h after a failure), never throws
|
|
278
|
+
async _autoCheck() {
|
|
279
|
+
const st = _readJson(path.join(this._dataDir, _STATE_FILE)) || {};
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
if (st.lastCheck && now - Date.parse(st.lastCheck) < this._maxAge) return [];
|
|
282
|
+
if (st.lastAttempt && st.lastError && now - Date.parse(st.lastAttempt) < Math.min(this._maxAge, _RETRY_AFTER_FAILURE_MS)) return [];
|
|
283
|
+
const at = new Date().toISOString();
|
|
211
284
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
285
|
+
const { updated, source } = await _updateTables(this._dataDir, {
|
|
286
|
+
fetchImpl: this._fetch, timeout: _AUTO_TIMEOUT, log: (m) => this._log(m),
|
|
287
|
+
current: (key, local) => this._currentTs(key, local),
|
|
288
|
+
});
|
|
289
|
+
this._saveState({ lastCheck: at, lastAttempt: at, lastError: null, source, lastUpdated: updated });
|
|
290
|
+
return updated;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
this._log(`[warn] automatic reference data check failed: ${err.message}`);
|
|
293
|
+
this._saveState({ lastAttempt: at, lastError: err.message });
|
|
214
294
|
return [];
|
|
215
295
|
}
|
|
216
296
|
}
|
|
217
297
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Check for new reference tables now and download the changed ones.
|
|
300
|
+
* Targets the custom dbPath when one is set, otherwise the data dir.
|
|
301
|
+
*
|
|
302
|
+
* @param {object} [options]
|
|
303
|
+
* @param {boolean} [options.force=false] - Re-download every table even if up to date.
|
|
304
|
+
* @param {boolean} [options.catalog=false] - Also check for a new product catalogue.
|
|
305
|
+
* @param {number} [options.timeout=30000] - Per-request timeout in ms.
|
|
306
|
+
* @returns {Promise<string[]>} Files written (tables, last_update.json, id_catalog.json).
|
|
307
|
+
* @throws {Error} When offline, or when the API and the GitHub mirror are both unreachable.
|
|
308
|
+
*/
|
|
309
|
+
async update({ force = false, catalog = false, timeout = _MANUAL_TIMEOUT } = {}) {
|
|
310
|
+
if (this._offline) {
|
|
311
|
+
throw new Error('TigerTagDB is offline (offline: true or TIGERTAG_OFFLINE=1): update() makes no network call.');
|
|
312
|
+
}
|
|
313
|
+
const dir = this._custom || this._dataDir;
|
|
314
|
+
const at = new Date().toISOString();
|
|
315
|
+
const { updated, source } = await _updateTables(dir, {
|
|
316
|
+
force, fetchImpl: this._fetch, timeout, log: (m) => this._log(m),
|
|
317
|
+
current: this._custom
|
|
318
|
+
? (key, local) => (fs.existsSync(path.join(dir, _DATASETS[key][1])) ? (local[key] ?? null) : null)
|
|
319
|
+
: (key, local) => (force ? null : this._currentTs(key, local)),
|
|
320
|
+
});
|
|
321
|
+
if (!this._custom) this._saveState({ lastCheck: at, lastAttempt: at, lastError: null, source, lastUpdated: updated });
|
|
322
|
+
if (catalog) {
|
|
323
|
+
const { refreshCatalog, catalogInfo } = require('./catalog');
|
|
324
|
+
const before = catalogInfo({ dataDir: this._dataDir }).fetchedAt;
|
|
325
|
+
await refreshCatalog({ dataDir: this._dataDir, fetch: this._fetch, offline: false });
|
|
326
|
+
if (catalogInfo({ dataDir: this._dataDir }).fetchedAt !== before) updated.push('id_catalog.json');
|
|
327
|
+
}
|
|
328
|
+
this._reloadAll();
|
|
329
|
+
return updated;
|
|
230
330
|
}
|
|
231
331
|
|
|
232
332
|
/**
|
|
233
|
-
*
|
|
333
|
+
* Deprecated alias of update(): check for new tables now.
|
|
234
334
|
* @param {boolean} [force=false] - Re-download all files even if up to date.
|
|
235
335
|
* @returns {Promise<string[]>} List of filenames that were downloaded/updated.
|
|
236
336
|
*/
|
|
237
337
|
async sync(force = false) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
338
|
+
return this.update({ force });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Resolve every table: custom folder exclusively, else newest of data dir / bundled
|
|
342
|
+
_resolve() {
|
|
343
|
+
const out = {};
|
|
344
|
+
if (this._custom) {
|
|
345
|
+
const lu = _readJson(path.join(this._custom, _LAST_UPDATE)) || {};
|
|
346
|
+
const missing = Object.values(_DATASETS).map(([, fn]) => fn)
|
|
347
|
+
.filter((fn) => !fs.existsSync(path.join(this._custom, fn)));
|
|
348
|
+
if (missing.length) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`TigerTag database folder ${this._custom} is missing: ${missing.join(', ')}. A custom dbPath is `
|
|
351
|
+
+ 'used exclusively (no fallback to the bundled copy). Copy the files there, or download them with '
|
|
352
|
+
+ `syncDatabases('${this._custom}') / tigertag update --db "${this._custom}".`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
for (const [key, [, fn]] of Object.entries(_DATASETS)) {
|
|
356
|
+
out[key] = { file: fn, source: 'custom', path: path.join(this._custom, fn), timestamp: lu[key] ?? null };
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
const bundledLu = _readJson(path.join(_BUNDLED_DB_PATH, _LAST_UPDATE)) || {};
|
|
361
|
+
const dataLu = _readJson(path.join(this._dataDir, _LAST_UPDATE)) || {};
|
|
362
|
+
for (const [key, [, fn]] of Object.entries(_DATASETS)) {
|
|
363
|
+
const dlFile = path.join(this._dataDir, fn);
|
|
364
|
+
const dlTs = fs.existsSync(dlFile) ? (dataLu[key] ?? null) : null;
|
|
365
|
+
const bTs = bundledLu[key] ?? null;
|
|
366
|
+
out[key] = dlTs != null && (bTs == null || dlTs > bTs)
|
|
367
|
+
? { file: fn, source: 'downloaded', path: dlFile, timestamp: dlTs }
|
|
368
|
+
: { file: fn, source: 'bundled', path: path.join(_BUNDLED_DB_PATH, fn), timestamp: bTs };
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_reloadAll() {
|
|
374
|
+
this._sources = this._resolve();
|
|
375
|
+
for (const [key, src] of Object.entries(this._sources)) {
|
|
376
|
+
let data = _readJson(src.path);
|
|
377
|
+
if (!Array.isArray(data) && src.source === 'downloaded') { // broken download → bundled copy
|
|
378
|
+
const fb = path.join(_BUNDLED_DB_PATH, src.file);
|
|
379
|
+
data = _readJson(fb);
|
|
380
|
+
this._sources[key] = { ...src, source: 'bundled', path: fb };
|
|
381
|
+
}
|
|
382
|
+
if (!Array.isArray(data)) {
|
|
383
|
+
if (src.source === 'custom') throw new Error(`TigerTag database file ${src.path} is not a valid JSON list.`);
|
|
384
|
+
data = [];
|
|
385
|
+
}
|
|
386
|
+
this[_PROPS[key]] = data;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Where every table comes from, and the update / catalogue status.
|
|
392
|
+
*
|
|
393
|
+
* @returns {{ offline: boolean, autoUpdate: boolean, maxAge: number, dataDir: string, customDir: string|null,
|
|
394
|
+
* lastCheck: string|null, lastAttempt: string|null, lastError: string|null,
|
|
395
|
+
* tables: Object<string, { file: string, source: 'custom'|'downloaded'|'bundled', path: string,
|
|
396
|
+
* timestamp: number|null }>, catalog: object }}
|
|
397
|
+
*/
|
|
398
|
+
info() {
|
|
399
|
+
const st = this._custom ? {} : this._state();
|
|
400
|
+
const { catalogInfo } = require('./catalog');
|
|
401
|
+
return {
|
|
402
|
+
offline: this._offline,
|
|
403
|
+
autoUpdate: this._autoUpdate,
|
|
404
|
+
maxAge: this._maxAge,
|
|
405
|
+
dataDir: this._dataDir,
|
|
406
|
+
customDir: this._custom,
|
|
407
|
+
lastCheck: st.lastCheck || null,
|
|
408
|
+
lastAttempt: st.lastAttempt || null,
|
|
409
|
+
lastError: st.lastError || null,
|
|
410
|
+
tables: JSON.parse(JSON.stringify(this._sources)),
|
|
411
|
+
catalog: catalogInfo({ dataDir: this._dataDir }),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static _find(table, idValue) {
|
|
416
|
+
return table.find((e) => e.id === idValue) || null;
|
|
241
417
|
}
|
|
242
418
|
|
|
243
419
|
/**
|
|
@@ -302,4 +478,4 @@ class TigerTagDB {
|
|
|
302
478
|
|
|
303
479
|
TigerTagDB.REQUIRED_FILES = Object.values(_DATASETS).map(([, fn]) => fn);
|
|
304
480
|
|
|
305
|
-
module.exports = { TigerTagDB, syncDatabases, _BUNDLED_DB_PATH };
|
|
481
|
+
module.exports = { TigerTagDB, syncDatabases, _BUNDLED_DB_PATH, DEFAULT_UPDATE_MAX_AGE_MS };
|
package/src/index.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* tigertag — JavaScript SDK for TigerTag RFID material identification.
|
|
25
25
|
*
|
|
26
26
|
* Spec : https://github.com/TigerTag-Project/TigerTag-RFID-Guide
|
|
27
|
-
* Protocol: TigerTag Open Source v2.
|
|
27
|
+
* Protocol: TigerTag Open Source v2.2
|
|
28
28
|
*
|
|
29
29
|
* Quick start:
|
|
30
30
|
* const { TigerTag } = require('tigertag');
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* console.log(tag.toRawDict()); // raw protocol fields
|
|
38
38
|
* console.log(tag.toDict()); // enriched with labels, hex colors, dates
|
|
39
39
|
* console.log(tag.verify()); // ECDSA signature result
|
|
40
|
+
*
|
|
41
|
+
* const plus = await TigerTag.fromCatalog(3527039449); // TigerTag+ from the catalogue
|
|
40
42
|
*/
|
|
41
43
|
|
|
42
44
|
const {
|
|
@@ -54,7 +56,11 @@ const {
|
|
|
54
56
|
} = require('./tag');
|
|
55
57
|
|
|
56
58
|
const { TigerTagDB, syncDatabases } = require('./db');
|
|
59
|
+
const { defaultDataDir } = require('./datadir');
|
|
57
60
|
const { SignatureResult, ecdsaRawToDer } = require('./signature');
|
|
61
|
+
const {
|
|
62
|
+
CATALOG_URL, loadCatalog, refreshCatalog, catalogInfo, catalogEntry, defaultCacheDir: catalogCacheDir,
|
|
63
|
+
} = require('./catalog');
|
|
58
64
|
|
|
59
65
|
module.exports = {
|
|
60
66
|
TigerTag,
|
|
@@ -62,7 +68,14 @@ module.exports = {
|
|
|
62
68
|
SignatureResult,
|
|
63
69
|
ApiDiff,
|
|
64
70
|
syncDatabases,
|
|
71
|
+
defaultDataDir,
|
|
65
72
|
ecdsaRawToDer,
|
|
73
|
+
loadCatalog,
|
|
74
|
+
refreshCatalog,
|
|
75
|
+
catalogInfo,
|
|
76
|
+
catalogEntry,
|
|
77
|
+
catalogCacheDir,
|
|
78
|
+
CATALOG_URL,
|
|
66
79
|
CHIP_DUMP_LEN,
|
|
67
80
|
FULL_DATA_LEN,
|
|
68
81
|
MIN_DATA_LEN,
|
package/src/signature.js
CHANGED
|
@@ -65,7 +65,7 @@ class SignatureResult {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
toString() {
|
|
68
|
-
const base = SignatureResult.
|
|
68
|
+
const base = SignatureResult._LABELS[this.status] || `? ${this.status}`;
|
|
69
69
|
return this.detail ? `${base} ${this.detail}` : base;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -85,13 +85,13 @@ SignatureResult.NO_CRYPTO = 'no_crypto';
|
|
|
85
85
|
SignatureResult.NO_KEY = 'no_key';
|
|
86
86
|
SignatureResult.NO_UID = 'no_uid';
|
|
87
87
|
|
|
88
|
-
SignatureResult.
|
|
89
|
-
valid: '
|
|
90
|
-
invalid: '
|
|
91
|
-
unsigned: '
|
|
92
|
-
no_crypto: '
|
|
93
|
-
no_key: '
|
|
94
|
-
no_uid: '
|
|
88
|
+
SignatureResult._LABELS = {
|
|
89
|
+
valid: 'VALID',
|
|
90
|
+
invalid: 'INVALID',
|
|
91
|
+
unsigned: 'NOT SIGNED',
|
|
92
|
+
no_crypto: 'NO CRYPTO — crypto not available',
|
|
93
|
+
no_key: 'NO PUBLIC KEY — public key not found in id_version.json',
|
|
94
|
+
no_uid: 'NO UID — UID unavailable, provide a full 180-byte chip dump',
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
/**
|