@arcaelas/whatsapp 4.5.0 → 6.0.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/README.md +282 -239
- package/build/cjs/decorators.js +0 -1
- package/build/cjs/index.d.ts +6 -4
- package/build/cjs/index.js +22 -3
- package/build/cjs/lib/bot/decorator.d.ts +2 -0
- package/build/cjs/lib/bot/decorator.js +6 -1
- package/build/cjs/lib/bot/decorators.d.ts +2 -2
- package/build/cjs/lib/bot/decorators.js +4 -4
- package/build/cjs/lib/bot/index.js +0 -1
- package/build/cjs/lib/chat/index.d.ts +381 -204
- package/build/cjs/lib/chat/index.js +243 -212
- package/build/cjs/lib/contact/index.d.ts +162 -179
- package/build/cjs/lib/contact/index.js +96 -191
- package/build/cjs/lib/internal.d.ts +40 -0
- package/build/cjs/lib/internal.js +38 -0
- package/build/cjs/lib/message/index.d.ts +468 -289
- package/build/cjs/lib/message/index.js +871 -743
- package/build/cjs/lib/status/index.d.ts +60 -70
- package/build/cjs/lib/status/index.js +106 -112
- package/build/cjs/lib/store/engine/index.d.ts +35 -7
- package/build/cjs/lib/store/engine/index.js +6 -4
- package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +48 -8
- package/build/cjs/lib/store/engine/lib/file_system/index.js +117 -66
- package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
- package/build/cjs/lib/store/engine/lib/index.js +212 -0
- package/build/cjs/lib/store/engine/lib/redis/index.d.ts +44 -15
- package/build/cjs/lib/store/engine/lib/redis/index.js +75 -43
- package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
- package/build/cjs/lib/store/engine/lib/s3/index.js +112 -65
- package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +110 -0
- package/build/cjs/lib/store/engine/lib/sqlite/index.js +132 -0
- package/build/cjs/lib/store/index.d.ts +7 -3
- package/build/cjs/lib/store/index.js +14 -5
- package/build/cjs/lib/whatsapp/index.d.ts +106 -68
- package/build/cjs/lib/whatsapp/index.js +393 -167
- package/build/esm/decorators.js +0 -1
- package/build/esm/index.d.ts +6 -4
- package/build/esm/index.js +5 -3
- package/build/esm/lib/bot/decorator.d.ts +2 -0
- package/build/esm/lib/bot/decorator.js +6 -1
- package/build/esm/lib/bot/decorators.d.ts +2 -2
- package/build/esm/lib/bot/decorators.js +4 -4
- package/build/esm/lib/bot/index.js +0 -1
- package/build/esm/lib/chat/index.d.ts +381 -204
- package/build/esm/lib/chat/index.js +243 -212
- package/build/esm/lib/contact/index.d.ts +162 -179
- package/build/esm/lib/contact/index.js +96 -190
- package/build/esm/lib/internal.d.ts +40 -0
- package/build/esm/lib/internal.js +34 -0
- package/build/esm/lib/message/index.d.ts +468 -289
- package/build/esm/lib/message/index.js +866 -738
- package/build/esm/lib/status/index.d.ts +60 -70
- package/build/esm/lib/status/index.js +105 -111
- package/build/esm/lib/store/engine/index.d.ts +35 -7
- package/build/esm/lib/store/engine/index.js +4 -3
- package/build/esm/lib/store/engine/lib/file_system/index.d.ts +48 -8
- package/build/esm/lib/store/engine/lib/file_system/index.js +117 -66
- package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
- package/build/esm/lib/store/engine/lib/index.js +205 -0
- package/build/esm/lib/store/engine/lib/redis/index.d.ts +44 -15
- package/build/esm/lib/store/engine/lib/redis/index.js +73 -41
- package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
- package/build/esm/lib/store/engine/lib/s3/index.js +111 -64
- package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +110 -0
- package/build/esm/lib/store/engine/lib/sqlite/index.js +128 -0
- package/build/esm/lib/store/index.d.ts +7 -3
- package/build/esm/lib/store/index.js +13 -5
- package/build/esm/lib/whatsapp/index.d.ts +106 -68
- package/build/esm/lib/whatsapp/index.js +395 -169
- package/package.json +25 -6
- package/build/cjs/decorators.js.map +0 -1
- package/build/cjs/index.js.map +0 -1
- package/build/cjs/lib/bot/decorator.js.map +0 -1
- package/build/cjs/lib/bot/decorators.js.map +0 -1
- package/build/cjs/lib/bot/index.js.map +0 -1
- package/build/cjs/lib/chat/index.js.map +0 -1
- package/build/cjs/lib/contact/index.js.map +0 -1
- package/build/cjs/lib/message/index.js.map +0 -1
- package/build/cjs/lib/status/index.js.map +0 -1
- package/build/cjs/lib/store/engine/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/file_system/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/redis/index.js.map +0 -1
- package/build/cjs/lib/store/engine/lib/s3/index.js.map +0 -1
- package/build/cjs/lib/store/index.js.map +0 -1
- package/build/cjs/lib/whatsapp/index.js.map +0 -1
- package/build/cjs/test.d.ts +0 -1
- package/build/cjs/test.js +0 -71
- package/build/cjs/test.js.map +0 -1
- package/build/esm/decorators.js.map +0 -1
- package/build/esm/index.js.map +0 -1
- package/build/esm/lib/bot/decorator.js.map +0 -1
- package/build/esm/lib/bot/decorators.js.map +0 -1
- package/build/esm/lib/bot/index.js.map +0 -1
- package/build/esm/lib/chat/index.js.map +0 -1
- package/build/esm/lib/contact/index.js.map +0 -1
- package/build/esm/lib/message/index.js.map +0 -1
- package/build/esm/lib/status/index.js.map +0 -1
- package/build/esm/lib/store/engine/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/file_system/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/redis/index.js.map +0 -1
- package/build/esm/lib/store/engine/lib/s3/index.js.map +0 -1
- package/build/esm/lib/store/index.js.map +0 -1
- package/build/esm/lib/whatsapp/index.js.map +0 -1
- package/build/esm/test.d.ts +0 -1
- package/build/esm/test.js +0 -66
- package/build/esm/test.js.map +0 -1
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* @description Driver de persistencia con Redis.
|
|
4
4
|
* Redis persistence driver.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
* Normaliza un path colapsando slashes redundantes.
|
|
8
|
-
* Normalizes a path by collapsing redundant slashes.
|
|
9
|
-
*/
|
|
10
|
-
function normalize_path(path) {
|
|
11
|
-
return path.replace(/\/+/g, '/').replace(/^\/|\/$/g, '');
|
|
12
|
-
}
|
|
6
|
+
import { normalize_path, split_path } from '../../../../../lib/store/engine/lib/index.js';
|
|
13
7
|
/**
|
|
14
8
|
* Driver de persistencia con Redis.
|
|
15
9
|
*
|
|
16
10
|
* Keyspaces:
|
|
17
11
|
* - `<prefix>:doc:<path>` → string del documento.
|
|
18
|
-
* - `<prefix>:idx:<parent>` → sorted set (score
|
|
12
|
+
* - `<prefix>:idx:<parent>` → sorted set (score explícito o de escritura, member=path completo).
|
|
19
13
|
*
|
|
20
|
-
* `list` combina ZREVRANGE + MGET
|
|
14
|
+
* El índice ordenado vive en Redis: `list` combina ZREVRANGE + MGET (dos round-trips, O(log N + M))
|
|
15
|
+
* y `count` es un ZCARD O(1); las escrituras agrupan documento e índice en un pipeline para que
|
|
16
|
+
* no queden documentos huérfanos del índice si el proceso muere entre ambas operaciones.
|
|
17
|
+
*
|
|
18
|
+
* Redis persistence driver. The sorted index lives in Redis: `list` combines ZREVRANGE + MGET
|
|
19
|
+
* and `count` is an O(1) ZCARD; writes group document and index in a pipeline so no document
|
|
20
|
+
* is left orphaned from the index when the process dies between both operations.
|
|
21
21
|
*
|
|
22
22
|
* @example
|
|
23
23
|
* import IORedis from 'ioredis';
|
|
@@ -36,15 +36,6 @@ export class RedisEngine {
|
|
|
36
36
|
_idx_key(parent) {
|
|
37
37
|
return `${this._prefix}:idx:${normalize_path(parent)}`;
|
|
38
38
|
}
|
|
39
|
-
/** @internal */
|
|
40
|
-
_split(path) {
|
|
41
|
-
const full = normalize_path(path);
|
|
42
|
-
const i = full.lastIndexOf('/');
|
|
43
|
-
if (i === -1) {
|
|
44
|
-
return { parent: '', full };
|
|
45
|
-
}
|
|
46
|
-
return { parent: full.slice(0, i), full };
|
|
47
|
-
}
|
|
48
39
|
/**
|
|
49
40
|
* Lee el valor de un documento.
|
|
50
41
|
* Reads a document's value.
|
|
@@ -53,38 +44,62 @@ export class RedisEngine {
|
|
|
53
44
|
return this._client.get(this._doc_key(path));
|
|
54
45
|
}
|
|
55
46
|
/**
|
|
56
|
-
* Escribe el valor y
|
|
57
|
-
*
|
|
47
|
+
* Escribe el valor y su entrada de índice en una sola operación; `score` fija el orden
|
|
48
|
+
* de `list` (por defecto, la hora de escritura).
|
|
49
|
+
* Writes the value and its index entry in a single operation; `score` drives `list`
|
|
50
|
+
* ordering (write time by default).
|
|
58
51
|
*/
|
|
59
|
-
async set(path, value) {
|
|
60
|
-
const { parent
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
async set(path, value, score) {
|
|
53
|
+
const { parent } = split_path(path);
|
|
54
|
+
const full = normalize_path(path);
|
|
55
|
+
const doc_key = this._doc_key(full);
|
|
56
|
+
const idx_key = this._idx_key(parent);
|
|
57
|
+
const rank = score ?? Date.now();
|
|
58
|
+
const pipeline = this._client.pipeline?.();
|
|
59
|
+
if (pipeline) {
|
|
60
|
+
pipeline.set(doc_key, value);
|
|
61
|
+
pipeline.zadd(idx_key, rank, full);
|
|
62
|
+
await pipeline.exec();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await this._client.set(doc_key, value);
|
|
66
|
+
await this._client.zadd(idx_key, rank, full);
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
/**
|
|
65
|
-
* Elimina el doc, su índice y todo el sub-árbol.
|
|
66
|
-
* Deletes the doc, its index, and the entire subtree.
|
|
70
|
+
* Elimina el doc, su entrada de índice y todo el sub-árbol.
|
|
71
|
+
* Deletes the doc, its index entry, and the entire subtree.
|
|
67
72
|
*/
|
|
68
73
|
async unset(path) {
|
|
69
|
-
const { parent
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const { parent } = split_path(path);
|
|
75
|
+
const full = normalize_path(path);
|
|
76
|
+
const pipeline = this._client.pipeline?.();
|
|
77
|
+
if (pipeline) {
|
|
78
|
+
pipeline.del([this._doc_key(full), `${this._doc_key(full)}:bin`]);
|
|
79
|
+
pipeline.zrem(this._idx_key(parent), full);
|
|
80
|
+
await pipeline.exec();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
await this._client.del([this._doc_key(full), `${this._doc_key(full)}:bin`]);
|
|
84
|
+
await this._client.zrem(this._idx_key(parent), full);
|
|
85
|
+
}
|
|
86
|
+
await Promise.all([
|
|
87
|
+
this._delete_pattern(`${this._prefix}:doc:${full}/*`),
|
|
88
|
+
this._delete_pattern(`${this._prefix}:idx:${full}`),
|
|
89
|
+
this._delete_pattern(`${this._prefix}:idx:${full}/*`),
|
|
90
|
+
]);
|
|
75
91
|
return true;
|
|
76
92
|
}
|
|
77
93
|
/**
|
|
78
|
-
* Lista valores de los hijos directos, ordenados por
|
|
79
|
-
* Lists direct children values ordered by
|
|
94
|
+
* Lista valores de los hijos directos, ordenados por score DESC en dos round-trips.
|
|
95
|
+
* Lists direct children values ordered by score DESC in two round-trips.
|
|
80
96
|
*/
|
|
81
97
|
async list(path, offset = 0, limit = 50) {
|
|
82
|
-
const
|
|
83
|
-
const members = await this._client.zrevrange(this._idx_key(full), offset, offset + limit - 1);
|
|
98
|
+
const members = await this._client.zrevrange(this._idx_key(path), offset, offset + limit - 1);
|
|
84
99
|
if (members.length === 0) {
|
|
85
100
|
return [];
|
|
86
101
|
}
|
|
87
|
-
const raws = await this._client.mget(members.map((
|
|
102
|
+
const raws = await this._client.mget(members.map((member) => this._doc_key(member)));
|
|
88
103
|
return raws.filter((raw) => raw !== null);
|
|
89
104
|
}
|
|
90
105
|
/**
|
|
@@ -92,8 +107,26 @@ export class RedisEngine {
|
|
|
92
107
|
* Counts direct children in O(1) via ZCARD.
|
|
93
108
|
*/
|
|
94
109
|
async count(path) {
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
return this._client.zcard(this._idx_key(path));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Lee el binario del documento. Requiere un cliente con soporte de buffers (ioredis lo
|
|
114
|
+
* trae con `getBuffer`); sin él la librería cae al documento serializado.
|
|
115
|
+
* Reads the document binary. Requires a buffer-capable client (ioredis ships `getBuffer`);
|
|
116
|
+
* without it the library falls back to the serialized document.
|
|
117
|
+
*/
|
|
118
|
+
async get_buffer(path) {
|
|
119
|
+
return (await this._client.getBuffer?.(`${this._doc_key(path)}:bin`)) ?? null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Escribe el binario del documento en su propia key, sin JSON ni base64.
|
|
123
|
+
* Writes the document binary in its own key, with no JSON nor base64.
|
|
124
|
+
*/
|
|
125
|
+
async set_buffer(path, data, score) {
|
|
126
|
+
const { parent } = split_path(path);
|
|
127
|
+
const full = normalize_path(path);
|
|
128
|
+
await this._client.setBuffer?.(`${this._doc_key(full)}:bin`, data);
|
|
129
|
+
await this._client.zadd(this._idx_key(parent), score ?? Date.now(), full);
|
|
97
130
|
}
|
|
98
131
|
/**
|
|
99
132
|
* Vacía todo el prefix del cliente.
|
|
@@ -111,7 +144,7 @@ export class RedisEngine {
|
|
|
111
144
|
async _delete_pattern(pattern) {
|
|
112
145
|
let cursor = '0';
|
|
113
146
|
do {
|
|
114
|
-
const [next, batch] = await this._client.scan(cursor, 'MATCH', pattern, 'COUNT',
|
|
147
|
+
const [next, batch] = await this._client.scan(cursor, 'MATCH', pattern, 'COUNT', 500);
|
|
115
148
|
cursor = next;
|
|
116
149
|
if (batch.length > 0) {
|
|
117
150
|
await this._client.del(batch);
|
|
@@ -119,4 +152,3 @@ export class RedisEngine {
|
|
|
119
152
|
} while (cursor !== '0');
|
|
120
153
|
}
|
|
121
154
|
}
|
|
122
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -33,7 +33,26 @@ interface S3EngineOptions {
|
|
|
33
33
|
* Default: `false` (no cache).
|
|
34
34
|
*/
|
|
35
35
|
cache?: false | CacheOptions;
|
|
36
|
+
/** Prefijos indexados simultáneamente en memoria. / Simultaneously in-memory indexed prefixes. */
|
|
37
|
+
cached?: number;
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Driver de persistencia con AWS S3.
|
|
41
|
+
*
|
|
42
|
+
* Para que `list`/`count` no recorran todo el prefijo en cada página mantiene un índice
|
|
43
|
+
* ordenado por prefijo (`SortedIndex`), acotado por LRU y respaldado en el objeto `.order`:
|
|
44
|
+
* ahí viven los scores explícitos que S3 no puede representar (`LastModified` es de sólo
|
|
45
|
+
* lectura), de modo que el orden cronológico sobrevive a los re-syncs igual que en los
|
|
46
|
+
* demás drivers. Si el objeto `.order` falta, el índice se reconstruye listando el prefijo
|
|
47
|
+
* una única vez y ordenando por `LastModified`.
|
|
48
|
+
*
|
|
49
|
+
* AWS S3 persistence driver. To keep `list`/`count` from walking the whole prefix on every
|
|
50
|
+
* page it maintains a per-prefix sorted index (`SortedIndex`), LRU-bounded and backed by the
|
|
51
|
+
* `.order` object: it holds the explicit scores S3 cannot represent (`LastModified` is
|
|
52
|
+
* read-only), so chronological order survives re-syncs just like on the other drivers. When
|
|
53
|
+
* `.order` is missing the index is rebuilt listing the prefix once and ordering by
|
|
54
|
+
* `LastModified`.
|
|
55
|
+
*/
|
|
37
56
|
export declare class S3Engine implements Engine {
|
|
38
57
|
private readonly _client;
|
|
39
58
|
private readonly _bucket;
|
|
@@ -41,6 +60,8 @@ export declare class S3Engine implements Engine {
|
|
|
41
60
|
private readonly _cache_opts;
|
|
42
61
|
/** @internal Caché local de documentos, con su timer de expiración. / Local cache of documents, with its expiration timer. */
|
|
43
62
|
private readonly _cache;
|
|
63
|
+
/** @internal Índices por prefijo, acotados por LRU. / Per-prefix indexes, LRU-bounded. */
|
|
64
|
+
private readonly _indexes;
|
|
44
65
|
constructor(options: S3EngineOptions);
|
|
45
66
|
private _key;
|
|
46
67
|
/**
|
|
@@ -55,11 +76,33 @@ export declare class S3Engine implements Engine {
|
|
|
55
76
|
* Replaces the previous timer when the key was already cached so timers don't pile up.
|
|
56
77
|
*/
|
|
57
78
|
private _cache_set;
|
|
79
|
+
/**
|
|
80
|
+
* @internal
|
|
81
|
+
* Índice del prefijo: de la caché, del objeto `.order` o reconstruido listando el prefijo
|
|
82
|
+
* una sola vez y ordenando por `LastModified`.
|
|
83
|
+
* Prefix index: from the cache, from the `.order` object or rebuilt listing the prefix
|
|
84
|
+
* once and ordering by `LastModified`.
|
|
85
|
+
*/
|
|
86
|
+
private _index;
|
|
58
87
|
get(key: string): Promise<string | null>;
|
|
59
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Escribe el valor y registra su score en el índice del prefijo, que S3 no puede
|
|
90
|
+
* derivar por sí solo (`LastModified` es de sólo lectura).
|
|
91
|
+
* Writes the value and records its score in the prefix index, which S3 cannot derive
|
|
92
|
+
* on its own (`LastModified` is read-only).
|
|
93
|
+
*/
|
|
94
|
+
set(key: string, value: string, score?: number): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Lista los valores de los hijos directos, ordenados por score DESC.
|
|
97
|
+
* Lists direct children values ordered by score DESC.
|
|
98
|
+
*/
|
|
60
99
|
list(path: string, offset?: number, limit?: number): Promise<string[]>;
|
|
61
100
|
delete_prefix(prefix: string): Promise<number>;
|
|
62
101
|
unset(path: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Cuenta hijos directos desde el índice del prefijo.
|
|
104
|
+
* Counts direct children from the prefix index.
|
|
105
|
+
*/
|
|
63
106
|
count(path: string): Promise<number>;
|
|
64
107
|
clear(): Promise<void>;
|
|
65
108
|
}
|
|
@@ -4,13 +4,26 @@
|
|
|
4
4
|
* AWS S3 persistence driver (optional local cache).
|
|
5
5
|
*/
|
|
6
6
|
import { DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
7
|
+
import { IndexCache, normalize_path, SortedIndex, split_path } from '../../../../../lib/store/engine/lib/index.js';
|
|
8
|
+
/** Objeto que guarda el orden de los hijos de un prefijo. / Object holding the ordering of a prefix children. */
|
|
9
|
+
const ORDER_KEY = '.order';
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
11
|
+
* Driver de persistencia con AWS S3.
|
|
12
|
+
*
|
|
13
|
+
* Para que `list`/`count` no recorran todo el prefijo en cada página mantiene un índice
|
|
14
|
+
* ordenado por prefijo (`SortedIndex`), acotado por LRU y respaldado en el objeto `.order`:
|
|
15
|
+
* ahí viven los scores explícitos que S3 no puede representar (`LastModified` es de sólo
|
|
16
|
+
* lectura), de modo que el orden cronológico sobrevive a los re-syncs igual que en los
|
|
17
|
+
* demás drivers. Si el objeto `.order` falta, el índice se reconstruye listando el prefijo
|
|
18
|
+
* una única vez y ordenando por `LastModified`.
|
|
19
|
+
*
|
|
20
|
+
* AWS S3 persistence driver. To keep `list`/`count` from walking the whole prefix on every
|
|
21
|
+
* page it maintains a per-prefix sorted index (`SortedIndex`), LRU-bounded and backed by the
|
|
22
|
+
* `.order` object: it holds the explicit scores S3 cannot represent (`LastModified` is
|
|
23
|
+
* read-only), so chronological order survives re-syncs just like on the other drivers. When
|
|
24
|
+
* `.order` is missing the index is rebuilt listing the prefix once and ordering by
|
|
25
|
+
* `LastModified`.
|
|
10
26
|
*/
|
|
11
|
-
function normalize_path(path) {
|
|
12
|
-
return path.replace(/\/+/g, '/').replace(/^\/|\/$/g, '');
|
|
13
|
-
}
|
|
14
27
|
export class S3Engine {
|
|
15
28
|
constructor(options) {
|
|
16
29
|
/** @internal Caché local de documentos, con su timer de expiración. / Local cache of documents, with its expiration timer. */
|
|
@@ -19,6 +32,7 @@ export class S3Engine {
|
|
|
19
32
|
this._bucket = options.bucket;
|
|
20
33
|
this._prefix = options.basedir.endsWith('/') ? options.basedir : `${options.basedir}/`;
|
|
21
34
|
this._cache_opts = options.cache ?? false;
|
|
35
|
+
this._indexes = new IndexCache(options.cached ?? 12);
|
|
22
36
|
}
|
|
23
37
|
_key(key) {
|
|
24
38
|
return `${this._prefix}${normalize_path(key).replace(/@/g, '_at_')}`;
|
|
@@ -46,69 +60,108 @@ export class S3Engine {
|
|
|
46
60
|
timer: setTimeout(() => this._cache.delete(path), this._cache_opts.ttl),
|
|
47
61
|
});
|
|
48
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* @internal
|
|
65
|
+
* Índice del prefijo: de la caché, del objeto `.order` o reconstruido listando el prefijo
|
|
66
|
+
* una sola vez y ordenando por `LastModified`.
|
|
67
|
+
* Prefix index: from the cache, from the `.order` object or rebuilt listing the prefix
|
|
68
|
+
* once and ordering by `LastModified`.
|
|
69
|
+
*/
|
|
70
|
+
async _index(path) {
|
|
71
|
+
const key = normalize_path(path);
|
|
72
|
+
const cached = this._indexes.get(key);
|
|
73
|
+
if (cached) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
const parent = `${this._key(key)}/`;
|
|
77
|
+
const index = new SortedIndex(async (entries) => {
|
|
78
|
+
await this._client
|
|
79
|
+
.send(new PutObjectCommand({
|
|
80
|
+
Bucket: this._bucket,
|
|
81
|
+
Key: `${parent}${ORDER_KEY}`,
|
|
82
|
+
Body: entries.map(([name, score]) => `${score}\t${name}`).join('\n'),
|
|
83
|
+
ContentType: 'text/plain',
|
|
84
|
+
}))
|
|
85
|
+
.catch(() => null);
|
|
86
|
+
});
|
|
87
|
+
const persisted = await this._client
|
|
88
|
+
.send(new GetObjectCommand({ Bucket: this._bucket, Key: `${parent}${ORDER_KEY}` }))
|
|
89
|
+
.then((res) => res.Body?.transformToString('utf-8') ?? null)
|
|
90
|
+
.catch(() => null);
|
|
91
|
+
if (persisted) {
|
|
92
|
+
index.load(persisted
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.map((line) => {
|
|
96
|
+
const cut = line.indexOf('\t');
|
|
97
|
+
return [line.slice(cut + 1), Number(line.slice(0, cut))];
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const children = new Map();
|
|
102
|
+
let token;
|
|
103
|
+
do {
|
|
104
|
+
const res = await this._client.send(new ListObjectsV2Command({ Bucket: this._bucket, Prefix: parent, ContinuationToken: token, MaxKeys: 1000 }));
|
|
105
|
+
for (const object of res.Contents ?? []) {
|
|
106
|
+
const rest = object.Key?.slice(parent.length);
|
|
107
|
+
const name = rest?.split('/')[0];
|
|
108
|
+
if (name && name !== ORDER_KEY) {
|
|
109
|
+
children.set(name, Math.max(children.get(name) ?? 0, object.LastModified?.getTime() ?? 0));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
token = res.NextContinuationToken;
|
|
113
|
+
} while (token);
|
|
114
|
+
index.load(children);
|
|
115
|
+
}
|
|
116
|
+
this._indexes.set(key, index);
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
49
119
|
async get(key) {
|
|
50
120
|
if (this._cacheable(key) && this._cache.has(key)) {
|
|
51
121
|
return this._cache.get(key).value;
|
|
52
122
|
}
|
|
53
|
-
const value = await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
})();
|
|
123
|
+
const value = await this._client
|
|
124
|
+
.send(new GetObjectCommand({ Bucket: this._bucket, Key: this._key(key) }))
|
|
125
|
+
.then((res) => res.Body?.transformToString('utf-8') ?? null)
|
|
126
|
+
.catch(() => null);
|
|
62
127
|
if (this._cacheable(key)) {
|
|
63
128
|
this._cache_set(key, value);
|
|
64
129
|
}
|
|
65
130
|
return value;
|
|
66
131
|
}
|
|
67
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Escribe el valor y registra su score en el índice del prefijo, que S3 no puede
|
|
134
|
+
* derivar por sí solo (`LastModified` es de sólo lectura).
|
|
135
|
+
* Writes the value and records its score in the prefix index, which S3 cannot derive
|
|
136
|
+
* on its own (`LastModified` is read-only).
|
|
137
|
+
*/
|
|
138
|
+
async set(key, value, score) {
|
|
68
139
|
const remote = this._client.send(new PutObjectCommand({ Bucket: this._bucket, Key: this._key(key), Body: value, ContentType: 'application/json' }));
|
|
69
140
|
if (this._cacheable(key)) {
|
|
70
141
|
this._cache_set(key, value);
|
|
71
142
|
}
|
|
143
|
+
const { parent, name } = split_path(key);
|
|
144
|
+
// A diferencia del filesystem, S3 no tiene dónde guardar el score fuera del índice
|
|
145
|
+
// (`LastModified` es de sólo lectura), así que el índice se carga aunque nadie haya
|
|
146
|
+
// paginado todavía; si no, el orden cronológico se perdería en la primera escritura.
|
|
147
|
+
// Unlike the filesystem, S3 has nowhere to keep the score outside the index
|
|
148
|
+
// (`LastModified` is read-only), so the index is loaded even when nobody paginated
|
|
149
|
+
// yet; otherwise chronological order would be lost on the first write.
|
|
150
|
+
(await this._index(parent)).set(name, score ?? Date.now());
|
|
72
151
|
await remote;
|
|
73
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Lista los valores de los hijos directos, ordenados por score DESC.
|
|
155
|
+
* Lists direct children values ordered by score DESC.
|
|
156
|
+
*/
|
|
74
157
|
async list(path, offset = 0, limit = 50) {
|
|
75
|
-
const parent =
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
do {
|
|
79
|
-
const res = await this._client.send(new ListObjectsV2Command({
|
|
80
|
-
Bucket: this._bucket,
|
|
81
|
-
Prefix: parent,
|
|
82
|
-
ContinuationToken: token,
|
|
83
|
-
MaxKeys: 1000,
|
|
84
|
-
}));
|
|
85
|
-
for (const obj of res.Contents ?? []) {
|
|
86
|
-
if (!obj.Key)
|
|
87
|
-
continue;
|
|
88
|
-
const rest = obj.Key.slice(parent.length);
|
|
89
|
-
const child_key = `${parent}${rest.split('/')[0]}`;
|
|
90
|
-
const mtime = obj.LastModified?.getTime() ?? 0;
|
|
91
|
-
children.set(child_key, Math.max(children.get(child_key) ?? 0, mtime));
|
|
92
|
-
}
|
|
93
|
-
token = res.NextContinuationToken;
|
|
94
|
-
} while (token);
|
|
95
|
-
const ordered = [...children.entries()]
|
|
96
|
-
.sort((a, b) => b[1] - a[1])
|
|
97
|
-
.slice(offset, offset + limit)
|
|
98
|
-
.map(([key]) => key);
|
|
99
|
-
const values = await Promise.all(ordered.map(async (key) => {
|
|
100
|
-
try {
|
|
101
|
-
const res = await this._client.send(new GetObjectCommand({ Bucket: this._bucket, Key: key }));
|
|
102
|
-
return (await res.Body?.transformToString('utf-8')) ?? null;
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
}));
|
|
158
|
+
const parent = normalize_path(path);
|
|
159
|
+
const page = (await this._index(parent)).page(offset, limit);
|
|
160
|
+
const values = await Promise.all(page.map((name) => this.get(`${parent}/${name}`)));
|
|
108
161
|
return values.filter((value) => value !== null);
|
|
109
162
|
}
|
|
110
163
|
async delete_prefix(prefix) {
|
|
111
|
-
const full_prefix =
|
|
164
|
+
const full_prefix = this._key(prefix);
|
|
112
165
|
let deleted = 0;
|
|
113
166
|
let token;
|
|
114
167
|
do {
|
|
@@ -136,30 +189,24 @@ export class S3Engine {
|
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
191
|
}
|
|
192
|
+
const { parent, name } = split_path(path);
|
|
193
|
+
(await this._index(parent)).delete(name);
|
|
194
|
+
this._indexes.drop(normalize_path(path));
|
|
139
195
|
return (await this.delete_prefix(path)) > 0;
|
|
140
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Cuenta hijos directos desde el índice del prefijo.
|
|
199
|
+
* Counts direct children from the prefix index.
|
|
200
|
+
*/
|
|
141
201
|
async count(path) {
|
|
142
|
-
|
|
143
|
-
let total = 0;
|
|
144
|
-
let token;
|
|
145
|
-
do {
|
|
146
|
-
const res = await this._client.send(new ListObjectsV2Command({
|
|
147
|
-
Bucket: this._bucket,
|
|
148
|
-
Prefix: full_prefix,
|
|
149
|
-
ContinuationToken: token,
|
|
150
|
-
MaxKeys: 1000,
|
|
151
|
-
}));
|
|
152
|
-
total += res.Contents?.length ?? 0;
|
|
153
|
-
token = res.NextContinuationToken;
|
|
154
|
-
} while (token);
|
|
155
|
-
return total;
|
|
202
|
+
return (await this._index(path)).size;
|
|
156
203
|
}
|
|
157
204
|
async clear() {
|
|
158
205
|
for (const { timer } of this._cache.values()) {
|
|
159
206
|
clearTimeout(timer);
|
|
160
207
|
}
|
|
161
208
|
this._cache.clear();
|
|
209
|
+
this._indexes.clear();
|
|
162
210
|
await this.delete_prefix('');
|
|
163
211
|
}
|
|
164
212
|
}
|
|
165
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file store/engine/lib/sqlite/index.ts
|
|
3
|
+
* @description Driver de persistencia con SQLite.
|
|
4
|
+
* SQLite persistence driver.
|
|
5
|
+
*/
|
|
6
|
+
import type { Engine } from '../../../../../lib/store/engine/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Sentencia preparada, con la forma que exponen `better-sqlite3` y `node:sqlite`.
|
|
9
|
+
* Prepared statement, shaped as `better-sqlite3` and `node:sqlite` expose it.
|
|
10
|
+
*/
|
|
11
|
+
interface SQLiteStatement {
|
|
12
|
+
run(...params: unknown[]): unknown;
|
|
13
|
+
get(...params: unknown[]): unknown;
|
|
14
|
+
all(...params: unknown[]): unknown[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Interface mínima de la base SQLite. La cumplen `better-sqlite3` (`new Database(file)`) y
|
|
18
|
+
* el `node:sqlite` nativo (`new DatabaseSync(file)`), así que el driver no agrega ninguna
|
|
19
|
+
* dependencia: la base se inyecta ya abierta, igual que el cliente de `RedisEngine`.
|
|
20
|
+
* Minimal SQLite database interface. Both `better-sqlite3` (`new Database(file)`) and the
|
|
21
|
+
* native `node:sqlite` (`new DatabaseSync(file)`) satisfy it, so this driver adds no
|
|
22
|
+
* dependency: the database is injected already open, just like `RedisEngine`'s client.
|
|
23
|
+
*/
|
|
24
|
+
export interface SQLiteDatabase {
|
|
25
|
+
exec(sql: string): unknown;
|
|
26
|
+
prepare(sql: string): SQLiteStatement;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Driver de persistencia con SQLite: una tabla `(path, parent, score, value)` con índice
|
|
30
|
+
* `(parent, score DESC)`.
|
|
31
|
+
*
|
|
32
|
+
* Es el driver más eficiente de los integrados porque delega en el motor lo que los demás
|
|
33
|
+
* resuelven a mano: `list` es un `ORDER BY score DESC LIMIT/OFFSET` sobre el índice (sin
|
|
34
|
+
* índice en memoria ni archivo de orden), `count` un `COUNT(*)` y el `unset` en cascada una
|
|
35
|
+
* sola sentencia por rango de `path`. Medido sobre un chat real de 55.146 mensajes frente al
|
|
36
|
+
* filesystem: 220 MB → 64 MB en disco, primer `list` 115 ms → 0,6 ms y dos archivos en total.
|
|
37
|
+
*
|
|
38
|
+
* SQLite persistence driver: a single `(path, parent, score, value)` table with a
|
|
39
|
+
* `(parent, score DESC)` index. The most efficient built-in driver, since it delegates to the
|
|
40
|
+
* engine what the others hand-roll: `list` is an indexed `ORDER BY score DESC LIMIT/OFFSET`
|
|
41
|
+
* (no in-memory index, no order file), `count` a `COUNT(*)` and cascading `unset` a single
|
|
42
|
+
* range statement. Measured on a real 55,146-message chat against the filesystem: 220 MB →
|
|
43
|
+
* 64 MB on disk, first `list` 115 ms → 0.6 ms and two files in total.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* import Database from 'better-sqlite3';
|
|
47
|
+
* const engine = new SQLiteEngine(new Database('.sessions/584144709840.db'));
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* import { DatabaseSync } from 'node:sqlite';
|
|
51
|
+
* const engine = new SQLiteEngine(new DatabaseSync('.sessions/584144709840.db'));
|
|
52
|
+
*/
|
|
53
|
+
export declare class SQLiteEngine implements Engine {
|
|
54
|
+
private readonly _db;
|
|
55
|
+
private readonly _read;
|
|
56
|
+
private readonly _write;
|
|
57
|
+
private readonly _remove;
|
|
58
|
+
private readonly _page;
|
|
59
|
+
private readonly _total;
|
|
60
|
+
private readonly _wipe;
|
|
61
|
+
private readonly _read_blob;
|
|
62
|
+
private readonly _write_blob;
|
|
63
|
+
/**
|
|
64
|
+
* @param _db - Base SQLite ya abierta / Already open SQLite database
|
|
65
|
+
* @param table - Tabla donde viven los documentos / Table holding the documents
|
|
66
|
+
*/
|
|
67
|
+
constructor(_db: SQLiteDatabase, table?: string);
|
|
68
|
+
/**
|
|
69
|
+
* Lee el valor de un documento.
|
|
70
|
+
* Reads a document's value.
|
|
71
|
+
*/
|
|
72
|
+
get(path: string): Promise<string | null>;
|
|
73
|
+
/**
|
|
74
|
+
* Escribe el valor del documento; `score` fija el orden de `list` (por defecto, la hora
|
|
75
|
+
* de escritura).
|
|
76
|
+
* Writes the document value; `score` drives `list` ordering (write time by default).
|
|
77
|
+
*/
|
|
78
|
+
set(path: string, value: string, score?: number): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Elimina el documento y todos sus descendientes. Idempotente.
|
|
81
|
+
* Deletes the document and every descendant. Idempotent.
|
|
82
|
+
*/
|
|
83
|
+
unset(path: string): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Lista los valores de los hijos directos, ordenados por score DESC.
|
|
86
|
+
* Lists direct children values ordered by score DESC.
|
|
87
|
+
*/
|
|
88
|
+
list(path: string, offset?: number, limit?: number): Promise<string[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Cuenta los hijos directos.
|
|
91
|
+
* Counts direct children.
|
|
92
|
+
*/
|
|
93
|
+
count(path: string): Promise<number>;
|
|
94
|
+
/**
|
|
95
|
+
* Lee el binario del documento, o null si la fila no lo tiene.
|
|
96
|
+
* Reads the document binary, or null when the row has none.
|
|
97
|
+
*/
|
|
98
|
+
get_buffer(path: string): Promise<Buffer | null>;
|
|
99
|
+
/**
|
|
100
|
+
* Escribe el binario del documento como BLOB, sin JSON ni base64.
|
|
101
|
+
* Writes the document binary as a BLOB, with no JSON nor base64.
|
|
102
|
+
*/
|
|
103
|
+
set_buffer(path: string, data: Buffer, score?: number): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Vacía la tabla completa.
|
|
106
|
+
* Clears the entire table.
|
|
107
|
+
*/
|
|
108
|
+
clear(): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
export {};
|