feat: 索引の射影 — 本文ごと落ちる索引が「次ページ」を書けなくしている
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend-api-design, databases
Research direction
Start by reading src/publishChecks.ts, src/firestore/appViews.ts, and the mirror/mirrorOf handling, then inspect the view declarations and src/view/protocol.ts. Define the projection and validation behavior described for article indexes, including same-batch writes and unchanged behavior for existing apps; confirm the relevant checks and repository tests pass.
Written by the indexing model from the issue text.
Description
記事を出すアプリ(AI Journal, /a/ai-journal)で「次の10本」を出そうとして、書けないことが分かった。ページ側の問題ではなく、索引がレコードを丸ごと落とす設計の帰結なので、ここに置く。
何が起きたか
索引は最新 10 本しか出せていない。11 本目以降へ送る「次へ」を足そうとして views[].limit を 50 に上げたところ、check が正しく拒否した:
views[0] reads 50 articles whose drawn text may total 61100 bytes each, so one open of the
index costs a reader at least 3055000 bytes — above 1000000 ... the only way to a cheap
index is a second collection carrying title and summary alone.
拒否は正しい。問題は、この拒否から出る道が今ひとつも無いことである。作者に残された選択肢は 3 つで、どれも索引を諦めている:
maxBytes.bodyを下げる — 索引を深くするために本文の長さを削る。無関係な 2 つが宣言の上で結びついている。limitを上限(MAX_INDEX_BYTES / Σmaxbytes、body 60,000 なら 16 本)で止める — コレクションは伸び続けるので、古い記事は静かに索引から落ちていく。記事自体は/a/{slug}/{id}で生きているのに、辿り着く道だけが消える。- 拒否文が勧める「title と summary だけの第二のコレクション」を作る — 投稿・修正・削除のたびに 2 レコードを同期する仕掛けをアプリの作者が書く。書き手向けの指示も倍になり、同期は原子的でない。
3 番目が本来の解であり、だからこそプラットフォームの仕事だと思う。
今どうなっているか(確認したこと)
- 索引の読みはビジターのブラウザで一発。mulmoserver
src/firestore/appViews.ts:253-276—getDocs(query(items, orderBy(stampField,"desc"), limit(n)))。startAfterは import すらされていない。 - データは push 一方通行。ページからホストに頼める ask は
lookup(自分の 1 行)とopen(記事へ遷移)だけで、「続きをくれ」は wire に無い(src/view/protocol.tsのVIEW_MESSAGE)。 - ルールはフィールドを落とせない(原則 5)ので索引は body ごと落ちる。ゆえに
MAX_INDEX_BYTES = 1_000_000の掛け算検査(src/publishChecks.ts、drawnTextFieldsを使っている箇所)。行数の上限MAX_VIEW_LIMIT = 1000は縛っていない。縛っているのはバイト数である。
つまり 3 つが直列に効いている: 射影できない → 索引が高い → 深くできない → ページ送りが書けない。手前から解くべきで、view.page() を先に足しても解決しない。
提案 1(本命): 索引の射影をホストが維持する
宣言したフィールドだけを持つ軽いレコードを、記事の書き込みと同じバッチで索引コレクションに書く。
{
"collections": {
"articles": { "index": { "collection": "article-index",
"fields": ["title", "summary", "byline", "tags"] } },
"article-index": { "indexOf": "articles" }
},
"views": [
{ "id": "public", "audience": "public", "path": "views/home.html",
"collections": ["article-index"], "limit": { "article-index": 200 },
"article": { "collection": "articles", "title": "title", "body": "body", ... } }
]
}
mirror/mirrorOfが先例である。予約の有無を slot のstateに写す 2 文書 1 バッチを、ルールはすでに受けている。あちらは 1 フィールドの状態同期、こちらは宣言したフィールドの複写で、必要な性質(原子性・片割れの禁止・publish が両半分を要求すること)は同じ。だから Cloud Function もトリガも要らない。- 索引側の id は記事と同じ(
idFrom: "slug"がそのまま効く)。stampFieldは複写され、limitの順序付けはそのまま。 - 効果: 1 本あたり約 1,400 バイト(title 200 + summary 800 + byline 100 + tags 200 + slug + stamp)。同じ 1 MB の予算で 約 700 本。本文の長さ制限は 1 バイトも下げなくてよい。 ページ送りはアプリ側のクライアントコードのままで何年も持つ。
- 検査は減るのではなく移る: 掛け算は索引コレクションの
maxBytesに対して行い、articleを持つビューが本文を持つコレクションを直接読んでいたら「索引を宣言せよ」と言えるようになる。今の拒否文が代替を持てるようになる、というのがこの提案の核心。
要検討:
- 削除の片割れ(記事だけ消えて索引が残る)を、mirror と同じくルールで塞げるか。
correctが索引フィールドに触れたときの複写。書き込みが 1 バッチである限り同じ話のはず。- 既存アプリ:
indexを書かなければ射影は 1 バイトも変わらないこと。
提案 2(後続): view.page(cid, { before, rows })
提案 1 の後、記事が窓を超えたときに効く。lookup と同じ形で足せる:
VIEW_MESSAGEにpage/pageResultBridgePortsに optional なpageポート(lookupと同じく、無いホストは「わからない」を返す。mineの「absence は UNKNOWN であって no ではない」約束をそのまま踏襲する)- mulmoserver の
viewConstraintsにstartAfter(stampField は文字列順=時刻順なのでwhere(stamp, "<", cursor)でも足りる)
単独では解にならないことを書いておく: 射影が無いままだと「次へ」1 回で最大 60 万バイト落ちる。ただしコストの性質は変わる — 今は全員が毎回払う固定費、pull は押した人だけが払う変動費。なのでバイトの掛け算検査は「初回 push の分だけ」に緩められるはずで、その判断は提案 1 が入ってからでよい。
やらないこと
view.query()(タグ絞り込み・月別アーカイブ・検索)は別の話。索引が軽くなっている(提案 1)ことが前提で、複合インデックスの管理も要る。ここには含めない。
出口
提案 1 は宣言・検査・射影の追加なので、この repo が先頭。mulmoserver 側は索引コレクションを読むだけで、ページ送り自体はアプリの HTML で書ける。提案 2 は wire protocol を跨ぐので、live のときと同じ 3 リポジトリの順番が要る。
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 9m
- Merged PRs (30d)
- 21
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from receptron/sharedapp
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 38/100
receptron/sharedapp#82 · 2 comments ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 42/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
Maintainers usually reply within 1 day
All issues in receptron/sharedapp
Similar issues
-
verified
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 5 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
anomalyco/opencode#59589 · 1 comment ·
Maintainers usually reply within 1 day
-
Bug: During run init command, finish the git initialize process without question to user('Initialize a new git repository?')Possibly taken @nishu-murmu claimed this today. Openpending-triage
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
wxt-dev/wxt#2697 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
knaw-huc/textannoviz#739 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day