@agentshouse/mdruntime 0.1.0-cfbf80d8d4644 → 0.1.0-cfe148b32e3d8

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 (256) hide show
  1. package/AGENTS.md +299 -214
  2. package/README.md +21 -10
  3. package/dist/adapter.d.ts +126 -29
  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 +28 -5
  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 +207 -46
  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 -13
  105. package/dist/client.d.ts.map +1 -1
  106. package/dist/client.js +105 -16
  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 -18
  114. package/dist/connection.d.ts.map +1 -1
  115. package/dist/connection.js +3572 -640
  116. package/dist/conversation.d.ts +39 -12
  117. package/dist/conversation.d.ts.map +1 -1
  118. package/dist/conversation.js +552 -177
  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 +9 -1
  123. package/dist/durability.d.ts.map +1 -1
  124. package/dist/entry.d.ts +19 -0
  125. package/dist/entry.d.ts.map +1 -0
  126. package/dist/entry.js +151 -0
  127. package/dist/expectation.d.ts +21 -0
  128. package/dist/expectation.d.ts.map +1 -0
  129. package/dist/expectation.js +70 -0
  130. package/dist/failure.d.ts +13 -0
  131. package/dist/failure.d.ts.map +1 -0
  132. package/dist/failure.js +19 -0
  133. package/dist/fence.d.ts +6 -0
  134. package/dist/fence.d.ts.map +1 -0
  135. package/dist/fence.js +10 -0
  136. package/dist/file.d.ts +2 -10
  137. package/dist/file.d.ts.map +1 -1
  138. package/dist/file.js +3 -120
  139. package/dist/fulltext.d.ts +27 -0
  140. package/dist/fulltext.d.ts.map +1 -0
  141. package/dist/fulltext.js +368 -0
  142. package/dist/host.d.ts +15 -2
  143. package/dist/host.d.ts.map +1 -1
  144. package/dist/host.js +111 -20
  145. package/dist/identity.d.ts +4 -0
  146. package/dist/identity.d.ts.map +1 -1
  147. package/dist/identity.js +16 -1
  148. package/dist/index.d.ts +21 -10
  149. package/dist/index.d.ts.map +1 -1
  150. package/dist/index.js +8 -0
  151. package/dist/live.d.ts +22 -0
  152. package/dist/live.d.ts.map +1 -0
  153. package/dist/live.js +123 -0
  154. package/dist/memory.d.ts +12 -4
  155. package/dist/memory.d.ts.map +1 -1
  156. package/dist/memory.js +97 -25
  157. package/dist/messages.d.ts +28 -0
  158. package/dist/messages.d.ts.map +1 -0
  159. package/dist/messages.js +90 -0
  160. package/dist/model.d.ts +14 -4
  161. package/dist/model.d.ts.map +1 -1
  162. package/dist/model.js +12 -1
  163. package/dist/name.d.ts +4 -0
  164. package/dist/name.d.ts.map +1 -0
  165. package/dist/name.js +122 -0
  166. package/dist/observation.d.ts +19 -2
  167. package/dist/observation.d.ts.map +1 -1
  168. package/dist/observation.js +113 -12
  169. package/dist/open.d.ts +2 -0
  170. package/dist/open.d.ts.map +1 -0
  171. package/dist/open.js +1 -0
  172. package/dist/opened.d.ts +22 -0
  173. package/dist/opened.d.ts.map +1 -0
  174. package/dist/opened.js +107 -0
  175. package/dist/outcome.d.ts +62 -3
  176. package/dist/outcome.d.ts.map +1 -1
  177. package/dist/outcome.js +67 -1
  178. package/dist/overlay.d.ts +15 -5
  179. package/dist/overlay.d.ts.map +1 -1
  180. package/dist/overlay.js +136 -180
  181. package/dist/query.d.ts +65 -5
  182. package/dist/query.d.ts.map +1 -1
  183. package/dist/query.js +100 -26
  184. package/dist/reach.d.ts +11 -4
  185. package/dist/reach.d.ts.map +1 -1
  186. package/dist/reach.js +194 -57
  187. package/dist/react.d.ts +31 -19
  188. package/dist/react.d.ts.map +1 -1
  189. package/dist/react.js +13 -21
  190. package/dist/readiness.d.ts +68 -0
  191. package/dist/readiness.d.ts.map +1 -0
  192. package/dist/readiness.js +33 -0
  193. package/dist/reads.d.ts +11 -0
  194. package/dist/reads.d.ts.map +1 -0
  195. package/dist/reads.js +100 -0
  196. package/dist/recovery.d.ts +25 -0
  197. package/dist/recovery.d.ts.map +1 -0
  198. package/dist/recovery.js +198 -0
  199. package/dist/refresh.d.ts +16 -8
  200. package/dist/refresh.d.ts.map +1 -1
  201. package/dist/refresh.js +93 -55
  202. package/dist/replica.d.ts +60 -33
  203. package/dist/replica.d.ts.map +1 -1
  204. package/dist/replica.js +319 -153
  205. package/dist/room-draft.d.ts +40 -9
  206. package/dist/room-draft.d.ts.map +1 -1
  207. package/dist/room-draft.js +153 -37
  208. package/dist/rows.d.ts +30 -34
  209. package/dist/rows.d.ts.map +1 -1
  210. package/dist/rows.js +170 -81
  211. package/dist/save.d.ts +71 -4
  212. package/dist/save.d.ts.map +1 -1
  213. package/dist/save.js +815 -148
  214. package/dist/search.d.ts +41 -29
  215. package/dist/search.d.ts.map +1 -1
  216. package/dist/search.js +37 -105
  217. package/dist/seed.d.ts +16 -0
  218. package/dist/seed.d.ts.map +1 -0
  219. package/dist/seed.js +75 -0
  220. package/dist/shared.d.ts +16 -0
  221. package/dist/shared.d.ts.map +1 -0
  222. package/dist/shared.js +431 -0
  223. package/dist/status.d.ts +6 -2
  224. package/dist/status.d.ts.map +1 -1
  225. package/dist/status.js +0 -8
  226. package/dist/store.d.ts +124 -58
  227. package/dist/store.d.ts.map +1 -1
  228. package/dist/store.js +359 -344
  229. package/dist/stored.d.ts +145 -0
  230. package/dist/stored.d.ts.map +1 -0
  231. package/dist/stored.js +567 -0
  232. package/dist/tables.d.ts +342 -0
  233. package/dist/tables.d.ts.map +1 -0
  234. package/dist/tables.js +576 -0
  235. package/dist/turn.d.ts +9 -0
  236. package/dist/turn.d.ts.map +1 -0
  237. package/dist/turn.js +75 -0
  238. package/dist/validation.d.ts +30 -0
  239. package/dist/validation.d.ts.map +1 -0
  240. package/dist/validation.js +162 -0
  241. package/dist/windows.d.ts +22 -0
  242. package/dist/windows.d.ts.map +1 -0
  243. package/dist/windows.js +89 -0
  244. package/dist/wire.d.ts +169 -33
  245. package/dist/wire.d.ts.map +1 -1
  246. package/dist/wire.js +68 -31
  247. package/dist/words.d.ts +19 -0
  248. package/dist/words.d.ts.map +1 -0
  249. package/dist/words.js +150 -0
  250. package/package.json +33 -15
  251. package/dist/attachment.d.ts +0 -3
  252. package/dist/attachment.d.ts.map +0 -1
  253. package/dist/attachment.js +0 -8
  254. package/dist/read-side.d.ts +0 -18
  255. package/dist/read-side.d.ts.map +0 -1
  256. package/dist/read-side.js +0 -377
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,10 +299,9 @@ function incoming(collection, removed) {
190
299
  if (named !== null)
191
300
  note(named, path);
192
301
  }
193
- for (const link of links(document, path))
194
- note(link.target, path);
302
+ yield ROW_WEIGHT + collection.text[path].length;
195
303
  }
196
- const [first] = [...found.keys()].sort();
304
+ const [first] = yield* sorting(found.keys());
197
305
  if (first === undefined)
198
306
  return null;
199
307
  const every = new Set();
@@ -202,18 +310,26 @@ function incoming(collection, removed) {
202
310
  every.add(file);
203
311
  return {
204
312
  kind: 'refused',
205
- 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
+ },
206
319
  };
207
320
  }
208
321
  function affected(change) {
209
322
  return change.kind === 'move' ? [change.path, change.to] : [change.path];
210
323
  }
211
- function guard(changes, input) {
212
- const existing = new Set([
213
- ...Object.keys(input.visible.documents),
214
- ...input.visible.binaryTargets,
215
- ]);
216
- 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()) {
217
333
  for (const path of affected(change)) {
218
334
  const normalized = normalize(path);
219
335
  if (normalized === null || normalized === '')
@@ -221,22 +337,36 @@ function guard(changes, input) {
221
337
  if (covered(input.providerOwnedPaths, normalized)) {
222
338
  return refuse('provider-owned-path', normalized);
223
339
  }
224
- const operation = change.kind === 'remove' ? 'delete' : 'write';
225
- if (!covered(input.operations[operation], normalized))
340
+ const operation = change.kind === 'remove' ? removing : 'write';
341
+ if (!input.local && !covered(input.operations[operation], normalized))
226
342
  return refuse(`${operation}-grant`, normalized);
227
343
  }
228
- if (change.kind === 'create') {
344
+ if (change.kind === 'create' || change.kind === 'arrival') {
229
345
  if (existing.has(change.path))
230
346
  return refuse('path-collision', change.path);
231
347
  existing.add(change.path);
232
348
  continue;
233
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
+ }
234
364
  if (change.kind === 'move') {
235
365
  if (!existing.has(change.path))
236
366
  return refuse('path-absent', change.path);
237
367
  if (existing.has(change.to))
238
368
  return refuse('path-collision', change.to);
239
- if (unrevisionedDocument(input.visible, change.path)) {
369
+ if (unrevised(input, change.path)) {
240
370
  return refuse('unknown-revision', change.path);
241
371
  }
242
372
  existing.delete(change.path);
@@ -246,30 +376,53 @@ function guard(changes, input) {
246
376
  if (!existing.has(change.path))
247
377
  return refuse('path-absent', change.path);
248
378
  if (change.kind === 'remove') {
249
- if (unrevisionedDocument(input.visible, change.path)) {
379
+ if (unrevised(input, change.path)) {
250
380
  return refuse('unknown-revision', change.path);
251
381
  }
252
382
  existing.delete(change.path);
253
383
  continue;
254
384
  }
255
385
  if (change.kind === 'replace') {
256
- if (input.visible.revisions[change.path] === undefined) {
386
+ if (unrevised(input, change.path)) {
257
387
  return refuse('unknown-revision', change.path);
258
388
  }
259
389
  continue;
260
390
  }
261
- if (input.visible.collection.documents[change.path] === undefined) {
391
+ if (input.visible.collection.document(change.path) === undefined) {
262
392
  return refuse('path-absent', change.path);
263
393
  }
264
394
  }
265
395
  return null;
266
396
  }
267
- function unrevisionedDocument(visible, path) {
268
- 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}`;
269
419
  }
270
420
  function existence(visible, path) {
271
421
  const revision = visible.revisions[path];
272
- 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 };
273
426
  }
274
427
  function sameTarget(precondition, change) {
275
428
  if (precondition.kind !== change.kind || precondition.path !== change.path)
@@ -277,25 +430,25 @@ function sameTarget(precondition, change) {
277
430
  return ((precondition.kind !== 'field' && precondition.kind !== 'section') ||
278
431
  ((change.kind === 'field' || change.kind === 'section') && precondition.name === change.name));
279
432
  }
280
- function preconditionsOf(changes, visible, bases) {
433
+ function* preconditionsOf(changes, visible, bases) {
281
434
  const preconditions = [];
282
435
  const seen = new Set();
283
436
  const add = (precondition) => {
284
- const part = precondition.kind === 'field' || precondition.kind === 'section' ? precondition.name : '';
285
- const identity = `${precondition.kind} ${precondition.path} ${part}`;
286
- if (seen.has(identity))
437
+ const held = identity(precondition);
438
+ if (seen.has(held))
287
439
  return;
288
- seen.add(identity);
440
+ seen.add(held);
289
441
  preconditions.push(precondition);
290
442
  };
291
443
  for (const change of changes) {
444
+ yield ROW_WEIGHT;
292
445
  if (change.kind === 'field' || change.kind === 'section' || change.kind === 'preamble') {
293
446
  const based = bases.find((base) => sameTarget(base, change));
294
447
  if (based !== undefined) {
295
448
  add(based);
296
449
  continue;
297
450
  }
298
- const document = visible.collection.documents[change.path];
451
+ const document = visible.collection.document(change.path);
299
452
  if (change.kind === 'field') {
300
453
  add({
301
454
  kind: 'field',
@@ -320,6 +473,13 @@ function preconditionsOf(changes, visible, bases) {
320
473
  add({ kind: 'absent', path: change.path });
321
474
  continue;
322
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
+ }
323
483
  add(existence(visible, change.path));
324
484
  if (change.kind === 'move')
325
485
  add({ kind: 'absent', path: change.to });
@@ -329,10 +489,13 @@ function preconditionsOf(changes, visible, bases) {
329
489
  function depth(path) {
330
490
  return path.split('/').length;
331
491
  }
332
- function exceeded(limits, changes, resulting) {
333
- 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));
334
494
  const created = changes
335
- .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')
336
499
  .map((change) => (change.kind === 'move' ? change.to : change.path));
337
500
  for (const limit of limits) {
338
501
  const value = limit.value;
@@ -346,9 +509,13 @@ function exceeded(limits, changes, resulting) {
346
509
  return refuse('provider-limit', undefined, limit.name);
347
510
  }
348
511
  if (limit.name === 'document-size') {
349
- const large = written.find((path) => (resulting.documents[path] ?? '').length > value);
350
- if (large !== undefined)
351
- 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
+ }
352
519
  }
353
520
  if (limit.name === 'path-length') {
354
521
  const long = created.find((path) => path.length > value);
@@ -378,7 +545,7 @@ function relativeField(path, name, value, handle, definition) {
378
545
  !opened.ok ||
379
546
  !JSON.stringify(value ?? null).includes(`/rooms/${handle}/`))
380
547
  return value;
381
- const held = apply(opened.document, { kind: 'field', name, value, prior: undefined });
548
+ const held = apply(opened.document, [{ kind: 'field', name, value, prior: undefined }]);
382
549
  if (held.outcome !== 'applied')
383
550
  return value;
384
551
  const result = relativize(held.document, path, handle, definition);
@@ -391,7 +558,7 @@ function relativeChanges(input) {
391
558
  return input.changes.map((change) => {
392
559
  const definition = typed(change.path);
393
560
  if (change.kind === 'field') {
394
- const type = visible.collection.documents[change.path]?.type;
561
+ const type = visible.collection.document(change.path)?.type;
395
562
  const value = relativeField(change.path, change.name, change.value, handle, definition(type));
396
563
  return value === change.value ? change : { ...change, value };
397
564
  }
@@ -414,65 +581,454 @@ function relativeChanges(input) {
414
581
  return { ...change, fields, sections };
415
582
  });
416
583
  }
417
- export function buildBatch(input) {
418
- const changes = relativeChanges(input);
419
- const structural = changes.find((change) => change.kind === 'move' || change.kind === 'remove');
420
- if (structural !== undefined &&
421
- Object.entries(input.visible.collection.documents).some(([path, document]) => document.type !== undefined &&
422
- document.type !== 'any' &&
423
- governing(input.composition, path).status === 'unavailable'))
424
- 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]);
425
761
  if (changes.length === 0)
426
- return refuse('no-change');
427
- const rewritten = rewrites(changes, input.visible);
428
- if (rewritten.kind === 'refused')
429
- return rewritten;
430
- const refusal = guard(changes, input);
431
- if (refusal !== null)
432
- return refusal;
433
- const preconditions = preconditionsOf(changes, input.visible, input.bases);
434
- const predicted = anticipated(changes, preconditions, rewritten.changes, input.visible);
435
- const resulting = overlay(input.visible, [
436
- { id: '', baseRoomVersion: input.baseRoomVersion, ...predicted },
437
- ]);
438
- 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) {
439
914
  if (removed.size > 0) {
440
- const referenced = incoming(parseCollection(resulting.documents, input.composition), removed);
915
+ const referenced = await stepped(incoming(collectionOf(resulting.documents, composition), removed, null));
441
916
  if (referenced !== null)
442
917
  return referenced;
443
918
  }
444
- const typed = (content) => {
445
- if (content === undefined)
446
- return false;
447
- const parsed = parse(content);
448
- return parsed.ok && parsed.document.type !== undefined && parsed.document.type !== 'any';
449
- };
450
- const typedPaths = [...new Set(predicted.changes.flatMap(affected))].filter((path) => typed(input.visible.documents[path]) || typed(resulting.documents[path]));
451
- const needsModel = typedPaths.length > 0;
452
- 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');
453
929
  if (withoutModel !== undefined)
454
930
  return refuse('model-unavailable', withoutModel);
455
- const validationText = needsModel
456
- ? resulting.documents
457
- : Object.fromEntries(Object.entries(resulting.documents).filter(([path]) => predicted.changes.some((change) => affected(change).includes(path))));
458
- 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);
459
934
  if (verdict.verdict === 'refuse') {
460
935
  return {
461
936
  kind: 'refused',
462
937
  refusal: {
463
- rule: verdict.rule,
938
+ code: verdict.rule,
939
+ retryable: false,
464
940
  file: verdict.file,
465
941
  ...(verdict.location === undefined ? {} : { location: verdict.location }),
466
942
  ...(verdict.referrers === undefined ? {} : { referrers: verdict.referrers }),
467
943
  },
468
944
  };
469
945
  }
470
- 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));
471
1030
  if (limited !== null)
472
1031
  return limited;
473
- const validated = typedPaths
474
- .map((path) => governing(input.composition, path))
475
- .find((model) => model.status !== 'unavailable');
476
1032
  return {
477
1033
  kind: 'batch',
478
1034
  batch: immutable({
@@ -481,15 +1037,126 @@ export function buildBatch(input) {
481
1037
  changes,
482
1038
  preconditions,
483
1039
  ...(input.writtenBy === undefined ? {} : { writtenBy: input.writtenBy }),
484
- ...(validated === undefined
485
- ? {}
486
- : {
487
- validation: {
488
- identity: validated.snapshot.identity,
489
- version: validated.snapshot.version,
490
- source: validated.status,
491
- },
492
- }),
1040
+ ...validationOf(judged.validated),
493
1041
  }),
494
1042
  };
495
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
+ }