tigertag 1.0.6 → 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 +190 -3
- package/LICENSE +202 -0
- package/README.md +181 -47
- package/bin/tigertag.js +74 -23
- package/database/id_aspect.json +5 -0
- package/database/id_brand.json +996 -1
- package/database/id_catalog.json.gz +0 -0
- package/database/id_catalog.meta.json +9 -0
- package/database/id_material.json +2325 -1
- package/database/id_type.json +18 -1
- package/database/id_version.json +30 -1
- package/database/last_update.json +1 -1
- package/package.json +12 -3
- package/src/catalog.js +285 -0
- package/src/datadir.js +65 -0
- package/src/db.js +337 -148
- package/src/index.js +32 -6
- package/src/signature.js +26 -13
- package/src/tag.js +227 -15
- package/LICENSE.md +0 -21
package/src/db.js
CHANGED
|
@@ -1,22 +1,62 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// Copyright (C) 2025 TigerTag
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// TigerTag SDK
|
|
6
|
+
// Copyright (c) 2025-2026 TigerTag Corp.
|
|
7
|
+
//
|
|
8
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
// you may not use this file except in compliance with the License.
|
|
10
|
+
// You may obtain a copy of the License at
|
|
11
|
+
//
|
|
12
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
//
|
|
14
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
// See the License for the specific language governing permissions and
|
|
18
|
+
// limitations under the License.
|
|
19
|
+
//
|
|
20
|
+
// Implementing the TigerTag protocol requires no licence and no payment.
|
|
21
|
+
// https://github.com/TigerTag-Project/TigerTag-RFID-Guide/blob/main/LICENSING.md
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
|
-
* 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.
|
|
12
43
|
*/
|
|
13
44
|
|
|
14
45
|
const fs = require('fs');
|
|
15
46
|
const path = require('path');
|
|
47
|
+
const { defaultDataDir, resolveOffline, BUNDLED_DIR } = require('./datadir');
|
|
16
48
|
|
|
17
49
|
const _API_BASE = 'https://api.tigertag.io/api:tigertag';
|
|
18
50
|
const _GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/TigerTag-Project/TigerTag-RFID-Guide/main/database';
|
|
19
|
-
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';
|
|
20
60
|
|
|
21
61
|
// Maps last_update key → [API endpoint, local filename]
|
|
22
62
|
const _DATASETS = {
|
|
@@ -29,202 +69,351 @@ const _DATASETS = {
|
|
|
29
69
|
measure_units: ['measure_unit/get/all', 'id_measure_unit.json'],
|
|
30
70
|
};
|
|
31
71
|
|
|
32
|
-
|
|
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
|
+
};
|
|
33
77
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* Tries the live TigerTag API first; falls back to the GitHub mirror.
|
|
37
|
-
* Only downloads files whose timestamp has changed.
|
|
38
|
-
*
|
|
39
|
-
* @param {string} [dbPath] - Folder where JSON files are stored (created if missing).
|
|
40
|
-
* @param {object} [options]
|
|
41
|
-
* @param {boolean} [options.force=false] - Re-download all files even if up to date.
|
|
42
|
-
* @param {boolean} [options.verbose=true] - Print progress to stdout.
|
|
43
|
-
* @returns {Promise<string[]>} List of filenames that were downloaded/updated.
|
|
44
|
-
*/
|
|
45
|
-
async function syncDatabases(dbPath, { force = false, verbose = true } = {}) {
|
|
46
|
-
const resolvedPath = path.resolve(dbPath || _BUNDLED_DB_PATH);
|
|
47
|
-
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
78
|
+
const _BUNDLED_DB_PATH = BUNDLED_DIR;
|
|
79
|
+
const _backgroundChecks = new Map(); // dataDir → Promise (one automatic check per process)
|
|
48
80
|
|
|
49
|
-
|
|
81
|
+
function _readJson(file) {
|
|
82
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
83
|
+
}
|
|
50
84
|
|
|
51
|
-
|
|
85
|
+
function _writeJson(file, data) {
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
|
88
|
+
}
|
|
52
89
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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);
|
|
64
101
|
}
|
|
102
|
+
}
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
// One request for every table timestamp: TigerTag API first, GitHub mirror as fallback
|
|
105
|
+
async function _remoteLastUpdate(fetchImpl, timeout, log) {
|
|
68
106
|
try {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
datasetUrlFn = (endpoint, _filename) => `${_API_BASE}/${endpoint}`;
|
|
73
|
-
_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}` };
|
|
74
110
|
} catch (exc) {
|
|
75
|
-
|
|
111
|
+
log(`[warn] TigerTag API unreachable (${exc.message}), falling back to GitHub mirror`);
|
|
76
112
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
datasetUrlFn = (_endpoint, filename) => `${_GITHUB_RAW_BASE}/${filename}`;
|
|
81
|
-
_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}` };
|
|
82
116
|
} catch (exc2) {
|
|
83
117
|
throw new Error(
|
|
84
|
-
`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.`,
|
|
85
120
|
);
|
|
86
121
|
}
|
|
87
122
|
}
|
|
123
|
+
}
|
|
88
124
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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) || {};
|
|
96
135
|
const updated = [];
|
|
97
|
-
|
|
98
136
|
for (const [key, [endpoint, filename]] of Object.entries(_DATASETS)) {
|
|
99
|
-
const remoteTs =
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
_log(`[ok] ${filename}: up to date`);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
_log(`[sync] ${filename}: ${localTs} → ${remoteTs}`);
|
|
114
|
-
const controller = new AbortController();
|
|
115
|
-
const timer = setTimeout(() => controller.abort(), _HTTP_TIMEOUT);
|
|
116
|
-
try {
|
|
117
|
-
const resp = await fetch(datasetUrlFn(endpoint, filename), { signal: controller.signal });
|
|
118
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
119
|
-
const data = await resp.json();
|
|
120
|
-
fs.writeFileSync(localFile, JSON.stringify(data, null, 2), 'utf8');
|
|
121
|
-
updated.push(filename);
|
|
122
|
-
} finally {
|
|
123
|
-
clearTimeout(timer);
|
|
124
|
-
}
|
|
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);
|
|
125
147
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!updated.includes('last_update.json')) updated.push('last_update.json');
|
|
148
|
+
if (updated.length) {
|
|
149
|
+
_writeJson(lastUpdateFile, local);
|
|
150
|
+
updated.push(_LAST_UPDATE);
|
|
130
151
|
}
|
|
152
|
+
return { updated, source: remote.source };
|
|
153
|
+
}
|
|
131
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
|
+
});
|
|
132
178
|
return updated;
|
|
133
179
|
}
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
182
|
* Loads and exposes TigerTag JSON reference databases.
|
|
137
183
|
*
|
|
138
|
-
* Ships with bundled JSON files so the SDK works offline immediately after install
|
|
139
|
-
*
|
|
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.
|
|
140
186
|
*
|
|
141
187
|
* All ID lookups return the full JSON entry object (or null if not found).
|
|
142
188
|
* The JSON files are the single source of truth — no hardcoded ID mappings.
|
|
143
189
|
*
|
|
144
190
|
* @example
|
|
145
|
-
* 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
|
|
146
194
|
* const mat = db.material(38219);
|
|
147
195
|
* console.log(mat.label); // "PLA"
|
|
148
|
-
* console.log(mat.density); // 1.24
|
|
149
196
|
*/
|
|
150
197
|
class TigerTagDB {
|
|
151
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
|
+
*
|
|
152
202
|
* @param {object} [options]
|
|
153
|
-
* @param {string}
|
|
154
|
-
*
|
|
155
|
-
* @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.
|
|
156
214
|
*/
|
|
157
|
-
constructor({
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
165
|
-
this.
|
|
166
|
-
this.
|
|
167
|
-
this.
|
|
168
|
-
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
|
+
}
|
|
169
238
|
}
|
|
170
239
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
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();
|
|
186
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)) || {}; }
|
|
187
272
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
+ missing.map((fn) => ` - ${fn}`).join('\n')
|
|
191
|
-
+ '\n\n Call await db.sync() to download them.\n\n',
|
|
192
|
-
);
|
|
273
|
+
_saveState(patch) {
|
|
274
|
+
try { _writeJson(path.join(this._dataDir, _STATE_FILE), { ...this._state(), ...patch }); } catch { /* read-only */ }
|
|
193
275
|
}
|
|
194
276
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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();
|
|
198
284
|
try {
|
|
199
|
-
|
|
200
|
-
|
|
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 });
|
|
201
294
|
return [];
|
|
202
295
|
}
|
|
203
296
|
}
|
|
204
297
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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;
|
|
217
330
|
}
|
|
218
331
|
|
|
219
332
|
/**
|
|
220
|
-
*
|
|
333
|
+
* Deprecated alias of update(): check for new tables now.
|
|
221
334
|
* @param {boolean} [force=false] - Re-download all files even if up to date.
|
|
222
335
|
* @returns {Promise<string[]>} List of filenames that were downloaded/updated.
|
|
223
336
|
*/
|
|
224
337
|
async sync(force = false) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
228
417
|
}
|
|
229
418
|
|
|
230
419
|
/**
|
|
@@ -289,4 +478,4 @@ class TigerTagDB {
|
|
|
289
478
|
|
|
290
479
|
TigerTagDB.REQUIRED_FILES = Object.values(_DATASETS).map(([, fn]) => fn);
|
|
291
480
|
|
|
292
|
-
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
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
// Copyright (C) 2025 TigerTag
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// TigerTag SDK
|
|
6
|
+
// Copyright (c) 2025-2026 TigerTag Corp.
|
|
7
|
+
//
|
|
8
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
// you may not use this file except in compliance with the License.
|
|
10
|
+
// You may obtain a copy of the License at
|
|
11
|
+
//
|
|
12
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
//
|
|
14
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
// See the License for the specific language governing permissions and
|
|
18
|
+
// limitations under the License.
|
|
19
|
+
//
|
|
20
|
+
// Implementing the TigerTag protocol requires no licence and no payment.
|
|
21
|
+
// https://github.com/TigerTag-Project/TigerTag-RFID-Guide/blob/main/LICENSING.md
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
24
|
* tigertag — JavaScript SDK for TigerTag RFID material identification.
|
|
12
25
|
*
|
|
13
26
|
* Spec : https://github.com/TigerTag-Project/TigerTag-RFID-Guide
|
|
14
|
-
* Protocol: TigerTag Open Source v2.
|
|
27
|
+
* Protocol: TigerTag Open Source v2.2
|
|
15
28
|
*
|
|
16
29
|
* Quick start:
|
|
17
30
|
* const { TigerTag } = require('tigertag');
|
|
@@ -24,6 +37,8 @@
|
|
|
24
37
|
* console.log(tag.toRawDict()); // raw protocol fields
|
|
25
38
|
* console.log(tag.toDict()); // enriched with labels, hex colors, dates
|
|
26
39
|
* console.log(tag.verify()); // ECDSA signature result
|
|
40
|
+
*
|
|
41
|
+
* const plus = await TigerTag.fromCatalog(3527039449); // TigerTag+ from the catalogue
|
|
27
42
|
*/
|
|
28
43
|
|
|
29
44
|
const {
|
|
@@ -41,7 +56,11 @@ const {
|
|
|
41
56
|
} = require('./tag');
|
|
42
57
|
|
|
43
58
|
const { TigerTagDB, syncDatabases } = require('./db');
|
|
59
|
+
const { defaultDataDir } = require('./datadir');
|
|
44
60
|
const { SignatureResult, ecdsaRawToDer } = require('./signature');
|
|
61
|
+
const {
|
|
62
|
+
CATALOG_URL, loadCatalog, refreshCatalog, catalogInfo, catalogEntry, defaultCacheDir: catalogCacheDir,
|
|
63
|
+
} = require('./catalog');
|
|
45
64
|
|
|
46
65
|
module.exports = {
|
|
47
66
|
TigerTag,
|
|
@@ -49,7 +68,14 @@ module.exports = {
|
|
|
49
68
|
SignatureResult,
|
|
50
69
|
ApiDiff,
|
|
51
70
|
syncDatabases,
|
|
71
|
+
defaultDataDir,
|
|
52
72
|
ecdsaRawToDer,
|
|
73
|
+
loadCatalog,
|
|
74
|
+
refreshCatalog,
|
|
75
|
+
catalogInfo,
|
|
76
|
+
catalogEntry,
|
|
77
|
+
catalogCacheDir,
|
|
78
|
+
CATALOG_URL,
|
|
53
79
|
CHIP_DUMP_LEN,
|
|
54
80
|
FULL_DATA_LEN,
|
|
55
81
|
MIN_DATA_LEN,
|