@nxtedition/lib 26.0.5 → 26.0.7

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 +79 -42
  2. package/http.js +1 -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,32 @@ 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 = new LRUCache({ max: 4096, ...opts?.lru })
59
+ this.#db = new DatabaseSync(location, { timeout: 20, ...opts?.db })
60
+ this.#db.exec(`
61
+ PRAGMA journal_mode = WAL;
62
+ PRAGMA synchronous = NORMAL;
63
+ PRAGMA temp_store = memory;
64
+ PRAGMA optimize;
65
+
66
+ CREATE TABLE IF NOT EXISTS cache (
67
+ key TEXT PRIMARY KEY NOT NULL,
68
+ val TEXT NOT NULL,
69
+ ttl INTEGER NOT NULL,
70
+ stale INTEGER NOT NULL
71
+ );
72
+ `)
73
+
74
+ this.#getQuery = this.#db.prepare(`SELECT val, ttl, stale FROM cache WHERE key = ?`)
75
+ this.#setQuery = this.#db.prepare(
76
+ `INSERT OR REPLACE INTO cache (key, val, ttl, stale) VALUES (?, ?, ?, ?)`,
77
+ )
78
+ this.#delQuery = this.#db.prepare(`DELETE FROM cache WHERE key = ?`)
79
+ }
80
+
81
+ close() {
82
+ this.#db?.close()
50
83
  }
51
84
 
52
85
  /**
@@ -60,54 +93,66 @@ export class AsyncCache {
60
93
  throw new TypeError('keySelector must return a non-empty string')
61
94
  }
62
95
 
96
+ const now = fastNow()
97
+
63
98
  let cached = this.#lru.get(key)
64
99
 
65
- if (cached) {
66
- const now = Date.now()
100
+ if (cached === undefined) {
101
+ try {
102
+ const ret = this.#getQuery?.get(key)
103
+ if (ret !== undefined) {
104
+ cached = {
105
+ ttl: ret.ttl,
106
+ stale: ret.stale,
107
+ value: JSON.parse(ret.val),
108
+ }
109
+ this.#lru.set(key, cached)
110
+ }
111
+ } catch {
112
+ // Do nothing...
113
+ }
114
+ }
67
115
 
68
- if (now < cached.expire) {
116
+ if (cached !== undefined) {
117
+ if (now < cached.ttl) {
69
118
  return { value: cached.value, async: false }
70
119
  }
71
120
 
72
- if (now - cached.expire >= this.#stale(cached.value, key)) {
73
- // stale-while-revalidate has also expired, dont use cached value
74
- cached = null
121
+ if (now > cached.stale) {
122
+ // stale-while-revalidate has ttld, purge cached value.
123
+ this.#lru.delete(key)
124
+ try {
125
+ this.#delQuery.run(key)
126
+ } catch {
127
+ // Do nothing...
128
+ }
129
+
130
+ cached = undefined
75
131
  }
76
132
  }
77
133
 
78
134
  let promise
79
-
80
135
  if (this.#valueSelector) {
81
136
  promise = this.#dedupe.get(key)
82
- if (!promise) {
137
+ if (promise === undefined) {
83
138
  promise = this.#valueSelector(...args).then(
84
139
  (value) => {
85
140
  this.set(key, value)
86
- return [null, value]
141
+ return value
87
142
  },
88
143
  (err) => {
89
144
  this.delete(key)
90
- return [err, null]
145
+ throw err
91
146
  },
92
147
  )
148
+ promise.catch(noop)
93
149
  this.#dedupe.set(key, promise)
94
150
  }
95
151
  }
96
152
 
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
- }
153
+ return cached !== undefined
154
+ ? { value: cached.value, async: false }
155
+ : { value: promise, async: true }
111
156
  }
112
157
 
113
158
  /**
@@ -119,28 +164,20 @@ export class AsyncCache {
119
164
  throw new TypeError('key must be a non-empty string')
120
165
  }
121
166
 
167
+ const now = fastNow()
122
168
  const cached = {
123
- expire: Date.now() + this.#ttl(value, key),
169
+ ttl: now + this.#ttl(value, key),
170
+ stale: now + this.#stale(value, key),
124
171
  value,
125
172
  }
126
173
 
127
174
  this.#lru.set(key, cached)
128
175
  this.#dedupe.delete(key)
129
- }
130
176
 
131
- /**
132
- * @param {string} key
133
- */
134
- delete(key) {
135
- if (typeof key !== 'string' || key.length === 0) {
136
- throw new TypeError('key must be a non-empty string')
177
+ try {
178
+ this.#setQuery?.run(key, JSON.stringify(value), cached.ttl, cached.stale)
179
+ } catch {
180
+ // Do nothing...
137
181
  }
138
-
139
- this.#lru.delete(key)
140
- this.#dedupe.delete(key)
141
- }
142
-
143
- clear() {
144
- // TODO (fix): Implement...
145
182
  }
146
183
  }
package/http.js CHANGED
@@ -249,7 +249,7 @@ export async function requestMiddleware(ctx, next) {
249
249
  await thenable
250
250
  }
251
251
 
252
- if (!res.destroyed || !res.writableEnded) {
252
+ if (!res.destroyed && !res.writableEnded) {
253
253
  throw new Error('Response not completed')
254
254
  }
255
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "26.0.5",
3
+ "version": "26.0.7",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",