lakutata 3.0.0 → 3.1.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 (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/CLI.md ADDED
@@ -0,0 +1,274 @@
1
+ # Lakutata Command Line
2
+
3
+ The command line adapter turns the controller actions marked with `@CLIAction` into the commands of a command line
4
+ tool. It is built on [Commander](https://github.com/tj/commander.js). Install `commander` (version 12 to 15) next to
5
+ `lakutata`, and `@inquirer/prompts` (version 7 or 8) if the tool should ask for missing values:
6
+
7
+ ```bash
8
+ npm install commander @inquirer/prompts
9
+ ```
10
+
11
+ ## 1. A first tool
12
+
13
+ ```typescript
14
+ import {Application, DTO} from 'lakutata'
15
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
16
+ import {CLIAction} from 'lakutata/decorator/ctrl'
17
+ import {Expect} from 'lakutata/decorator/dto'
18
+ import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
19
+
20
+ class GreetDTO extends DTO {
21
+ @Expect(DTO.String().required().description('The name to greet'))
22
+ public name: string
23
+
24
+ @Expect(DTO.Number().integer().min(1).default(1).description('How many times'))
25
+ public times: number
26
+
27
+ @Expect(DTO.Boolean().default(false).description('Greet in capitals'))
28
+ public loud: boolean
29
+ }
30
+
31
+ class ToolController extends Controller {
32
+ @CLIAction('greet <name>', GreetDTO, {description: 'Greet someone'})
33
+ public async greet(input: GreetDTO): Promise<string> {
34
+ const text: string = `Hello, ${input.name}`
35
+ return Array(input.times).fill(input.loud ? text.toUpperCase() : text).join('\n')
36
+ }
37
+ }
38
+
39
+ Application.run(() => ({
40
+ id: 'tool.app',
41
+ name: 'Tool',
42
+ components: {
43
+ entrypoint: buildEntrypoints({
44
+ controllers: [ToolController],
45
+ cli: commanderCLIEntrypoint({bin: 'tool', description: 'A small tool', version: '1.0.0'})
46
+ })
47
+ },
48
+ bootstrap: ['entrypoint']
49
+ }))
50
+ ```
51
+
52
+ ```text
53
+ $ node dist/index.js greet Ada --times 2 --loud
54
+ HELLO, ADA
55
+ HELLO, ADA
56
+ ```
57
+
58
+ What happens:
59
+
60
+ 1. The application launches. The entrypoint must be listed in `bootstrap`: otherwise nothing creates it and no command
61
+ runs.
62
+ 2. Once the application has launched (every object in `bootstrap` is created and the other adapters have started), the
63
+ adapter reads the command line and runs the matching action.
64
+ 3. The result goes to stdout, an error to stderr.
65
+ 4. The application stops and the process exits with code 0 if the command succeeded, 1 if it failed.
66
+
67
+ `bin` is the name of the program in the help. Without it, the adapter takes the name of the script file, which is
68
+ `index` for `node dist/index.js`.
69
+
70
+ ## 2. Commands and arguments
71
+
72
+ `@CLIAction(command, DTO?, options?)` (from `lakutata/decorator/ctrl`) declares a command. The `command` string is the
73
+ name of the command followed by its arguments, in the syntax of Commander:
74
+
75
+ | Syntax | Meaning |
76
+ |---|---|
77
+ | `<name>` | A required argument |
78
+ | `[name]` | An optional argument |
79
+ | `<files...>` | One or more arguments, collected in an array (`[files...]`: zero or more) |
80
+
81
+ Each argument goes to the DTO property of the same name, converted to the type of the property. Extra arguments are an
82
+ error.
83
+
84
+ **Groups.** The words before the name of the command are groups. `@CLIAction('file copy <files...>', CopyDTO)` is the
85
+ command `copy` of the group `file`, run as `tool file copy a.txt b.txt`. Groups can be nested (`'pack linux deb'`). The
86
+ `groups` option of the adapter describes them in the help, and `tool file --help` lists the commands of a group. The
87
+ help of a tool with the command `greet` and the group `file`:
88
+
89
+ ```text
90
+ $ tool --help
91
+ Usage: tool [options] [command]
92
+
93
+ A small tool
94
+
95
+ Options:
96
+ -V, --version output the version number
97
+ -h, --help display help for command
98
+
99
+ Commands:
100
+ greet [options] <name> Greet someone
101
+ file Work on files
102
+ help [command] display help for command
103
+ ```
104
+
105
+ **Help text.** The `description` action option is the line of the command in the help. Without it, the help shows the
106
+ name of the action (`ToolController:greet`).
107
+
108
+ **Prefixes.** `@Prefix({command: 'user'})` on a controller joins the prefix to each of its commands with a colon:
109
+ `@CLIAction('get <id>')` becomes `tool user:get 42`. The prefix is part of the name of the command, not a group.
110
+
111
+ ## 3. Options
112
+
113
+ The other properties of the DTO are options. An option is named after its property in kebab case (`keepTimes` gives
114
+ `--keep-times`), and the type of the property decides its form:
115
+
116
+ | Type of the property | Option | Example |
117
+ |---|---|---|
118
+ | boolean | A flag | `--loud` |
119
+ | boolean whose default is `true` | A flag and its negation | `--keep-times`, `--no-keep-times` |
120
+ | `DTO.Alternatives(DTO.Boolean(), …)` | A flag with an optional value | `--color`, `--color always` |
121
+ | number | A number | `--times 2` |
122
+ | array | One or more values, also collected from repeated options | `--ports 80 443` |
123
+ | object | A JSON value | `--filter '{"active":true}'` |
124
+ | anything else | A string | `--mode fast` |
125
+
126
+ The help shows the `.description()` of each property, its default and its allowed values (`.valid()`). A value outside
127
+ the allowed list, or text where a number is expected, is refused before the action runs:
128
+
129
+ ```text
130
+ $ tool file copy --help
131
+ Usage: tool file copy [options] <files...>
132
+
133
+ Copy files
134
+
135
+ Options:
136
+ --mode <value> The copy mode (choices: "fast", "safe", default: "safe")
137
+ --keep-times Keep the timestamps (default: true)
138
+ --no-keep-times Turn off --keep-times
139
+ --ports <values...> The ports
140
+ -h, --help display help for command
141
+
142
+ $ tool file copy a.txt --mode slow
143
+ error: option '--mode <value>' argument 'slow' is invalid. Allowed choices are "fast", "safe".
144
+ ```
145
+
146
+ - A required property without a default must be given, unless the tool asks for it (see section 5).
147
+ - The options have no short form such as `-m`.
148
+ - A command without a DTO accepts no option, and its arguments reach the action as strings.
149
+
150
+ The DTO then validates the input as for any action: its defaults are applied and the unknown keys removed (see
151
+ [DTO Validation](DTOValidation.md)).
152
+
153
+ ## 4. Output and exit codes
154
+
155
+ The value returned by the action is written to stdout:
156
+
157
+ | Value returned | Output |
158
+ |---|---|
159
+ | A string | The string, with a final line break added if it has none |
160
+ | `null`, `undefined` | Nothing |
161
+ | A `Uint8Array` (a `Buffer`) | The bytes as they are |
162
+ | Any other value | Indented JSON |
163
+ | `Result.file(path)` (or a `Buffer`, or a stream) | The content of the file |
164
+ | `Result.events(producer)` | The data of each event, as it is sent |
165
+ | `Result.status(status, data)` | The data only |
166
+
167
+ `Result` comes from `lakutata/com/entrypoint`. The `format` option replaces this formatting: it receives each result
168
+ (and each event) and returns a string, bytes, or `undefined` to write nothing.
169
+
170
+ Where the messages go, and the exit code:
171
+
172
+ | Situation | Output | Exit code |
173
+ |---|---|---|
174
+ | The command succeeded | The result, on stdout | 0 |
175
+ | The action, a middleware, the validation or the access control failed | `error: <message>` on stderr | 1 |
176
+ | An unknown command, a missing argument, an invalid value, extra arguments | Commander's message on stderr | 1 |
177
+ | `--help`, `--version` | stdout | 0 |
178
+ | No command at all | The help on stderr | 1 |
179
+
180
+ ```text
181
+ $ tool greet
182
+ error: missing required argument 'name'
183
+ $ tool greet Ada --times x
184
+ error: option '--times <value>' argument 'x' is invalid. Not a number.
185
+ ```
186
+
187
+ Inside the action, `this.context` is a `CLIContext`: its `command` is the command as declared, with its prefix
188
+ (`'user:get <id>'`), and its `data` holds the arguments and options read from the command line, before validation.
189
+
190
+ ## 5. Prompts
191
+
192
+ When the tool runs in a terminal and `@inquirer/prompts` is installed, it asks for the missing values instead of
193
+ failing. It asks for each required property that has no value and no default, one after another. A missing required
194
+ argument (`<name>`) is not asked for: it is an error.
195
+
196
+ | Property | Prompt |
197
+ |---|---|
198
+ | boolean | Yes or no |
199
+ | With allowed values (`.valid()`) | A list to choose from |
200
+ | An array of allowed values | A list of check boxes |
201
+ | number | A number |
202
+ | Named like a password, a secret, a token, an API key or a credential | A hidden input, shown as `*` |
203
+ | Anything else | A text; an array as values separated by commas, an object as JSON |
204
+
205
+ Each answer is checked against the schema of its property at once: an invalid answer shows the error and the question
206
+ is asked again. The question is the `.description()` of the property, followed by its name.
207
+
208
+ The `interactive` option decides when the tool asks:
209
+
210
+ - `'auto'` (the default): when stdin and stdout are both a terminal. When the input or the output is piped, as in a
211
+ script, a missing value is an error instead of a question.
212
+ - `true`: always, as long as `@inquirer/prompts` is installed.
213
+ - `false`: never.
214
+
215
+ ## 6. The options of the adapter
216
+
217
+ | Option | Default | Meaning |
218
+ |---|---|---|
219
+ | `bin` | The name of the script file | The name of the program in the help |
220
+ | `description` | | The description of the program in the help |
221
+ | `version` | | Adds `-V, --version`, which prints it |
222
+ | `groups` | | The descriptions of the groups: `{file: 'Work on files', 'pack linux': 'Linux packages'}` |
223
+ | `argv` | `process.argv.slice(2)` | The arguments to read |
224
+ | `interactive` | `'auto'` | When to ask for the missing values (see section 5) |
225
+ | `stdin`, `stdout`, `stderr` | The streams of the process | Where the prompts read, and where the results and the errors are written |
226
+ | `format` | See section 4 | Turns a result into the text or the bytes to write |
227
+ | `noCommand` | `'help'` | Without arguments: `'help'` writes the help and exits with code 1, `'ignore'` runs nothing and leaves the application running |
228
+ | `exit` | `'application'` | After the command: `'application'` stops the application and exits with the code of the command, `'process'` only sets `process.exitCode`, `'none'` does nothing, and a function receives the code |
229
+ | `configure` | | `(program) => void`, called with the Commander program once the commands are added, before the arguments are read: global options, hooks |
230
+ | `translate` | | Translates the help and the prompts (see [I18n](I18n.md#help-and-prompts-of-a-command-line-tool)) |
231
+ | `middlewares` | | The middlewares of the commands of this adapter, run after the ones of the entrypoint |
232
+ | `name` | | The name of the adapter, to tell several apart |
233
+
234
+ ## 7. Commands in a server application
235
+
236
+ An application can serve HTTP and also provide commands. By default, though, the command line adapter stops the
237
+ application after its command, and without arguments it writes the help and exits. To start the server when the
238
+ application runs without arguments, and to run a command otherwise, set `noCommand: 'ignore'`:
239
+
240
+ ```typescript
241
+ import {Application} from 'lakutata'
242
+ import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
243
+ import {CLIAction, GET} from 'lakutata/decorator/ctrl'
244
+ import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
245
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
246
+
247
+ class AppController extends Controller {
248
+ @GET('/hello')
249
+ public async hello(): Promise<string> {
250
+ return 'Hello'
251
+ }
252
+
253
+ @CLIAction('migrate', {description: 'Migrate the database'})
254
+ public async migrate(): Promise<string> {
255
+ return 'Migrated'
256
+ }
257
+ }
258
+
259
+ Application.run(() => ({
260
+ id: 'shop.app',
261
+ name: 'Shop',
262
+ components: {
263
+ entrypoint: buildEntrypoints({
264
+ controllers: [AppController],
265
+ cli: commanderCLIEntrypoint({bin: 'shop', noCommand: 'ignore'}),
266
+ http: expressHTTPEntrypoint({port: 8080})
267
+ })
268
+ },
269
+ bootstrap: ['entrypoint']
270
+ }))
271
+ ```
272
+
273
+ `node dist/index.js` starts the server and keeps running. `node dist/index.js migrate` runs the command once the server
274
+ has started, then stops the application.
@@ -0,0 +1,379 @@
1
+ # Lakutata Cache
2
+
3
+ The **Cacher component** (`lakutata/com/cacher`, package `@lakutata/cache`) is a key-value cache whose entries expire.
4
+ It keeps them in the memory of the process, in a file, or on a server that the instances of the application share:
5
+ Redis, Memcache, MongoDB, SQLite, PostgreSQL or MySQL. Several stores make tiers, the fastest first. Its `wrap()` method
6
+ returns a cached value, or computes it and caches it when it is missing.
7
+
8
+ It is not the query cache of the databases (the `cache` option of a `Database`, see [Database](Database.md)).
9
+
10
+ ## 1. A first cache
11
+
12
+ ```typescript
13
+ import {Application, Component} from 'lakutata'
14
+ import {Inject} from 'lakutata/decorator/di'
15
+ import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
16
+
17
+ type Product = { id: number, name: string }
18
+
19
+ class Catalog extends Component {
20
+ @Inject('cache')
21
+ protected readonly cache: Cacher
22
+
23
+ //Loaded once, then read from the cache until the entry expires
24
+ public async product(id: number): Promise<Product> {
25
+ return await this.cache.wrap(`product:${id}`, (): Promise<Product> => this.load(id))
26
+ }
27
+
28
+ public async rename(id: number, name: string): Promise<void> {
29
+ //Update the database here, then delete the cached copy
30
+ await this.cache.del(`product:${id}`)
31
+ }
32
+
33
+ protected async load(id: number): Promise<Product> {
34
+ return {id: id, name: `Product ${id}`}
35
+ }
36
+ }
37
+
38
+ Application
39
+ .run(() => ({
40
+ id: 'catalog.app',
41
+ name: 'Catalog',
42
+ components: {
43
+ //In memory; the entries expire after 5 minutes
44
+ cache: buildCacherOptions({ttl: 300000}),
45
+ catalog: {class: Catalog}
46
+ }
47
+ }))
48
+ .onLaunched(async (app: Application): Promise<void> => {
49
+ const catalog: Catalog = await app.getObject<Catalog>('catalog')
50
+ console.log(await catalog.product(1))
51
+ const cache: Cacher = await app.getObject<Cacher>('cache')
52
+ //This entry expires after 30 seconds
53
+ await cache.set('greeting', 'Hello', 30000)
54
+ console.log(await cache.get<string>('greeting'))
55
+ })
56
+ ```
57
+
58
+ - `buildCacherOptions` declares the component under the name of your choice; the other objects inject it by that name
59
+ (`@Inject('cache')`) or get it with `getObject('cache')`. The whole application shares one instance.
60
+ - Without `stores`, the cache keeps its entries in the memory of the process.
61
+ - The durations are numbers of milliseconds.
62
+ - The queues can keep the results of their messages in a cache (see [Queue](Queue.md), section 1).
63
+
64
+ ## 2. Options
65
+
66
+ | Option | Default | Meaning |
67
+ |---|---|---|
68
+ | `stores` | one memory store | The store, or the list of stores with the fastest first (sections 5 and 6) |
69
+ | `ttl` | none: the entries never expire | How long an entry is kept when a write gives no TTL, in milliseconds (a positive integer) |
70
+ | `refreshThreshold` | none | The default threshold of the background refresh of `wrap()`, in milliseconds (section 4) |
71
+ | `refreshAllStores` | `false` | Whether a background refresh writes the new value to all the stores (section 5) |
72
+ | `nonBlocking` | `false` | Whether the writes and the deletions return without waiting for the stores (section 5) |
73
+
74
+ The option `cacheId` is accepted and not used.
75
+
76
+ - Invalid options fail the application's start with `InvalidObjectOptionsException`.
77
+ - The stores connect when the component is created. When it is listed in `bootstrap`, it is created while the
78
+ application starts, and a store that cannot connect fails the start. Otherwise it is created the first time it is
79
+ used, and that `getObject()` or that injection fails with the store's error.
80
+ - When the application stops, the cache waits for its background refreshes, then disconnects its stores (the file
81
+ store saves its last changes first).
82
+
83
+ ## 3. Reading and writing
84
+
85
+ | Method | What it does | Returns |
86
+ |---|---|---|
87
+ | `set(key, value, ttl?)` | Caches a value in all the stores, replacing the previous one | The value |
88
+ | `get(key)` | Reads a value | The value; `undefined` when the key is missing or expired |
89
+ | `multipleSet([{key, value, ttl?}])` | Caches several values | The entries written |
90
+ | `multipleGet(keys)` | Reads several values | The values, in the order of the keys |
91
+ | `getTTL(key)` | Reads when the entry expires | A time in milliseconds since the epoch, or `-1` |
92
+ | `wrap(key, fn, options?)` | Reads a value, or computes it and caches it (section 4) | The value |
93
+ | `del(key)` | Deletes a key from all the stores | `true` |
94
+ | `multipleDel(keys)` | Deletes several keys | `true` |
95
+ | `clear()` | Deletes all the entries of each store, or those of its namespace (section 6) | `true` |
96
+
97
+ - **TTL.** The TTL of a write replaces the `ttl` option, and `0` makes the entry never expire. The `ttl` option itself
98
+ cannot be `0`: leave it out when the entries should never expire. A TTL is a number, never a text such as `'1h'`.
99
+ - **`get()` never fails.** A store that fails to read counts as a miss, and its error is emitted as an `'error'`
100
+ event (section 8). A cached `undefined` cannot be told apart from a miss.
101
+ - **`getTTL()` returns a point in time**, not the time left: the time left is `expires - Date.now()`. It returns `-1`
102
+ when the key is missing or expired, and when the entry never expires.
103
+ - **`multipleSet()` and `multipleDel()` emit no `'set'` or `'del'` event**, unlike `set()`, `wrap()` and `del()`.
104
+ - `del()` and `multipleDel()` return `true` even when the key did not exist.
105
+ - A store that fails to write or delete fails the call, unless `nonBlocking` is set (section 5).
106
+
107
+ ## 4. `wrap()`
108
+
109
+ `wrap(key, fn, options)` returns the cached value of the key. When the key is missing or expired, it calls `fn`, caches
110
+ its result and returns it. It is the usual way to cache the result of a query or of a call to another service:
111
+
112
+ ```typescript
113
+ import type {Cacher} from 'lakutata/com/cacher'
114
+
115
+ declare const cache: Cacher
116
+ declare function loadRates(): Promise<Record<string, number>>
117
+
118
+ //Loaded at most once per hour; in its last 5 minutes, returned while it is loaded again in the background
119
+ const rates: Record<string, number> = await cache.wrap('rates', loadRates, {ttl: 3600000, refreshThreshold: 300000})
120
+
121
+ type User = { id: number, guest: boolean }
122
+ //A TTL that depends on the value
123
+ const user: User = await cache.wrap('user:1', async (): Promise<User> => ({id: 1, guest: true}), {
124
+ ttl: (value: User): number => value.guest ? 60000 : 3600000
125
+ })
126
+ ```
127
+
128
+ - The third argument is a TTL (`wrap(key, fn, 60000)`) or `{ttl, refreshThreshold}`, each of them a number or a
129
+ function of the value. They default to the `ttl` and `refreshThreshold` options.
130
+ - While a `wrap()` of a key is in progress, the other calls on that key share it and its result. This holds within one
131
+ component: two instances of the application may both compute the value. Use a lock (see [Mutex](Mutex.md)) when it
132
+ must be computed only once.
133
+ - When `fn` fails, `wrap()` fails with its error, and nothing is cached. A result of `undefined` is cached like any
134
+ other value.
135
+ - **Background refresh.** When the cached value has less time left than the refresh threshold, `wrap()` returns it at
136
+ once and calls `fn` in the background, one refresh per key at a time. The new value replaces the old one, and a
137
+ `'refresh'` event reports it; when `fn` fails, the event reports the error and the old value stays. An entry that
138
+ never expires is never refreshed, and a threshold higher than the TTL refreshes the value on every call.
139
+ - With several stores, a value found in a lower store is copied to the stores above it, with the time it has left
140
+ (section 5).
141
+
142
+ ## 5. Several stores
143
+
144
+ ```typescript
145
+ import {Application} from 'lakutata'
146
+ import {buildCacherOptions} from 'lakutata/com/cacher'
147
+
148
+ Application.run(() => ({
149
+ id: 'tiers.app',
150
+ name: 'Tiers',
151
+ components: {
152
+ cache: buildCacherOptions({
153
+ //Each instance keeps its copies in memory, in front of the Redis that all the instances share
154
+ stores: [
155
+ {type: 'memory'},
156
+ {type: 'redis', host: '127.0.0.1', namespace: 'tiers'}
157
+ ],
158
+ ttl: 60000
159
+ })
160
+ }
161
+ }))
162
+ ```
163
+
164
+ - A read asks the stores in order and returns the first entry found. `multipleGet()` asks the next store only for the
165
+ keys that are still missing.
166
+ - A write or a deletion goes to all the stores at once.
167
+ - Only `wrap()` copies an entry found in a lower store to the stores above it; `get()` does not.
168
+ - A background refresh writes the new value to the store that held the old one and to the stores above it, or to all
169
+ the stores with `refreshAllStores: true`.
170
+ - The memory of an instance keeps its copy until the entry expires, even when another instance changes or deletes the
171
+ key. Keep the TTL short when every instance must see the changes quickly.
172
+ - **`nonBlocking: true`.** The writes and the deletions return at once, without waiting for the stores, and never fail:
173
+ their failures are emitted as events (section 8). With several stores, a read asks all of them at once and returns
174
+ the first entry found, which may not be the first store's.
175
+ - All the stores connect when the cache is created. When one of them fails, the others disconnect, and the creation
176
+ fails with its error.
177
+
178
+ ## 6. The stores
179
+
180
+ | `type` | Where the entries are | Install |
181
+ |---|---|---|
182
+ | `'memory'` | In the memory of the process (the store of a cache without `stores`) | Nothing |
183
+ | `'file'` | In memory, saved to a JSON file | Nothing |
184
+ | `'redis'` | On Redis or Valkey | `redis` (4 or 5) |
185
+ | `'memcache'` | On memcached | `memjs` |
186
+ | `'mongo'` | In a MongoDB collection | `mongodb` |
187
+ | `'sqlite'` | In a table of a SQLite file | `better-sqlite3` (12); nothing on Bun |
188
+ | `'postgres'` | In a PostgreSQL table | `pg` |
189
+ | `'mysql'` | In a MySQL or MariaDB table | `mysql2` |
190
+
191
+ - A store whose package is not installed fails the cache's creation with `CacheDriverNotFoundException`; its message
192
+ names the package to install.
193
+ - Every store but the memory one keeps the values as JSON (section 7).
194
+ - Every store but the memory one takes a `namespace`, which keeps apart the applications that share a server or a
195
+ table: `clear()` then deletes only the entries of the namespace. Without a namespace, `clear()` deletes all the
196
+ entries of the store (for Redis and Memcache, see below).
197
+ - In the SQLite, PostgreSQL and MySQL stores, nothing deletes the expired rows in the background: an expired row stays
198
+ in the table until it is read (it is then deleted), overwritten or cleared.
199
+
200
+ ### Memory
201
+
202
+ | Option | Default | Meaning |
203
+ |---|---|---|
204
+ | `sweepInterval` | `60000` | How often the expired entries are removed, in milliseconds (an expired entry is a miss anyway) |
205
+
206
+ The values are kept as they are, not copied (section 7). Each process has its own entries, lost when it ends.
207
+
208
+ ### File
209
+
210
+ | Option | Default | Meaning |
211
+ |---|---|---|
212
+ | `filename` | required | The JSON file, relative to the working directory; it is created with its directories |
213
+ | `writeDelay` | `100` | How long after a change the file is saved, in milliseconds; the changes made meanwhile are saved together |
214
+ | `expiredCheckDelay` | `3600000` | The minimum time between two removals of the expired entries from the file, in milliseconds |
215
+ | `namespace` | none | |
216
+
217
+ - The entries are in memory. The file is read when the cache is created, and each save rewrites all of it (through a
218
+ temporary file), so the store suits small caches. The last changes are saved when the application stops.
219
+ - A file belongs to one process: another process reads it only when its own cache is created, and each save replaces
220
+ the entries that the other processes saved.
221
+ - A file that is not JSON fails the cache's creation. A failed save is emitted as an `'error'` event.
222
+
223
+ ### Redis
224
+
225
+ | Option | Default | Meaning |
226
+ |---|---|---|
227
+ | `host` | required | |
228
+ | `port` | `6379` | |
229
+ | `database` | `0` | The index of the database |
230
+ | `username`, `password` | none | |
231
+ | `tls` | `false` | Whether to connect with TLS |
232
+ | `namespace` | none | |
233
+ | `keyPrefixSeparator` | `'::'` | The separator that follows the namespace in the keys |
234
+ | `connectTimeout` | `5000` | How long the first connection may take, in milliseconds |
235
+ | `throwOnConnectError` | `true` | Whether a failed first connection fails the cache's creation |
236
+ | `reconnect` | `true` | Whether to reconnect after the connection is lost (after 100 ms per attempt, 3 seconds at most) |
237
+ | `keepAlive` | the driver's | How long the connection stays idle before the first TCP keep-alive probe, in milliseconds |
238
+ | `useUnlink` | `true` | Whether to delete with `UNLINK` rather than `DEL` |
239
+ | `clearBatchSize` | `1000` | How many keys `clear()` scans and deletes at a time |
240
+ | `noNamespaceAffectsAll` | `false` | Whether `clear()` without a namespace empties the whole database (`FLUSHDB`) |
241
+
242
+ - The keys are `{namespace}::{namespace}:{key}` (the format of lakutata 2.x), or the key itself without a namespace.
243
+ Redis deletes the expired entries itself.
244
+ - Without a namespace, `clear()` deletes the string keys of the database that are outside the namespaces (the keys
245
+ without `::`): the namespaced keys and the keys of the other types (hashes, lists…) stay. With
246
+ `noNamespaceAffectsAll: true`, it empties the database.
247
+ - `clear()` scans the keys by type, which needs Redis 6 or later.
248
+ - While the connection is down, the commands fail at once instead of waiting: the reads are misses, the writes fail.
249
+ With `throwOnConnectError: false`, the cache is created even when Redis is down, and keeps trying to connect.
250
+ - The store uses the package `redis`, or `@redis/client` when `redis` is not installed.
251
+
252
+ ### Memcache
253
+
254
+ | Option | Default | Meaning |
255
+ |---|---|---|
256
+ | `host` | required | |
257
+ | `port` | `11211` | |
258
+ | `username`, `password` | none | For a server that requires SASL authentication |
259
+ | `namespace` | none | |
260
+
261
+ - The client connects at its first command: a server that cannot be reached does not fail the cache's creation, but
262
+ its reads are misses and its writes fail.
263
+ - memcached counts the TTLs in seconds: an entry may stay up to two seconds longer on the server, but it is a miss
264
+ once its TTL has passed.
265
+ - `clear()` empties the whole server, whatever the namespace: the entries of the other applications are deleted too.
266
+
267
+ ### MongoDB
268
+
269
+ | Option | Default | Meaning |
270
+ |---|---|---|
271
+ | `host`, `database`, `collection` | required | The collection is created when it is missing |
272
+ | `port` | `27017` | |
273
+ | `username`, `password` | none | The user is authenticated against the `admin` database |
274
+ | `namespace` | none | |
275
+
276
+ - One document per entry. When the cache is created, it creates a unique index on the keys, and a TTL index by which
277
+ MongoDB deletes the expired documents, about once a minute; an expired document is a miss before that.
278
+ - A failed connection fails the cache's creation.
279
+
280
+ ### SQLite
281
+
282
+ | Option | Default | Meaning |
283
+ |---|---|---|
284
+ | `database` | required | The database file, relative to the working directory, in a directory that exists; or `':memory:'` |
285
+ | `table` | required | The table, created when it is missing |
286
+ | `busyTimeout` | `10000` | How long a statement waits for a database locked by another connection, in milliseconds |
287
+ | `namespace` | none | |
288
+
289
+ - The characters of `table` other than letters, digits and `_` are removed.
290
+ - The statements run synchronously: a statement that waits for a locked database blocks the event loop, up to
291
+ `busyTimeout`.
292
+ - `better-sqlite3` is a native addon built for one runtime: an Electron application needs its build for its version of
293
+ Electron. On Bun, the store uses `bun:sqlite` instead.
294
+
295
+ ### PostgreSQL
296
+
297
+ | Option | Default | Meaning |
298
+ |---|---|---|
299
+ | `host`, `database`, `table` | required | |
300
+ | `port` | `5432` | |
301
+ | `schema` | `'public'` | The schema of the table |
302
+ | `username`, `password` | the driver's | Without `username`, the `PGUSER` environment variable, else the user of the process |
303
+ | `maxPoolSize` | `10` | The maximum number of connections |
304
+ | `namespace` | none | |
305
+
306
+ - When the cache is created, it creates the schema and the table when they are missing:
307
+ `(key VARCHAR(255) PRIMARY KEY, value TEXT)`. PostgreSQL folds a plain name to lower case: `table: 'Entries'`
308
+ creates `entries`. A failed connection or creation fails the cache's creation.
309
+ - A key longer than 255 characters, its namespace included, fails its write.
310
+
311
+ ### MySQL
312
+
313
+ | Option | Default | Meaning |
314
+ |---|---|---|
315
+ | `host`, `database`, `table` | required | |
316
+ | `port` | `3306` | |
317
+ | `username`, `password` | the driver's | |
318
+ | `namespace` | none | |
319
+
320
+ - When the cache is created, it creates the table when it is missing:
321
+ `(id VARCHAR(255) PRIMARY KEY, value MEDIUMTEXT)`, its keys case sensitive. A failed connection or creation fails
322
+ the cache's creation.
323
+ - A key longer than 255 characters, its namespace included, fails its write, and a value is limited to 16 MB of JSON.
324
+
325
+ ## 7. What the stores keep
326
+
327
+ The memory store keeps the values themselves: an object changed after it is cached is changed in the cache too, and a
328
+ read returns the cached object. Copy the objects when this matters.
329
+
330
+ The other stores keep the values as JSON, so some values come back different:
331
+
332
+ | Cached | Read back |
333
+ |---|---|
334
+ | A `Date` | Its ISO string (`'2026-01-02T03:04:05.000Z'`) |
335
+ | An instance of a class | A plain object with its fields, without its methods |
336
+ | A `Map` or a `Set` | `{}` |
337
+ | `NaN`, `Infinity` | `null` |
338
+ | A `Buffer` | A `Buffer` |
339
+ | A `bigint` | The write fails with a `TypeError` |
340
+
341
+ With a memory store in front of another store, the same key can return the value itself or its JSON copy, depending on
342
+ the store that holds it. Cache the values that JSON keeps as they are: plain objects, arrays, strings, numbers and
343
+ booleans.
344
+
345
+ ## 8. Events and errors
346
+
347
+ | Event | When | The listener receives |
348
+ |---|---|---|
349
+ | `'set'` | `set()` or `wrap()` wrote a value, or failed to | `{key, value, error?}` |
350
+ | `'del'` | `del()` deleted a key, or failed to | `{key, error?}` |
351
+ | `'clear'` | `clear()` finished | Nothing, or the error |
352
+ | `'refresh'` | A background refresh of `wrap()` finished | `{key, value, error?}`: the new value, or the old one and the error |
353
+ | `'error'` | A store failed and no other event reports it (see below) | The error |
354
+
355
+ ```typescript
356
+ import type {Cacher, OnRefreshEventData} from 'lakutata/com/cacher'
357
+
358
+ declare const cache: Cacher
359
+
360
+ cache.on('refresh', (data: OnRefreshEventData): void => {
361
+ if (data.error) console.warn(`Refreshing ${data.key} failed, the cached value is kept`)
362
+ })
363
+ //The failed reads, the lost connections, the failed saves of the file…
364
+ cache.on('error', (error: Error): void => console.error('Cache store error', error))
365
+ ```
366
+
367
+ - The `'error'` event reports the failed reads (counted as misses), the failures of `multipleSet()` and
368
+ `multipleDel()` and of the copies made by `wrap()`, the lost connections and the failed saves of the file store.
369
+ Without a listener, these errors are dropped: listen to it to know when a store is failing.
370
+ - The listeners are called asynchronously, after the event is emitted.
371
+ - Without `nonBlocking`, a store that fails to write or delete fails the call with the error of its driver. With
372
+ `nonBlocking`, `'set'` and `'del'` are emitted before the stores are written, and a failure emits a second event that
373
+ carries the error.
374
+
375
+ | Error | When |
376
+ |---|---|
377
+ | `InvalidObjectOptionsException` | The options are invalid, when the application starts |
378
+ | `CacheDriverNotFoundException` (`E_CACHE_DRIVER_NOT_FOUND`) | The package of a store is not installed, when the cache is created |
379
+ | The errors of the drivers | A store cannot connect when the cache is created, or fails a write or a deletion |