@nxtedition/lib 26.0.6 → 26.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cache.js +90 -38
  2. package/http.js +3 -1
  3. package/package.json +1 -1
package/cache.js CHANGED
@@ -1,7 +1,11 @@
1
+ import { DatabaseSync } from 'node:sqlite'
1
2
  import { LRUCache } from 'lru-cache'
3
+ import { fastNow } from './time.js'
4
+
5
+ function noop() {}
2
6
 
3
7
  export class AsyncCache {
4
- /** @type LRUCache<string, { expire: Number, value: any } **/
8
+ /** @type LRUCache<string, { ttl: number, stale: number, value: any } **/
5
9
  #lru
6
10
  #valueSelector
7
11
  #keySelector
@@ -9,9 +13,12 @@ export class AsyncCache {
9
13
  #ttl
10
14
  #stale
11
15
 
12
- constructor(location, valueSelector, keySelector, opts) {
13
- this.#lru = new LRUCache({ max: 4096 })
16
+ #db
17
+ #getQuery
18
+ #setQuery
19
+ #delQuery
14
20
 
21
+ constructor(location, valueSelector, keySelector, opts) {
15
22
  if (typeof location === 'string') {
16
23
  // Do nothing...
17
24
  } else {
@@ -47,6 +54,33 @@ export class AsyncCache {
47
54
  } else {
48
55
  throw new TypeError('stale must be a undefined, number or a function')
49
56
  }
57
+
58
+ this.#lru =
59
+ opts?.lru === false || opts?.lru === null ? null : new LRUCache({ max: 4096, ...opts?.lru })
60
+ this.#db = new DatabaseSync(location, { timeout: 20, ...opts?.db })
61
+ this.#db.exec(`
62
+ PRAGMA journal_mode = WAL;
63
+ PRAGMA synchronous = NORMAL;
64
+ PRAGMA temp_store = memory;
65
+ PRAGMA optimize;
66
+
67
+ CREATE TABLE IF NOT EXISTS cache (
68
+ key TEXT PRIMARY KEY NOT NULL,
69
+ val TEXT NOT NULL,
70
+ ttl INTEGER NOT NULL,
71
+ stale INTEGER NOT NULL
72
+ );
73
+ `)
74
+
75
+ this.#getQuery = this.#db.prepare(`SELECT val, ttl, stale FROM cache WHERE key = ?`)
76
+ this.#setQuery = this.#db.prepare(
77
+ `INSERT OR REPLACE INTO cache (key, val, ttl, stale) VALUES (?, ?, ?, ?)`,
78
+ )
79
+ this.#delQuery = this.#db.prepare(`DELETE FROM cache WHERE key = ?`)
80
+ }
81
+
82
+ close() {
83
+ this.#db?.close()
50
84
  }
51
85
 
52
86
  /**
@@ -60,54 +94,66 @@ export class AsyncCache {
60
94
  throw new TypeError('keySelector must return a non-empty string')
61
95
  }
62
96
 
63
- let cached = this.#lru.get(key)
97
+ const now = fastNow()
98
+
99
+ let cached = this.#lru?.get(key)
64
100
 
65
- if (cached) {
66
- const now = Date.now()
101
+ if (cached === undefined) {
102
+ try {
103
+ const ret = this.#getQuery?.get(key)
104
+ if (ret !== undefined) {
105
+ cached = {
106
+ ttl: ret.ttl,
107
+ stale: ret.stale,
108
+ value: JSON.parse(ret.val),
109
+ }
110
+ this.#lru?.set(key, cached)
111
+ }
112
+ } catch {
113
+ // Do nothing...
114
+ }
115
+ }
67
116
 
68
- if (now < cached.expire) {
117
+ if (cached !== undefined) {
118
+ if (now < cached.ttl) {
69
119
  return { value: cached.value, async: false }
70
120
  }
71
121
 
72
- if (now - cached.expire >= this.#stale(cached.value, key)) {
73
- // stale-while-revalidate has also expired, dont use cached value
74
- cached = null
122
+ if (now > cached.stale) {
123
+ // stale-while-revalidate has ttld, purge cached value.
124
+ this.#lru?.delete(key)
125
+ try {
126
+ this.#delQuery?.run(key)
127
+ } catch {
128
+ // Do nothing...
129
+ }
130
+
131
+ cached = undefined
75
132
  }
76
133
  }
77
134
 
78
135
  let promise
79
-
80
136
  if (this.#valueSelector) {
81
137
  promise = this.#dedupe.get(key)
82
- if (!promise) {
138
+ if (promise === undefined) {
83
139
  promise = this.#valueSelector(...args).then(
84
140
  (value) => {
85
141
  this.set(key, value)
86
- return [null, value]
142
+ return value
87
143
  },
88
144
  (err) => {
89
145
  this.delete(key)
90
- return [err, null]
146
+ throw err
91
147
  },
92
148
  )
149
+ promise.catch(noop)
93
150
  this.#dedupe.set(key, promise)
94
151
  }
95
152
  }
96
153
 
97
- if (cached) {
98
- return { value: cached.value, async: false }
99
- }
100
-
101
- return {
102
- value: promise.then(([err, val]) => {
103
- if (err) {
104
- throw err
105
- }
106
-
107
- return val
108
- }),
109
- async: true,
110
- }
154
+ return cached !== undefined
155
+ ? { value: cached.value, async: false }
156
+ : { value: promise, async: true }
111
157
  }
112
158
 
113
159
  /**
@@ -119,28 +165,34 @@ export class AsyncCache {
119
165
  throw new TypeError('key must be a non-empty string')
120
166
  }
121
167
 
168
+ const now = fastNow()
122
169
  const cached = {
123
- expire: Date.now() + this.#ttl(value, key),
170
+ ttl: now + this.#ttl(value, key),
171
+ stale: now + this.#stale(value, key),
124
172
  value,
125
173
  }
126
174
 
127
- this.#lru.set(key, cached)
175
+ this.#lru?.set(key, cached)
128
176
  this.#dedupe.delete(key)
177
+
178
+ try {
179
+ this.#setQuery?.run(key, JSON.stringify(value), cached.ttl, cached.stale)
180
+ } catch {
181
+ // Do nothing...
182
+ }
129
183
  }
130
184
 
131
- /**
132
- * @param {string} key
133
- */
134
185
  delete(key) {
135
186
  if (typeof key !== 'string' || key.length === 0) {
136
187
  throw new TypeError('key must be a non-empty string')
137
188
  }
138
189
 
139
- this.#lru.delete(key)
140
- this.#dedupe.delete(key)
141
- }
190
+ this.#lru?.delete(key)
142
191
 
143
- clear() {
144
- // TODO (fix): Implement...
192
+ try {
193
+ this.#delQuery?.run(key)
194
+ } catch {
195
+ // Do nothing...
196
+ }
145
197
  }
146
198
  }
package/http.js CHANGED
@@ -166,7 +166,9 @@ export async function upgradeMiddleware(ctx, next) {
166
166
  await thenable
167
167
  }
168
168
 
169
- assert(socket.destroyed || socket.writableEnded, 'stream not completed')
169
+ if (!socket.destroyed && !socket.writableEnded) {
170
+ throw new Error('Stream not completed')
171
+ }
170
172
 
171
173
  const elapsedTime = performance.now() - startTime
172
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "26.0.6",
3
+ "version": "26.0.8",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",