@agentshouse/mdruntime 0.1.0-c1cc928d1d256 → 0.1.0-c1ffd2bc60355

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 (257) hide show
  1. package/AGENTS.md +296 -232
  2. package/README.md +21 -10
  3. package/dist/adapter.d.ts +123 -32
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/attached-model.d.ts +4 -1
  6. package/dist/attached-model.d.ts.map +1 -1
  7. package/dist/attached-model.js +13 -3
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +18 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +36 -5
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +187 -45
  17. package/dist/chain.d.ts +11 -0
  18. package/dist/chain.d.ts.map +1 -0
  19. package/dist/chain.js +64 -0
  20. package/dist/change-set.d.ts +9 -14
  21. package/dist/change-set.d.ts.map +1 -1
  22. package/dist/change-set.js +11 -56
  23. package/dist/change.d.ts +45 -0
  24. package/dist/change.d.ts.map +1 -1
  25. package/dist/change.js +34 -0
  26. package/dist/chat/acts.d.ts +33 -0
  27. package/dist/chat/acts.d.ts.map +1 -0
  28. package/dist/chat/acts.js +36 -0
  29. package/dist/chat/context.d.ts +3 -0
  30. package/dist/chat/context.d.ts.map +1 -0
  31. package/dist/chat/context.js +10 -0
  32. package/dist/chat/earlier.d.ts +10 -0
  33. package/dist/chat/earlier.d.ts.map +1 -0
  34. package/dist/chat/earlier.js +54 -0
  35. package/dist/chat/house.d.ts +25 -0
  36. package/dist/chat/house.d.ts.map +1 -0
  37. package/dist/chat/house.js +38 -0
  38. package/dist/chat/input.d.ts +11 -0
  39. package/dist/chat/input.d.ts.map +1 -0
  40. package/dist/chat/input.js +92 -0
  41. package/dist/chat/kit.d.ts +61 -0
  42. package/dist/chat/kit.d.ts.map +1 -0
  43. package/dist/chat/kit.js +123 -0
  44. package/dist/chat/lasting.d.ts +6 -0
  45. package/dist/chat/lasting.d.ts.map +1 -0
  46. package/dist/chat/lasting.js +19 -0
  47. package/dist/chat/list.d.ts +28 -0
  48. package/dist/chat/list.d.ts.map +1 -0
  49. package/dist/chat/list.js +103 -0
  50. package/dist/chat/markdown.d.ts +20 -0
  51. package/dist/chat/markdown.d.ts.map +1 -0
  52. package/dist/chat/markdown.js +229 -0
  53. package/dist/chat/message.d.ts +7 -0
  54. package/dist/chat/message.d.ts.map +1 -0
  55. package/dist/chat/message.js +254 -0
  56. package/dist/chat/messages.d.ts +35 -0
  57. package/dist/chat/messages.d.ts.map +1 -0
  58. package/dist/chat/messages.js +210 -0
  59. package/dist/chat/name.d.ts +7 -0
  60. package/dist/chat/name.d.ts.map +1 -0
  61. package/dist/chat/name.js +37 -0
  62. package/dist/chat/opened.d.ts +12 -0
  63. package/dist/chat/opened.d.ts.map +1 -0
  64. package/dist/chat/opened.js +19 -0
  65. package/dist/chat/overlay.d.ts +32 -0
  66. package/dist/chat/overlay.d.ts.map +1 -0
  67. package/dist/chat/overlay.js +103 -0
  68. package/dist/chat/parts.d.ts +58 -0
  69. package/dist/chat/parts.d.ts.map +1 -0
  70. package/dist/chat/parts.js +120 -0
  71. package/dist/chat/pieces.d.ts +62 -0
  72. package/dist/chat/pieces.d.ts.map +1 -0
  73. package/dist/chat/pieces.js +1110 -0
  74. package/dist/chat/process.d.ts +20 -0
  75. package/dist/chat/process.d.ts.map +1 -0
  76. package/dist/chat/process.js +32 -0
  77. package/dist/chat/provider.d.ts +70 -0
  78. package/dist/chat/provider.d.ts.map +1 -0
  79. package/dist/chat/provider.js +635 -0
  80. package/dist/chat/question.d.ts +9 -0
  81. package/dist/chat/question.d.ts.map +1 -0
  82. package/dist/chat/question.js +69 -0
  83. package/dist/chat/questions.d.ts +31 -0
  84. package/dist/chat/questions.d.ts.map +1 -0
  85. package/dist/chat/questions.js +111 -0
  86. package/dist/chat/read.d.ts +5 -0
  87. package/dist/chat/read.d.ts.map +1 -0
  88. package/dist/chat/read.js +13 -0
  89. package/dist/chat/ready.d.ts +37 -0
  90. package/dist/chat/ready.d.ts.map +1 -0
  91. package/dist/chat/ready.js +93 -0
  92. package/dist/chat/room-file.d.ts +8 -0
  93. package/dist/chat/room-file.d.ts.map +1 -0
  94. package/dist/chat/room-file.js +72 -0
  95. package/dist/chat/steady.d.ts +3 -0
  96. package/dist/chat/steady.d.ts.map +1 -0
  97. package/dist/chat/steady.js +26 -0
  98. package/dist/chat/style.d.ts +4 -0
  99. package/dist/chat/style.d.ts.map +1 -0
  100. package/dist/chat/style.js +307 -0
  101. package/dist/chat.d.ts +15 -0
  102. package/dist/chat.d.ts.map +1 -0
  103. package/dist/chat.js +8 -0
  104. package/dist/client.d.ts +33 -14
  105. package/dist/client.d.ts.map +1 -1
  106. package/dist/client.js +74 -19
  107. package/dist/collection.d.ts +9 -13
  108. package/dist/collection.d.ts.map +1 -1
  109. package/dist/collection.js +47 -56
  110. package/dist/compile.d.ts +16 -0
  111. package/dist/compile.d.ts.map +1 -0
  112. package/dist/compile.js +721 -0
  113. package/dist/connection.d.ts +57 -19
  114. package/dist/connection.d.ts.map +1 -1
  115. package/dist/connection.js +3561 -663
  116. package/dist/conversation.d.ts +41 -12
  117. package/dist/conversation.d.ts.map +1 -1
  118. package/dist/conversation.js +585 -180
  119. package/dist/digest.d.ts +7 -0
  120. package/dist/digest.d.ts.map +1 -0
  121. package/dist/digest.js +113 -0
  122. package/dist/durability.d.ts +5 -1
  123. package/dist/durability.d.ts.map +1 -1
  124. package/dist/durability.js +5 -0
  125. package/dist/entry.d.ts +19 -0
  126. package/dist/entry.d.ts.map +1 -0
  127. package/dist/entry.js +151 -0
  128. package/dist/expectation.d.ts +21 -0
  129. package/dist/expectation.d.ts.map +1 -0
  130. package/dist/expectation.js +70 -0
  131. package/dist/failure.d.ts +13 -0
  132. package/dist/failure.d.ts.map +1 -0
  133. package/dist/failure.js +19 -0
  134. package/dist/fence.d.ts +6 -0
  135. package/dist/fence.d.ts.map +1 -0
  136. package/dist/fence.js +10 -0
  137. package/dist/file.d.ts +2 -10
  138. package/dist/file.d.ts.map +1 -1
  139. package/dist/file.js +3 -120
  140. package/dist/fulltext.d.ts +27 -0
  141. package/dist/fulltext.d.ts.map +1 -0
  142. package/dist/fulltext.js +368 -0
  143. package/dist/host.d.ts +15 -3
  144. package/dist/host.d.ts.map +1 -1
  145. package/dist/host.js +132 -22
  146. package/dist/identity.d.ts +4 -0
  147. package/dist/identity.d.ts.map +1 -1
  148. package/dist/identity.js +16 -1
  149. package/dist/index.d.ts +20 -9
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +8 -0
  152. package/dist/live.d.ts +22 -0
  153. package/dist/live.d.ts.map +1 -0
  154. package/dist/live.js +123 -0
  155. package/dist/memory.d.ts +11 -4
  156. package/dist/memory.d.ts.map +1 -1
  157. package/dist/memory.js +92 -27
  158. package/dist/messages.d.ts +28 -0
  159. package/dist/messages.d.ts.map +1 -0
  160. package/dist/messages.js +90 -0
  161. package/dist/model.d.ts +7 -4
  162. package/dist/model.d.ts.map +1 -1
  163. package/dist/model.js +10 -1
  164. package/dist/name.d.ts +4 -0
  165. package/dist/name.d.ts.map +1 -0
  166. package/dist/name.js +122 -0
  167. package/dist/observation.d.ts +19 -2
  168. package/dist/observation.d.ts.map +1 -1
  169. package/dist/observation.js +113 -12
  170. package/dist/open.d.ts +2 -0
  171. package/dist/open.d.ts.map +1 -0
  172. package/dist/open.js +1 -0
  173. package/dist/opened.d.ts +22 -0
  174. package/dist/opened.d.ts.map +1 -0
  175. package/dist/opened.js +107 -0
  176. package/dist/outcome.d.ts +62 -3
  177. package/dist/outcome.d.ts.map +1 -1
  178. package/dist/outcome.js +67 -1
  179. package/dist/overlay.d.ts +15 -5
  180. package/dist/overlay.d.ts.map +1 -1
  181. package/dist/overlay.js +136 -180
  182. package/dist/query.d.ts +64 -4
  183. package/dist/query.d.ts.map +1 -1
  184. package/dist/query.js +51 -7
  185. package/dist/reach.d.ts +11 -4
  186. package/dist/reach.d.ts.map +1 -1
  187. package/dist/reach.js +203 -59
  188. package/dist/react.d.ts +12 -8
  189. package/dist/react.d.ts.map +1 -1
  190. package/dist/react.js +8 -17
  191. package/dist/readiness.d.ts +64 -0
  192. package/dist/readiness.d.ts.map +1 -0
  193. package/dist/readiness.js +33 -0
  194. package/dist/reads.d.ts +11 -0
  195. package/dist/reads.d.ts.map +1 -0
  196. package/dist/reads.js +100 -0
  197. package/dist/recovery.d.ts +25 -0
  198. package/dist/recovery.d.ts.map +1 -0
  199. package/dist/recovery.js +198 -0
  200. package/dist/refresh.d.ts +19 -8
  201. package/dist/refresh.d.ts.map +1 -1
  202. package/dist/refresh.js +127 -55
  203. package/dist/replica.d.ts +60 -33
  204. package/dist/replica.d.ts.map +1 -1
  205. package/dist/replica.js +318 -152
  206. package/dist/room-draft.d.ts +41 -9
  207. package/dist/room-draft.d.ts.map +1 -1
  208. package/dist/room-draft.js +186 -37
  209. package/dist/rows.d.ts +30 -35
  210. package/dist/rows.d.ts.map +1 -1
  211. package/dist/rows.js +116 -67
  212. package/dist/save.d.ts +71 -4
  213. package/dist/save.d.ts.map +1 -1
  214. package/dist/save.js +815 -146
  215. package/dist/search.d.ts +41 -29
  216. package/dist/search.d.ts.map +1 -1
  217. package/dist/search.js +37 -105
  218. package/dist/seed.d.ts +16 -0
  219. package/dist/seed.d.ts.map +1 -0
  220. package/dist/seed.js +75 -0
  221. package/dist/shared.d.ts +16 -0
  222. package/dist/shared.d.ts.map +1 -0
  223. package/dist/shared.js +431 -0
  224. package/dist/status.d.ts +6 -2
  225. package/dist/status.d.ts.map +1 -1
  226. package/dist/status.js +0 -8
  227. package/dist/store.d.ts +124 -61
  228. package/dist/store.d.ts.map +1 -1
  229. package/dist/store.js +358 -354
  230. package/dist/stored.d.ts +145 -0
  231. package/dist/stored.d.ts.map +1 -0
  232. package/dist/stored.js +567 -0
  233. package/dist/tables.d.ts +354 -0
  234. package/dist/tables.d.ts.map +1 -0
  235. package/dist/tables.js +624 -0
  236. package/dist/turn.d.ts +9 -0
  237. package/dist/turn.d.ts.map +1 -0
  238. package/dist/turn.js +75 -0
  239. package/dist/validation.d.ts +30 -0
  240. package/dist/validation.d.ts.map +1 -0
  241. package/dist/validation.js +162 -0
  242. package/dist/windows.d.ts +22 -0
  243. package/dist/windows.d.ts.map +1 -0
  244. package/dist/windows.js +89 -0
  245. package/dist/wire.d.ts +168 -33
  246. package/dist/wire.d.ts.map +1 -1
  247. package/dist/wire.js +67 -31
  248. package/dist/words.d.ts +19 -0
  249. package/dist/words.d.ts.map +1 -0
  250. package/dist/words.js +150 -0
  251. package/package.json +34 -16
  252. package/dist/attachment.d.ts +0 -3
  253. package/dist/attachment.d.ts.map +0 -1
  254. package/dist/attachment.js +0 -8
  255. package/dist/read-side.d.ts +0 -18
  256. package/dist/read-side.d.ts.map +0 -1
  257. package/dist/read-side.js +0 -433
package/dist/save.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import { definitionOf } from './model.js';
2
- import { apply, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, serialize, target, } from '@agentshouse/mdmodel';
3
- import { immutable, } from './change.js';
4
- import { governing, parseCollection, validateCollection, } from './collection.js';
2
+ import { apply, ARRAY_INDEX, ATTACHMENT, links, parse, pointerSegments, references, relative, relativize, retarget, roomReferences, serialize, target, } from '@agentshouse/mdmodel';
3
+ import { immutable, leaving, queuedFile, departure, } from './change.js';
4
+ import { collectionOf, documentsOf, governing, } from './collection.js';
5
5
  import { changeBatchIdentity } from './identity.js';
6
- import { overlay } from './overlay.js';
6
+ import { overlaying } from './overlay.js';
7
7
  import { covered, normalize } from './paths.js';
8
- function refuse(rule, file, location) {
8
+ import { sorting } from './shared.js';
9
+ import { paced, ROW_WEIGHT, stepped } from './turn.js';
10
+ import { validatedCollection } from './validation.js';
11
+ function refuse(code, file, location) {
9
12
  return {
10
13
  kind: 'refused',
11
14
  refusal: {
12
- rule,
15
+ code,
16
+ retryable: false,
13
17
  ...(file === undefined ? {} : { file }),
14
18
  ...(location === undefined ? {} : { location }),
15
19
  },
@@ -19,7 +23,7 @@ function repointed(document, definition, from, to, moves, settled) {
19
23
  const parsed = parse(document.content, from);
20
24
  if (!parsed.ok)
21
25
  return [];
22
- let next = parsed.document;
26
+ const changes = [];
23
27
  const names = new Set();
24
28
  for (const { pointer, value } of definition === undefined
25
29
  ? []
@@ -31,38 +35,39 @@ function repointed(document, definition, from, to, moves, settled) {
31
35
  const destination = moves.get(named) ?? named;
32
36
  if (target(to, value) === destination)
33
37
  continue;
34
- const applied = apply(next, {
38
+ changes.push({
35
39
  kind: 'reference',
36
40
  pointer,
37
41
  value: retarget(value, relative(to, destination)),
38
42
  prior: value,
39
43
  });
40
- if (applied.outcome !== 'applied')
41
- continue;
42
- next = applied.document;
43
44
  names.add(name);
44
45
  }
46
+ if (changes.length === 0)
47
+ return [];
48
+ const applied = apply(parsed.document, changes);
49
+ if (applied.outcome !== 'applied')
50
+ return [];
51
+ const next = applied.document;
45
52
  return [...names].map((name) => ({ kind: 'field', path: from, name, value: next.fields[name] }));
46
53
  }
47
- function edited(document, path, changes) {
48
- let next = document;
54
+ function edited(document, changes) {
55
+ const parts = new Map();
49
56
  for (const change of changes) {
50
- if (change.path !== path)
51
- continue;
52
- if (change.kind !== 'section' && change.kind !== 'preamble')
53
- continue;
54
- const applied = apply(next, change.kind === 'preamble'
55
- ? { kind: 'preamble', value: change.value, prior: next.preamble }
56
- : {
57
+ if (change.kind === 'preamble')
58
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
59
+ if (change.kind === 'section')
60
+ parts.set(`section ${change.name}`, {
57
61
  kind: 'section',
58
62
  name: change.name,
59
63
  value: change.value,
60
- prior: next.sections[change.name],
64
+ prior: document.sections[change.name] ?? '',
61
65
  });
62
- if (applied.outcome === 'applied')
63
- next = applied.document;
64
66
  }
65
- return next;
67
+ if (parts.size === 0)
68
+ return document;
69
+ const applied = apply(document, [...parts.values()]);
70
+ return applied.outcome === 'applied' ? applied.document : document;
66
71
  }
67
72
  function bodies(path, before, after) {
68
73
  const changes = [];
@@ -77,7 +82,7 @@ function bodies(path, before, after) {
77
82
  return changes;
78
83
  }
79
84
  function relinks(path, moved, before, moves) {
80
- let after = before;
85
+ const changes = [];
81
86
  for (const link of links(before, path)) {
82
87
  const destination = moves.get(link.target) ?? link.target;
83
88
  if (destination === link.target && moved === path)
@@ -85,19 +90,23 @@ function relinks(path, moved, before, moves) {
85
90
  const to = relative(moved, destination);
86
91
  if (to === relative(path, link.target))
87
92
  continue;
88
- const applied = apply(after, { kind: 'link', href: link.href, to });
89
- if (applied.outcome === 'refuse') {
90
- return {
91
- kind: 'refused',
92
- refusal: {
93
- rule: applied.refusal.rule,
94
- file: path,
95
- ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
96
- },
97
- };
98
- }
99
- after = applied.document;
93
+ changes.push({ kind: 'link', href: link.href, to });
100
94
  }
95
+ if (changes.length === 0)
96
+ return { kind: 'changes', changes: [] };
97
+ const applied = apply(before, changes);
98
+ if (applied.outcome === 'refuse') {
99
+ return {
100
+ kind: 'refused',
101
+ refusal: {
102
+ code: applied.refusal.rule,
103
+ retryable: false,
104
+ file: path,
105
+ ...(applied.refusal.location === undefined ? {} : { location: applied.refusal.location }),
106
+ },
107
+ };
108
+ }
109
+ const after = applied.document;
101
110
  if (after === before)
102
111
  return { kind: 'changes', changes: [] };
103
112
  if (before.type === undefined) {
@@ -105,74 +114,161 @@ function relinks(path, moved, before, moves) {
105
114
  }
106
115
  return { kind: 'changes', changes: bodies(path, before, after) };
107
116
  }
108
- function subsumed(merged, path) {
109
- for (let index = merged.length - 1; index >= 0; index -= 1) {
110
- const item = merged[index];
111
- if (item.path !== path)
112
- continue;
113
- if (item.kind === 'section' || item.kind === 'preamble')
114
- merged.splice(index, 1);
115
- }
117
+ function partKey(change) {
118
+ return change.kind === 'section' ? `section ${change.name}` : change.kind;
116
119
  }
117
- function rewrites(changes, visible) {
120
+ function* rewrites(changes, visible) {
118
121
  const moves = new Map();
119
122
  const settled = new Set();
120
- for (const change of changes) {
123
+ const parts = new Map();
124
+ for (const [index, change] of changes.entries()) {
121
125
  if (change.kind === 'move')
122
126
  moves.set(change.path, change.to);
123
- if (change.kind === 'remove' || change.kind === 'replace' || change.kind === 'create') {
127
+ if (change.kind === 'remove' ||
128
+ change.kind === 'replace' ||
129
+ change.kind === 'create' ||
130
+ change.kind === 'binary') {
124
131
  settled.add(change.path);
125
132
  }
126
133
  if (change.kind === 'field')
127
134
  settled.add(`${change.path} ${change.name}`);
135
+ if (isPart(change)) {
136
+ const held = parts.get(change.path);
137
+ if (held === undefined)
138
+ parts.set(change.path, [index]);
139
+ else
140
+ held.push(index);
141
+ }
128
142
  }
129
143
  if (moves.size === 0)
130
144
  return { kind: 'changes', changes };
131
145
  const merged = [...changes];
132
146
  const added = [];
133
- for (const [path, document] of Object.entries(visible.collection.documents)) {
147
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
134
148
  if (settled.has(path))
135
149
  continue;
136
150
  const moved = moves.get(path) ?? path;
137
151
  const definition = definitionOf(governing(visible.collection.composition, path), document.type);
138
- added.push(...repointed(document, definition, path, moved, moves, settled));
139
- const relinked = relinks(path, moved, edited(document, path, merged), moves);
152
+ const repointing = repointed(document, definition, path, moved, moves, settled);
153
+ added.push(...repointing);
154
+ const own = parts.get(path) ?? [];
155
+ const relinked = relinks(path, moved, edited(document, own.map((index) => merged[index])), moves);
140
156
  if (relinked.kind === 'refused')
141
157
  return relinked;
158
+ const keyed = new Map();
159
+ for (const index of own) {
160
+ const key = partKey(merged[index]);
161
+ if (!keyed.has(key))
162
+ keyed.set(key, index);
163
+ }
142
164
  for (const change of relinked.changes) {
143
165
  if (change.kind === 'replace') {
144
- subsumed(merged, path);
166
+ for (const index of own) {
167
+ const kind = merged[index]?.kind;
168
+ if (kind === 'section' || kind === 'preamble')
169
+ merged[index] = null;
170
+ }
145
171
  added.push(change);
146
172
  continue;
147
173
  }
148
- const index = merged.findIndex((item) => item.kind === change.kind &&
149
- item.path === path &&
150
- (change.kind !== 'section' || (item.kind === 'section' && item.name === change.name)));
151
- if (index === -1)
174
+ const index = keyed.get(partKey(change));
175
+ if (index === undefined)
152
176
  added.push(change);
153
177
  else
154
178
  merged[index] = change;
155
179
  }
180
+ const text = visible.collection.text[path].length;
181
+ const rewritten = repointing.length > 0 || relinked.changes.length > 0;
182
+ yield ROW_WEIGHT + text + (rewritten ? text : 0);
156
183
  }
157
- return { kind: 'changes', changes: [...added, ...merged] };
184
+ return {
185
+ kind: 'changes',
186
+ changes: [...added, ...merged.filter((change) => change !== null)],
187
+ };
158
188
  }
159
- function anticipated(changes, preconditions, rewritten, visible) {
160
- const derived = rewritten.filter((change) => !changes.includes(change));
189
+ function* anticipated(changes, preconditions, rewritten, visible) {
190
+ const given = new Set(changes);
191
+ const derived = rewritten.filter((change) => !given.has(change));
161
192
  return {
162
193
  changes: rewritten,
163
- preconditions: [...preconditionsOf(derived, visible, []), ...preconditions],
194
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...preconditions],
164
195
  };
165
196
  }
166
- export function providerRewritten(batch, visible) {
167
- const rewritten = rewrites(batch.changes, visible);
197
+ export function* providerRewritten(batch, visible) {
198
+ const rewritten = yield* rewrites(batch.changes, visible);
168
199
  if (rewritten.kind === 'refused')
169
200
  return batch;
170
201
  return {
171
202
  ...batch,
172
- ...anticipated(batch.changes, batch.preconditions, rewritten.changes, visible),
203
+ ...(yield* anticipated(batch.changes, batch.preconditions, rewritten.changes, visible)),
204
+ };
205
+ }
206
+ function crossed(document, definition, path, to) {
207
+ const changes = [];
208
+ for (const link of links(document, path)) {
209
+ const written = to(link.target, link.href);
210
+ if (written !== undefined)
211
+ changes.push({ kind: 'link', href: link.href, to: written });
212
+ }
213
+ for (const { pointer, value } of definition === undefined
214
+ ? []
215
+ : references(document, definition)) {
216
+ const named = target(path, value);
217
+ const written = named === null ? undefined : to(named, value);
218
+ if (written !== undefined) {
219
+ changes.push({ kind: 'reference', pointer, value: retarget(value, written), prior: value });
220
+ }
221
+ }
222
+ return changes;
223
+ }
224
+ function rewritten(path, document, changes) {
225
+ if (changes.length === 0)
226
+ return [];
227
+ const applied = apply(document, changes);
228
+ if (applied.outcome !== 'applied')
229
+ return [];
230
+ const after = applied.document;
231
+ if (document.type === undefined)
232
+ return [{ kind: 'replace', path, content: serialize(after) }];
233
+ const names = new Set(changes.flatMap((change) => change.kind === 'reference' ? [pointerSegments(change.pointer)[0] ?? ''] : []));
234
+ return [
235
+ ...[...names].map((name) => ({ kind: 'field', path, name, value: after.fields[name] })),
236
+ ...bodies(path, document, after),
237
+ ];
238
+ }
239
+ function roomPathOf(handle, path) {
240
+ return `/rooms/${handle}/${path}`;
241
+ }
242
+ export function* leavingRewritten(batch, visible) {
243
+ const between = batch.between;
244
+ if (between === undefined)
245
+ return batch;
246
+ const moved = new Map(between.moves.map(({ path, to }) => [path, roomPathOf(between.destination.handle, to)]));
247
+ const derived = [];
248
+ for (const [path, document] of yield* documentsOf(visible.collection)) {
249
+ if (moved.has(path))
250
+ continue;
251
+ const definition = definitionOf(governing(visible.collection.composition, path), document.type);
252
+ derived.push(...rewritten(path, document, crossed(document, definition, path, (named) => moved.get(named))));
253
+ yield ROW_WEIGHT + visible.collection.text[path].length;
254
+ }
255
+ if (derived.length === 0)
256
+ return batch;
257
+ return {
258
+ ...batch,
259
+ changes: [...derived, ...batch.changes],
260
+ preconditions: [...(yield* preconditionsOf(derived, visible, [])), ...batch.preconditions],
173
261
  };
174
262
  }
175
- function incoming(collection, removed) {
263
+ export function rewriting(batch, locator) {
264
+ return leaving(batch, locator) || batch.changes.some((change) => change.kind === 'move');
265
+ }
266
+ export function* effectiveIn(batch, locator, visible) {
267
+ return leaving(batch, locator)
268
+ ? yield* leavingRewritten(batch, visible)
269
+ : yield* providerRewritten(batch, visible);
270
+ }
271
+ function* incoming(collection, removed, candidates) {
176
272
  const found = new Map();
177
273
  const note = (target, file) => {
178
274
  if (!removed.has(target))
@@ -181,7 +277,20 @@ function incoming(collection, removed) {
181
277
  referrers.add(file);
182
278
  found.set(target, referrers);
183
279
  };
184
- for (const [path, document] of Object.entries(collection.documents)) {
280
+ const read = function* (paths) {
281
+ const seen = new Set();
282
+ for (const path of paths) {
283
+ if (seen.has(path))
284
+ continue;
285
+ seen.add(path);
286
+ const document = collection.document(path);
287
+ if (document !== undefined)
288
+ yield [path, document];
289
+ }
290
+ };
291
+ for (const [path, document] of candidates === null
292
+ ? yield* documentsOf(collection)
293
+ : read(candidates)) {
185
294
  const definition = definitionOf(governing(collection.composition, path), document.type);
186
295
  for (const { value, to } of definition === undefined ? [] : references(document, definition)) {
187
296
  if (to.includes(ATTACHMENT))
@@ -190,8 +299,9 @@ function incoming(collection, removed) {
190
299
  if (named !== null)
191
300
  note(named, path);
192
301
  }
302
+ yield ROW_WEIGHT + collection.text[path].length;
193
303
  }
194
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
195
305
  if (first === undefined)
196
306
  return null;
197
307
  const every = new Set();
@@ -200,18 +310,26 @@ function incoming(collection, removed) {
200
310
  every.add(file);
201
311
  return {
202
312
  kind: 'refused',
203
- refusal: { rule: 'referenced-missing-file', file: first, referrers: [...every].sort() },
313
+ refusal: {
314
+ code: 'referenced-missing-file',
315
+ retryable: false,
316
+ file: first,
317
+ referrers: yield* sorting(every),
318
+ },
204
319
  };
205
320
  }
206
321
  function affected(change) {
207
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
208
323
  }
209
- function guard(changes, input) {
210
- const existing = new Set([
211
- ...Object.keys(input.visible.documents),
212
- ...input.visible.binaryTargets,
213
- ]);
214
- for (const change of changes) {
324
+ function guard(changes, input, removing) {
325
+ const overridden = new Map();
326
+ const existing = {
327
+ has: (path) => overridden.get(path) ??
328
+ (Object.hasOwn(input.visible.documents, path) || Object.hasOwn(input.visible.binaries, path)),
329
+ add: (path) => void overridden.set(path, true),
330
+ delete: (path) => void overridden.set(path, false),
331
+ };
332
+ for (const [index, change] of changes.entries()) {
215
333
  for (const path of affected(change)) {
216
334
  const normalized = normalize(path);
217
335
  if (normalized === null || normalized === '')
@@ -219,22 +337,36 @@ function guard(changes, input) {
219
337
  if (covered(input.providerOwnedPaths, normalized)) {
220
338
  return refuse('provider-owned-path', normalized);
221
339
  }
222
- const operation = change.kind === 'remove' ? 'delete' : 'write';
223
- if (!covered(input.operations[operation], normalized))
340
+ const operation = change.kind === 'remove' ? removing : 'write';
341
+ if (!input.local && !covered(input.operations[operation], normalized))
224
342
  return refuse(`${operation}-grant`, normalized);
225
343
  }
226
- if (change.kind === 'create') {
344
+ if (change.kind === 'create' || change.kind === 'arrival') {
227
345
  if (existing.has(change.path))
228
346
  return refuse('path-collision', change.path);
229
347
  existing.add(change.path);
230
348
  continue;
231
349
  }
350
+ if (change.kind === 'binary') {
351
+ if (input.visible.documents[change.path] !== undefined ||
352
+ (existing.has(change.path) &&
353
+ input.bases.some((base) => base.kind === 'absent' && base.path === change.path))) {
354
+ return refuse('path-collision', change.path);
355
+ }
356
+ if (!input.local &&
357
+ existing.has(change.path) &&
358
+ input.visible.revisions[departure(changes, index)] === undefined) {
359
+ return refuse('unknown-revision', change.path);
360
+ }
361
+ existing.add(change.path);
362
+ continue;
363
+ }
232
364
  if (change.kind === 'move') {
233
365
  if (!existing.has(change.path))
234
366
  return refuse('path-absent', change.path);
235
367
  if (existing.has(change.to))
236
368
  return refuse('path-collision', change.to);
237
- if (unrevisionedDocument(input.visible, change.path)) {
369
+ if (unrevised(input, change.path)) {
238
370
  return refuse('unknown-revision', change.path);
239
371
  }
240
372
  existing.delete(change.path);
@@ -244,30 +376,53 @@ function guard(changes, input) {
244
376
  if (!existing.has(change.path))
245
377
  return refuse('path-absent', change.path);
246
378
  if (change.kind === 'remove') {
247
- if (unrevisionedDocument(input.visible, change.path)) {
379
+ if (unrevised(input, change.path)) {
248
380
  return refuse('unknown-revision', change.path);
249
381
  }
250
382
  existing.delete(change.path);
251
383
  continue;
252
384
  }
253
385
  if (change.kind === 'replace') {
254
- if (input.visible.revisions[change.path] === undefined) {
386
+ if (unrevised(input, change.path)) {
255
387
  return refuse('unknown-revision', change.path);
256
388
  }
257
389
  continue;
258
390
  }
259
- if (input.visible.collection.documents[change.path] === undefined) {
391
+ if (input.visible.collection.document(change.path) === undefined) {
260
392
  return refuse('path-absent', change.path);
261
393
  }
262
394
  }
263
395
  return null;
264
396
  }
265
- function unrevisionedDocument(visible, path) {
266
- return visible.revisions[path] === undefined && !visible.binaryTargets.includes(path);
397
+ function unrevised(input, path) {
398
+ return (!input.local && input.queued?.has(path) !== true && input.visible.revisions[path] === undefined);
399
+ }
400
+ export function queuedDocuments(queue) {
401
+ const paths = new Set();
402
+ for (const batch of queue) {
403
+ for (const change of batch.changes) {
404
+ if (change.kind === 'binary' || (change.kind === 'arrival' && !('content' in change)))
405
+ continue;
406
+ if (change.kind === 'remove')
407
+ paths.delete(change.path);
408
+ else if (change.kind !== 'move')
409
+ paths.add(change.path);
410
+ else if (paths.delete(change.path))
411
+ paths.add(change.to);
412
+ }
413
+ }
414
+ return paths;
415
+ }
416
+ function identity(precondition) {
417
+ const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
418
+ return `${precondition.kind} ${precondition.path} ${part}`;
267
419
  }
268
420
  function existence(visible, path) {
269
421
  const revision = visible.revisions[path];
270
- return revision === undefined ? { kind: 'present', path } : { kind: 'revision', path, revision };
422
+ if (revision !== undefined)
423
+ return { kind: 'revision', path, revision };
424
+ const content = visible.documents[path];
425
+ return content === undefined ? { kind: 'present', path } : { kind: 'content', path, content };
271
426
  }
272
427
  function sameTarget(precondition, change) {
273
428
  if (precondition.kind !== change.kind || precondition.path !== change.path)
@@ -275,25 +430,25 @@ function sameTarget(precondition, change) {
275
430
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
276
431
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
277
432
  }
278
- function preconditionsOf(changes, visible, bases) {
433
+ function* preconditionsOf(changes, visible, bases) {
279
434
  const preconditions = [];
280
435
  const seen = new Set();
281
436
  const add = (precondition) => {
282
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
283
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
284
- if (seen.has(identity))
437
+ const held = identity(precondition);
438
+ if (seen.has(held))
285
439
  return;
286
- seen.add(identity);
440
+ seen.add(held);
287
441
  preconditions.push(precondition);
288
442
  };
289
443
  for (const change of changes) {
444
+ yield ROW_WEIGHT;
290
445
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
291
446
  const based = bases.find((base) => sameTarget(base, change));
292
447
  if (based !== undefined) {
293
448
  add(based);
294
449
  continue;
295
450
  }
296
- const document = visible.collection.documents[change.path];
451
+ const document = visible.collection.document(change.path);
297
452
  if (change.kind === 'field') {
298
453
  add({
299
454
  kind: 'field',
@@ -318,6 +473,13 @@ function preconditionsOf(changes, visible, bases) {
318
473
  add({ kind: 'absent', path: change.path });
319
474
  continue;
320
475
  }
476
+ if (change.kind === 'binary') {
477
+ const revision = visible.revisions[change.path];
478
+ add(visible.binaries[change.path] === undefined || revision === undefined
479
+ ? { kind: 'absent', path: change.path }
480
+ : { kind: 'revision', path: change.path, revision });
481
+ continue;
482
+ }
321
483
  add(existence(visible, change.path));
322
484
  if (change.kind === 'move')
323
485
  add({ kind: 'absent', path: change.to });
@@ -327,10 +489,13 @@ function preconditionsOf(changes, visible, bases) {
327
489
  function depth(path) {
328
490
  return path.split('/').length;
329
491
  }
330
- function exceeded(limits, changes, resulting) {
331
- const written = changes.map((change) => (change.kind === 'move' ? change.to : change.path));
492
+ function* exceeding(limits, changes, effective, resulting) {
493
+ const written = effective.map((change) => (change.kind === 'move' ? change.to : change.path));
332
494
  const created = changes
333
- .filter((change) => change.kind === 'create' || change.kind === 'move')
495
+ .filter((change) => change.kind === 'create' ||
496
+ change.kind === 'move' ||
497
+ change.kind === 'binary' ||
498
+ change.kind === 'arrival')
334
499
  .map((change) => (change.kind === 'move' ? change.to : change.path));
335
500
  for (const limit of limits) {
336
501
  const value = limit.value;
@@ -344,9 +509,13 @@ function exceeded(limits, changes, resulting) {
344
509
  return refuse('provider-limit', undefined, limit.name);
345
510
  }
346
511
  if (limit.name === 'document-size') {
347
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
348
- if (large !== undefined)
349
- return refuse('provider-limit', large, limit.name);
512
+ for (const path of written) {
513
+ const text = resulting.documents[path] ?? '';
514
+ if (new TextEncoder().encode(text).byteLength > value) {
515
+ return refuse('provider-limit', path, limit.name);
516
+ }
517
+ yield ROW_WEIGHT + text.length;
518
+ }
350
519
  }
351
520
  if (limit.name === 'path-length') {
352
521
  const long = created.find((path) => path.length > value);
@@ -376,7 +545,7 @@ function relativeField(path, name, value, handle, definition) {
376
545
  !opened.ok ||
377
546
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
378
547
  return value;
379
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
548
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
380
549
  if (held.outcome !== 'applied')
381
550
  return value;
382
551
  const result = relativize(held.document, path, handle, definition);
@@ -389,7 +558,7 @@ function relativeChanges(input) {
389
558
  return input.changes.map((change) => {
390
559
  const definition = typed(change.path);
391
560
  if (change.kind === 'field') {
392
- const type = visible.collection.documents[change.path]?.type;
561
+ const type = visible.collection.document(change.path)?.type;
393
562
  const value = relativeField(change.path, change.name, change.value, handle, definition(type));
394
563
  return value === change.value ? change : { ...change, value };
395
564
  }
@@ -412,65 +581,454 @@ function relativeChanges(input) {
412
581
  return { ...change, fields, sections };
413
582
  });
414
583
  }
415
- export function buildBatch(input) {
416
- const changes = relativeChanges(input);
417
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
418
- if (structural !== undefined &&
419
- Object.entries(input.visible.collection.documents).some(([path, document]) => document.type !== undefined &&
420
- document.type !== 'any' &&
421
- governing(input.composition, path).status === 'unavailable'))
422
- return refuse('model-unavailable', structural.path);
584
+ function unqueueable(file) {
585
+ return { kind: 'refused', refusal: { code: 'unknown-revision', retryable: false, file } };
586
+ }
587
+ function isPart(change) {
588
+ return change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble';
589
+ }
590
+ function touched(change) {
591
+ return change.kind === 'move' ? [change.path, change.to] : [change.path];
592
+ }
593
+ function partsApplied(content, path, changes) {
594
+ const parsed = parse(content, path);
595
+ if (!parsed.ok)
596
+ return null;
597
+ const document = parsed.document;
598
+ const parts = new Map();
599
+ for (const change of changes) {
600
+ if (change.kind === 'field')
601
+ parts.set(`field ${change.name}`, {
602
+ kind: 'field',
603
+ name: change.name,
604
+ value: change.value,
605
+ prior: document.fields[change.name],
606
+ });
607
+ else if (change.kind === 'section')
608
+ parts.set(`section ${change.name}`, {
609
+ kind: 'section',
610
+ name: change.name,
611
+ value: change.value,
612
+ prior: document.sections[change.name] ?? '',
613
+ });
614
+ else
615
+ parts.set('preamble', { kind: 'preamble', value: change.value, prior: document.preamble });
616
+ }
617
+ const applied = apply(document, [...parts.values()]);
618
+ return applied.outcome === 'applied' ? serialize(applied.document) : null;
619
+ }
620
+ function settledParts(changes, deferred) {
621
+ for (const [held, parts] of deferred) {
622
+ const content = partsApplied(held.content, held.path, parts);
623
+ if (content === null)
624
+ return held.path;
625
+ changes[changes.indexOf(held)] = { ...held, content };
626
+ }
627
+ deferred.clear();
628
+ return null;
629
+ }
630
+ function folded(changes, change, appending, deferred) {
631
+ const held = changes.find((item) => item.path === change.path && (item.kind === 'create' || item.kind === 'replace'));
632
+ const whole = held === undefined ? -1 : changes.indexOf(held);
633
+ const occupied = changes.some((item) => !isPart(item) && item !== held && touched(item).includes(change.path));
634
+ if (isPart(change)) {
635
+ const same = changes.findIndex((item) => item.kind === change.kind &&
636
+ item.path === change.path &&
637
+ (change.kind === 'preamble' || ('name' in item && item.name === change.name)));
638
+ if (same !== -1) {
639
+ changes[same] = change;
640
+ return 'folded';
641
+ }
642
+ if (held === undefined) {
643
+ if (!appending)
644
+ return 'skipped';
645
+ if (occupied)
646
+ return 'refused';
647
+ changes.push(change);
648
+ return 'appended';
649
+ }
650
+ if ('content' in held) {
651
+ const parts = deferred.get(held);
652
+ if (parts === undefined)
653
+ deferred.set(held, [change]);
654
+ else
655
+ parts.push(change);
656
+ return 'folded';
657
+ }
658
+ if (change.kind === 'preamble')
659
+ return 'refused';
660
+ changes[whole] =
661
+ change.kind === 'field'
662
+ ? { ...held, fields: { ...held.fields, [change.name]: change.value } }
663
+ : { ...held, sections: { ...held.sections, [change.name]: change.value } };
664
+ return 'folded';
665
+ }
666
+ if (change.kind === 'replace') {
667
+ if (held !== undefined) {
668
+ if (!('content' in held))
669
+ return 'refused';
670
+ changes[whole] = { ...held, content: change.content };
671
+ return 'folded';
672
+ }
673
+ const parts = changes.filter((item) => isPart(item) && item.path === change.path);
674
+ if (parts.length === 0 && !appending)
675
+ return 'skipped';
676
+ if (occupied)
677
+ return 'refused';
678
+ changes.splice(0, changes.length, ...changes.filter((item) => !parts.includes(item)), change);
679
+ return 'appended';
680
+ }
681
+ if (changes.some((item) => touched(change).some((path) => touched(item).includes(path))))
682
+ return 'refused';
683
+ changes.push(change);
684
+ return 'appended';
685
+ }
686
+ function* relinked(batch, move, before, after, appending) {
687
+ const derived = yield* rewrites([move], after);
688
+ if (derived.kind === 'refused')
689
+ return derived;
690
+ const changes = [...batch.changes];
691
+ const appended = [];
692
+ let changed = false;
693
+ const deferred = new Map();
694
+ for (const change of derived.changes) {
695
+ if (change === move)
696
+ continue;
697
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
698
+ if (unsettled !== null)
699
+ return unqueueable(unsettled);
700
+ const outcome = folded(changes, change, appending, deferred);
701
+ if (outcome === 'refused')
702
+ return unqueueable(change.path);
703
+ if (outcome === 'appended')
704
+ appended.push(change);
705
+ if (outcome !== 'skipped')
706
+ changed = true;
707
+ }
708
+ const unsettled = settledParts(changes, deferred);
709
+ if (unsettled !== null)
710
+ return unqueueable(unsettled);
711
+ if (!changed)
712
+ return { kind: 'relinked', batch };
713
+ const held = new Set(batch.preconditions.map(identity));
714
+ const added = yield* preconditionsOf(appended, before, []);
715
+ return {
716
+ kind: 'relinked',
717
+ batch: immutable({
718
+ ...batch,
719
+ id: changeBatchIdentity(),
720
+ changes,
721
+ preconditions: [
722
+ ...batch.preconditions,
723
+ ...added.filter((precondition) => !held.has(identity(precondition))),
724
+ ],
725
+ }),
726
+ };
727
+ }
728
+ function leading(changes, at) {
729
+ const from = departure(changes, at);
730
+ return new Set(changes.filter((change, index) => index < at && change.kind === 'move' && departure(changes, index) === from));
731
+ }
732
+ function created(batch, at) {
733
+ const file = batch.changes[at];
734
+ return (leading(batch.changes, at).size === 0 &&
735
+ batch.preconditions.some((precondition) => precondition.kind === 'absent' && precondition.path === file.path));
736
+ }
737
+ function withdrawnFile(batch, change, at) {
738
+ const file = batch.changes[at];
739
+ const from = departure(batch.changes, at);
740
+ const moves = leading(batch.changes, at);
741
+ const fresh = created(batch, at);
742
+ const vacated = new Set([...moves].map((move) => move.to));
743
+ if (fresh)
744
+ vacated.add(file.path);
745
+ const replaced = fresh
746
+ ? change.kind === 'remove'
747
+ ? []
748
+ : [{ ...file, path: change.to }]
749
+ : change.kind === 'remove'
750
+ ? [{ kind: 'remove', path: from }]
751
+ : change.to === from
752
+ ? [{ ...file, path: from }]
753
+ : [
754
+ { kind: 'move', path: from, to: change.to },
755
+ { ...file, path: change.to },
756
+ ];
757
+ const added = change.kind === 'move' && (fresh || change.to !== from)
758
+ ? [{ kind: 'absent', path: change.to }]
759
+ : [];
760
+ const changes = batch.changes.flatMap((item) => item === file ? replaced : item.kind === 'move' && moves.has(item) ? [] : [item]);
423
761
  if (changes.length === 0)
424
- return refuse('no-change');
425
- const rewritten = rewrites(changes, input.visible);
426
- if (rewritten.kind === 'refused')
427
- return rewritten;
428
- const refusal = guard(changes, input);
429
- if (refusal !== null)
430
- return refusal;
431
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
432
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
433
- const resulting = overlay(input.visible, [
434
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
435
- ]);
436
- const removed = new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path));
762
+ return null;
763
+ const preconditions = [
764
+ ...added,
765
+ ...batch.preconditions.filter((precondition) => precondition.kind !== 'absent' || !vacated.has(precondition.path)),
766
+ ];
767
+ return immutable({ ...batch, id: changeBatchIdentity(), changes, preconditions });
768
+ }
769
+ export function* requeuing(queue, changes, visible, changeable, state) {
770
+ const next = [...queue];
771
+ const origins = new Map();
772
+ const withdrawn = new Map();
773
+ const owners = new Set();
774
+ const queued = new Set();
775
+ const kept = [];
776
+ const originOf = (batch) => origins.get(batch) ?? batch.id;
777
+ const open = (batch) => origins.has(batch) || changeable(batch);
778
+ for (const change of changes) {
779
+ const held = visible.binaries[change.path];
780
+ if ((change.kind !== 'move' && change.kind !== 'remove') || held?.state !== 'pending') {
781
+ kept.push(change);
782
+ continue;
783
+ }
784
+ let index = next.length - 1;
785
+ while (index >= 0 && queuedFile(next[index], change.path, held.localId) === -1)
786
+ index -= 1;
787
+ const owner = next[index];
788
+ if (owner === undefined || !open(owner))
789
+ return unqueueable(change.path);
790
+ const following = next.slice(index);
791
+ const rebuilt = [];
792
+ let before = change.kind === 'move' ? yield* state(next.slice(0, index)) : visible;
793
+ for (const [offset, batch] of following.entries()) {
794
+ let current = batch;
795
+ if (change.kind === 'move') {
796
+ const after = yield* state(next.slice(0, index + offset + 1));
797
+ const appending = offset === 0 && created(batch, queuedFile(batch, change.path, held.localId));
798
+ const relinking = yield* relinked(batch, change, before, after, appending);
799
+ if (relinking.kind === 'refused')
800
+ return relinking;
801
+ current = relinking.batch;
802
+ before = after;
803
+ }
804
+ if (current !== batch && (!open(batch) || batch.between !== undefined))
805
+ return unqueueable(change.path);
806
+ rebuilt.push(offset === 0
807
+ ? withdrawnFile(current, change, queuedFile(current, change.path, held.localId))
808
+ : current);
809
+ }
810
+ for (const [offset, batch] of following.entries()) {
811
+ const replaced = rebuilt[offset];
812
+ if (replaced === batch)
813
+ continue;
814
+ const origin = originOf(batch);
815
+ withdrawn.set(origin, replaced);
816
+ if (replaced !== null)
817
+ origins.set(replaced, origin);
818
+ }
819
+ owners.add(originOf(owner));
820
+ next.splice(index, following.length, ...rebuilt.filter((batch) => batch !== null));
821
+ queued.add(change.path);
822
+ }
823
+ return {
824
+ kind: 'requeued',
825
+ queue: withdrawn.size === 0 ? queue : next,
826
+ changes: kept,
827
+ queued,
828
+ withdrawn,
829
+ owners,
830
+ };
831
+ }
832
+ function covering(precondition) {
833
+ return precondition.kind === 'field' ||
834
+ precondition.kind === 'section' ||
835
+ precondition.kind === 'preamble'
836
+ ? identity(precondition)
837
+ : `path ${precondition.path}`;
838
+ }
839
+ export function joined(queue, owner, extra) {
840
+ const later = new Set(queue.slice(queue.indexOf(owner) + 1).flatMap((batch) => batch.changes.flatMap(touched)));
841
+ const clash = extra.changes.flatMap(touched).find((path) => later.has(path));
842
+ if (clash !== undefined)
843
+ return unqueueable(clash);
844
+ const changes = [...owner.changes];
845
+ const deferred = new Map();
846
+ for (const change of extra.changes) {
847
+ const unsettled = isPart(change) ? null : settledParts(changes, deferred);
848
+ if (unsettled !== null)
849
+ return unqueueable(unsettled);
850
+ if (folded(changes, change, true, deferred) === 'refused')
851
+ return unqueueable(change.path);
852
+ }
853
+ const unsettled = settledParts(changes, deferred);
854
+ if (unsettled !== null)
855
+ return unqueueable(unsettled);
856
+ const held = new Set(owner.preconditions.map(covering));
857
+ const written = new Set(owner.changes.flatMap((change) => change.kind === 'create' || change.kind === 'replace' ? [change.path] : []));
858
+ const preconditions = [
859
+ ...owner.preconditions,
860
+ ...extra.preconditions.filter((precondition) => {
861
+ const key = covering(precondition);
862
+ return (!held.has(key) && (key === `path ${precondition.path}` || !written.has(precondition.path)));
863
+ }),
864
+ ];
865
+ const batch = immutable({
866
+ ...owner,
867
+ ...(extra.validation === undefined ? {} : { validation: extra.validation }),
868
+ id: changeBatchIdentity(),
869
+ changes,
870
+ preconditions,
871
+ });
872
+ return { kind: 'joined', batch, queue: queue.map((item) => (item === owner ? batch : item)) };
873
+ }
874
+ export function* limiting(queue, batch, limits, state) {
875
+ const at = queue.indexOf(batch);
876
+ const rewritten = yield* providerRewritten(batch, yield* state(queue.slice(0, at)));
877
+ const limited = yield* exceeding(limits, batch.changes, rewritten.changes, yield* state(queue.slice(0, at + 1)));
878
+ return limited?.kind === 'refused' ? limited.refusal : null;
879
+ }
880
+ function typedText(content) {
881
+ if (content === undefined)
882
+ return false;
883
+ const parsed = parse(content);
884
+ return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
885
+ }
886
+ function touchedLibrary(resulting, touched, composition) {
887
+ const entries = touched
888
+ .flatMap((path) => {
889
+ const content = resulting.documents[path];
890
+ const binary = resulting.binaries[path] !== undefined;
891
+ return content === undefined && !binary ? [] : [{ path, content: content ?? null }];
892
+ })
893
+ .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
894
+ const contents = new Map(entries.map(({ path, content }) => [path, content]));
895
+ const binaries = new Set(Object.keys(resulting.binaries).filter((path) => resulting.binaries[path] !== undefined));
896
+ return validatedCollection(() => Promise.resolve({ entries, next: null }), async (paths) => {
897
+ const known = new Map();
898
+ const pace = paced();
899
+ for (const path of paths) {
900
+ const content = contents.get(path);
901
+ if (binaries.has(path))
902
+ known.set(path, { type: undefined, binary: true });
903
+ else if (content !== undefined && content !== null) {
904
+ const read = parse(content, path);
905
+ if (read.ok)
906
+ known.set(path, { type: read.document.type ?? 'any', binary: false });
907
+ await pace(content.length + ROW_WEIGHT);
908
+ }
909
+ }
910
+ return known;
911
+ }, composition);
912
+ }
913
+ async function judging(visible, resulting, composition, validate, removed, touched) {
437
914
  if (removed.size > 0) {
438
- const referenced = incoming(parseCollection(resulting.documents, input.composition), removed);
915
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
439
916
  if (referenced !== null)
440
917
  return referenced;
441
918
  }
442
- const typed = (content) => {
443
- if (content === undefined)
444
- return false;
445
- const parsed = parse(content);
446
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
447
- };
448
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
449
- const needsModel = typedPaths.length > 0;
450
- const withoutModel = typedPaths.find((path) => governing(input.composition, path).status === 'unavailable');
919
+ const typedPaths = [];
920
+ const pace = paced();
921
+ for (const path of touched) {
922
+ const before = visible.documents[path];
923
+ const after = resulting.documents[path];
924
+ if (typedText(before) || typedText(after))
925
+ typedPaths.push(path);
926
+ await pace((before?.length ?? 0) + (after?.length ?? 0) + ROW_WEIGHT);
927
+ }
928
+ const withoutModel = typedPaths.find((path) => governing(composition, path).status === 'unavailable');
451
929
  if (withoutModel !== undefined)
452
930
  return refuse('model-unavailable', withoutModel);
453
- const validationText = needsModel
454
- ? resulting.documents
455
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
456
- const verdict = validateCollection(validationText, resulting.binaryTargets, input.composition);
931
+ const verdict = typedPaths.length > 0
932
+ ? await validate(resulting, touched)
933
+ : await touchedLibrary(resulting, touched, composition);
457
934
  if (verdict.verdict === 'refuse') {
458
935
  return {
459
936
  kind: 'refused',
460
937
  refusal: {
461
- rule: verdict.rule,
938
+ code: verdict.rule,
939
+ retryable: false,
462
940
  file: verdict.file,
463
941
  ...(verdict.location === undefined ? {} : { location: verdict.location }),
464
942
  ...(verdict.referrers === undefined ? {} : { referrers: verdict.referrers }),
465
943
  },
466
944
  };
467
945
  }
468
- const limited = exceeded(input.providerLimits, changes, resulting);
946
+ return {
947
+ kind: 'judged',
948
+ validated: typedPaths
949
+ .map((path) => governing(composition, path))
950
+ .find((model) => model.status !== 'unavailable'),
951
+ };
952
+ }
953
+ function placed(reference) {
954
+ const key = roomPathOf(reference.handle, reference.path);
955
+ if (reference.kind === 'link')
956
+ return `link ${key}`;
957
+ if (reference.kind === 'item')
958
+ return `section ${reference.section} ${key}`;
959
+ const pointer = reference.pointer
960
+ .split('/')
961
+ .map((segment) => (ARRAY_INDEX.test(segment) ? '-' : segment))
962
+ .join('/');
963
+ return `field ${pointer} ${key}`;
964
+ }
965
+ function roomReferencesIn(path, content, composition) {
966
+ if (content === undefined)
967
+ return [];
968
+ const parsed = parse(content, path);
969
+ if (!parsed.ok)
970
+ return [];
971
+ return roomReferences(path, content, definitionOf(governing(composition, path), parsed.document.type));
972
+ }
973
+ function* admittedRooms(changes, visible, resulting, composition) {
974
+ const from = new Map();
975
+ for (const [index, change] of changes.entries()) {
976
+ if (change.kind === 'move')
977
+ from.set(change.to, departure(changes, index));
978
+ }
979
+ for (const path of [...new Set(changes.flatMap(affected))].sort()) {
980
+ const now = resulting.documents[path];
981
+ if (now === undefined)
982
+ continue;
983
+ const held = new Set(roomReferencesIn(path, visible.documents[from.get(path) ?? path], composition).map(placed));
984
+ for (const reference of roomReferencesIn(path, now, composition)) {
985
+ if (reference.kind === 'link' || reference.crossRoom === true)
986
+ continue;
987
+ if (held.has(placed(reference)))
988
+ continue;
989
+ return refuse('unresolved-reference', path, reference.kind === 'field' ? reference.pointer : reference.section);
990
+ }
991
+ yield ROW_WEIGHT + now.length;
992
+ }
993
+ return null;
994
+ }
995
+ function validationOf(validated) {
996
+ return validated === undefined
997
+ ? {}
998
+ : {
999
+ validation: {
1000
+ identity: validated.snapshot.identity,
1001
+ version: validated.snapshot.version,
1002
+ source: validated.status,
1003
+ },
1004
+ };
1005
+ }
1006
+ async function batching(input) {
1007
+ const changes = relativeChanges(input);
1008
+ const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
1009
+ if (structural !== undefined && input.unmodeled) {
1010
+ return refuse('model-unavailable', structural.path);
1011
+ }
1012
+ if (changes.length === 0)
1013
+ return refuse('no-change');
1014
+ const rewritten = await stepped(rewrites(changes, input.visible));
1015
+ if (rewritten.kind === 'refused')
1016
+ return rewritten;
1017
+ const refusal = guard(changes, input, 'delete');
1018
+ if (refusal !== null)
1019
+ return refusal;
1020
+ const preconditions = await stepped(preconditionsOf(changes, input.visible, input.bases));
1021
+ const predicted = await stepped(anticipated(changes, preconditions, rewritten.changes, input.visible));
1022
+ const resulting = await stepped(overlaying(input.visible, [{ id: '', baseRoomVersion: input.baseRoomVersion, ...predicted }]));
1023
+ const judged = await judging(input.visible, resulting, input.composition, input.validate, new Set(changes.filter((change) => change.kind === 'remove').map((change) => change.path)), [...new Set(predicted.changes.flatMap(affected))]);
1024
+ if (judged.kind === 'refused')
1025
+ return judged;
1026
+ const crossing = await stepped(admittedRooms(changes, input.visible, resulting, input.composition));
1027
+ if (crossing !== null)
1028
+ return crossing;
1029
+ const limited = await stepped(exceeding(input.providerLimits, changes, predicted.changes, resulting));
469
1030
  if (limited !== null)
470
1031
  return limited;
471
- const validated = typedPaths
472
- .map((path) => governing(input.composition, path))
473
- .find((model) => model.status !== 'unavailable');
474
1032
  return {
475
1033
  kind: 'batch',
476
1034
  batch: immutable({
@@ -479,15 +1037,126 @@ export function buildBatch(input) {
479
1037
  changes,
480
1038
  preconditions,
481
1039
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
482
- ...(validated === undefined
483
- ? {}
484
- : {
485
- validation: {
486
- identity: validated.snapshot.identity,
487
- version: validated.snapshot.version,
488
- source: validated.status,
489
- },
490
- }),
1040
+ ...validationOf(judged.validated),
491
1041
  }),
492
1042
  };
493
1043
  }
1044
+ export const MOVE_DOCUMENTS = 'move-documents';
1045
+ function heldAt(visible, path) {
1046
+ return visible.documents[path] !== undefined || visible.binaries[path] !== undefined;
1047
+ }
1048
+ function arrivingText(path, to, source, moved) {
1049
+ const content = source.visible.documents[path];
1050
+ const document = source.visible.collection.document(path);
1051
+ if (document === undefined)
1052
+ return content;
1053
+ const definition = definitionOf(governing(source.composition, path), document.type);
1054
+ const changes = crossed(document, definition, path, (named, href) => {
1055
+ const at = moved.get(named);
1056
+ if (at !== undefined)
1057
+ return target(to, href) === at ? undefined : relative(to, at);
1058
+ return heldAt(source.visible, named) ? roomPathOf(source.handle, named) : undefined;
1059
+ });
1060
+ if (changes.length === 0)
1061
+ return content;
1062
+ const applied = apply(document, changes);
1063
+ return applied.outcome === 'applied' ? serialize(applied.document) : content;
1064
+ }
1065
+ function arrivalOf({ path, to }, source, moved) {
1066
+ const { documents, revisions, fileIds, changes, binaries } = source.visible;
1067
+ const kept = {
1068
+ ...(fileIds[path] === undefined ? {} : { fileId: fileIds[path] }),
1069
+ ...(changes[path] === undefined ? {} : { change: changes[path] }),
1070
+ };
1071
+ const heads = {
1072
+ ...(revisions[path] === undefined ? {} : { revision: revisions[path] }),
1073
+ ...kept,
1074
+ };
1075
+ const binary = binaries[path];
1076
+ if (binary?.state === 'accepted') {
1077
+ const { byteLength, mediaType, sha256 } = binary;
1078
+ return {
1079
+ kind: 'arrival',
1080
+ path: to,
1081
+ binary: { byteLength, mediaType, sha256 },
1082
+ ...heads,
1083
+ };
1084
+ }
1085
+ const content = arrivingText(path, to, source, moved);
1086
+ return {
1087
+ kind: 'arrival',
1088
+ path: to,
1089
+ content,
1090
+ ...(content === documents[path] ? heads : kept),
1091
+ };
1092
+ }
1093
+ async function moving(input) {
1094
+ const { source, destination, moves } = input;
1095
+ if (moves.length === 0)
1096
+ return refuse('no-change');
1097
+ const removals = moves.map(({ path }) => ({ kind: 'remove', path }));
1098
+ const leaves = guard(removals, { ...source, local: input.local, bases: [] }, 'write');
1099
+ if (leaves !== null)
1100
+ return leaves;
1101
+ const uploading = moves.find(({ path }) => source.visible.binaries[path]?.state === 'pending');
1102
+ if (uploading !== undefined)
1103
+ return refuse('unknown-revision', uploading.path);
1104
+ const moved = new Map(moves.map(({ path, to }) => [path, to]));
1105
+ const arrivals = moves.map((move) => arrivalOf(move, source, moved));
1106
+ const arrives = guard(arrivals, { ...destination, local: input.local, bases: [] }, 'write');
1107
+ if (arrives !== null)
1108
+ return arrives;
1109
+ const bound = input.providerLimits.find((limit) => limit.name === MOVE_DOCUMENTS)?.value;
1110
+ if (typeof bound === 'number' && moves.length > bound) {
1111
+ return refuse('provider-limit', undefined, MOVE_DOCUMENTS);
1112
+ }
1113
+ if (source.unmodeled)
1114
+ return refuse('model-unavailable', moves[0].path);
1115
+ const id = changeBatchIdentity();
1116
+ const between = {
1117
+ source: { room: source.room, baseRoomVersion: source.baseRoomVersion },
1118
+ destination: {
1119
+ room: destination.room,
1120
+ baseRoomVersion: destination.baseRoomVersion,
1121
+ handle: destination.handle,
1122
+ },
1123
+ moves,
1124
+ };
1125
+ const written = input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy };
1126
+ const leavingBatch = {
1127
+ id,
1128
+ baseRoomVersion: source.baseRoomVersion,
1129
+ changes: removals,
1130
+ preconditions: moves.map(({ path }) => existence(source.visible, path)),
1131
+ ...written,
1132
+ between,
1133
+ };
1134
+ const effective = await stepped(leavingRewritten(leavingBatch, source.visible));
1135
+ const left = await stepped(overlaying(source.visible, [effective]));
1136
+ const sourceJudged = await judging(source.visible, left, source.composition, source.validate, new Set(moves.map(({ path }) => path)), [...new Set(effective.changes.map((change) => change.path))]);
1137
+ if (sourceJudged.kind === 'refused')
1138
+ return sourceJudged;
1139
+ const arrivingBatch = {
1140
+ id,
1141
+ baseRoomVersion: destination.baseRoomVersion,
1142
+ changes: arrivals,
1143
+ preconditions: moves.map(({ to }) => ({ kind: 'absent', path: to })),
1144
+ ...written,
1145
+ between,
1146
+ };
1147
+ const arrived = await stepped(overlaying(destination.visible, [arrivingBatch]));
1148
+ const destinationJudged = await judging(destination.visible, arrived, destination.composition, destination.validate, new Set(), moves.map(({ to }) => to));
1149
+ if (destinationJudged.kind === 'refused')
1150
+ return destinationJudged;
1151
+ const limited = (await stepped(exceeding(input.providerLimits, removals, effective.changes, left))) ??
1152
+ (await stepped(exceeding(input.providerLimits, arrivals, arrivals, arrived)));
1153
+ if (limited !== null)
1154
+ return limited;
1155
+ return { kind: 'moved', leaving: immutable(leavingBatch), arriving: immutable(arrivingBatch) };
1156
+ }
1157
+ export function buildMove(input) {
1158
+ return moving(input);
1159
+ }
1160
+ export function buildBatch(input) {
1161
+ return batching(input);
1162
+ }