@nuxodin/item 0.6.18 → 0.6.20
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/package.json
CHANGED
package/src/AsyncDataPoint.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
set: value => fetch('https://example.com/todos/1', {method: 'PUT', body: JSON.stringify(value)}).then(res => res.json())
|
|
6
6
|
});
|
|
7
7
|
datapoint.set({title: 'foo', completed: true}); // Returns promise - or undefined if value is already the same - do not rely on return value
|
|
8
|
+
// a write overtaken by a newer one settles as that one does: "this value or a newer one is saved"
|
|
8
9
|
datapoint.get().then(value => console.log(value));
|
|
9
10
|
|
|
10
11
|
// API
|
|
11
12
|
datapoint.onchange = ({value}) => console.log('value changed', value);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// options, passed to the constructor (or changed in datapoint.options):
|
|
14
|
+
// ttl: 5000 (cache in ms), debounceMs: 5 (setter delay), getRetry / setRetry, getRetryDelay / setRetryDelay,
|
|
15
|
+
// optimistic: true (until the sending is done, the value is the new one, though the server may fail)
|
|
15
16
|
datapoint.setLocal({title: 'foo', completed: true}); // set value without saving it to the master (the value comes from the master through an other channel / trusted source)
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
createSetter;
|
|
22
23
|
#expectedValue = null; // value while saving
|
|
23
24
|
#setter = null; // current setter promise
|
|
25
|
+
#writing = Promise.resolve(); // the write running, the next one starts after it
|
|
24
26
|
#getter = null; // current/cached getter promise
|
|
25
27
|
#getterExpiry = Infinity; // ttl is checked lazily on access — no timer per cache entry
|
|
26
28
|
// expired getters count as gone (lazy ttl)
|
|
@@ -62,11 +64,15 @@
|
|
|
62
64
|
#createSetter(value) {
|
|
63
65
|
const {setRetry, setRetryDelay, debounceMs} = this.options;
|
|
64
66
|
const promise = abortablePromise((resolve, reject) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const {signal} = promise.controller;
|
|
68
|
+
// one write at a time: a slow one landing last would leave the remote with an old value
|
|
69
|
+
const run = this.#writing.then(() => signal.aborted || retryAsync(
|
|
70
|
+
() => this.createSetter(value, signal),
|
|
67
71
|
setRetry,
|
|
68
72
|
setRetryDelay
|
|
69
|
-
)
|
|
73
|
+
));
|
|
74
|
+
this.#writing = run.catch(() => {});
|
|
75
|
+
run.then(resolve, reject);
|
|
70
76
|
}, debounceMs);
|
|
71
77
|
|
|
72
78
|
makePromiseTransparent(promise);
|
|
@@ -83,18 +89,12 @@
|
|
|
83
89
|
promise.then(
|
|
84
90
|
value => {
|
|
85
91
|
if (this.#getter !== promise) return;
|
|
86
|
-
if (!isEqual(oldValue, value)) {
|
|
87
|
-
try {
|
|
88
|
-
this.onchange?.({value, oldValue});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error('[item.js] AsyncDataPoint onchange error:', err);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
92
|
+
if (!isEqual(oldValue, value)) this.#notify({value, oldValue});
|
|
93
93
|
},
|
|
94
94
|
error => {
|
|
95
95
|
if (this.#getter !== promise) return;
|
|
96
96
|
if (error?.name === 'AbortError') return;
|
|
97
|
-
this
|
|
97
|
+
this.#notify({error, oldValue});
|
|
98
98
|
}
|
|
99
99
|
);
|
|
100
100
|
|
|
@@ -103,6 +103,11 @@
|
|
|
103
103
|
this.#getterExpiry = duration > 0 ? Date.now() + duration : Infinity;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
#notify(event) {
|
|
107
|
+
try { this.onchange?.(event); }
|
|
108
|
+
catch (err) { console.error('[item.js] AsyncDataPoint onchange error:', err); }
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
setLocal(value) { this.#cacheGetter(transparentPromiseResolve(value)); }
|
|
107
112
|
setFromPromise(promise) { this.#cacheGetter(makePromiseTransparent(promise)); }
|
|
108
113
|
get isPending() { return this.#cachedGetter?.state === 'pending' || this.#setter?.state === 'pending'; }
|
|
@@ -124,18 +129,18 @@
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
set(value) {
|
|
132
|
+
const previous = this.#setter, writing = previous?.state === 'pending';
|
|
127
133
|
// Already setting this value
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
this.#setter?.controller.abort(); // abort previous setter
|
|
134
|
+
if (writing && isEqual(this.#expectedValue, value)) return previous;
|
|
131
135
|
|
|
132
|
-
// ignore if latest getter value is the same
|
|
133
136
|
// Value already set (no-op). Returns undefined to avoid falsy issues (value could be 0, false, etc.)
|
|
137
|
+
// While a write runs the loaded value is outdated: setting it back is a change.
|
|
134
138
|
const cached = this.#cachedGetter;
|
|
135
|
-
if (cached?.state === 'fulfilled' && isEqual(cached.value, value)) return;
|
|
139
|
+
if (!writing && cached?.state === 'fulfilled' && isEqual(cached.value, value)) return;
|
|
136
140
|
|
|
137
141
|
this.#expectedValue = value;
|
|
138
142
|
const promise = this.#createSetter(value);
|
|
143
|
+
previous?.controller.abort(promise); // the previous one settles as this one does
|
|
139
144
|
|
|
140
145
|
this.onpending?.();
|
|
141
146
|
|
|
@@ -146,13 +151,12 @@
|
|
|
146
151
|
this.#setter = null;
|
|
147
152
|
this.#expectedValue = null;
|
|
148
153
|
},
|
|
149
|
-
error => {
|
|
154
|
+
error => { // the failed setter stays: lastError reports it until the next write
|
|
150
155
|
if (this.#setter !== promise) return;
|
|
151
156
|
if (error?.name === 'AbortError') return;
|
|
152
157
|
this.#getter = null;
|
|
153
|
-
this.#setter = null;
|
|
154
158
|
this.#expectedValue = null;
|
|
155
|
-
this
|
|
159
|
+
this.#notify({ error }); }
|
|
156
160
|
);
|
|
157
161
|
|
|
158
162
|
return this.#setter = promise;
|
|
@@ -223,18 +227,21 @@
|
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
/**
|
|
226
|
-
* Returns a delayed Promise that can be aborted
|
|
227
|
-
*
|
|
228
|
-
* @param {
|
|
230
|
+
* Returns a delayed Promise that can be aborted. Aborted with a promise as reason, it settles as
|
|
231
|
+
* that one does (a newer write taking over), else it rejects with the AbortError.
|
|
232
|
+
* @param {Function} fn - Called with (resolve, reject) after the delay.
|
|
233
|
+
* @param {Number} [ms=1] - The time in milliseconds to delay before calling fn.
|
|
229
234
|
* @returns {Promise<any>} - A delayed Promise that can be aborted.
|
|
230
235
|
*/
|
|
231
236
|
function abortablePromise(fn, ms=1) { // delayed and therfore abortable within ms
|
|
232
237
|
const controller = new AbortController();
|
|
238
|
+
const {signal} = controller;
|
|
233
239
|
const promise = new Promise((resolve, reject) => {
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
}
|
|
240
|
+
const settle = (ok) => (v) => {
|
|
241
|
+
if (!signal.aborted) return ok(v);
|
|
242
|
+
signal.reason?.then ? resolve(signal.reason) : reject(signal.reason);
|
|
243
|
+
};
|
|
244
|
+
setTimeout(() => signal.aborted ? settle()() : fn(settle(resolve), settle(reject)), ms);
|
|
238
245
|
});
|
|
239
246
|
promise.controller = controller;
|
|
240
247
|
return promise;
|
|
@@ -40,10 +40,7 @@ function secondaryIndexKind(prop) {
|
|
|
40
40
|
|
|
41
41
|
function usableIndexKind(table, name, prop) {
|
|
42
42
|
const kind = secondaryIndexKind(prop);
|
|
43
|
-
|
|
44
|
-
if (!issue) return kind;
|
|
45
|
-
console.warn(issue);
|
|
46
|
-
return null;
|
|
43
|
+
return kind && !indexIssue(table, name, prop, kind) ? kind : null;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
function indexIssue(table, name, prop, kind) {
|
|
@@ -109,6 +106,7 @@ export async function schemaToDb(
|
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
const stmts = [];
|
|
109
|
+
const warnings = [];
|
|
112
110
|
const nextTables = Object.keys(schema.properties ?? {});
|
|
113
111
|
const currTables = Object.keys(current.properties ?? {});
|
|
114
112
|
|
|
@@ -117,6 +115,11 @@ export async function schemaToDb(
|
|
|
117
115
|
const { fields, required } = tableData(schema.properties[table]);
|
|
118
116
|
const primaries = primaryFields(fields);
|
|
119
117
|
for (const p of primaries) required.add(p); // primary key columns are always NOT NULL
|
|
118
|
+
for (const [n, f] of fields) {
|
|
119
|
+
const kind = secondaryIndexKind(f);
|
|
120
|
+
const issue = kind && indexIssue(table, n, f, kind);
|
|
121
|
+
if (issue) warnings.push(issue); // the index is left out
|
|
122
|
+
}
|
|
120
123
|
|
|
121
124
|
if (!currTables.includes(table)) {
|
|
122
125
|
const cols = fields.map(([n, f]) =>
|
|
@@ -196,11 +199,8 @@ export async function schemaToDb(
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
for (const stmt of stmts)
|
|
200
|
-
|
|
201
|
-
await query(stmt);
|
|
202
|
-
}
|
|
203
|
-
return { diffs, executed: stmts };
|
|
202
|
+
for (const stmt of stmts) await query(stmt);
|
|
203
|
+
return { diffs, executed: stmts, warnings };
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
async function indexStatements(query, table, nextFields, currFields, { patch = false } = {}) {
|
|
@@ -32,13 +32,11 @@ function preserveDefault(prop, currProp) {
|
|
|
32
32
|
: { ...prop, default: currProp.default }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function secondaryIndexKind(
|
|
35
|
+
function secondaryIndexKind(prop) {
|
|
36
36
|
// hnsw needs a fixed length and an operator class; the application builds vector indexes
|
|
37
37
|
if (prop['x-vector']) return null
|
|
38
38
|
if (prop['x-index'] === 'unique') return 'UNIQUE INDEX'
|
|
39
39
|
if (prop['x-index'] === true) return 'INDEX'
|
|
40
|
-
if (prop['x-index'] === 'fulltext')
|
|
41
|
-
console.warn(`Skip FULLTEXT INDEX ${table}.${name}: PostgreSQL fulltext requires an expression index`)
|
|
42
40
|
return null
|
|
43
41
|
}
|
|
44
42
|
|
|
@@ -114,6 +112,7 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
const stmts = []
|
|
115
|
+
const warnings = []
|
|
117
116
|
const nextTables = Object.keys(schema.properties ?? {})
|
|
118
117
|
const currTables = Object.keys(current.properties ?? {})
|
|
119
118
|
|
|
@@ -122,6 +121,10 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
122
121
|
const { fields, required } = tableData(schema.properties[table])
|
|
123
122
|
const primaries = primaryFields(fields)
|
|
124
123
|
for (const p of primaries) required.add(p)
|
|
124
|
+
for (const [n, f] of fields) {
|
|
125
|
+
if (f['x-index'] === 'fulltext' && !f['x-vector'])
|
|
126
|
+
warnings.push(`Skip FULLTEXT INDEX ${table}.${n}: PostgreSQL fulltext requires an expression index`)
|
|
127
|
+
}
|
|
125
128
|
|
|
126
129
|
if (!currTables.includes(table)) {
|
|
127
130
|
const cols = fields.map(([n, f]) =>
|
|
@@ -135,7 +138,7 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
135
138
|
for (const [n, f] of fields) if (f['$comment']) stmts.push(commentStmt(table, n, f['$comment']))
|
|
136
139
|
for (const [n, f] of fields) {
|
|
137
140
|
if (primarySet.has(n)) continue
|
|
138
|
-
const kind = secondaryIndexKind(
|
|
141
|
+
const kind = secondaryIndexKind(f)
|
|
139
142
|
if (kind) stmts.push(createIndex(table, n, kind))
|
|
140
143
|
}
|
|
141
144
|
} else {
|
|
@@ -177,11 +180,8 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
177
180
|
const vectors = nextTables.some(t => tableData(schema.properties[t]).fields.some(([, f]) => f['x-vector']))
|
|
178
181
|
if (vectors && stmts.length) stmts.unshift('CREATE EXTENSION IF NOT EXISTS vector;')
|
|
179
182
|
|
|
180
|
-
for (const stmt of stmts)
|
|
181
|
-
|
|
182
|
-
await query(stmt)
|
|
183
|
-
}
|
|
184
|
-
return { diffs, executed: stmts }
|
|
183
|
+
for (const stmt of stmts) await query(stmt)
|
|
184
|
+
return { diffs, executed: stmts, warnings }
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
async function indexStatements(query, table, nextFields, currFields, { patch = false } = {}) {
|
|
@@ -198,7 +198,7 @@ async function indexStatements(query, table, nextFields, currFields, { patch = f
|
|
|
198
198
|
|
|
199
199
|
for (const [name, prop] of nextFields) {
|
|
200
200
|
if (nextPrimary.has(name)) continue
|
|
201
|
-
const nextKind = secondaryIndexKind(
|
|
201
|
+
const nextKind = secondaryIndexKind(prop)
|
|
202
202
|
const curr = indexes.secondary.find(i => i.column === name)
|
|
203
203
|
const currKind = curr ? curr.unique ? 'UNIQUE INDEX' : 'INDEX' : null
|
|
204
204
|
const changed = curr && currKind !== nextKind
|
|
@@ -209,7 +209,7 @@ async function indexStatements(query, table, nextFields, currFields, { patch = f
|
|
|
209
209
|
|
|
210
210
|
if (!patch) {
|
|
211
211
|
for (const curr of indexes.secondary) {
|
|
212
|
-
const wanted = nextFields.find(([name, prop]) => name === curr.column && secondaryIndexKind(
|
|
212
|
+
const wanted = nextFields.find(([name, prop]) => name === curr.column && secondaryIndexKind(prop))
|
|
213
213
|
if (!wanted) before.push(`DROP INDEX ${quoteId(curr.key)};`)
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -34,8 +34,6 @@ function createBody(table, fields, primaries) {
|
|
|
34
34
|
const solo = soloIntPrimary(fields, primaries)
|
|
35
35
|
const cols = fields.map(([n, f]) => {
|
|
36
36
|
const isPk = n === solo
|
|
37
|
-
if (f['x-autoincrement'] && !isPk)
|
|
38
|
-
console.warn(`Skip AUTOINCREMENT ${table}.${n}: SQLite auto-increments only a sole INTEGER PRIMARY KEY — nothing fills this column`)
|
|
39
37
|
// Only `INTEGER PRIMARY KEY` is the rowid alias, and AUTOINCREMENT is legal nowhere else —
|
|
40
38
|
// so a boolean key keeps the integer spelling, which SQLite would otherwise reject.
|
|
41
39
|
let def = toFieldDef(n, isPk ? { ...f, type: 'integer' } : f)
|
|
@@ -50,11 +48,17 @@ function indexKind(table, name, prop) {
|
|
|
50
48
|
if (prop['x-vector']) return null // sqlite-vec scans; a B-tree over vectors helps nothing
|
|
51
49
|
if (prop['x-index'] === 'unique') return 'UNIQUE INDEX'
|
|
52
50
|
if (prop['x-index'] === true) return 'INDEX'
|
|
53
|
-
if (prop['x-index'] === 'fulltext')
|
|
54
|
-
console.warn(`Skip FULLTEXT INDEX ${table}.${name}: SQLite fulltext requires a virtual FTS table`)
|
|
55
51
|
return null
|
|
56
52
|
}
|
|
57
53
|
|
|
54
|
+
/** What the schema asks of a field that SQLite cannot do; the field is created without it. */
|
|
55
|
+
function fieldWarning(table, name, prop, solo) {
|
|
56
|
+
if (prop['x-autoincrement'] && name !== solo)
|
|
57
|
+
return `Skip AUTOINCREMENT ${table}.${name}: SQLite auto-increments only a sole INTEGER PRIMARY KEY — nothing fills this column`
|
|
58
|
+
if (prop['x-index'] === 'fulltext' && !prop['x-vector'])
|
|
59
|
+
return `Skip FULLTEXT INDEX ${table}.${name}: SQLite fulltext requires a virtual FTS table`
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
function createIndex(table, name, kind) {
|
|
59
63
|
return `CREATE ${kind} ${quoteId(indexName(table, name))} ON ${quoteId(table)} (${quoteId(name)});`
|
|
60
64
|
}
|
|
@@ -94,12 +98,18 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
const stmts = []
|
|
101
|
+
const warnings = []
|
|
97
102
|
const nextTables = Object.keys(schema.properties ?? {})
|
|
98
103
|
const currTables = Object.keys(current.properties ?? {})
|
|
99
104
|
|
|
100
105
|
for (const table of nextTables) {
|
|
101
106
|
const fields = tableFields(schema.properties[table])
|
|
102
107
|
const primaries = fields.filter(([, f]) => f['x-index'] === 'primary').map(([n]) => n)
|
|
108
|
+
const solo = soloIntPrimary(fields, primaries)
|
|
109
|
+
for (const [n, f] of fields) {
|
|
110
|
+
const warning = fieldWarning(table, n, f, solo)
|
|
111
|
+
if (warning) warnings.push(warning)
|
|
112
|
+
}
|
|
103
113
|
|
|
104
114
|
if (!currTables.includes(table)) {
|
|
105
115
|
// Idea: WITHOUT ROWID when the primary key is not a single integer column and all other columns
|
|
@@ -144,9 +154,6 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
144
154
|
}
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
for (const stmt of stmts)
|
|
148
|
-
|
|
149
|
-
await query(stmt)
|
|
150
|
-
}
|
|
151
|
-
return { diffs, executed: stmts }
|
|
157
|
+
for (const stmt of stmts) await query(stmt)
|
|
158
|
+
return { diffs, executed: stmts, warnings }
|
|
152
159
|
}
|