@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.
Files changed (106) hide show
  1. package/README.md +282 -239
  2. package/build/cjs/decorators.js +0 -1
  3. package/build/cjs/index.d.ts +6 -4
  4. package/build/cjs/index.js +22 -3
  5. package/build/cjs/lib/bot/decorator.d.ts +2 -0
  6. package/build/cjs/lib/bot/decorator.js +6 -1
  7. package/build/cjs/lib/bot/decorators.d.ts +2 -2
  8. package/build/cjs/lib/bot/decorators.js +4 -4
  9. package/build/cjs/lib/bot/index.js +0 -1
  10. package/build/cjs/lib/chat/index.d.ts +381 -204
  11. package/build/cjs/lib/chat/index.js +243 -212
  12. package/build/cjs/lib/contact/index.d.ts +162 -179
  13. package/build/cjs/lib/contact/index.js +96 -191
  14. package/build/cjs/lib/internal.d.ts +40 -0
  15. package/build/cjs/lib/internal.js +38 -0
  16. package/build/cjs/lib/message/index.d.ts +468 -289
  17. package/build/cjs/lib/message/index.js +871 -743
  18. package/build/cjs/lib/status/index.d.ts +60 -70
  19. package/build/cjs/lib/status/index.js +106 -112
  20. package/build/cjs/lib/store/engine/index.d.ts +35 -7
  21. package/build/cjs/lib/store/engine/index.js +6 -4
  22. package/build/cjs/lib/store/engine/lib/file_system/index.d.ts +48 -8
  23. package/build/cjs/lib/store/engine/lib/file_system/index.js +117 -66
  24. package/build/cjs/lib/store/engine/lib/index.d.ts +112 -0
  25. package/build/cjs/lib/store/engine/lib/index.js +212 -0
  26. package/build/cjs/lib/store/engine/lib/redis/index.d.ts +44 -15
  27. package/build/cjs/lib/store/engine/lib/redis/index.js +75 -43
  28. package/build/cjs/lib/store/engine/lib/s3/index.d.ts +44 -1
  29. package/build/cjs/lib/store/engine/lib/s3/index.js +112 -65
  30. package/build/cjs/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  31. package/build/cjs/lib/store/engine/lib/sqlite/index.js +132 -0
  32. package/build/cjs/lib/store/index.d.ts +7 -3
  33. package/build/cjs/lib/store/index.js +14 -5
  34. package/build/cjs/lib/whatsapp/index.d.ts +106 -68
  35. package/build/cjs/lib/whatsapp/index.js +393 -167
  36. package/build/esm/decorators.js +0 -1
  37. package/build/esm/index.d.ts +6 -4
  38. package/build/esm/index.js +5 -3
  39. package/build/esm/lib/bot/decorator.d.ts +2 -0
  40. package/build/esm/lib/bot/decorator.js +6 -1
  41. package/build/esm/lib/bot/decorators.d.ts +2 -2
  42. package/build/esm/lib/bot/decorators.js +4 -4
  43. package/build/esm/lib/bot/index.js +0 -1
  44. package/build/esm/lib/chat/index.d.ts +381 -204
  45. package/build/esm/lib/chat/index.js +243 -212
  46. package/build/esm/lib/contact/index.d.ts +162 -179
  47. package/build/esm/lib/contact/index.js +96 -190
  48. package/build/esm/lib/internal.d.ts +40 -0
  49. package/build/esm/lib/internal.js +34 -0
  50. package/build/esm/lib/message/index.d.ts +468 -289
  51. package/build/esm/lib/message/index.js +866 -738
  52. package/build/esm/lib/status/index.d.ts +60 -70
  53. package/build/esm/lib/status/index.js +105 -111
  54. package/build/esm/lib/store/engine/index.d.ts +35 -7
  55. package/build/esm/lib/store/engine/index.js +4 -3
  56. package/build/esm/lib/store/engine/lib/file_system/index.d.ts +48 -8
  57. package/build/esm/lib/store/engine/lib/file_system/index.js +117 -66
  58. package/build/esm/lib/store/engine/lib/index.d.ts +112 -0
  59. package/build/esm/lib/store/engine/lib/index.js +205 -0
  60. package/build/esm/lib/store/engine/lib/redis/index.d.ts +44 -15
  61. package/build/esm/lib/store/engine/lib/redis/index.js +73 -41
  62. package/build/esm/lib/store/engine/lib/s3/index.d.ts +44 -1
  63. package/build/esm/lib/store/engine/lib/s3/index.js +111 -64
  64. package/build/esm/lib/store/engine/lib/sqlite/index.d.ts +110 -0
  65. package/build/esm/lib/store/engine/lib/sqlite/index.js +128 -0
  66. package/build/esm/lib/store/index.d.ts +7 -3
  67. package/build/esm/lib/store/index.js +13 -5
  68. package/build/esm/lib/whatsapp/index.d.ts +106 -68
  69. package/build/esm/lib/whatsapp/index.js +395 -169
  70. package/package.json +25 -6
  71. package/build/cjs/decorators.js.map +0 -1
  72. package/build/cjs/index.js.map +0 -1
  73. package/build/cjs/lib/bot/decorator.js.map +0 -1
  74. package/build/cjs/lib/bot/decorators.js.map +0 -1
  75. package/build/cjs/lib/bot/index.js.map +0 -1
  76. package/build/cjs/lib/chat/index.js.map +0 -1
  77. package/build/cjs/lib/contact/index.js.map +0 -1
  78. package/build/cjs/lib/message/index.js.map +0 -1
  79. package/build/cjs/lib/status/index.js.map +0 -1
  80. package/build/cjs/lib/store/engine/index.js.map +0 -1
  81. package/build/cjs/lib/store/engine/lib/file_system/index.js.map +0 -1
  82. package/build/cjs/lib/store/engine/lib/redis/index.js.map +0 -1
  83. package/build/cjs/lib/store/engine/lib/s3/index.js.map +0 -1
  84. package/build/cjs/lib/store/index.js.map +0 -1
  85. package/build/cjs/lib/whatsapp/index.js.map +0 -1
  86. package/build/cjs/test.d.ts +0 -1
  87. package/build/cjs/test.js +0 -71
  88. package/build/cjs/test.js.map +0 -1
  89. package/build/esm/decorators.js.map +0 -1
  90. package/build/esm/index.js.map +0 -1
  91. package/build/esm/lib/bot/decorator.js.map +0 -1
  92. package/build/esm/lib/bot/decorators.js.map +0 -1
  93. package/build/esm/lib/bot/index.js.map +0 -1
  94. package/build/esm/lib/chat/index.js.map +0 -1
  95. package/build/esm/lib/contact/index.js.map +0 -1
  96. package/build/esm/lib/message/index.js.map +0 -1
  97. package/build/esm/lib/status/index.js.map +0 -1
  98. package/build/esm/lib/store/engine/index.js.map +0 -1
  99. package/build/esm/lib/store/engine/lib/file_system/index.js.map +0 -1
  100. package/build/esm/lib/store/engine/lib/redis/index.js.map +0 -1
  101. package/build/esm/lib/store/engine/lib/s3/index.js.map +0 -1
  102. package/build/esm/lib/store/index.js.map +0 -1
  103. package/build/esm/lib/whatsapp/index.js.map +0 -1
  104. package/build/esm/test.d.ts +0 -1
  105. package/build/esm/test.js +0 -66
  106. package/build/esm/test.js.map +0 -1
@@ -6,40 +6,100 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.FileSystemEngine = void 0;
9
+ const node_crypto_1 = require("node:crypto");
9
10
  const promises_1 = require("node:fs/promises");
10
11
  const node_path_1 = require("node:path");
12
+ const lib_1 = require("../../../../../lib/store/engine/lib");
11
13
  const INDEX_FILE = 'index.json';
12
- /**
13
- * Normaliza un path colapsando slashes redundantes.
14
- * Normalizes a path by collapsing redundant slashes.
15
- */
16
- function normalize_path(path) {
17
- return path.replace(/\/+/g, '/').replace(/^\/|\/$/g, '');
18
- }
14
+ /** Índice de orden persistido junto a los hijos del directorio. / Ordering index persisted next to the directory children. */
15
+ const ORDER_FILE = '.order';
16
+ /** Binario del documento, guardado crudo junto a su JSON. / Document binary, stored raw next to its JSON. */
17
+ const BINARY_FILE = 'content.bin';
19
18
  /**
20
19
  * Driver de persistencia en sistema de archivos.
21
20
  * Cada documento se almacena como `<base>/<path>/index.json` de modo que un recurso pueda
22
- * coexistir con sub-recursos anidados.
21
+ * coexistir con sub-recursos anidados. Las escrituras son atómicas (tmp + rename) y el score
22
+ * de orden viaja en el mtime del archivo.
23
+ *
24
+ * Para que `list`/`count` cuesten O(limit) mantiene un índice ordenado por directorio
25
+ * (`SortedIndex`), acotado por LRU y respaldado en `<dir>/.order`: al abrir un directorio
26
+ * carga ese archivo y sólo reconstruye con `readdir` + `stat` cuando el conteo no coincide.
27
+ * Asume un único proceso escritor sobre el directorio base.
23
28
  *
24
29
  * Filesystem persistence driver. Each document lives at `<base>/<path>/index.json` so a
25
- * resource can coexist with nested sub-resources.
30
+ * resource can coexist with nested sub-resources. Writes are atomic (tmp + rename) and the
31
+ * ordering score travels in the file mtime.
32
+ *
33
+ * To keep `list`/`count` at O(limit) it maintains a per-directory sorted index
34
+ * (`SortedIndex`), LRU-bounded and backed by `<dir>/.order`: opening a directory loads that
35
+ * file and only rebuilds via `readdir` + `stat` when the count does not match. Assumes a
36
+ * single writer process over the base directory.
26
37
  *
27
38
  * @example
28
39
  * const engine = new FileSystemEngine('/tmp/wa');
29
40
  * await engine.set('/chat/123', JSON.stringify({ name: 'John' }));
30
41
  */
31
42
  class FileSystemEngine {
32
- constructor(_base) {
43
+ /**
44
+ * @param _base - Directorio raíz del almacén / Store root directory
45
+ * @param cached - Directorios indexados simultáneamente en memoria / Simultaneously in-memory indexed directories
46
+ */
47
+ constructor(_base, cached = 12) {
33
48
  this._base = _base;
49
+ this._indexes = new lib_1.IndexCache(cached);
34
50
  }
35
51
  /** @internal */
36
52
  _dir(path) {
37
- return (0, node_path_1.join)(this._base, normalize_path(path));
53
+ return (0, node_path_1.join)(this._base, (0, lib_1.normalize_path)(path));
38
54
  }
39
55
  /** @internal */
40
56
  _file(path) {
41
57
  return (0, node_path_1.join)(this._dir(path), INDEX_FILE);
42
58
  }
59
+ /**
60
+ * @internal
61
+ * Índice del directorio: lo toma de la caché, del `.order` persistido (validando el conteo
62
+ * contra `readdir`) o lo reconstruye leyendo el mtime de cada hijo.
63
+ * Directory index: taken from the cache, from the persisted `.order` (validating the count
64
+ * against `readdir`) or rebuilt reading each child's mtime.
65
+ */
66
+ async _index(path) {
67
+ const key = (0, lib_1.normalize_path)(path);
68
+ const cached = this._indexes.get(key);
69
+ if (cached) {
70
+ return cached;
71
+ }
72
+ const dir = (0, node_path_1.join)(this._base, key);
73
+ const index = new lib_1.SortedIndex(async (entries) => {
74
+ await this._write((0, node_path_1.join)(dir, ORDER_FILE), entries.map(([name, score]) => `${score}\t${name}`).join('\n')).catch(() => { });
75
+ });
76
+ const children = (await (0, promises_1.readdir)(dir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory());
77
+ const persisted = (await (0, promises_1.readFile)((0, node_path_1.join)(dir, ORDER_FILE), 'utf-8').catch(() => null))
78
+ ?.split('\n')
79
+ .filter(Boolean)
80
+ .map((line) => {
81
+ const cut = line.indexOf('\t');
82
+ return [line.slice(cut + 1), Number(line.slice(0, cut))];
83
+ });
84
+ if (persisted?.length === children.length) {
85
+ index.load(persisted);
86
+ }
87
+ else {
88
+ const scanned = await Promise.all(children.map(async (entry) => {
89
+ const found = await (0, promises_1.stat)((0, node_path_1.join)(dir, entry.name, INDEX_FILE)).catch(() => null);
90
+ return found ? [entry.name, found.mtimeMs] : null;
91
+ }));
92
+ index.load(scanned.filter((entry) => entry !== null));
93
+ }
94
+ this._indexes.set(key, index);
95
+ return index;
96
+ }
97
+ /** @internal Escritura atómica: archivo temporal y rename sobre el destino. / Atomic write: temp file plus rename onto the target. */
98
+ async _write(file, value) {
99
+ const tmp = `${file}.${(0, node_crypto_1.randomBytes)(6).toString('hex')}.tmp`;
100
+ await (0, promises_1.writeFile)(tmp, value, 'utf-8');
101
+ await (0, promises_1.rename)(tmp, file);
102
+ }
43
103
  /**
44
104
  * Lee el valor de un documento.
45
105
  * Reads a document's value.
@@ -53,13 +113,22 @@ class FileSystemEngine {
53
113
  }
54
114
  }
55
115
  /**
56
- * Escribe el valor de un documento, creando los directorios necesarios.
57
- * Writes a document's value, creating directories as needed.
116
+ * Escribe el valor de un documento de forma atómica creando los directorios necesarios;
117
+ * con `score` fija el mtime del archivo (y por tanto el orden de `list`).
118
+ * Atomically writes a document's value creating directories as needed; `score` fixes the
119
+ * file mtime (and therefore `list` ordering).
58
120
  */
59
- async set(path, value) {
121
+ async set(path, value, score) {
60
122
  const dir = this._dir(path);
61
123
  await (0, promises_1.mkdir)(dir, { recursive: true });
62
- await (0, promises_1.writeFile)((0, node_path_1.join)(dir, INDEX_FILE), value, 'utf-8');
124
+ const file = (0, node_path_1.join)(dir, INDEX_FILE);
125
+ await this._write(file, value);
126
+ const when = score ?? Date.now();
127
+ if (score !== undefined) {
128
+ await (0, promises_1.utimes)(file, new Date(score), new Date(score)).catch(() => { });
129
+ }
130
+ const { parent, name } = (0, lib_1.split_path)(path);
131
+ this._indexes.get(parent)?.set(name, when);
63
132
  }
64
133
  /**
65
134
  * Elimina el documento y todos sus descendientes. Idempotente.
@@ -72,70 +141,52 @@ class FileSystemEngine {
72
141
  catch {
73
142
  /* idempotent */
74
143
  }
144
+ const { parent, name } = (0, lib_1.split_path)(path);
145
+ this._indexes.get(parent)?.delete(name);
146
+ this._indexes.drop((0, lib_1.normalize_path)(path));
75
147
  return true;
76
148
  }
77
149
  /**
78
- * Lista los valores de los hijos directos, ordenados por mtime DESC.
79
- * Lists direct children values ordered by mtime DESC.
150
+ * Lista los valores de los hijos directos, ordenados por score DESC.
151
+ * Lists direct children values ordered by score DESC.
80
152
  */
81
153
  async list(path, offset = 0, limit = 50) {
82
154
  const dir = this._dir(path);
83
- try {
84
- const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
85
- const stats = await Promise.all(entries
86
- .filter((e) => e.isDirectory())
87
- .map(async (entry) => {
88
- const file = (0, node_path_1.join)(dir, entry.name, INDEX_FILE);
89
- try {
90
- const st = await (0, promises_1.stat)(file);
91
- return { file, mtime: st.mtimeMs };
92
- }
93
- catch {
94
- return null;
95
- }
96
- }));
97
- const valid = stats.filter((x) => x !== null);
98
- valid.sort((a, b) => b.mtime - a.mtime);
99
- const page = valid.slice(offset, offset + limit);
100
- const values = await Promise.all(page.map(async ({ file }) => {
101
- try {
102
- return await (0, promises_1.readFile)(file, 'utf-8');
103
- }
104
- catch {
105
- return null;
106
- }
107
- }));
108
- return values.filter((value) => value !== null);
109
- }
110
- catch {
111
- return [];
112
- }
155
+ const page = (await this._index(path)).page(offset, limit);
156
+ const values = await Promise.all(page.map((name) => (0, promises_1.readFile)((0, node_path_1.join)(dir, name, INDEX_FILE), 'utf-8').catch(() => null)));
157
+ return values.filter((value) => value !== null);
113
158
  }
114
159
  /**
115
160
  * Cuenta los hijos directos que tienen un documento válido.
116
161
  * Counts direct children with a valid document.
117
162
  */
118
163
  async count(path) {
164
+ return (await this._index(path)).size;
165
+ }
166
+ /**
167
+ * Lee el binario del documento, o null si no existe.
168
+ * Reads the document binary, or null when missing.
169
+ */
170
+ async get_buffer(path) {
171
+ return (0, promises_1.readFile)((0, node_path_1.join)(this._dir(path), BINARY_FILE)).catch(() => null);
172
+ }
173
+ /**
174
+ * Escribe el binario del documento como archivo crudo, sin JSON ni base64.
175
+ * Writes the document binary as a raw file, with no JSON nor base64.
176
+ */
177
+ async set_buffer(path, data, score) {
119
178
  const dir = this._dir(path);
120
- try {
121
- const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
122
- let total = 0;
123
- await Promise.all(entries
124
- .filter((e) => e.isDirectory())
125
- .map(async (entry) => {
126
- try {
127
- await (0, promises_1.stat)((0, node_path_1.join)(dir, entry.name, INDEX_FILE));
128
- total++;
129
- }
130
- catch {
131
- /* not a valid child doc */
132
- }
133
- }));
134
- return total;
135
- }
136
- catch {
137
- return 0;
179
+ await (0, promises_1.mkdir)(dir, { recursive: true });
180
+ const file = (0, node_path_1.join)(dir, BINARY_FILE);
181
+ const tmp = `${file}.${(0, node_crypto_1.randomBytes)(6).toString('hex')}.tmp`;
182
+ await (0, promises_1.writeFile)(tmp, data);
183
+ await (0, promises_1.rename)(tmp, file);
184
+ const when = score ?? Date.now();
185
+ if (score !== undefined) {
186
+ await (0, promises_1.utimes)(file, new Date(score), new Date(score)).catch(() => { });
138
187
  }
188
+ const { parent, name } = (0, lib_1.split_path)(path);
189
+ this._indexes.get(parent)?.set(name, when);
139
190
  }
140
191
  /**
141
192
  * Vacía completamente el almacén.
@@ -148,7 +199,7 @@ class FileSystemEngine {
148
199
  catch {
149
200
  /* idempotent */
150
201
  }
202
+ this._indexes.clear();
151
203
  }
152
204
  }
153
205
  exports.FileSystemEngine = FileSystemEngine;
154
- //# sourceMappingURL=index.js.map
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file store/engine/lib/index.ts
3
+ * @description Utilidades compartidas por los drivers: índice ordenado por directorio,
4
+ * caché LRU de índices y normalización de rutas.
5
+ * Shared driver utilities: per-directory sorted index, LRU index cache and path normalization.
6
+ */
7
+ /**
8
+ * @internal
9
+ * Índice ordenado por score DESC de los hijos de un directorio, mantenido incrementalmente:
10
+ * `page()` recorta sin re-ordenar y `set`/`delete` reubican una sola entrada por búsqueda
11
+ * binaria. Persiste su contenido con el callback `_flush`, agrupando ráfagas de escritura.
12
+ *
13
+ * Score-DESC sorted index of a directory's children, kept incrementally: `page()` slices
14
+ * without re-sorting and `set`/`delete` relocate a single entry via binary search. Persists
15
+ * through the `_flush` callback, coalescing write bursts.
16
+ */
17
+ export declare class SortedIndex {
18
+ private readonly _flush;
19
+ private readonly _delay;
20
+ /** @internal Nombres ordenados por score DESC. / Names ordered by score DESC. */
21
+ private _order;
22
+ /** @internal Score vigente de cada nombre. / Current score per name. */
23
+ private _scores;
24
+ private _dirty;
25
+ private _timer;
26
+ /**
27
+ * @param _flush - Persiste el índice; recibe los pares ordenados / Persists the index; receives the ordered pairs
28
+ * @param _delay - Espera antes de persistir, agrupando ráfagas / Wait before persisting, coalescing bursts
29
+ */
30
+ constructor(_flush: (entries: [string, number][]) => Promise<void>, _delay?: number);
31
+ /** Cantidad de hijos indexados. / Indexed children count. */
32
+ get size(): number;
33
+ /** Pares `[nombre, score]` en orden DESC. / `[name, score]` pairs in DESC order. */
34
+ get entries(): [string, number][];
35
+ /**
36
+ * Reemplaza el índice completo con las entradas dadas, ordenándolas una sola vez.
37
+ * Replaces the whole index with the given entries, sorting them once.
38
+ */
39
+ load(entries: Iterable<[string, number]>): void;
40
+ /** Página de nombres en orden DESC, sin re-ordenar. / Page of names in DESC order, no re-sorting. */
41
+ page(offset: number, limit: number): string[];
42
+ /**
43
+ * Inserta o reubica un hijo con su score, manteniendo el orden.
44
+ * Inserts or relocates a child with its score, keeping the order.
45
+ */
46
+ set(name: string, score: number): void;
47
+ /**
48
+ * Quita un hijo del índice.
49
+ * Removes a child from the index.
50
+ */
51
+ delete(name: string): void;
52
+ /**
53
+ * Cancela la persistencia pendiente; se usa al descartar el índice de la caché.
54
+ * Cancels the pending persistence; used when dropping the index from the cache.
55
+ */
56
+ dispose(): void;
57
+ /** @internal Saca el nombre de la lista ordenada si estaba presente. / Detaches the name from the ordered list when present. */
58
+ private _detach;
59
+ /** @internal Primera posición cuyo score es menor o igual al dado (orden DESC). / First position whose score is lower or equal to the given one (DESC order). */
60
+ private _locate;
61
+ /** @internal Programa la persistencia diferida. / Schedules the deferred persistence. */
62
+ private _schedule;
63
+ }
64
+ /**
65
+ * @internal
66
+ * Caché LRU de índices por directorio: mantiene acotada la memoria del driver descartando
67
+ * el directorio menos usado cuando se supera el límite.
68
+ * Per-directory LRU cache of indexes: keeps driver memory bounded by dropping the
69
+ * least-recently-used directory once the limit is exceeded.
70
+ */
71
+ export declare class IndexCache {
72
+ private readonly _limit;
73
+ private readonly _cache;
74
+ /** @param _limit - Directorios indexados simultáneamente / Simultaneously indexed directories */
75
+ constructor(_limit?: number);
76
+ /**
77
+ * Índice ya cargado del directorio, marcándolo como recién usado.
78
+ * Already loaded index for the directory, marking it as recently used.
79
+ */
80
+ get(key: string): SortedIndex | undefined;
81
+ /**
82
+ * Registra el índice del directorio, descartando el menos usado si se supera el límite.
83
+ * Registers the directory index, dropping the least used one when over the limit.
84
+ */
85
+ set(key: string, index: SortedIndex): void;
86
+ /**
87
+ * Descarta el índice del directorio y de todo su sub-árbol.
88
+ * Drops the directory index and its whole subtree.
89
+ */
90
+ drop(key: string): void;
91
+ /** Vacía la caché completa. / Clears the whole cache. */
92
+ clear(): void;
93
+ }
94
+ /**
95
+ * Normaliza un path colapsando slashes redundantes y recortando los extremos.
96
+ * Normalizes a path collapsing redundant slashes and trimming both ends.
97
+ *
98
+ * @param path - Ruta cruda / Raw path
99
+ * @returns Ruta normalizada / Normalized path
100
+ */
101
+ export declare function normalize_path(path: string): string;
102
+ /**
103
+ * Separa una ruta normalizada en directorio padre y nombre del hijo.
104
+ * Splits a normalized path into parent directory and child name.
105
+ *
106
+ * @param path - Ruta a separar / Path to split
107
+ * @returns `{ parent, name }` / `{ parent, name }`
108
+ */
109
+ export declare function split_path(path: string): {
110
+ parent: string;
111
+ name: string;
112
+ };
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ /**
3
+ * @file store/engine/lib/index.ts
4
+ * @description Utilidades compartidas por los drivers: índice ordenado por directorio,
5
+ * caché LRU de índices y normalización de rutas.
6
+ * Shared driver utilities: per-directory sorted index, LRU index cache and path normalization.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IndexCache = exports.SortedIndex = void 0;
10
+ exports.normalize_path = normalize_path;
11
+ exports.split_path = split_path;
12
+ /**
13
+ * @internal
14
+ * Índice ordenado por score DESC de los hijos de un directorio, mantenido incrementalmente:
15
+ * `page()` recorta sin re-ordenar y `set`/`delete` reubican una sola entrada por búsqueda
16
+ * binaria. Persiste su contenido con el callback `_flush`, agrupando ráfagas de escritura.
17
+ *
18
+ * Score-DESC sorted index of a directory's children, kept incrementally: `page()` slices
19
+ * without re-sorting and `set`/`delete` relocate a single entry via binary search. Persists
20
+ * through the `_flush` callback, coalescing write bursts.
21
+ */
22
+ class SortedIndex {
23
+ /**
24
+ * @param _flush - Persiste el índice; recibe los pares ordenados / Persists the index; receives the ordered pairs
25
+ * @param _delay - Espera antes de persistir, agrupando ráfagas / Wait before persisting, coalescing bursts
26
+ */
27
+ constructor(_flush, _delay = 1_000) {
28
+ this._flush = _flush;
29
+ this._delay = _delay;
30
+ /** @internal Nombres ordenados por score DESC. / Names ordered by score DESC. */
31
+ this._order = [];
32
+ /** @internal Score vigente de cada nombre. / Current score per name. */
33
+ this._scores = new Map();
34
+ this._dirty = false;
35
+ this._timer = null;
36
+ }
37
+ /** Cantidad de hijos indexados. / Indexed children count. */
38
+ get size() {
39
+ return this._order.length;
40
+ }
41
+ /** Pares `[nombre, score]` en orden DESC. / `[name, score]` pairs in DESC order. */
42
+ get entries() {
43
+ return this._order.map((name) => [name, this._scores.get(name) ?? 0]);
44
+ }
45
+ /**
46
+ * Reemplaza el índice completo con las entradas dadas, ordenándolas una sola vez.
47
+ * Replaces the whole index with the given entries, sorting them once.
48
+ */
49
+ load(entries) {
50
+ const pairs = [...entries].sort((a, b) => b[1] - a[1]);
51
+ this._order = pairs.map(([name]) => name);
52
+ this._scores = new Map(pairs);
53
+ }
54
+ /** Página de nombres en orden DESC, sin re-ordenar. / Page of names in DESC order, no re-sorting. */
55
+ page(offset, limit) {
56
+ return this._order.slice(offset, offset + limit);
57
+ }
58
+ /**
59
+ * Inserta o reubica un hijo con su score, manteniendo el orden.
60
+ * Inserts or relocates a child with its score, keeping the order.
61
+ */
62
+ set(name, score) {
63
+ this._detach(name);
64
+ this._order.splice(this._locate(score), 0, name);
65
+ this._scores.set(name, score);
66
+ this._schedule();
67
+ }
68
+ /**
69
+ * Quita un hijo del índice.
70
+ * Removes a child from the index.
71
+ */
72
+ delete(name) {
73
+ if (this._scores.has(name)) {
74
+ this._detach(name);
75
+ this._schedule();
76
+ }
77
+ }
78
+ /**
79
+ * Cancela la persistencia pendiente; se usa al descartar el índice de la caché.
80
+ * Cancels the pending persistence; used when dropping the index from the cache.
81
+ */
82
+ dispose() {
83
+ if (this._timer) {
84
+ clearTimeout(this._timer);
85
+ this._timer = null;
86
+ }
87
+ }
88
+ /** @internal Saca el nombre de la lista ordenada si estaba presente. / Detaches the name from the ordered list when present. */
89
+ _detach(name) {
90
+ const previous = this._scores.get(name);
91
+ if (previous !== undefined) {
92
+ let position = this._locate(previous);
93
+ while (position < this._order.length && this._order[position] !== name) {
94
+ position++;
95
+ }
96
+ if (position < this._order.length) {
97
+ this._order.splice(position, 1);
98
+ }
99
+ this._scores.delete(name);
100
+ }
101
+ }
102
+ /** @internal Primera posición cuyo score es menor o igual al dado (orden DESC). / First position whose score is lower or equal to the given one (DESC order). */
103
+ _locate(score) {
104
+ let low = 0;
105
+ let high = this._order.length;
106
+ while (low < high) {
107
+ const middle = (low + high) >> 1;
108
+ if ((this._scores.get(this._order[middle]) ?? 0) > score) {
109
+ low = middle + 1;
110
+ }
111
+ else {
112
+ high = middle;
113
+ }
114
+ }
115
+ return low;
116
+ }
117
+ /** @internal Programa la persistencia diferida. / Schedules the deferred persistence. */
118
+ _schedule() {
119
+ this._dirty = true;
120
+ if (!this._timer) {
121
+ this._timer = setTimeout(() => {
122
+ this._timer = null;
123
+ if (this._dirty) {
124
+ this._dirty = false;
125
+ void this._flush(this.entries).catch(() => { });
126
+ }
127
+ }, this._delay);
128
+ this._timer.unref?.();
129
+ }
130
+ }
131
+ }
132
+ exports.SortedIndex = SortedIndex;
133
+ /**
134
+ * @internal
135
+ * Caché LRU de índices por directorio: mantiene acotada la memoria del driver descartando
136
+ * el directorio menos usado cuando se supera el límite.
137
+ * Per-directory LRU cache of indexes: keeps driver memory bounded by dropping the
138
+ * least-recently-used directory once the limit is exceeded.
139
+ */
140
+ class IndexCache {
141
+ /** @param _limit - Directorios indexados simultáneamente / Simultaneously indexed directories */
142
+ constructor(_limit = 12) {
143
+ this._limit = _limit;
144
+ this._cache = new Map();
145
+ }
146
+ /**
147
+ * Índice ya cargado del directorio, marcándolo como recién usado.
148
+ * Already loaded index for the directory, marking it as recently used.
149
+ */
150
+ get(key) {
151
+ const index = this._cache.get(key);
152
+ if (index) {
153
+ this._cache.delete(key);
154
+ this._cache.set(key, index);
155
+ }
156
+ return index;
157
+ }
158
+ /**
159
+ * Registra el índice del directorio, descartando el menos usado si se supera el límite.
160
+ * Registers the directory index, dropping the least used one when over the limit.
161
+ */
162
+ set(key, index) {
163
+ this._cache.set(key, index);
164
+ while (this._cache.size > this._limit) {
165
+ const oldest = this._cache.keys().next().value;
166
+ this._cache.get(oldest)?.dispose();
167
+ this._cache.delete(oldest);
168
+ }
169
+ }
170
+ /**
171
+ * Descarta el índice del directorio y de todo su sub-árbol.
172
+ * Drops the directory index and its whole subtree.
173
+ */
174
+ drop(key) {
175
+ for (const cached of [...this._cache.keys()]) {
176
+ if (cached === key || cached.startsWith(`${key}/`)) {
177
+ this._cache.get(cached)?.dispose();
178
+ this._cache.delete(cached);
179
+ }
180
+ }
181
+ }
182
+ /** Vacía la caché completa. / Clears the whole cache. */
183
+ clear() {
184
+ for (const index of this._cache.values()) {
185
+ index.dispose();
186
+ }
187
+ this._cache.clear();
188
+ }
189
+ }
190
+ exports.IndexCache = IndexCache;
191
+ /**
192
+ * Normaliza un path colapsando slashes redundantes y recortando los extremos.
193
+ * Normalizes a path collapsing redundant slashes and trimming both ends.
194
+ *
195
+ * @param path - Ruta cruda / Raw path
196
+ * @returns Ruta normalizada / Normalized path
197
+ */
198
+ function normalize_path(path) {
199
+ return path.replace(/\/+/g, '/').replace(/^\/|\/$/g, '');
200
+ }
201
+ /**
202
+ * Separa una ruta normalizada en directorio padre y nombre del hijo.
203
+ * Splits a normalized path into parent directory and child name.
204
+ *
205
+ * @param path - Ruta a separar / Path to split
206
+ * @returns `{ parent, name }` / `{ parent, name }`
207
+ */
208
+ function split_path(path) {
209
+ const full = normalize_path(path);
210
+ const cut = full.lastIndexOf('/');
211
+ return cut === -1 ? { parent: '', name: full } : { parent: full.slice(0, cut), name: full.slice(cut + 1) };
212
+ }
@@ -5,28 +5,45 @@
5
5
  */
6
6
  import type { Engine } from '../../../../../lib/store/engine';
7
7
  /**
8
- * Interface mínima del cliente Redis (compatible con ioredis).
9
- * Minimal Redis client interface (ioredis-compatible).
8
+ * Interface mínima del cliente Redis (compatible con ioredis). `pipeline` es opcional:
9
+ * cuando existe, las escrituras que tocan documento e índice viajan en un solo round-trip.
10
+ * Minimal Redis client interface (ioredis-compatible). `pipeline` is optional: when present,
11
+ * writes touching document and index travel in a single round-trip.
10
12
  */
11
13
  export interface RedisClient {
12
14
  get(key: string): Promise<string | null>;
13
15
  set(key: string, value: string): Promise<unknown>;
14
- del(keys: string | string[]): Promise<unknown>;
16
+ del(...keys: unknown[]): Promise<unknown>;
15
17
  mget(keys: string[]): Promise<(string | null)[]>;
16
18
  scan(cursor: number | string, ...args: unknown[]): Promise<[string, string[]]>;
17
19
  zadd(key: string, score: number, member: string): Promise<unknown>;
18
- zrem(key: string, members: string | string[]): Promise<unknown>;
20
+ zrem(...args: unknown[]): Promise<unknown>;
19
21
  zrevrange(key: string, start: number, stop: number): Promise<string[]>;
20
22
  zcard(key: string): Promise<number>;
23
+ getBuffer?(...args: unknown[]): Promise<Buffer | null>;
24
+ setBuffer?(...args: unknown[]): Promise<unknown>;
25
+ pipeline?(): {
26
+ set(key: string, value: string): unknown;
27
+ del(...keys: unknown[]): unknown;
28
+ zadd(key: string, score: number, member: string): unknown;
29
+ zrem(...args: unknown[]): unknown;
30
+ exec(): Promise<unknown>;
31
+ };
21
32
  }
22
33
  /**
23
34
  * Driver de persistencia con Redis.
24
35
  *
25
36
  * Keyspaces:
26
37
  * - `<prefix>:doc:<path>` → string del documento.
27
- * - `<prefix>:idx:<parent>` → sorted set (score=mtime, member=path completo).
38
+ * - `<prefix>:idx:<parent>` → sorted set (score explícito o de escritura, member=path completo).
28
39
  *
29
- * `list` combina ZREVRANGE + MGET O(log N + M) en una sola round-trip.
40
+ * El índice ordenado vive en Redis: `list` combina ZREVRANGE + MGET (dos round-trips, O(log N + M))
41
+ * y `count` es un ZCARD O(1); las escrituras agrupan documento e índice en un pipeline para que
42
+ * no queden documentos huérfanos del índice si el proceso muere entre ambas operaciones.
43
+ *
44
+ * Redis persistence driver. The sorted index lives in Redis: `list` combines ZREVRANGE + MGET
45
+ * and `count` is an O(1) ZCARD; writes group document and index in a pipeline so no document
46
+ * is left orphaned from the index when the process dies between both operations.
30
47
  *
31
48
  * @example
32
49
  * import IORedis from 'ioredis';
@@ -40,26 +57,26 @@ export declare class RedisEngine implements Engine {
40
57
  private _doc_key;
41
58
  /** @internal */
42
59
  private _idx_key;
43
- /** @internal */
44
- private _split;
45
60
  /**
46
61
  * Lee el valor de un documento.
47
62
  * Reads a document's value.
48
63
  */
49
64
  get(path: string): Promise<string | null>;
50
65
  /**
51
- * Escribe el valor y actualiza el índice del padre con el timestamp actual.
52
- * Writes the value and updates the parent's index with current timestamp.
66
+ * Escribe el valor y su entrada de índice en una sola operación; `score` fija el orden
67
+ * de `list` (por defecto, la hora de escritura).
68
+ * Writes the value and its index entry in a single operation; `score` drives `list`
69
+ * ordering (write time by default).
53
70
  */
54
- set(path: string, value: string): Promise<void>;
71
+ set(path: string, value: string, score?: number): Promise<void>;
55
72
  /**
56
- * Elimina el doc, su índice y todo el sub-árbol.
57
- * Deletes the doc, its index, and the entire subtree.
73
+ * Elimina el doc, su entrada de índice y todo el sub-árbol.
74
+ * Deletes the doc, its index entry, and the entire subtree.
58
75
  */
59
76
  unset(path: string): Promise<boolean>;
60
77
  /**
61
- * Lista valores de los hijos directos, ordenados por mtime DESC en una round-trip.
62
- * Lists direct children values ordered by mtime DESC in a single round-trip.
78
+ * Lista valores de los hijos directos, ordenados por score DESC en dos round-trips.
79
+ * Lists direct children values ordered by score DESC in two round-trips.
63
80
  */
64
81
  list(path: string, offset?: number, limit?: number): Promise<string[]>;
65
82
  /**
@@ -67,6 +84,18 @@ export declare class RedisEngine implements Engine {
67
84
  * Counts direct children in O(1) via ZCARD.
68
85
  */
69
86
  count(path: string): Promise<number>;
87
+ /**
88
+ * Lee el binario del documento. Requiere un cliente con soporte de buffers (ioredis lo
89
+ * trae con `getBuffer`); sin él la librería cae al documento serializado.
90
+ * Reads the document binary. Requires a buffer-capable client (ioredis ships `getBuffer`);
91
+ * without it the library falls back to the serialized document.
92
+ */
93
+ get_buffer(path: string): Promise<Buffer | null>;
94
+ /**
95
+ * Escribe el binario del documento en su propia key, sin JSON ni base64.
96
+ * Writes the document binary in its own key, with no JSON nor base64.
97
+ */
98
+ set_buffer(path: string, data: Buffer, score?: number): Promise<void>;
70
99
  /**
71
100
  * Vacía todo el prefix del cliente.
72
101
  * Clears the entire client prefix.