@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c4478f1a5ac2a

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 (192) hide show
  1. package/AGENTS.md +196 -97
  2. package/README.md +22 -12
  3. package/dist/adapter.d.ts +149 -64
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +1 -9
  6. package/dist/attached-model.d.ts +11 -0
  7. package/dist/attached-model.d.ts.map +1 -0
  8. package/dist/attached-model.js +47 -0
  9. package/dist/attempt.d.ts +16 -0
  10. package/dist/attempt.d.ts.map +1 -0
  11. package/dist/attempt.js +70 -0
  12. package/dist/binary.d.ts +23 -0
  13. package/dist/binary.d.ts.map +1 -0
  14. package/dist/binary.js +8 -0
  15. package/dist/bootstrap.d.ts +20 -6
  16. package/dist/bootstrap.d.ts.map +1 -1
  17. package/dist/bootstrap.js +110 -38
  18. package/dist/change-set.d.ts +2 -0
  19. package/dist/change-set.d.ts.map +1 -1
  20. package/dist/change-set.js +49 -21
  21. package/dist/change.d.ts +15 -0
  22. package/dist/change.d.ts.map +1 -1
  23. package/dist/change.js +26 -1
  24. package/dist/chat/acts.d.ts +33 -0
  25. package/dist/chat/acts.d.ts.map +1 -0
  26. package/dist/chat/acts.js +36 -0
  27. package/dist/chat/context.d.ts +3 -0
  28. package/dist/chat/context.d.ts.map +1 -0
  29. package/dist/chat/context.js +10 -0
  30. package/dist/chat/input.d.ts +9 -0
  31. package/dist/chat/input.d.ts.map +1 -0
  32. package/dist/chat/input.js +99 -0
  33. package/dist/chat/kit.d.ts +60 -0
  34. package/dist/chat/kit.d.ts.map +1 -0
  35. package/dist/chat/kit.js +123 -0
  36. package/dist/chat/lasting.d.ts +6 -0
  37. package/dist/chat/lasting.d.ts.map +1 -0
  38. package/dist/chat/lasting.js +19 -0
  39. package/dist/chat/list.d.ts +26 -0
  40. package/dist/chat/list.d.ts.map +1 -0
  41. package/dist/chat/list.js +62 -0
  42. package/dist/chat/markdown.d.ts +20 -0
  43. package/dist/chat/markdown.d.ts.map +1 -0
  44. package/dist/chat/markdown.js +188 -0
  45. package/dist/chat/message.d.ts +6 -0
  46. package/dist/chat/message.d.ts.map +1 -0
  47. package/dist/chat/message.js +192 -0
  48. package/dist/chat/messages.d.ts +21 -0
  49. package/dist/chat/messages.d.ts.map +1 -0
  50. package/dist/chat/messages.js +151 -0
  51. package/dist/chat/name.d.ts +6 -0
  52. package/dist/chat/name.d.ts.map +1 -0
  53. package/dist/chat/name.js +37 -0
  54. package/dist/chat/opened.d.ts +11 -0
  55. package/dist/chat/opened.d.ts.map +1 -0
  56. package/dist/chat/opened.js +17 -0
  57. package/dist/chat/overlay.d.ts +31 -0
  58. package/dist/chat/overlay.d.ts.map +1 -0
  59. package/dist/chat/overlay.js +97 -0
  60. package/dist/chat/parts.d.ts +57 -0
  61. package/dist/chat/parts.d.ts.map +1 -0
  62. package/dist/chat/parts.js +119 -0
  63. package/dist/chat/process.d.ts +20 -0
  64. package/dist/chat/process.d.ts.map +1 -0
  65. package/dist/chat/process.js +32 -0
  66. package/dist/chat/provider.d.ts +63 -0
  67. package/dist/chat/provider.d.ts.map +1 -0
  68. package/dist/chat/provider.js +620 -0
  69. package/dist/chat/question.d.ts +9 -0
  70. package/dist/chat/question.d.ts.map +1 -0
  71. package/dist/chat/question.js +80 -0
  72. package/dist/chat/questions.d.ts +31 -0
  73. package/dist/chat/questions.d.ts.map +1 -0
  74. package/dist/chat/questions.js +111 -0
  75. package/dist/chat/read.d.ts +5 -0
  76. package/dist/chat/read.d.ts.map +1 -0
  77. package/dist/chat/read.js +13 -0
  78. package/dist/chat/ready.d.ts +33 -0
  79. package/dist/chat/ready.d.ts.map +1 -0
  80. package/dist/chat/ready.js +91 -0
  81. package/dist/chat/style.d.ts +4 -0
  82. package/dist/chat/style.d.ts.map +1 -0
  83. package/dist/chat/style.js +302 -0
  84. package/dist/chat.d.ts +14 -0
  85. package/dist/chat.d.ts.map +1 -0
  86. package/dist/chat.js +8 -0
  87. package/dist/client.d.ts +23 -19
  88. package/dist/client.d.ts.map +1 -1
  89. package/dist/client.js +69 -32
  90. package/dist/collection.d.ts +21 -14
  91. package/dist/collection.d.ts.map +1 -1
  92. package/dist/collection.js +270 -32
  93. package/dist/connection.d.ts +36 -38
  94. package/dist/connection.d.ts.map +1 -1
  95. package/dist/connection.js +1605 -841
  96. package/dist/conversation.d.ts +43 -19
  97. package/dist/conversation.d.ts.map +1 -1
  98. package/dist/conversation.js +523 -210
  99. package/dist/entry.d.ts +17 -0
  100. package/dist/entry.d.ts.map +1 -0
  101. package/dist/entry.js +156 -0
  102. package/dist/expectation.d.ts +21 -0
  103. package/dist/expectation.d.ts.map +1 -0
  104. package/dist/expectation.js +70 -0
  105. package/dist/failure.d.ts +13 -0
  106. package/dist/failure.d.ts.map +1 -0
  107. package/dist/failure.js +19 -0
  108. package/dist/fence.d.ts +6 -0
  109. package/dist/fence.d.ts.map +1 -0
  110. package/dist/fence.js +10 -0
  111. package/dist/host.d.ts +11 -3
  112. package/dist/host.d.ts.map +1 -1
  113. package/dist/host.js +54 -69
  114. package/dist/identity.d.ts +5 -0
  115. package/dist/identity.d.ts.map +1 -1
  116. package/dist/identity.js +19 -1
  117. package/dist/index.d.ts +12 -7
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +4 -1
  120. package/dist/memory.d.ts +15 -7
  121. package/dist/memory.d.ts.map +1 -1
  122. package/dist/memory.js +147 -47
  123. package/dist/model.d.ts +14 -12
  124. package/dist/model.d.ts.map +1 -1
  125. package/dist/model.js +15 -3
  126. package/dist/observation.d.ts +6 -1
  127. package/dist/observation.d.ts.map +1 -1
  128. package/dist/observation.js +38 -17
  129. package/dist/open.d.ts +2 -0
  130. package/dist/open.d.ts.map +1 -0
  131. package/dist/open.js +1 -0
  132. package/dist/outcome.d.ts +35 -22
  133. package/dist/outcome.d.ts.map +1 -1
  134. package/dist/outcome.js +42 -1
  135. package/dist/overlay.d.ts +15 -3
  136. package/dist/overlay.d.ts.map +1 -1
  137. package/dist/overlay.js +94 -77
  138. package/dist/paths.d.ts +0 -1
  139. package/dist/paths.d.ts.map +1 -1
  140. package/dist/paths.js +0 -16
  141. package/dist/query.d.ts +40 -6
  142. package/dist/query.d.ts.map +1 -1
  143. package/dist/query.js +78 -36
  144. package/dist/reach.d.ts +42 -0
  145. package/dist/reach.d.ts.map +1 -0
  146. package/dist/reach.js +294 -0
  147. package/dist/react.d.ts +41 -0
  148. package/dist/react.d.ts.map +1 -0
  149. package/dist/react.js +64 -0
  150. package/dist/read-side.d.ts +16 -5
  151. package/dist/read-side.d.ts.map +1 -1
  152. package/dist/read-side.js +405 -137
  153. package/dist/recovery.d.ts +25 -0
  154. package/dist/recovery.d.ts.map +1 -0
  155. package/dist/recovery.js +197 -0
  156. package/dist/refresh.d.ts +12 -1
  157. package/dist/refresh.d.ts.map +1 -1
  158. package/dist/refresh.js +45 -36
  159. package/dist/replica.d.ts +10 -7
  160. package/dist/replica.d.ts.map +1 -1
  161. package/dist/replica.js +122 -79
  162. package/dist/room-draft.d.ts +29 -9
  163. package/dist/room-draft.d.ts.map +1 -1
  164. package/dist/room-draft.js +137 -26
  165. package/dist/rows.d.ts +10 -14
  166. package/dist/rows.d.ts.map +1 -1
  167. package/dist/rows.js +85 -50
  168. package/dist/save.d.ts +32 -3
  169. package/dist/save.d.ts.map +1 -1
  170. package/dist/save.js +465 -93
  171. package/dist/search.d.ts +31 -8
  172. package/dist/search.d.ts.map +1 -1
  173. package/dist/search.js +109 -44
  174. package/dist/shared.d.ts +11 -0
  175. package/dist/shared.d.ts.map +1 -0
  176. package/dist/shared.js +368 -0
  177. package/dist/status.d.ts +2 -2
  178. package/dist/status.d.ts.map +1 -1
  179. package/dist/status.js +0 -8
  180. package/dist/store.d.ts +71 -32
  181. package/dist/store.d.ts.map +1 -1
  182. package/dist/store.js +509 -221
  183. package/dist/turn.d.ts +7 -0
  184. package/dist/turn.d.ts.map +1 -0
  185. package/dist/turn.js +45 -0
  186. package/dist/wire.d.ts +58 -19
  187. package/dist/wire.d.ts.map +1 -1
  188. package/dist/wire.js +65 -67
  189. package/package.json +31 -7
  190. package/dist/attachment.d.ts +0 -3
  191. package/dist/attachment.d.ts.map +0 -1
  192. package/dist/attachment.js +0 -8
package/dist/read-side.js CHANGED
@@ -1,62 +1,71 @@
1
- import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, gt, gte, inArray, isNull, isUndefined, lt, lte, not, or, toArray, } from '@tanstack/db';
2
- import { rowsOf } from './observation.js';
1
+ import { and, BasicIndex, configureIndexDevMode, count, createCollection, createLiveQueryCollection, deepEquals, eq, inArray, or, toArray, } from '@tanstack/db';
2
+ import { ARRAY_INDEX, parse, pointerSegments } from '@agentshouse/mdmodel';
3
3
  import { documentKey } from './rows.js';
4
+ import { after, inUnits, ROW_WEIGHT } from './turn.js';
4
5
  function every(expressions) {
5
6
  const [first, second, ...rest] = expressions;
6
7
  return second === undefined ? first : and(first, second, ...rest);
7
8
  }
8
- function some(expressions) {
9
- const [first, second, ...rest] = expressions;
10
- return second === undefined ? first : or(first, second, ...rest);
9
+ function same(held, value) {
10
+ return Array.isArray(held) ? held.includes(value) : held === value;
11
11
  }
12
- function equal(field, value) {
13
- return or(eq(field, value), inArray(value, field));
12
+ function ordered(held, [operator, bound]) {
13
+ if (typeof held !== typeof bound || held === null || typeof held === 'object')
14
+ return false;
15
+ const left = held;
16
+ const right = bound;
17
+ if (operator === 'gt')
18
+ return left > right;
19
+ if (operator === 'gte')
20
+ return left >= right;
21
+ if (operator === 'lt')
22
+ return left < right;
23
+ return left <= right;
14
24
  }
15
- function tested(test, field, size, written) {
25
+ function satisfied(test, held) {
16
26
  if (test.op === 'eq')
17
- return test.value === null ? isNull(field) : equal(field, test.value);
18
- if (test.op === 'ne') {
19
- return test.value === null
20
- ? not(isNull(field))
21
- : or(isUndefined(field), isNull(field), not(equal(field, test.value)));
22
- }
23
- if (test.op === 'in' || test.op === 'nin') {
24
- const matches = test.values.length === 0
25
- ? inArray(field, [])
26
- : some(test.values.map((value) => (value === null ? isNull(field) : equal(field, value))));
27
- if (test.op === 'in')
28
- return matches;
29
- return some([
30
- isUndefined(field),
31
- ...(test.values.includes(null) ? [] : [isNull(field)]),
32
- not(matches),
33
- ]);
34
- }
35
- if (test.op === 'range') {
36
- const compare = { gt, gte, lt, lte };
37
- return every(test.bounds.map(([operator, bound]) => compare[operator](field, bound)));
38
- }
39
- if (test.op === 'all') {
40
- return every([not(isUndefined(size)), ...test.values.map((value) => inArray(value, field))]);
41
- }
42
- if (test.op === 'size')
43
- return eq(size, test.size);
44
- return test.exists ? not(isUndefined(written)) : isUndefined(written);
27
+ return same(held, test.value);
28
+ if (test.op === 'ne')
29
+ return !same(held, test.value);
30
+ if (test.op === 'in')
31
+ return test.values.some((value) => same(held, value));
32
+ if (test.op === 'nin')
33
+ return !test.values.some((value) => same(held, value));
34
+ if (test.op === 'range')
35
+ return test.bounds.every((bound) => ordered(held, bound));
36
+ if (test.op === 'all')
37
+ return Array.isArray(held) && test.values.every((value) => same(held, value));
38
+ return Array.isArray(held) && held.length === test.size;
39
+ }
40
+ function tested(test, reached) {
41
+ if (test.op === 'exists')
42
+ return test.exists === (reached !== undefined);
43
+ if (reached === undefined)
44
+ return test.op === 'ne' || test.op === 'nin';
45
+ return reached.some((held) => satisfied(test, held));
45
46
  }
46
47
  function conditioned(condition, row) {
47
48
  if ('every' in condition)
48
- return every(condition.every.map((item) => conditioned(item, row)));
49
+ return condition.every.every((item) => conditioned(item, row));
49
50
  if ('some' in condition)
50
- return some(condition.some.map((item) => conditioned(item, row)));
51
- return tested(condition.test, row.values[condition.field], row.sizes[condition.field], row.fields[condition.field]);
51
+ return condition.some.some((item) => conditioned(item, row));
52
+ return tested(condition.test, row.reached[condition.field]);
53
+ }
54
+ function narrowed(query, where, alias) {
55
+ return where === null ? query : query.fn.where((row) => conditioned(where, row[alias]));
56
+ }
57
+ function reached(reach, room, document) {
58
+ return reach === null
59
+ ? []
60
+ : [or(inArray(room, [...reach.rooms]), inArray(document, [...reach.documents]))];
52
61
  }
53
- function scoped(scope, row, rooms) {
62
+ function scoped(scope, row, rooms, reach) {
54
63
  return every([
55
64
  eq(row.kind, 'document'),
56
65
  ...(rooms === null ? [] : [inArray(row.room, [...rooms])]),
66
+ ...reached(reach, row.room, row.key),
57
67
  ...(scope.types === null ? [] : [inArray(row.type, [...scope.types])]),
58
68
  ...(scope.pathPrefix === null ? [] : [inArray(scope.pathPrefix, row.directories)]),
59
- ...(scope.where === null ? [] : [conditioned(scope.where, row)]),
60
69
  ]);
61
70
  }
62
71
  function keyed(key, row) {
@@ -68,37 +77,35 @@ function live(query) {
68
77
  startSync: true,
69
78
  });
70
79
  }
71
- function related(q, rows, scope, field, kept) {
80
+ function related(q, rows, scope, field, kept, reach) {
72
81
  const matched = kept === 'source' ? 'target' : 'source';
73
82
  const from = q.from({ r: rows });
74
- return from
75
- .where(({ r }) => every([eq(r.kind, 'reference'), ...(field === null ? [] : [eq(r.field, field)])]))
83
+ const joined = from
84
+ .where(({ r }) => every([eq(r.kind, 'reference'), ...(field === null ? [] : [inArray(field, r.forms)])]))
76
85
  .innerJoin({ m: rows }, ({ r, m }) => eq(r[matched], m.key))
77
- .where(({ m }) => scoped(scope, m, null))
86
+ .where(({ m }) => scoped(scope, m, null, reach));
87
+ return narrowed(joined, scope.where, 'm')
78
88
  .groupBy(({ r }) => r[kept])
79
89
  .select(({ r }) => ({ key: r[kept] }));
80
90
  }
81
91
  function filtered(q, rows, plan) {
82
92
  const from = q.from({ d: rows });
83
- let query = from.where(({ d }) => scoped(plan.root, d, plan.rooms));
93
+ let query = narrowed(from.where(({ d }) => scoped(plan.root, d, plan.rooms, plan.reach)), plan.root.where, 'd');
84
94
  if (plan.hop !== null) {
85
- const hop = related(q, rows, plan.hop, plan.hop.field, 'source');
95
+ const hop = related(q, rows, plan.hop, plan.hop.field, 'source', plan.reach);
86
96
  query = query.innerJoin({ h: hop }, ({ d, h }) => eq(d.key, h.key));
87
97
  }
88
98
  if (plan.backlinks !== null) {
89
- const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target');
99
+ const backlinks = related(q, rows, plan.backlinks, plan.backlinks.field, 'target', plan.reach);
90
100
  query = query.innerJoin({ k: backlinks }, ({ d, k }) => eq(d.key, k.key));
91
101
  }
92
102
  return query;
93
103
  }
94
- function named(prefix, names, source) {
95
- return Object.fromEntries(names.map((name, index) => [`${prefix}${index}`, source[name]]));
104
+ function bodied(shape) {
105
+ return shape.sections !== null || shape.preamble || shape.markdown;
96
106
  }
97
107
  function documents(q, rows, plan, shape) {
98
108
  let query = filtered(q, rows, plan);
99
- if (shape.sections !== null || shape.preamble || shape.markdown) {
100
- query = query.leftJoin({ b: rows }, ({ d, b }) => eq(d.body, b.key));
101
- }
102
109
  for (const { key, options } of shape.orderBy) {
103
110
  query = query.orderBy(({ d }) => keyed(key, d), options);
104
111
  }
@@ -107,24 +114,21 @@ function documents(q, rows, plan, shape) {
107
114
  }
108
115
  if (plan.limit !== null)
109
116
  query = query.limit(plan.limit);
110
- return query.select(({ d, b }) => ({
117
+ return query.select(({ d }) => ({
111
118
  room: d.room,
112
119
  path: d.path,
113
120
  type: d.type,
114
- ...(shape.fields === 'all' ? { fields: d.fields } : {}),
115
- ...(Array.isArray(shape.fields) ? named('field', shape.fields, d.fields) : {}),
116
- ...(shape.sections === 'all' ? { sections: b.sections } : {}),
117
- ...(Array.isArray(shape.sections) ? named('section', shape.sections, b.sections) : {}),
118
- ...(shape.preamble ? { preamble: b.preamble } : {}),
119
- ...(shape.markdown ? { markdown: b.markdown } : {}),
120
- ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d)) } : {}),
121
+ binary: d.binary,
122
+ ...(shape.fields === null ? {} : { fields: d.fields }),
123
+ ...(bodied(shape) ? { text: d.text } : {}),
124
+ ...(shape.backlinks ? { backlinks: toArray(backlinked(q, rows, d, plan.reach)) } : {}),
121
125
  ...(shape.change ? { change: d.change } : {}),
122
126
  }));
123
127
  }
124
- function backlinked(q, rows, document) {
128
+ function backlinked(q, rows, document, reach) {
125
129
  const from = q.from({ r: rows });
126
130
  return from
127
- .where(({ r }) => eq(r.target, document.key))
131
+ .where(({ r }) => every([eq(r.target, document.key), ...reached(reach, r.room, r.source)]))
128
132
  .orderBy(({ r }) => r.path, { stringSort: 'lexical' })
129
133
  .orderBy(({ r }) => r.field, { stringSort: 'lexical' })
130
134
  .select(({ r }) => ({ path: r.path, type: r.type, field: r.field }));
@@ -162,31 +166,98 @@ function built(rows, plan) {
162
166
  },
163
167
  };
164
168
  }
165
- function present(entries) {
166
- return Object.fromEntries(entries.filter(([, value]) => value !== undefined));
167
- }
168
- function part(row, prefix, names, whole) {
169
+ function sectionsOf(sections, names) {
169
170
  if (names === 'all')
170
- return whole;
171
- return present(names.map((name, index) => [name, row[`${prefix}${index}`]]));
171
+ return sections;
172
+ return Object.fromEntries(names.flatMap((name) => (Object.hasOwn(sections, name) ? [[name, sections[name]]] : [])));
173
+ }
174
+ function bodyOf(row, key, bodies) {
175
+ const text = row.text;
176
+ if (text === null)
177
+ return null;
178
+ const held = bodies?.get(key);
179
+ if (held !== undefined && held.text === text)
180
+ return held.document;
181
+ const read = parse(text, row.path);
182
+ const document = read.ok ? read.document : null;
183
+ bodies?.set(key, { text, document });
184
+ return document;
185
+ }
186
+ function masked(pointers) {
187
+ const root = { whole: false, members: new Map() };
188
+ for (const pointer of pointers) {
189
+ let mask = root;
190
+ for (const token of pointerSegments(pointer)) {
191
+ const next = mask.members.get(token) ?? { whole: false, members: new Map() };
192
+ mask.members.set(token, next);
193
+ mask = next;
194
+ }
195
+ mask.whole = true;
196
+ }
197
+ return root;
198
+ }
199
+ function merged(left, right) {
200
+ if (left === undefined)
201
+ return right;
202
+ const members = new Map(left.members);
203
+ for (const [token, mask] of right.members)
204
+ members.set(token, merged(members.get(token), mask));
205
+ return { whole: left.whole || right.whole, members };
172
206
  }
173
- function documentOf(row, shape) {
174
- if (row.type === null)
175
- return { room: row.room, path: row.path };
207
+ function cut(value, mask) {
208
+ if (mask.whole)
209
+ return value;
210
+ if (Array.isArray(value)) {
211
+ const each = {
212
+ whole: false,
213
+ members: new Map([...mask.members].filter(([token]) => !ARRAY_INDEX.test(token))),
214
+ };
215
+ const kept = [];
216
+ value.forEach((item, index) => {
217
+ const part = cut(item, merged(mask.members.get(String(index)), each));
218
+ if (part !== undefined)
219
+ kept[index] = part;
220
+ });
221
+ return kept.length === 0 ? undefined : kept;
222
+ }
223
+ if (typeof value !== 'object' || value === null)
224
+ return undefined;
225
+ const kept = Object.entries(value).flatMap(([key, held]) => {
226
+ const member = mask.members.get(key);
227
+ const part = member === undefined ? undefined : cut(held, member);
228
+ return part === undefined ? [] : [[key, part]];
229
+ });
230
+ return kept.length === 0 ? undefined : Object.fromEntries(kept);
231
+ }
232
+ function selected(fields, pointers) {
233
+ return (cut(fields, masked(pointers)) ?? {});
234
+ }
235
+ function documentOf(key, row, shape, bodies) {
236
+ if (row.type === null) {
237
+ const binary = row.binary;
238
+ return {
239
+ room: row.room,
240
+ path: row.path,
241
+ ...(binary === null ? {} : { binaryFile: binary }),
242
+ };
243
+ }
244
+ const body = bodied(shape) ? bodyOf(row, key, bodies) : null;
176
245
  return {
177
246
  room: row.room,
178
247
  path: row.path,
179
248
  type: row.type,
180
249
  ...(shape.fields === null
181
- ? {}
182
- : { fields: part(row, 'field', shape.fields, row.fields) }),
183
- ...(shape.sections === null
184
250
  ? {}
185
251
  : {
186
- sections: part(row, 'section', shape.sections, row.sections),
252
+ fields: shape.fields === 'all'
253
+ ? row.fields
254
+ : selected(row.fields, shape.fields),
187
255
  }),
188
- ...(shape.preamble ? { preamble: row.preamble } : {}),
189
- ...(shape.markdown ? { markdown: row.markdown } : {}),
256
+ ...(shape.sections === null || body === null
257
+ ? {}
258
+ : { sections: sectionsOf(body.sections, shape.sections) }),
259
+ ...(shape.preamble && body !== null ? { preamble: body.preamble } : {}),
260
+ ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
190
261
  ...(shape.backlinks
191
262
  ? {
192
263
  backlinks: row.backlinks.map(({ path, type, field }) => ({ path, type, field })),
@@ -195,30 +266,40 @@ function documentOf(row, shape) {
195
266
  ...(shape.change ? { change: row.change } : {}),
196
267
  };
197
268
  }
198
- function keyedOf(plan, entries) {
269
+ function weight(row) {
270
+ return ROW_WEIGHT + (typeof row?.text === 'string' ? row.text.length : 0);
271
+ }
272
+ function rowsOfPlan(plan, entries, bodies, each = (row) => row) {
199
273
  const shape = plan.shape;
200
274
  if (shape.kind === 'documents') {
201
- return {
202
- result: structuredClone({ documents: entries.map(([, row]) => documentOf(row, shape)) }),
203
- keys: entries.map(([key]) => key),
204
- };
275
+ const documents = [];
276
+ return after(inUnits(entries, ([key, row]) => {
277
+ documents.push(each(documentOf(key, row, shape, bodies)));
278
+ return weight(row);
279
+ }), () => ({ rows: { documents }, keys: entries.map(([key]) => key) }));
205
280
  }
206
281
  const bounded = shape.orderBy.length === 0 && plan.limit !== null ? entries.slice(0, plan.limit) : entries;
207
- return {
208
- result: structuredClone({
209
- groups: bounded.map(([, row]) => ({
210
- key: shape.groupBy.map((_, index) => row[`key${index}`]),
211
- count: row.count,
212
- })),
213
- }),
214
- keys: bounded.map(([key]) => key),
215
- };
282
+ const groups = [];
283
+ return after(inUnits(bounded, ([, row]) => {
284
+ groups.push(each({
285
+ key: shape.groupBy.map((_, index) => row[`key${index}`]),
286
+ count: row.count,
287
+ }));
288
+ return ROW_WEIGHT;
289
+ }), () => ({ rows: { groups }, keys: bounded.map(([key]) => key) }));
216
290
  }
217
- function reconciled(previous, next) {
218
- const before = rowsOf(previous.result);
219
- const held = new Map(previous.keys.map((key, index) => [key, before[index]]));
291
+ export function reconciled(previous, next) {
292
+ const before = previous.result;
293
+ const after = next.result;
294
+ if ('pending' in before || 'pending' in after) {
295
+ if (deepEquals(before, after))
296
+ return null;
297
+ return { keyed: next, message: next };
298
+ }
299
+ const earlier = 'groups' in before ? before.groups : before.documents;
300
+ const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
220
301
  const changed = [];
221
- const rows = rowsOf(next.result).map((row, index) => {
302
+ const rows = ('groups' in after ? after.groups : after.documents).map((row, index) => {
222
303
  const key = next.keys[index];
223
304
  const kept = held.get(key);
224
305
  if (kept !== undefined && deepEquals(kept, row))
@@ -227,22 +308,37 @@ function reconciled(previous, next) {
227
308
  return row;
228
309
  });
229
310
  const present = new Set(next.keys);
230
- const survivors = previous.keys.filter((key) => present.has(key));
231
311
  const removed = previous.keys.filter((key) => !present.has(key));
232
- const moved = survivors.length !== next.keys.length ||
233
- survivors.some((key, index) => key !== next.keys[index]);
234
- if (changed.length === 0 && removed.length === 0 && !moved)
312
+ const moved = reordered(previous.keys, next.keys, present);
313
+ const covered = !deepEquals(before.coverage, after.coverage);
314
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered)
235
315
  return null;
236
316
  return {
237
317
  keyed: {
238
- result: 'groups' in next.result
239
- ? { groups: rows }
240
- : { documents: rows },
318
+ result: 'groups' in after
319
+ ? { groups: rows, coverage: after.coverage }
320
+ : { documents: rows, coverage: after.coverage },
241
321
  keys: next.keys,
242
322
  },
243
- change: { changed, removed, order: moved ? next.keys : null },
323
+ message: {
324
+ changed,
325
+ removed,
326
+ order: moved ? next.keys : null,
327
+ ...(covered ? { coverage: after.coverage } : {}),
328
+ },
244
329
  };
245
330
  }
331
+ function reordered(previous, next, present) {
332
+ let at = 0;
333
+ for (const key of previous) {
334
+ if (!present.has(key))
335
+ continue;
336
+ if (next[at] !== key)
337
+ return true;
338
+ at += 1;
339
+ }
340
+ return at !== next.length;
341
+ }
246
342
  export function createReadSide() {
247
343
  configureIndexDevMode({ enabled: false });
248
344
  let sync;
@@ -262,23 +358,139 @@ export function createReadSide() {
262
358
  });
263
359
  const shared = new Map();
264
360
  function searched(row) {
265
- const body = rows.get(row.body);
361
+ const read = row.text === null ? null : parse(row.text, row.path);
362
+ if (read === null || !read.ok)
363
+ return null;
266
364
  return {
267
365
  room: row.room,
268
366
  path: row.path,
269
367
  type: row.type,
270
368
  fields: row.fields,
271
- preamble: body.preamble,
272
- sections: body.sections,
369
+ preamble: read.document.preamble,
370
+ sections: read.document.sections,
273
371
  };
274
372
  }
275
- function publish(entry) {
276
- const next = reconciled(entry.result, keyedOf(entry.plan, [...entry.built.live.entries()]));
277
- if (next === null)
373
+ function kept(entry, key, row) {
374
+ const held = entry.rows.get(key);
375
+ if (held !== undefined && deepEquals(held, row))
278
376
  return;
279
- entry.result = next.keyed;
280
- for (const listener of entry.listeners)
281
- listener(next.change);
377
+ entry.rows.set(key, row);
378
+ }
379
+ function rebuilt(entry) {
380
+ return after(rowsOfPlan(entry.plan, [...entry.built.live.entries()], entry.bodies), ({ rows: found, keys }) => {
381
+ const present = new Set(keys);
382
+ for (const key of [...entry.rows.keys()])
383
+ if (!present.has(key))
384
+ entry.rows.delete(key);
385
+ for (const key of [...entry.bodies.keys()]) {
386
+ if (!present.has(key))
387
+ entry.bodies.delete(key);
388
+ }
389
+ keys.forEach((key, index) => kept(entry, key, rowAt(found, index)));
390
+ return keys;
391
+ });
392
+ }
393
+ function refreshed(entry, dirty) {
394
+ const shape = entry.plan.shape;
395
+ const keys = [...entry.built.live.keys()];
396
+ const named = new Set([...dirty, ...keys.filter((key) => !entry.rows.has(key))]);
397
+ const values = [...named].map((key) => [key, entry.built.live.get(key)]);
398
+ const done = inUnits(values, ([key, value]) => {
399
+ if (value !== undefined)
400
+ kept(entry, key, documentOf(key, value, shape, entry.bodies));
401
+ else {
402
+ entry.rows.delete(key);
403
+ entry.bodies.delete(key);
404
+ }
405
+ return weight(value);
406
+ });
407
+ return after(done, () => keys);
408
+ }
409
+ function current(entry, keys, coverage) {
410
+ const found = keys.map((key) => entry.rows.get(key));
411
+ return {
412
+ result: entry.plan.shape.kind === 'groups'
413
+ ? { groups: found, coverage }
414
+ : { documents: found, coverage },
415
+ keys,
416
+ };
417
+ }
418
+ function delivered(entry, next) {
419
+ const previous = entry.result;
420
+ entry.result = next;
421
+ const message = previous === undefined ? null : changedFrom(previous, next);
422
+ if (message !== null)
423
+ for (const listener of entry.ready)
424
+ listener(message);
425
+ for (const listener of entry.fresh) {
426
+ entry.ready.add(listener);
427
+ listener(next);
428
+ }
429
+ entry.fresh.clear();
430
+ }
431
+ function run(entry) {
432
+ while (entry.listeners.size > 0 &&
433
+ (entry.whole || entry.dirty.size > 0 || entry.stale || entry.fresh.size > 0)) {
434
+ entry.stale = false;
435
+ const cover = entry.cover();
436
+ if ('pending' in cover) {
437
+ entry.whole = true;
438
+ entry.dirty.clear();
439
+ entry.rows.clear();
440
+ entry.bodies.clear();
441
+ delivered(entry, { result: cover, keys: [] });
442
+ break;
443
+ }
444
+ const whole = entry.whole ||
445
+ entry.plan.shape.kind === 'groups' ||
446
+ entry.result === undefined ||
447
+ 'pending' in entry.result.result;
448
+ const dirty = entry.dirty;
449
+ entry.dirty = new Set();
450
+ entry.whole = false;
451
+ const keys = whole ? rebuilt(entry) : refreshed(entry, dirty);
452
+ if (keys instanceof Promise) {
453
+ void keys
454
+ .then((found) => {
455
+ if (entry.listeners.size > 0)
456
+ delivered(entry, current(entry, found, cover.coverage));
457
+ run(entry);
458
+ })
459
+ .catch(() => {
460
+ entry.running = false;
461
+ });
462
+ return;
463
+ }
464
+ delivered(entry, current(entry, keys, cover.coverage));
465
+ }
466
+ entry.running = false;
467
+ }
468
+ function wake(entry) {
469
+ if (entry.subscription === null || entry.running)
470
+ return;
471
+ entry.running = true;
472
+ try {
473
+ run(entry);
474
+ }
475
+ catch (error) {
476
+ entry.running = false;
477
+ throw error;
478
+ }
479
+ }
480
+ function recover() {
481
+ for (const entry of shared.values()) {
482
+ if (entry.result === undefined || entry.subscription === null)
483
+ continue;
484
+ const cover = entry.cover();
485
+ const before = entry.result.result;
486
+ const held = 'pending' in cover
487
+ ? deepEquals(before, cover)
488
+ : 'coverage' in before && deepEquals(before.coverage, cover.coverage);
489
+ if (held)
490
+ continue;
491
+ entry.stale = true;
492
+ wake(entry);
493
+ }
282
494
  }
283
495
  return {
284
496
  apply(set) {
@@ -289,38 +501,52 @@ export function createReadSide() {
289
501
  for (const row of change.after)
290
502
  next.set(row.key, row);
291
503
  }
292
- if (next.size === 0)
293
- return;
294
- sync.begin();
295
- for (const [key, row] of next) {
296
- if (row === null) {
297
- if (rows.has(key))
298
- sync.write({ type: 'delete', key });
299
- continue;
504
+ if (next.size > 0) {
505
+ sync.begin();
506
+ for (const [key, row] of next) {
507
+ if (row === null) {
508
+ if (rows.has(key))
509
+ sync.write({ type: 'delete', key });
510
+ continue;
511
+ }
512
+ sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
300
513
  }
301
- sync.write({ type: rows.has(key) ? 'update' : 'insert', value: row });
514
+ sync.commit();
302
515
  }
303
- sync.commit();
516
+ recover();
304
517
  },
305
- async query(plan) {
518
+ recover,
519
+ async query(plan, at) {
306
520
  const { live: collection, dispose } = built(rows, plan);
307
521
  try {
308
522
  await collection.toArrayWhenReady();
309
- return keyedOf(plan, [...collection.entries()]).result;
523
+ const entries = [...collection.entries()];
524
+ const held = at();
525
+ const found = await rowsOfPlan(plan, entries, null, structuredClone);
526
+ return { rows: found.rows, at: held };
310
527
  }
311
528
  finally {
312
529
  dispose();
313
530
  }
314
531
  },
315
- watch(plan, identity, deliver) {
532
+ watch(plan, identity, cover, deliverTo) {
316
533
  let entry = shared.get(identity);
317
534
  if (entry === undefined) {
318
535
  const created = {
319
536
  plan,
320
537
  built: built(rows, plan),
538
+ cover,
321
539
  result: undefined,
540
+ rows: new Map(),
541
+ bodies: new Map(),
322
542
  listeners: new Set(),
543
+ fresh: new Set(),
544
+ ready: new Set(),
323
545
  subscription: null,
546
+ whole: true,
547
+ dirty: new Set(),
548
+ stale: false,
549
+ running: false,
324
550
  };
325
551
  created.built.live.on('status:change', ({ status }) => {
326
552
  if (status === 'cleaned-up' && created.listeners.size === 0)
@@ -330,29 +556,40 @@ export function createReadSide() {
330
556
  entry = created;
331
557
  }
332
558
  const observed = entry;
333
- observed.listeners.add(deliver);
559
+ observed.listeners.add(deliverTo);
334
560
  observed.built.live.onFirstReady(() => {
335
- if (!observed.listeners.has(deliver))
561
+ if (!observed.listeners.has(deliverTo))
336
562
  return;
563
+ observed.fresh.add(deliverTo);
337
564
  if (observed.subscription === null) {
338
- observed.result = keyedOf(observed.plan, [...observed.built.live.entries()]);
339
- observed.subscription = observed.built.live.subscribeChanges(() => {
340
- publish(observed);
565
+ observed.whole = true;
566
+ observed.subscription = observed.built.live.subscribeChanges((changes) => {
567
+ if (observed.plan.shape.kind === 'groups')
568
+ observed.whole = true;
569
+ else
570
+ for (const { key } of changes)
571
+ observed.dirty.add(key);
572
+ wake(observed);
341
573
  }, { includeInitialState: false });
342
574
  }
343
- deliver(observed.result);
575
+ wake(observed);
344
576
  });
345
577
  return () => {
346
- if (!observed.listeners.delete(deliver) || observed.listeners.size > 0)
578
+ observed.fresh.delete(deliverTo);
579
+ observed.ready.delete(deliverTo);
580
+ if (!observed.listeners.delete(deliverTo) || observed.listeners.size > 0)
347
581
  return;
348
582
  observed.subscription?.unsubscribe();
349
583
  observed.subscription = null;
584
+ observed.result = undefined;
585
+ observed.rows.clear();
586
+ observed.bodies.clear();
587
+ observed.dirty.clear();
588
+ observed.whole = true;
350
589
  };
351
590
  },
352
- documents() {
353
- return rows.toArray
354
- .filter((row) => row.kind === 'document' && row.type !== null)
355
- .map(searched);
591
+ room(room) {
592
+ return rows.toArray.flatMap((row) => row.kind !== 'document' || row.type === null || row.room !== room ? [] : [row.path]);
356
593
  },
357
594
  document(room, path) {
358
595
  const row = rows.get(documentKey(room, path));
@@ -361,6 +598,8 @@ export function createReadSide() {
361
598
  close() {
362
599
  for (const entry of shared.values()) {
363
600
  entry.listeners.clear();
601
+ entry.fresh.clear();
602
+ entry.ready.clear();
364
603
  entry.subscription?.unsubscribe();
365
604
  entry.built.dispose();
366
605
  }
@@ -369,3 +608,32 @@ export function createReadSide() {
369
608
  },
370
609
  };
371
610
  }
611
+ function changedFrom(previous, next) {
612
+ const before = previous.result;
613
+ const after = next.result;
614
+ if ('pending' in before || 'pending' in after)
615
+ return deepEquals(before, after) ? null : next;
616
+ const earlier = 'groups' in before ? before.groups : before.documents;
617
+ const held = new Map(previous.keys.map((key, index) => [key, earlier[index]]));
618
+ const later = 'groups' in after ? after.groups : after.documents;
619
+ const changed = [];
620
+ next.keys.forEach((key, index) => {
621
+ if (held.get(key) !== later[index])
622
+ changed.push([key, later[index]]);
623
+ });
624
+ const present = new Set(next.keys);
625
+ const removed = previous.keys.filter((key) => !present.has(key));
626
+ const moved = reordered(previous.keys, next.keys, present);
627
+ const covered = !deepEquals(before.coverage, after.coverage);
628
+ if (changed.length === 0 && removed.length === 0 && !moved && !covered)
629
+ return null;
630
+ return {
631
+ changed,
632
+ removed,
633
+ order: moved ? next.keys : null,
634
+ ...(covered ? { coverage: after.coverage } : {}),
635
+ };
636
+ }
637
+ function rowAt(rows, index) {
638
+ return 'groups' in rows ? rows.groups[index] : rows.documents[index];
639
+ }