@leemour/max-cli 0.1.0 → 0.3.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 +28 -12
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +3 -0
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/schema.d.ts +13 -1
- package/dist/cache/schema.d.ts.map +1 -1
- package/dist/cache/schema.js +159 -8
- package/dist/cache/schema.js.map +1 -1
- package/dist/cache/store.d.ts +115 -4
- package/dist/cache/store.d.ts.map +1 -1
- package/dist/cache/store.js +248 -27
- package/dist/cache/store.js.map +1 -1
- package/dist/client.d.ts +91 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +371 -33
- package/dist/client.js.map +1 -1
- package/dist/commands/chats.d.ts.map +1 -1
- package/dist/commands/chats.js +25 -8
- package/dist/commands/chats.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +51 -5
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/context.d.ts +4 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +4 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +43 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +10 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +22 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/me.d.ts +3 -0
- package/dist/commands/me.d.ts.map +1 -0
- package/dist/commands/me.js +18 -0
- package/dist/commands/me.js.map +1 -0
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +82 -3
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/paging.d.ts +41 -0
- package/dist/commands/paging.d.ts.map +1 -0
- package/dist/commands/paging.js +54 -0
- package/dist/commands/paging.js.map +1 -0
- package/dist/commands/send.d.ts +10 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +30 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +3 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/config.d.ts +16 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -1
- package/dist/config.js.map +1 -1
- package/dist/domain/map.d.ts +1 -0
- package/dist/domain/map.d.ts.map +1 -1
- package/dist/domain/map.js +44 -3
- package/dist/domain/map.js.map +1 -1
- package/dist/domain/models.d.ts +54 -0
- package/dist/domain/models.d.ts.map +1 -1
- package/dist/generated/operations.generated.d.ts +1 -0
- package/dist/generated/operations.generated.d.ts.map +1 -1
- package/dist/output.d.ts +7 -4
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +8 -11
- package/dist/output.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +4 -2
- package/dist/program.js.map +1 -1
- package/dist/protocol/connection.d.ts +10 -0
- package/dist/protocol/connection.d.ts.map +1 -1
- package/dist/protocol/connection.js +11 -3
- package/dist/protocol/connection.js.map +1 -1
- package/dist/protocol/session.d.ts +54 -0
- package/dist/protocol/session.d.ts.map +1 -0
- package/dist/protocol/session.js +57 -0
- package/dist/protocol/session.js.map +1 -0
- package/dist/rendering/messages.d.ts +19 -0
- package/dist/rendering/messages.d.ts.map +1 -0
- package/dist/rendering/messages.js +137 -0
- package/dist/rendering/messages.js.map +1 -0
- package/dist/runs/events.d.ts +1 -1
- package/dist/runs/recording.d.ts +2 -2
- package/dist/runs/recording.d.ts.map +1 -1
- package/dist/runs/recording.js +3 -3
- package/dist/runs/recording.js.map +1 -1
- package/dist/session/adopt.d.ts +22 -0
- package/dist/session/adopt.d.ts.map +1 -0
- package/dist/session/adopt.js +29 -0
- package/dist/session/adopt.js.map +1 -0
- package/dist/session/handshake.d.ts +14 -1
- package/dist/session/handshake.d.ts.map +1 -1
- package/dist/session/handshake.js +5 -5
- package/dist/session/handshake.js.map +1 -1
- package/dist/spec/operations/auth.d.ts +42 -0
- package/dist/spec/operations/auth.d.ts.map +1 -0
- package/dist/spec/operations/session.d.ts +16 -0
- package/dist/spec/operations/session.d.ts.map +1 -1
- package/dist/spec/operations/session.js +21 -1
- package/dist/spec/operations/session.js.map +1 -1
- package/dist/testing/sandbox.d.ts +2 -0
- package/dist/testing/sandbox.d.ts.map +1 -0
- package/dist/testing/sandbox.js +24 -0
- package/dist/testing/sandbox.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -14,9 +14,6 @@ max chats list --limit 5
|
|
|
14
14
|
max messages list "Иван Петров"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
> ⚠ **Версия `0.1.0` ещё не опубликована.** Под именем `@leemour/max-cli` на npm пока нет ничего.
|
|
18
|
-
> Пока — [установка из исходников](docs/installation.md).
|
|
19
|
-
|
|
20
17
|
## Что это даёт
|
|
21
18
|
|
|
22
19
|
- **Один запуск — одна операция.** Соединиться, сделать, напечатать, отключиться. Ничего не висит
|
|
@@ -33,12 +30,15 @@ max messages list "Иван Петров"
|
|
|
33
30
|
MAX схлопывает дубль — это измерено на реальном аккаунте, а не предположено.
|
|
34
31
|
- **Имя чата не угадывается.** Часть названия, подходящая к двум чатам, — это отказ со списком
|
|
35
32
|
кандидатов. Отправить не в тот разговор нельзя отменить.
|
|
36
|
-
- **Диагностика, в которой нет содержимого.** `--
|
|
33
|
+
- **Диагностика, в которой нет содержимого.** `--trace` показывает по строке на запрос,
|
|
37
34
|
`--record` кладёт их в каталог запуска на 30 дней. Операция, опкод, идентификаторы, байты,
|
|
38
35
|
длительности — да. Название чата, имя, текст, телефон, токен — никогда, ни обрезанными, ни
|
|
39
36
|
хэшем.
|
|
40
37
|
- **По умолчанию не записывается ничего.** Мессенджер, который сам собирает каталог с историей
|
|
41
38
|
того, кого вы читали, — это чужая жизнь в чужом логе; запись включается флагом.
|
|
39
|
+
- **Каждый вход спрашивает только то, что изменилось.** Метка времени прошлого входа уходит
|
|
40
|
+
обратно, и MAX присылает дельту, а не весь список заново — так что контакты остаются свежими,
|
|
41
|
+
не стоя ничего.
|
|
42
42
|
- **Два рантайма, и это проверяется.** Node 22+ и Bun; под обоими в CI выполняется собранная
|
|
43
43
|
команда, а не только проверка типов.
|
|
44
44
|
- **Мы выглядим как официальный клиент.** На проводе нет ни нашего имени, ни своего user-agent:
|
|
@@ -59,11 +59,16 @@ max messages list "Иван Петров"
|
|
|
59
59
|
Пакет — **`@leemour/max-cli`**, команда, которую он ставит, — **`max`**. Имя без области занято
|
|
60
60
|
чужим пакетом с 2018 года.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Запустить, ничего не устанавливая:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx @leemour/max-cli --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Поставить насовсем:
|
|
63
69
|
|
|
64
70
|
```sh
|
|
65
|
-
|
|
66
|
-
cd max-cli && pnpm install && pnpm build && pnpm link --global
|
|
71
|
+
npm install -g @leemour/max-cli
|
|
67
72
|
max --version # 0.1.0
|
|
68
73
|
```
|
|
69
74
|
|
|
@@ -93,16 +98,24 @@ export MAX_PROFILE=personal # или на всю сессию оболочки
|
|
|
93
98
|
|
|
94
99
|
```sh
|
|
95
100
|
max chats list --limit 5
|
|
96
|
-
max contacts list
|
|
101
|
+
max contacts list # люди, с кем есть личный чат
|
|
97
102
|
max messages list 0 --limit 50 # по id чата
|
|
98
103
|
max messages list "Иван Петров" # или по части названия
|
|
99
104
|
max messages send 0 "текст"
|
|
100
105
|
```
|
|
101
106
|
|
|
107
|
+
Списки листаются одинаково везде, а история — по времени, потому что она к нему и привязана:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
max contacts list --limit 5 --page 2 # шестой по десятый
|
|
111
|
+
max contacts list --all --order name # всё, по алфавиту
|
|
112
|
+
max messages list 0 --before 2026-09-20T01:00:00Z
|
|
113
|
+
```
|
|
114
|
+
|
|
102
115
|
Что делала команда:
|
|
103
116
|
|
|
104
117
|
```sh
|
|
105
|
-
max chats list --
|
|
118
|
+
max chats list --trace # показать по строке на запрос, ничего не сохраняя
|
|
106
119
|
max chats list --record # сохранить, ничего не показывая
|
|
107
120
|
max runs list # что делалось, новое сверху
|
|
108
121
|
max runs show <id>
|
|
@@ -126,8 +139,11 @@ max chats list --json
|
|
|
126
139
|
```
|
|
127
140
|
|
|
128
141
|
`--json` — это **ровно одно значение JSON на stdout и больше ничего**: ни спиннера, ни галочки, ни
|
|
129
|
-
предупреждения. То же самое включается само, когда stdout не терминал.
|
|
130
|
-
|
|
142
|
+
предупреждения. То же самое включается само, когда stdout не терминал. Любой список отвечает одним
|
|
143
|
+
объектом — `{ "items": […], "page": 1, "limit": 20, "hasMore": true }` — и `--all` с `--offline`
|
|
144
|
+
отвечают **тем же**, чтобы разбирающему ответ не приходилось ветвиться на две формы.
|
|
145
|
+
|
|
146
|
+
Ошибка уходит на stderr, а stdout остаётся пустым, поэтому отказ невозможно принять за результат:
|
|
131
147
|
|
|
132
148
|
```json
|
|
133
149
|
{"error":{"code":"authentication_error","message":"no session for profile \"default\" — run `max session start`"}}
|
|
@@ -144,7 +160,7 @@ stdout остаётся пустым, поэтому отказ невозмож
|
|
|
144
160
|
| [docs/usage.md](docs/usage.md) | вход, чтение, отправка, машинный режим |
|
|
145
161
|
| [docs/sessions.md](docs/sessions.md) | токен, ключница, профили |
|
|
146
162
|
| [docs/configuration.md](docs/configuration.md) | настройки, переменные, порядок разрешения |
|
|
147
|
-
| [docs/diagnostics.md](docs/diagnostics.md) | `--
|
|
163
|
+
| [docs/diagnostics.md](docs/diagnostics.md) | `--trace`, `--record`, `max runs` |
|
|
148
164
|
| [docs/security.md](docs/security.md) | что пишется на диск, а что никогда |
|
|
149
165
|
| [docs/troubleshooting.md](docs/troubleshooting.md) | по симптому: что делать, когда не работает |
|
|
150
166
|
| [docs/commands.md](docs/commands.md) | каждая команда и опция — **генерируется** из программы |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,YAAY,CAAA;AAEvD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,MAAM,EACf,qBAAkC,mBAAwB,KACzD,OAAO,CAAC,UAAU,GAAG,SAAS,CAehC,CAAA"}
|
package/dist/cache/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { chmodSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolvePaths } from "@leemour/cli-core";
|
|
4
4
|
import { openCache } from "./open.js";
|
|
5
|
+
import { NewerCacheError } from "./schema.js";
|
|
5
6
|
import { openStore } from "./store.js";
|
|
6
7
|
/**
|
|
7
8
|
* Opens this profile's record, **or gives up without failing the command**.
|
|
@@ -27,6 +28,8 @@ export const openProfileCache = async (profile, { env = process.env, onProblem }
|
|
|
27
28
|
};
|
|
28
29
|
/** The reason, never the path — a profile name and a home directory are nobody else's business. */
|
|
29
30
|
const asReason = (error) => {
|
|
31
|
+
if (error instanceof NewerCacheError)
|
|
32
|
+
return error.message;
|
|
30
33
|
const code = error?.code;
|
|
31
34
|
return typeof code === "string" ? code : error instanceof Error ? error.name : "an unknown problem";
|
|
32
35
|
};
|
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,OAAe,EACf,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,KAA0B,EAAE,EACzB,EAAE;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,2FAA2F;QAC3F,uBAAuB;QACvB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC5D,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,EAAE,CAAC,6DAA6D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC3F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,mGAAmG;AACnG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC1C,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAA;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACrG,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,OAAe,EACf,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,KAA0B,EAAE,EACzB,EAAE;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,2FAA2F;QAC3F,uBAAuB;QACvB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC5D,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,EAAE,CAAC,6DAA6D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC3F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,mGAAmG;AACnG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC1C,IAAI,KAAK,YAAY,eAAe;QAAE,OAAO,KAAK,CAAC,OAAO,CAAA;IAC1D,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAA;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACrG,CAAC,CAAA"}
|
package/dist/cache/schema.d.ts
CHANGED
|
@@ -3,12 +3,24 @@ import type { CacheDatabase } from "./driver.js";
|
|
|
3
3
|
* **Raise this on every change to the statements below.** The second schema change is the one that
|
|
4
4
|
* corrupts somebody's file, because the first is always made while the only copy is your own.
|
|
5
5
|
*/
|
|
6
|
-
export declare const SCHEMA_VERSION =
|
|
6
|
+
export declare const SCHEMA_VERSION = 4;
|
|
7
7
|
/**
|
|
8
8
|
* Brings a file up to date, and **refuses a file from the future** rather than writing to it.
|
|
9
9
|
*
|
|
10
10
|
* A newer `max` may have added a column this one does not know about. Reading it is survivable;
|
|
11
11
|
* writing to it is how one version quietly destroys what another stored.
|
|
12
|
+
*
|
|
13
|
+
* **An older file is rebuilt, not altered.** Everything in here comes back from MAX, so a
|
|
14
|
+
* column-by-column migration would be code that runs once, is tested never, and is how the second
|
|
15
|
+
* schema change corrupts somebody's file. What a rebuild costs is one full login — which is what
|
|
16
|
+
* every command did before the delta sync existed.
|
|
17
|
+
*
|
|
18
|
+
* ⚠ **This stops being the right answer the day the database holds something MAX cannot re-send.**
|
|
19
|
+
* Contacts that are in no chat would be exactly that (`RES-7`), and the day they arrive this
|
|
20
|
+
* becomes `ALTER TABLE … ADD COLUMN` and this paragraph gets rewritten.
|
|
12
21
|
*/
|
|
22
|
+
/** Ours, so its message is known to hold no path and can be shown as it is. */
|
|
23
|
+
export declare class NewerCacheError extends Error {
|
|
24
|
+
}
|
|
13
25
|
export declare const migrate: (database: CacheDatabase) => void;
|
|
14
26
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;AAqM/B;;;;;;;;;;;;;;GAcG;AACH,+EAA+E;AAC/E,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C,eAAO,MAAM,OAAO,GAAI,UAAU,aAAa,KAAG,IAgBjD,CAAA"}
|
package/dist/cache/schema.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* **Raise this on every change to the statements below.** The second schema change is the one that
|
|
3
3
|
* corrupts somebody's file, because the first is always made while the only copy is your own.
|
|
4
4
|
*/
|
|
5
|
-
export const SCHEMA_VERSION =
|
|
5
|
+
export const SCHEMA_VERSION = 4;
|
|
6
6
|
/**
|
|
7
7
|
* Everything the cache holds, and the indexes are part of it rather than an afterthought — each
|
|
8
8
|
* one exists for a query that is actually made.
|
|
@@ -25,12 +25,48 @@ const STATEMENTS = [
|
|
|
25
25
|
fetched_at INTEGER NOT NULL
|
|
26
26
|
)`,
|
|
27
27
|
`CREATE INDEX IF NOT EXISTS chats_by_recency ON chats (last_message_at DESC)`,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
name
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
/**
|
|
29
|
+
* **Everyone MAX has named for us, whatever the reason** — not an address book and not a contact
|
|
30
|
+
* list. A group member gets a row here because their name has to live somewhere; whether they
|
|
31
|
+
* are a *contact* is a question the query answers (a dialog with them exists), never a flag
|
|
32
|
+
* somebody has to maintain (`NEED-105`).
|
|
33
|
+
*
|
|
34
|
+
* `last_messaged_at` is stored rather than derived because it is the default order: recomputing
|
|
35
|
+
* `MAX(chats.last_message_at)` per person on every listing is the work this table exists to
|
|
36
|
+
* avoid. `source` records how we met them and is diagnostic — it is not what the filter reads.
|
|
37
|
+
*/
|
|
38
|
+
`CREATE TABLE IF NOT EXISTS people (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
name TEXT,
|
|
41
|
+
username TEXT,
|
|
42
|
+
description TEXT,
|
|
43
|
+
last_messaged_at INTEGER,
|
|
44
|
+
source TEXT NOT NULL,
|
|
45
|
+
fetched_at INTEGER NOT NULL
|
|
46
|
+
)`,
|
|
47
|
+
`CREATE INDEX IF NOT EXISTS people_by_recency ON people (last_messaged_at DESC)`,
|
|
48
|
+
/**
|
|
49
|
+
* Who is in what. It is what the client used to work out in memory and throw away, and it is how
|
|
50
|
+
* "which groups do I share with this person" becomes one indexed lookup — every chat in this
|
|
51
|
+
* database is one the owner is in, so the chats of a person *are* the shared set.
|
|
52
|
+
*/
|
|
53
|
+
`CREATE TABLE IF NOT EXISTS chat_members (
|
|
54
|
+
chat_id TEXT NOT NULL,
|
|
55
|
+
person_id TEXT NOT NULL,
|
|
56
|
+
PRIMARY KEY (chat_id, person_id)
|
|
57
|
+
)`,
|
|
58
|
+
`CREATE INDEX IF NOT EXISTS members_by_person ON chat_members (person_id)`,
|
|
59
|
+
/**
|
|
60
|
+
* Where the last login's `time` is kept, so the next one can ask for the delta instead of the
|
|
61
|
+
* whole collection. One row, and the `CHECK` is what keeps it that way.
|
|
62
|
+
*
|
|
63
|
+
* It lives here rather than in the profile's state file because a marker that outlives the rows
|
|
64
|
+
* it describes is a promise this database cannot keep: `max cache clear` has to forget both in
|
|
65
|
+
* the same breath, and a marker in another file would survive it.
|
|
66
|
+
*/
|
|
67
|
+
`CREATE TABLE IF NOT EXISTS sync_marker (
|
|
68
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
69
|
+
marker INTEGER NOT NULL
|
|
34
70
|
)`,
|
|
35
71
|
`CREATE TABLE IF NOT EXISTS messages (
|
|
36
72
|
chat_id TEXT NOT NULL,
|
|
@@ -42,11 +78,14 @@ const STATEMENTS = [
|
|
|
42
78
|
text TEXT NOT NULL,
|
|
43
79
|
outgoing INTEGER,
|
|
44
80
|
attachments TEXT NOT NULL,
|
|
81
|
+
link TEXT,
|
|
45
82
|
fetched_at INTEGER NOT NULL,
|
|
46
83
|
PRIMARY KEY (chat_id, id)
|
|
47
84
|
)`,
|
|
48
85
|
`CREATE INDEX IF NOT EXISTS messages_by_time ON messages (chat_id, time DESC)`,
|
|
49
86
|
`CREATE INDEX IF NOT EXISTS messages_by_update ON messages (chat_id, update_time)`,
|
|
87
|
+
/** `messages list --before <id>` resolves an id to its `time`, without knowing which chat. */
|
|
88
|
+
`CREATE INDEX IF NOT EXISTS messages_by_id ON messages (id)`,
|
|
50
89
|
/**
|
|
51
90
|
* The windows we claim to hold **completely**, which is what makes "absent means deleted" safe
|
|
52
91
|
* to believe. A message outside every range is a message we have never looked for; a message
|
|
@@ -68,6 +107,77 @@ const STATEMENTS = [
|
|
|
68
107
|
kind TEXT PRIMARY KEY,
|
|
69
108
|
at INTEGER NOT NULL
|
|
70
109
|
)`,
|
|
110
|
+
/**
|
|
111
|
+
* **Search, as three indexes over tables that already hold the text.**
|
|
112
|
+
*
|
|
113
|
+
* `content=` means FTS5 stores the index and **not a second copy of the words** — it reads the
|
|
114
|
+
* columns back from the base table by rowid. That matters twice: the file does not double, and
|
|
115
|
+
* message bodies do not get a second home in it.
|
|
116
|
+
*
|
|
117
|
+
* ⚠ **`trigram`, not `unicode61`, and the difference is the whole point.** Measured 2026-09-22
|
|
118
|
+
* on both runtimes: `unicode61` matches whole words or prefixes, so searching `етро` finds
|
|
119
|
+
* nothing, while `trigram` matches inside a word and finds `Иван Петров`. `chats.resolve`
|
|
120
|
+
* already matches with `includes()`, so substring is the behaviour that was already promised.
|
|
121
|
+
*
|
|
122
|
+
* Both tokenizers fold case for any alphabet, which `LIKE`, `lower()` and `COLLATE NOCASE` do
|
|
123
|
+
* **not** — those are ASCII-only, and a name search built on them silently misses half a Russian
|
|
124
|
+
* address book. That measurement is why this is FTS5 at all.
|
|
125
|
+
*
|
|
126
|
+
* ⚠ **A trigram index cannot answer a query shorter than three characters.** It returns nothing
|
|
127
|
+
* rather than failing, so the caller refuses such a query instead of printing an empty list —
|
|
128
|
+
* `src/client.ts`, and it refuses on every path so the answer cannot depend on whether a cache
|
|
129
|
+
* happens to exist.
|
|
130
|
+
*/
|
|
131
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS chats_fts USING fts5(title, content='chats', tokenize='trigram')`,
|
|
132
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS people_fts USING fts5(name, username, content='people', tokenize='trigram')`,
|
|
133
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(text, content='messages', tokenize='trigram')`,
|
|
134
|
+
/**
|
|
135
|
+
* **Triggers, because an external-content index does not follow its table on its own.**
|
|
136
|
+
*
|
|
137
|
+
* Measured: rename a chat with no trigger in place and a search for the *old* title still
|
|
138
|
+
* matches the row, handing back the new name. Nothing errors; the index is simply a lie. And
|
|
139
|
+
* SQLite reuses a freed rowid, so a deleted chat can bequeath its words to the next one.
|
|
140
|
+
*
|
|
141
|
+
* They are triggers rather than writes in TypeScript because there is more than one write path
|
|
142
|
+
* — the chat list, the people upsert, the delta merge, the message window — and the failure of
|
|
143
|
+
* forgetting one is invisible.
|
|
144
|
+
*
|
|
145
|
+
* ⚠ Verified on this project's real statements, not on a plain `UPDATE`: our writes are
|
|
146
|
+
* `INSERT … ON CONFLICT DO UPDATE`, and `people` resolves its columns with `coalesce`, so the
|
|
147
|
+
* trigger has to index the row that results rather than the values that arrived. `new.*` in an
|
|
148
|
+
* `AFTER UPDATE` trigger is the finished row, which is what makes a re-sent person with no name
|
|
149
|
+
* keep the name we already had.
|
|
150
|
+
*/
|
|
151
|
+
`CREATE TRIGGER IF NOT EXISTS chats_fts_ai AFTER INSERT ON chats BEGIN
|
|
152
|
+
INSERT INTO chats_fts(rowid, title) VALUES (new.rowid, new.title);
|
|
153
|
+
END`,
|
|
154
|
+
`CREATE TRIGGER IF NOT EXISTS chats_fts_ad AFTER DELETE ON chats BEGIN
|
|
155
|
+
INSERT INTO chats_fts(chats_fts, rowid, title) VALUES ('delete', old.rowid, old.title);
|
|
156
|
+
END`,
|
|
157
|
+
`CREATE TRIGGER IF NOT EXISTS chats_fts_au AFTER UPDATE ON chats BEGIN
|
|
158
|
+
INSERT INTO chats_fts(chats_fts, rowid, title) VALUES ('delete', old.rowid, old.title);
|
|
159
|
+
INSERT INTO chats_fts(rowid, title) VALUES (new.rowid, new.title);
|
|
160
|
+
END`,
|
|
161
|
+
`CREATE TRIGGER IF NOT EXISTS people_fts_ai AFTER INSERT ON people BEGIN
|
|
162
|
+
INSERT INTO people_fts(rowid, name, username) VALUES (new.rowid, new.name, new.username);
|
|
163
|
+
END`,
|
|
164
|
+
`CREATE TRIGGER IF NOT EXISTS people_fts_ad AFTER DELETE ON people BEGIN
|
|
165
|
+
INSERT INTO people_fts(people_fts, rowid, name, username) VALUES ('delete', old.rowid, old.name, old.username);
|
|
166
|
+
END`,
|
|
167
|
+
`CREATE TRIGGER IF NOT EXISTS people_fts_au AFTER UPDATE ON people BEGIN
|
|
168
|
+
INSERT INTO people_fts(people_fts, rowid, name, username) VALUES ('delete', old.rowid, old.name, old.username);
|
|
169
|
+
INSERT INTO people_fts(rowid, name, username) VALUES (new.rowid, new.name, new.username);
|
|
170
|
+
END`,
|
|
171
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN
|
|
172
|
+
INSERT INTO messages_fts(rowid, text) VALUES (new.rowid, new.text);
|
|
173
|
+
END`,
|
|
174
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN
|
|
175
|
+
INSERT INTO messages_fts(messages_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
|
|
176
|
+
END`,
|
|
177
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
|
|
178
|
+
INSERT INTO messages_fts(messages_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
|
|
179
|
+
INSERT INTO messages_fts(rowid, text) VALUES (new.rowid, new.text);
|
|
180
|
+
END`,
|
|
71
181
|
`CREATE TABLE IF NOT EXISTS fetch_lease (
|
|
72
182
|
chat_id TEXT NOT NULL,
|
|
73
183
|
anchor TEXT NOT NULL,
|
|
@@ -81,15 +191,56 @@ const STATEMENTS = [
|
|
|
81
191
|
*
|
|
82
192
|
* A newer `max` may have added a column this one does not know about. Reading it is survivable;
|
|
83
193
|
* writing to it is how one version quietly destroys what another stored.
|
|
194
|
+
*
|
|
195
|
+
* **An older file is rebuilt, not altered.** Everything in here comes back from MAX, so a
|
|
196
|
+
* column-by-column migration would be code that runs once, is tested never, and is how the second
|
|
197
|
+
* schema change corrupts somebody's file. What a rebuild costs is one full login — which is what
|
|
198
|
+
* every command did before the delta sync existed.
|
|
199
|
+
*
|
|
200
|
+
* ⚠ **This stops being the right answer the day the database holds something MAX cannot re-send.**
|
|
201
|
+
* Contacts that are in no chat would be exactly that (`RES-7`), and the day they arrive this
|
|
202
|
+
* becomes `ALTER TABLE … ADD COLUMN` and this paragraph gets rewritten.
|
|
84
203
|
*/
|
|
204
|
+
/** Ours, so its message is known to hold no path and can be shown as it is. */
|
|
205
|
+
export class NewerCacheError extends Error {
|
|
206
|
+
}
|
|
85
207
|
export const migrate = (database) => {
|
|
86
208
|
const current = Number(database.prepare("PRAGMA user_version").get()?.user_version ?? 0);
|
|
87
209
|
if (current > SCHEMA_VERSION) {
|
|
88
|
-
throw new
|
|
210
|
+
throw new NewerCacheError(`this cache was written by a newer max (schema ${current}, this one speaks ${SCHEMA_VERSION}) — ` +
|
|
89
211
|
"run `max cache clear`, or use the newer version");
|
|
90
212
|
}
|
|
213
|
+
if (current > 0 && current < SCHEMA_VERSION)
|
|
214
|
+
rebuild(database);
|
|
91
215
|
for (const statement of STATEMENTS)
|
|
92
216
|
database.exec(statement);
|
|
93
217
|
database.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
94
218
|
};
|
|
219
|
+
/**
|
|
220
|
+
* Drops what is there by asking the file rather than by listing what we think a previous version
|
|
221
|
+
* wrote. A version that added a table we have since forgotten would otherwise survive the rebuild
|
|
222
|
+
* and collide with a later name.
|
|
223
|
+
*
|
|
224
|
+
* The `fetched` table goes with the rest on purpose: it records that a collection was complete,
|
|
225
|
+
* and keeping it would claim a sweep whose rows have just been thrown away.
|
|
226
|
+
*/
|
|
227
|
+
const rebuild = (database) => {
|
|
228
|
+
// ⚠ **Virtual tables first.** An FTS5 index keeps four shadow tables of its own, and
|
|
229
|
+
// `sqlite_master` lists them as ordinary tables. Dropping one of those out from under a live
|
|
230
|
+
// index is how a rebuild leaves a corrupt file behind. Dropping the virtual table takes its
|
|
231
|
+
// shadows with it, and the second pass then finds only real tables.
|
|
232
|
+
//
|
|
233
|
+
// It happens to work without this today, because `sqlite_master` returns them in creation order
|
|
234
|
+
// and `IF EXISTS` swallows the leftovers. That is an accident of ordering, not a guarantee.
|
|
235
|
+
const virtual = database
|
|
236
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND sql LIKE 'CREATE VIRTUAL TABLE%'")
|
|
237
|
+
.all();
|
|
238
|
+
for (const { name } of virtual)
|
|
239
|
+
database.exec(`DROP TABLE IF EXISTS "${String(name)}"`);
|
|
240
|
+
const tables = database
|
|
241
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
242
|
+
.all();
|
|
243
|
+
for (const { name } of tables)
|
|
244
|
+
database.exec(`DROP TABLE IF EXISTS "${String(name)}"`);
|
|
245
|
+
};
|
|
95
246
|
//# sourceMappingURL=schema.js.map
|
package/dist/cache/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB;;;;;;;;;KASG;IACH,6EAA6E;IAE7E
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB;;;;;;;;;KASG;IACH,6EAA6E;IAE7E;;;;;;;;;OASG;IACH;;;;;;;;KAQG;IACH,gFAAgF;IAEhF;;;;OAIG;IACH;;;;KAIG;IACH,0EAA0E;IAE1E;;;;;;;OAOG;IACH;;;KAGG;IAEH;;;;;;;;;;;;;KAaG;IACH,8EAA8E;IAC9E,kFAAkF;IAClF,8FAA8F;IAC9F,4DAA4D;IAE5D;;;;OAIG;IACH;;;;;KAKG;IACH,0EAA0E;IAE1E;;;;OAIG;IACH;;;KAGG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,qGAAqG;IACrG,gHAAgH;IAChH,0GAA0G;IAE1G;;;;;;;;;;;;;;;;OAgBG;IACH;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;OAEK;IACL;;OAEK;IACL;;;OAGK;IAEL;;;;;;KAMG;CACJ,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,+EAA+E;AAC/E,MAAM,OAAO,eAAgB,SAAQ,KAAK;CAAG;AAE7C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IACvD,MAAM,OAAO,GAAG,MAAM,CACnB,QAAQ,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,EAAgC,EAAE,YAAY,IAAI,CAAC,CAChG,CAAA;IAED,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,MAAM,IAAI,eAAe,CACvB,iDAAiD,OAAO,qBAAqB,cAAc,MAAM;YAC/F,iDAAiD,CACpD,CAAA;IACH,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,cAAc;QAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAE9D,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5D,QAAQ,CAAC,IAAI,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAA;AAC1D,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IAChD,qFAAqF;IACrF,6FAA6F;IAC7F,4FAA4F;IAC5F,oEAAoE;IACpE,EAAE;IACF,gGAAgG;IAChG,4FAA4F;IAC5F,MAAM,OAAO,GAAG,QAAQ;SACrB,OAAO,CAAC,0FAA0F,CAAC;SACnG,GAAG,EAAE,CAAA;IACR,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEvF,MAAM,MAAM,GAAG,QAAQ;SACpB,OAAO,CAAC,kFAAkF,CAAC;SAC3F,GAAG,EAAE,CAAA;IAER,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxF,CAAC,CAAA"}
|
package/dist/cache/store.d.ts
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
|
-
import type { Chat, Contact, Id, Message } from "../domain/models.js";
|
|
1
|
+
import type { Chat, ChatKind, Contact, Id, Message, MessageHit } from "../domain/models.js";
|
|
2
2
|
import type { CacheDatabase } from "./driver.js";
|
|
3
|
+
/** How we came to know a person. Diagnostic — what makes somebody a *contact* is the query. */
|
|
4
|
+
export type PersonSource = "login" | "info" | "participant" | "sync";
|
|
5
|
+
export type PersonOrder = "recent" | "name";
|
|
6
|
+
export interface PageOptions {
|
|
7
|
+
order: PersonOrder;
|
|
8
|
+
limit: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
/** Part of a name, already checked to be long enough for the index (`src/client.ts`). */
|
|
11
|
+
query?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ChatPageOptions {
|
|
14
|
+
limit: number;
|
|
15
|
+
offset: number;
|
|
16
|
+
query?: string;
|
|
17
|
+
kind?: ChatKind;
|
|
18
|
+
}
|
|
19
|
+
export interface MessageSearch {
|
|
20
|
+
query: string;
|
|
21
|
+
/** One chat, or every chat we hold when absent. */
|
|
22
|
+
chatId?: Id;
|
|
23
|
+
limit: number;
|
|
24
|
+
offset: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One login's worth of change: the chats that moved, the people that changed, who is in which
|
|
28
|
+
* chat, and the `time` MAX answered with.
|
|
29
|
+
*
|
|
30
|
+
* `members` is keyed by chat id and holds that chat's **whole** membership minus ourselves, so a
|
|
31
|
+
* chat named here has its rows replaced rather than added to. A chat absent from the map keeps
|
|
32
|
+
* the members it already had.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* What a merge did, in counts and **nothing else** — no name, no username, no description. It is
|
|
36
|
+
* what `max contacts sync` prints, and a summary that named anybody would be the one place the
|
|
37
|
+
* sixth constraint leaks.
|
|
38
|
+
*
|
|
39
|
+
* `changed` is "already known and sent again". A delta carries only what moved, so for an ordinary
|
|
40
|
+
* login that is exactly what it sounds like; for a full re-take, where MAX resends everything, it
|
|
41
|
+
* reads as "re-sent" instead — which is what `full` in the command's output is there to say.
|
|
42
|
+
*/
|
|
43
|
+
export interface SyncSummary {
|
|
44
|
+
known: number;
|
|
45
|
+
added: number;
|
|
46
|
+
changed: number;
|
|
47
|
+
}
|
|
48
|
+
export interface SyncDelta {
|
|
49
|
+
chats: Chat[];
|
|
50
|
+
people: Contact[];
|
|
51
|
+
members: Map<Id, Id[]>;
|
|
52
|
+
marker: number;
|
|
53
|
+
}
|
|
3
54
|
export interface CacheOptions {
|
|
4
55
|
database: CacheDatabase;
|
|
5
56
|
/** Injected so a freshness test costs nothing and does not wait. */
|
|
@@ -10,17 +61,77 @@ export interface CacheStore {
|
|
|
10
61
|
chats: {
|
|
11
62
|
read(freshForMs: number): Chat[] | undefined;
|
|
12
63
|
write(chats: Chat[]): void;
|
|
64
|
+
/** One page, newest first, in SQL rather than by building the whole list and slicing it. */
|
|
65
|
+
page(options: ChatPageOptions): Chat[];
|
|
66
|
+
/** ⚠ Takes the same filter as `page`, or the two disagree about whether a next page exists. */
|
|
67
|
+
count(options?: {
|
|
68
|
+
query?: string;
|
|
69
|
+
kind?: ChatKind;
|
|
70
|
+
}): number;
|
|
13
71
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
72
|
+
people: {
|
|
73
|
+
/**
|
|
74
|
+
* One page of **contacts** — the people a one-to-one chat exists with. `recent` is
|
|
75
|
+
* `last_messaged_at` newest first, the never-messaged last, then by name.
|
|
76
|
+
*
|
|
77
|
+
* A group member is not in this answer, and nothing marks them as excluded: they are simply
|
|
78
|
+
* not in the set the query asks for (`NEED-105`).
|
|
79
|
+
*/
|
|
80
|
+
contacts(options: PageOptions): Contact[];
|
|
81
|
+
/** How many that query would return, so a page can say whether another one exists. */
|
|
82
|
+
countContacts(options?: {
|
|
83
|
+
query?: string;
|
|
84
|
+
}): number;
|
|
85
|
+
/** Everyone we can put a name to, contact or not. */
|
|
86
|
+
page(options: PageOptions): Contact[];
|
|
87
|
+
/** Everyone, counted — what `max contacts sync` reports as known. */
|
|
88
|
+
count(): number;
|
|
89
|
+
/** Carries how we met them. Never deletes: absence from a delta means unchanged. */
|
|
90
|
+
upsert(people: Contact[], source: PersonSource): void;
|
|
91
|
+
/** The chats this person is in — which, every chat here being one we are in, is the shared set. */
|
|
92
|
+
chatsWith(personId: Id): Id[];
|
|
93
|
+
/**
|
|
94
|
+
* Recomputes `last_messaged_at` from the dialogs we hold.
|
|
95
|
+
*
|
|
96
|
+
* ⚠ **Call it after any write that adds people.** A person's recency cannot be set while
|
|
97
|
+
* writing the chats, because most people are not known yet at that moment: the login names a
|
|
98
|
+
* handful and the rest arrive from a `CONTACT_INFO` that has not been sent. Measured on the
|
|
99
|
+
* real account 2026-09-21: 6 of 22 people had a recency and the other 16 sorted as
|
|
100
|
+
* never-messaged, which is every dialog partner the login did not name.
|
|
101
|
+
*/
|
|
102
|
+
refreshRecency(): void;
|
|
17
103
|
};
|
|
104
|
+
/** The `time` the last login answered with, or `undefined` for a store that has never synced. */
|
|
105
|
+
syncMarker(): number | undefined;
|
|
106
|
+
/** Rows, memberships, recency and the marker — **in one transaction**. */
|
|
107
|
+
mergeDelta(delta: SyncDelta): SyncSummary;
|
|
108
|
+
/** Makes the next login ask for everything again. */
|
|
109
|
+
forgetSyncMarker(): void;
|
|
18
110
|
messages: {
|
|
19
111
|
/** The newest `limit` messages, but only if the window we hold is both fresh and contiguous. */
|
|
20
112
|
read(chatId: Id, limit: number, freshForMs: number): Message[] | undefined;
|
|
21
113
|
write(chatId: Id, messages: Message[]): void;
|
|
22
114
|
/** After a send, what we hold for that chat is missing the message we just added. */
|
|
23
115
|
invalidate(chatId: Id): void;
|
|
116
|
+
/**
|
|
117
|
+
* When a message was sent, for `messages list --before <id>`.
|
|
118
|
+
*
|
|
119
|
+
* `undefined` for an id we have never stored — which is exactly what a deleted message looks
|
|
120
|
+
* like, since it is no longer in the history MAX returns either.
|
|
121
|
+
*/
|
|
122
|
+
timeOf(id: Id): number | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Messages whose text contains `query`, newest first, across every chat we hold or one.
|
|
125
|
+
*
|
|
126
|
+
* ⚠ **It answers from this database and never asks MAX**, because MAX has no search operation
|
|
127
|
+
* we know of. So it finds what has been read, not what exists — `src/client.ts` is where that
|
|
128
|
+
* is said out loud rather than left for somebody to discover.
|
|
129
|
+
*/
|
|
130
|
+
search(options: MessageSearch): MessageHit[];
|
|
131
|
+
countSearch(options: {
|
|
132
|
+
query: string;
|
|
133
|
+
chatId?: Id;
|
|
134
|
+
}): number;
|
|
24
135
|
};
|
|
25
136
|
/** True when this process may go to MAX for that window; false when somebody else already is. */
|
|
26
137
|
claim(chatId: Id, anchor: string, holder: string, forMs: number): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/cache/store.ts"],"names":[],"mappings":"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"}
|