dirsql 0.3.65 → 0.3.66
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/docs/getting-started.md +217 -150
- package/docs/index.md +0 -1
- package/docs/migrations.md +0 -3
- package/package.json +11 -11
- package/docs/api/index.md +0 -238
- package/docs/guide/async.md +0 -268
- package/docs/guide/crdt.md +0 -161
- package/docs/guide/persistence.md +0 -177
- package/docs/guide/querying.md +0 -221
- package/docs/guide/tables.md +0 -269
- package/docs/guide/watching.md +0 -273
package/docs/guide/watching.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
canonical: https://thekevinscott.github.io/dirsql/guide/watching
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# File Watching
|
|
6
|
-
|
|
7
|
-
> Online: <https://thekevinscott.github.io/dirsql/guide/watching>
|
|
8
|
-
|
|
9
|
-
`dirsql` can monitor the filesystem for changes and emit events when rows are inserted, updated, or deleted. This is useful for building reactive applications that respond to file changes in real time.
|
|
10
|
-
|
|
11
|
-
::: tip From the CLI
|
|
12
|
-
The `dirsql` HTTP server streams the same events over [`GET /events`](../cli/http-api.md#get-events) as a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. Each `data:` payload uses the same JSON schema described in [Event types](#event-types) below. See the [CLI section](../cli/) for the full server setup.
|
|
13
|
-
:::
|
|
14
|
-
|
|
15
|
-
## Starting a watch stream
|
|
16
|
-
|
|
17
|
-
::: code-group
|
|
18
|
-
|
|
19
|
-
```python [Python]
|
|
20
|
-
from dirsql import DirSQL, Table
|
|
21
|
-
import json
|
|
22
|
-
|
|
23
|
-
db = DirSQL(
|
|
24
|
-
"./my-project",
|
|
25
|
-
tables=[
|
|
26
|
-
Table(
|
|
27
|
-
ddl="CREATE TABLE comments (id TEXT, body TEXT, author TEXT)",
|
|
28
|
-
glob="comments/**/*.json",
|
|
29
|
-
extract=lambda path: [json.loads(open(path, encoding="utf-8").read())],
|
|
30
|
-
),
|
|
31
|
-
],
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
async for event in db.watch():
|
|
35
|
-
print(f"{event.action} on {event.table}: {event.row}")
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
```rust [Rust]
|
|
39
|
-
// `StreamExt` (for `.next()`) comes from the `futures` crate. dirsql only
|
|
40
|
-
// depends on `futures` under its `cli` feature, so add it to your project:
|
|
41
|
-
//
|
|
42
|
-
// cargo add futures
|
|
43
|
-
use dirsql::{DirSQL, RowEvent, Table, Value};
|
|
44
|
-
use futures::StreamExt;
|
|
45
|
-
use std::collections::HashMap;
|
|
46
|
-
|
|
47
|
-
// See `row_from_json` in getting-started.md for a reusable helper.
|
|
48
|
-
fn row_from_json(raw: &str) -> HashMap<String, Value> {
|
|
49
|
-
let v: serde_json::Value = serde_json::from_str(raw).unwrap();
|
|
50
|
-
let serde_json::Value::Object(obj) = v else { return HashMap::new() };
|
|
51
|
-
obj.into_iter()
|
|
52
|
-
.map(|(k, val)| {
|
|
53
|
-
let v = match val {
|
|
54
|
-
serde_json::Value::String(s) => Value::Text(s),
|
|
55
|
-
serde_json::Value::Number(n) => n
|
|
56
|
-
.as_i64()
|
|
57
|
-
.map(Value::Integer)
|
|
58
|
-
.unwrap_or_else(|| Value::Real(n.as_f64().unwrap_or(0.0))),
|
|
59
|
-
serde_json::Value::Bool(b) => Value::Integer(b as i64),
|
|
60
|
-
serde_json::Value::Null => Value::Null,
|
|
61
|
-
other => Value::Text(other.to_string()),
|
|
62
|
-
};
|
|
63
|
-
(k, v)
|
|
64
|
-
})
|
|
65
|
-
.collect()
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let db = DirSQL::new(
|
|
69
|
-
"./my-project",
|
|
70
|
-
vec![
|
|
71
|
-
Table::new(
|
|
72
|
-
"CREATE TABLE comments (id TEXT, body TEXT, author TEXT)",
|
|
73
|
-
"comments/**/*.json",
|
|
74
|
-
|path| vec![row_from_json(&std::fs::read_to_string(path).unwrap())],
|
|
75
|
-
),
|
|
76
|
-
],
|
|
77
|
-
)?;
|
|
78
|
-
|
|
79
|
-
let mut stream = db.watch()?;
|
|
80
|
-
while let Some(event) = stream.next().await {
|
|
81
|
-
match event {
|
|
82
|
-
RowEvent::Insert { table, row, file_path } => {
|
|
83
|
-
println!("insert on {table} ({file_path}): {row:?}")
|
|
84
|
-
}
|
|
85
|
-
RowEvent::Update { table, old_row, new_row, file_path } => {
|
|
86
|
-
println!("update on {table} ({file_path}): {old_row:?} -> {new_row:?}")
|
|
87
|
-
}
|
|
88
|
-
RowEvent::Delete { table, row, file_path } => {
|
|
89
|
-
println!("delete on {table} ({file_path}): {row:?}")
|
|
90
|
-
}
|
|
91
|
-
RowEvent::Error { table, file_path, error } => {
|
|
92
|
-
println!("error on {table:?} {file_path:?}: {error}")
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
```typescript [TypeScript]
|
|
99
|
-
import { readFileSync } from 'node:fs';
|
|
100
|
-
import { DirSQL, type TableDef } from 'dirsql';
|
|
101
|
-
|
|
102
|
-
const tables: TableDef[] = [
|
|
103
|
-
{
|
|
104
|
-
ddl: 'CREATE TABLE comments (id TEXT, body TEXT, author TEXT)',
|
|
105
|
-
glob: 'comments/**/*.json',
|
|
106
|
-
extract: (path) => [JSON.parse(readFileSync(path, 'utf8'))],
|
|
107
|
-
},
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
const db = new DirSQL({ root: './my-project', tables });
|
|
111
|
-
|
|
112
|
-
for await (const event of db.watch()) {
|
|
113
|
-
console.log(`${event.action} on ${event.table}:`, event.row);
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
:::
|
|
118
|
-
|
|
119
|
-
See [Async API](./async.md) for full details on the async `DirSQL` API (Python).
|
|
120
|
-
|
|
121
|
-
## Event types
|
|
122
|
-
|
|
123
|
-
Each event is a `RowEvent` object with these attributes:
|
|
124
|
-
|
|
125
|
-
### `insert`
|
|
126
|
-
|
|
127
|
-
A new row was added. This happens when a new file is created or an existing file gains additional rows.
|
|
128
|
-
|
|
129
|
-
::: code-group
|
|
130
|
-
|
|
131
|
-
```python [Python]
|
|
132
|
-
event.action # "insert"
|
|
133
|
-
event.table # "comments"
|
|
134
|
-
event.row # {"id": "abc", "body": "new comment", "author": "alice"}
|
|
135
|
-
event.old_row # None
|
|
136
|
-
event.file_path # "comments/abc/index.json"
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
```rust [Rust]
|
|
140
|
-
// RowEvent is an enum; match on the variant to destructure its fields.
|
|
141
|
-
RowEvent::Insert {
|
|
142
|
-
table, // "comments"
|
|
143
|
-
row, // {"id": "abc", "body": "new comment", "author": "alice"}
|
|
144
|
-
file_path, // "comments/abc/index.json"
|
|
145
|
-
} => { /* ... */ }
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
```typescript [TypeScript]
|
|
149
|
-
event.action // 'insert'
|
|
150
|
-
event.table // 'comments'
|
|
151
|
-
event.row // { id: 'abc', body: 'new comment', author: 'alice' }
|
|
152
|
-
event.oldRow // undefined
|
|
153
|
-
event.filePath // 'comments/abc/index.json'
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
:::
|
|
157
|
-
|
|
158
|
-
### `update`
|
|
159
|
-
|
|
160
|
-
An existing row was modified. `dirsql` diffs the old and new rows extracted from the file to detect changes.
|
|
161
|
-
|
|
162
|
-
::: code-group
|
|
163
|
-
|
|
164
|
-
```python [Python]
|
|
165
|
-
event.action # "update"
|
|
166
|
-
event.table # "comments"
|
|
167
|
-
event.row # {"id": "abc", "body": "edited comment", "author": "alice"}
|
|
168
|
-
event.old_row # {"id": "abc", "body": "original comment", "author": "alice"}
|
|
169
|
-
event.file_path # "comments/abc/index.json"
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
```rust [Rust]
|
|
173
|
-
RowEvent::Update {
|
|
174
|
-
table, // "comments"
|
|
175
|
-
old_row, // {"id": "abc", "body": "original comment", "author": "alice"}
|
|
176
|
-
new_row, // {"id": "abc", "body": "edited comment", "author": "alice"}
|
|
177
|
-
file_path, // "comments/abc/index.json"
|
|
178
|
-
} => { /* ... */ }
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
```typescript [TypeScript]
|
|
182
|
-
event.action // 'update'
|
|
183
|
-
event.table // 'comments'
|
|
184
|
-
event.row // { id: 'abc', body: 'edited comment', author: 'alice' }
|
|
185
|
-
event.oldRow // { id: 'abc', body: 'original comment', author: 'alice' }
|
|
186
|
-
event.filePath // 'comments/abc/index.json'
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
:::
|
|
190
|
-
|
|
191
|
-
### `delete`
|
|
192
|
-
|
|
193
|
-
A row was removed. This happens when a file is deleted or a file is modified to contain fewer rows.
|
|
194
|
-
|
|
195
|
-
::: code-group
|
|
196
|
-
|
|
197
|
-
```python [Python]
|
|
198
|
-
event.action # "delete"
|
|
199
|
-
event.table # "comments"
|
|
200
|
-
event.row # {"id": "abc", "body": "deleted comment", "author": "alice"}
|
|
201
|
-
event.old_row # None
|
|
202
|
-
event.file_path # "comments/abc/index.json"
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
```rust [Rust]
|
|
206
|
-
RowEvent::Delete {
|
|
207
|
-
table, // "comments"
|
|
208
|
-
row, // {"id": "abc", "body": "deleted comment", "author": "alice"}
|
|
209
|
-
file_path, // "comments/abc/index.json"
|
|
210
|
-
} => { /* ... */ }
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
```typescript [TypeScript]
|
|
214
|
-
event.action // 'delete'
|
|
215
|
-
event.table // 'comments'
|
|
216
|
-
event.row // { id: 'abc', body: 'deleted comment', author: 'alice' }
|
|
217
|
-
event.oldRow // undefined
|
|
218
|
-
event.filePath // 'comments/abc/index.json'
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
:::
|
|
222
|
-
|
|
223
|
-
### `error`
|
|
224
|
-
|
|
225
|
-
An error occurred while processing a file change. The file was modified but the extract function failed, or the file could not be read.
|
|
226
|
-
|
|
227
|
-
::: code-group
|
|
228
|
-
|
|
229
|
-
```python [Python]
|
|
230
|
-
event.action # "error"
|
|
231
|
-
event.table # "comments" (or None if the error isn't tied to a table)
|
|
232
|
-
event.error # "Extract error: ..."
|
|
233
|
-
event.file_path # "comments/abc/index.json"
|
|
234
|
-
event.row # None
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
```rust [Rust]
|
|
238
|
-
// `table` is `Option<String>`: `Some("comments")` when the failing
|
|
239
|
-
// file matched a table's glob; `None` for errors that aren't tied
|
|
240
|
-
// to a specific table (e.g. a watch-channel failure).
|
|
241
|
-
RowEvent::Error {
|
|
242
|
-
table, // Some("comments")
|
|
243
|
-
file_path, // PathBuf, e.g. "comments/abc/index.json"
|
|
244
|
-
error, // "Extract error: ..."
|
|
245
|
-
} => { /* ... */ }
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
```typescript [TypeScript]
|
|
249
|
-
event.action // 'error'
|
|
250
|
-
event.table // 'comments' (or null if the error isn't tied to a table)
|
|
251
|
-
event.error // 'Extract error: ...'
|
|
252
|
-
event.filePath // 'comments/abc/index.json'
|
|
253
|
-
event.row // undefined
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
:::
|
|
257
|
-
|
|
258
|
-
## How diffing works
|
|
259
|
-
|
|
260
|
-
When a file changes, `dirsql`:
|
|
261
|
-
|
|
262
|
-
1. Re-reads the file and calls the extract function to get new rows
|
|
263
|
-
2. Compares new rows against the previously extracted rows for that file
|
|
264
|
-
3. Emits insert, update, and delete events based on the diff
|
|
265
|
-
4. Updates the in-memory database to reflect the new state
|
|
266
|
-
|
|
267
|
-
Row identity is determined by position (row index within the file). If a file previously produced 3 rows and now produces 2, the first two rows are compared for updates and the third is emitted as a delete.
|
|
268
|
-
|
|
269
|
-
## Filesystem events
|
|
270
|
-
|
|
271
|
-
Under the hood, `dirsql` uses the `notify` crate (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) to receive filesystem events. Events are coalesced and filtered through the table matcher before being processed.
|
|
272
|
-
|
|
273
|
-
Files that do not match any table glob or that match an ignore pattern are silently skipped.
|