@shivaedev/work-board 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +110 -2
  3. package/assets/agent.js +37 -0
  4. package/assets/native.js +54 -25
  5. package/dist/board.d.ts +1 -0
  6. package/dist/board.d.ts.map +1 -1
  7. package/dist/board.js +15 -2
  8. package/dist/board.js.map +1 -1
  9. package/dist/browser/client.d.ts +19 -79
  10. package/dist/browser/client.d.ts.map +1 -1
  11. package/dist/browser/client.js +12 -1
  12. package/dist/browser/client.js.map +1 -1
  13. package/dist/browser/entry.d.ts +3 -0
  14. package/dist/browser/entry.d.ts.map +1 -0
  15. package/dist/browser/entry.js +7 -0
  16. package/dist/browser/entry.js.map +1 -0
  17. package/dist/browser/responses/Prompt.d.ts +10 -0
  18. package/dist/browser/responses/Prompt.d.ts.map +1 -0
  19. package/dist/browser/responses/Prompt.js +2 -0
  20. package/dist/browser/responses/Prompt.js.map +1 -0
  21. package/dist/browser/responses/answerBody.d.ts +4 -0
  22. package/dist/browser/responses/answerBody.d.ts.map +1 -0
  23. package/dist/browser/responses/answerBody.js +9 -0
  24. package/dist/browser/responses/answerBody.js.map +1 -0
  25. package/dist/browser/responses/draftSession.d.ts +8 -0
  26. package/dist/browser/responses/draftSession.d.ts.map +1 -0
  27. package/dist/browser/responses/draftSession.js +105 -0
  28. package/dist/browser/responses/draftSession.js.map +1 -0
  29. package/dist/browser/responses/drafts.d.ts +21 -0
  30. package/dist/browser/responses/drafts.d.ts.map +1 -0
  31. package/dist/browser/responses/drafts.js +68 -0
  32. package/dist/browser/responses/drafts.js.map +1 -0
  33. package/dist/browser/responses/preview.d.ts +4 -0
  34. package/dist/browser/responses/preview.d.ts.map +1 -0
  35. package/dist/browser/responses/preview.js +5 -0
  36. package/dist/browser/responses/preview.js.map +1 -0
  37. package/dist/browser/responses/questionnaireControls.d.ts +10 -0
  38. package/dist/browser/responses/questionnaireControls.d.ts.map +1 -0
  39. package/dist/browser/responses/questionnaireControls.js +68 -0
  40. package/dist/browser/responses/questionnaireControls.js.map +1 -0
  41. package/dist/browser/responses/schema.d.ts +168 -0
  42. package/dist/browser/responses/schema.d.ts.map +1 -0
  43. package/dist/browser/responses/schema.js +57 -0
  44. package/dist/browser/responses/schema.js.map +1 -0
  45. package/dist/browser/responses/setup.d.ts +2 -0
  46. package/dist/browser/responses/setup.d.ts.map +1 -0
  47. package/dist/browser/responses/setup.js +129 -0
  48. package/dist/browser/responses/setup.js.map +1 -0
  49. package/dist/browser/responses/submissionFields.d.ts +15 -0
  50. package/dist/browser/responses/submissionFields.d.ts.map +1 -0
  51. package/dist/browser/responses/submissionFields.js +14 -0
  52. package/dist/browser/responses/submissionFields.js.map +1 -0
  53. package/dist/browser/responses/submit.d.ts +4 -0
  54. package/dist/browser/responses/submit.d.ts.map +1 -0
  55. package/dist/browser/responses/submit.js +30 -0
  56. package/dist/browser/responses/submit.js.map +1 -0
  57. package/dist/browser/session.d.ts +1 -79
  58. package/dist/browser/session.d.ts.map +1 -1
  59. package/dist/cli.d.ts.map +1 -1
  60. package/dist/cli.js +24 -15
  61. package/dist/cli.js.map +1 -1
  62. package/dist/files/changes.d.ts +1 -0
  63. package/dist/files/changes.d.ts.map +1 -1
  64. package/dist/files/changes.js +1 -1
  65. package/dist/files/changes.js.map +1 -1
  66. package/dist/http/respond.d.ts.map +1 -1
  67. package/dist/http/respond.js +2 -0
  68. package/dist/http/respond.js.map +1 -1
  69. package/dist/metadata/backlinks.d.ts.map +1 -1
  70. package/dist/metadata/backlinks.js +5 -0
  71. package/dist/metadata/backlinks.js.map +1 -1
  72. package/dist/metadata/fields.d.ts +23 -1
  73. package/dist/metadata/fields.d.ts.map +1 -1
  74. package/dist/metadata/problems.d.ts.map +1 -1
  75. package/dist/metadata/problems.js +3 -0
  76. package/dist/metadata/problems.js.map +1 -1
  77. package/dist/metadata/schema.d.ts +23 -1
  78. package/dist/metadata/schema.d.ts.map +1 -1
  79. package/dist/metadata/schema.js +6 -2
  80. package/dist/metadata/schema.js.map +1 -1
  81. package/dist/page/attentionDetails.d.ts.map +1 -1
  82. package/dist/page/attentionDetails.js +2 -1
  83. package/dist/page/attentionDetails.js.map +1 -1
  84. package/dist/page/metadataDetails.d.ts.map +1 -1
  85. package/dist/page/metadataDetails.js +6 -0
  86. package/dist/page/metadataDetails.js.map +1 -1
  87. package/dist/page/navigationScript.js +1 -1
  88. package/dist/page/shell.d.ts +1 -1
  89. package/dist/page/shell.d.ts.map +1 -1
  90. package/dist/page/shell.js.map +1 -1
  91. package/dist/page/style.d.ts.map +1 -1
  92. package/dist/page/style.js +3 -1
  93. package/dist/page/style.js.map +1 -1
  94. package/dist/page/swap.d.ts +1 -1
  95. package/dist/page/swap.d.ts.map +1 -1
  96. package/dist/page/swap.js +3 -0
  97. package/dist/page/swap.js.map +1 -1
  98. package/dist/path/Identity.d.ts +3 -0
  99. package/dist/path/Identity.d.ts.map +1 -0
  100. package/dist/path/Identity.js +3 -0
  101. package/dist/path/Identity.js.map +1 -0
  102. package/dist/render/markdown.d.ts +1 -0
  103. package/dist/render/markdown.d.ts.map +1 -1
  104. package/dist/render/markdown.js +4 -2
  105. package/dist/render/markdown.js.map +1 -1
  106. package/dist/render/questionnaireDocuments.d.ts +3 -0
  107. package/dist/render/questionnaireDocuments.d.ts.map +1 -0
  108. package/dist/render/questionnaireDocuments.js +11 -0
  109. package/dist/render/questionnaireDocuments.js.map +1 -0
  110. package/dist/render/visualDocuments.d.ts.map +1 -1
  111. package/dist/render/visualDocuments.js +3 -0
  112. package/dist/render/visualDocuments.js.map +1 -1
  113. package/dist/responses/agent-cli.d.ts +2 -0
  114. package/dist/responses/agent-cli.d.ts.map +1 -0
  115. package/dist/responses/agent-cli.js +31 -0
  116. package/dist/responses/agent-cli.js.map +1 -0
  117. package/dist/responses/agent.d.ts +80 -0
  118. package/dist/responses/agent.d.ts.map +1 -0
  119. package/dist/responses/agent.js +49 -0
  120. package/dist/responses/agent.js.map +1 -0
  121. package/dist/responses/await.d.ts +39 -0
  122. package/dist/responses/await.d.ts.map +1 -0
  123. package/dist/responses/await.js +21 -0
  124. package/dist/responses/await.js.map +1 -0
  125. package/dist/responses/composerHtml.d.ts +27 -0
  126. package/dist/responses/composerHtml.d.ts.map +1 -0
  127. package/dist/responses/composerHtml.js +14 -0
  128. package/dist/responses/composerHtml.js.map +1 -0
  129. package/dist/responses/flags.d.ts +6 -0
  130. package/dist/responses/flags.d.ts.map +1 -0
  131. package/dist/responses/flags.js +23 -0
  132. package/dist/responses/flags.js.map +1 -0
  133. package/dist/responses/history.d.ts +20 -0
  134. package/dist/responses/history.d.ts.map +1 -0
  135. package/dist/responses/history.js +16 -0
  136. package/dist/responses/history.js.map +1 -0
  137. package/dist/responses/locate.d.ts +13 -0
  138. package/dist/responses/locate.d.ts.map +1 -0
  139. package/dist/responses/locate.js +55 -0
  140. package/dist/responses/locate.js.map +1 -0
  141. package/dist/responses/make.d.ts +3 -0
  142. package/dist/responses/make.d.ts.map +1 -0
  143. package/dist/responses/make.js +16 -0
  144. package/dist/responses/make.js.map +1 -0
  145. package/dist/responses/outcome.d.ts +71 -0
  146. package/dist/responses/outcome.d.ts.map +1 -0
  147. package/dist/responses/outcome.js +14 -0
  148. package/dist/responses/outcome.js.map +1 -0
  149. package/dist/responses/page.d.ts +6 -0
  150. package/dist/responses/page.d.ts.map +1 -0
  151. package/dist/responses/page.js +22 -0
  152. package/dist/responses/page.js.map +1 -0
  153. package/dist/responses/publish.d.ts +4 -0
  154. package/dist/responses/publish.d.ts.map +1 -0
  155. package/dist/responses/publish.js +56 -0
  156. package/dist/responses/publish.js.map +1 -0
  157. package/dist/responses/publishIn.d.ts +3 -0
  158. package/dist/responses/publishIn.d.ts.map +1 -0
  159. package/dist/responses/publishIn.js +69 -0
  160. package/dist/responses/publishIn.js.map +1 -0
  161. package/dist/responses/read.d.ts +36 -0
  162. package/dist/responses/read.d.ts.map +1 -0
  163. package/dist/responses/read.js +43 -0
  164. package/dist/responses/read.js.map +1 -0
  165. package/dist/responses/records.d.ts +12 -0
  166. package/dist/responses/records.d.ts.map +1 -0
  167. package/dist/responses/records.js +49 -0
  168. package/dist/responses/records.js.map +1 -0
  169. package/dist/responses/retry.d.ts +34 -0
  170. package/dist/responses/retry.d.ts.map +1 -0
  171. package/dist/responses/retry.js +14 -0
  172. package/dist/responses/retry.js.map +1 -0
  173. package/dist/responses/service.d.ts +142 -0
  174. package/dist/responses/service.d.ts.map +1 -0
  175. package/dist/responses/service.js +95 -0
  176. package/dist/responses/service.js.map +1 -0
  177. package/dist/responses/template.d.ts +7 -0
  178. package/dist/responses/template.d.ts.map +1 -0
  179. package/dist/responses/template.js +58 -0
  180. package/dist/responses/template.js.map +1 -0
  181. package/dist/responses/templateHtml.d.ts +7 -0
  182. package/dist/responses/templateHtml.d.ts.map +1 -0
  183. package/dist/responses/templateHtml.js +18 -0
  184. package/dist/responses/templateHtml.js.map +1 -0
  185. package/dist/responses/templateLinks.d.ts +3 -0
  186. package/dist/responses/templateLinks.d.ts.map +1 -0
  187. package/dist/responses/templateLinks.js +45 -0
  188. package/dist/responses/templateLinks.js.map +1 -0
  189. package/dist/responses/templateNodes.d.ts +17 -0
  190. package/dist/responses/templateNodes.d.ts.map +1 -0
  191. package/dist/responses/templateNodes.js +47 -0
  192. package/dist/responses/templateNodes.js.map +1 -0
  193. package/dist/responses/validateSupersedes.d.ts +5 -0
  194. package/dist/responses/validateSupersedes.d.ts.map +1 -0
  195. package/dist/responses/validateSupersedes.js +21 -0
  196. package/dist/responses/validateSupersedes.js.map +1 -0
  197. package/dist/responses/validatedInput.d.ts +3 -0
  198. package/dist/responses/validatedInput.d.ts.map +1 -0
  199. package/dist/responses/validatedInput.js +43 -0
  200. package/dist/responses/validatedInput.js.map +1 -0
  201. package/dist/responses/waitFor.d.ts +37 -0
  202. package/dist/responses/waitFor.d.ts.map +1 -0
  203. package/dist/responses/waitFor.js +47 -0
  204. package/dist/responses/waitFor.js.map +1 -0
  205. package/dist/rpc/contract.d.ts +125 -2
  206. package/dist/rpc/contract.d.ts.map +1 -1
  207. package/dist/rpc/contract.js +3 -2
  208. package/dist/rpc/contract.js.map +1 -1
  209. package/dist/rpc/responseContract.d.ts +260 -0
  210. package/dist/rpc/responseContract.d.ts.map +1 -0
  211. package/dist/rpc/responseContract.js +37 -0
  212. package/dist/rpc/responseContract.js.map +1 -0
  213. package/dist/rpc/server.d.ts +3 -1
  214. package/dist/rpc/server.d.ts.map +1 -1
  215. package/dist/rpc/server.js +6 -1
  216. package/dist/rpc/server.js.map +1 -1
  217. package/dist/serve-cli.d.ts +3 -0
  218. package/dist/serve-cli.d.ts.map +1 -0
  219. package/dist/serve-cli.js +20 -0
  220. package/dist/serve-cli.js.map +1 -0
  221. package/package.json +3 -3
  222. package/src/board.ts +16 -2
  223. package/src/browser/client.ts +30 -4
  224. package/src/browser/entry.ts +9 -0
  225. package/src/browser/responses/Prompt.ts +6 -0
  226. package/src/browser/responses/answerBody.ts +11 -0
  227. package/src/browser/responses/draftSession.ts +101 -0
  228. package/src/browser/responses/drafts.ts +82 -0
  229. package/src/browser/responses/preview.ts +7 -0
  230. package/src/browser/responses/questionnaireControls.ts +74 -0
  231. package/src/browser/responses/schema.ts +63 -0
  232. package/src/browser/responses/setup.ts +132 -0
  233. package/src/browser/responses/submissionFields.ts +15 -0
  234. package/src/browser/responses/submit.ts +33 -0
  235. package/src/cli.ts +14 -22
  236. package/src/files/changes.ts +2 -1
  237. package/src/http/respond.ts +2 -0
  238. package/src/metadata/backlinks.ts +5 -0
  239. package/src/metadata/problems.ts +3 -0
  240. package/src/metadata/schema.ts +6 -2
  241. package/src/page/attentionDetails.ts +2 -1
  242. package/src/page/metadataDetails.ts +6 -0
  243. package/src/page/navigationScript.ts +1 -1
  244. package/src/page/shell.ts +1 -1
  245. package/src/page/style.ts +3 -1
  246. package/src/page/swap.ts +3 -0
  247. package/src/path/Identity.ts +3 -0
  248. package/src/render/markdown.ts +5 -2
  249. package/src/render/questionnaireDocuments.ts +12 -0
  250. package/src/render/visualDocuments.ts +3 -0
  251. package/src/responses/agent-cli.ts +35 -0
  252. package/src/responses/agent.ts +59 -0
  253. package/src/responses/await.ts +21 -0
  254. package/src/responses/composerHtml.ts +28 -0
  255. package/src/responses/flags.ts +22 -0
  256. package/src/responses/history.ts +18 -0
  257. package/src/responses/locate.ts +60 -0
  258. package/src/responses/make.ts +17 -0
  259. package/src/responses/outcome.ts +15 -0
  260. package/src/responses/page.ts +30 -0
  261. package/src/responses/publish.ts +53 -0
  262. package/src/responses/publishIn.ts +64 -0
  263. package/src/responses/read.ts +45 -0
  264. package/src/responses/records.ts +55 -0
  265. package/src/responses/retry.ts +23 -0
  266. package/src/responses/service.ts +117 -0
  267. package/src/responses/template.ts +72 -0
  268. package/src/responses/templateHtml.ts +26 -0
  269. package/src/responses/templateLinks.ts +43 -0
  270. package/src/responses/templateNodes.ts +55 -0
  271. package/src/responses/validateSupersedes.ts +24 -0
  272. package/src/responses/validatedInput.ts +45 -0
  273. package/src/responses/waitFor.ts +58 -0
  274. package/src/rpc/contract.ts +3 -2
  275. package/src/rpc/responseContract.ts +46 -0
  276. package/src/rpc/server.ts +8 -1
  277. package/src/serve-cli.ts +37 -0
@@ -0,0 +1,55 @@
1
+ import { Schema } from "effect";
2
+ import type { RootContent } from "mdast";
3
+ import type { ContainerDirective } from "mdast-util-directive";
4
+ import type { Prompt } from "#browser/responses/Prompt.ts";
5
+ import { Identity } from "#path/Identity.ts";
6
+
7
+ const Id = Identity.check(Schema.isMaxLength(128));
8
+ const PromptFields = Schema.Struct({ id: Id, request: Schema.optional(Identity), select: Schema.optional(Schema.Literals(["one", "many", "text"])) });
9
+ const OptionFields = Schema.Struct({ id: Id });
10
+
11
+ export function sliceNode(source: string, node: RootContent) {
12
+ return source.slice(node.position?.start.offset, node.position?.end.offset);
13
+ }
14
+ export function isContainer(node: RootContent): node is ContainerDirective {
15
+ return node.type === "containerDirective";
16
+ }
17
+ function hasControl(node: RootContent): boolean {
18
+ return (
19
+ ((node.type === "containerDirective" || node.type === "leafDirective" || node.type === "textDirective")
20
+ && ["question", "option"].includes(node.name))
21
+ || ("children" in node && node.children.some(hasControl))
22
+ );
23
+ }
24
+ export function promptFrom(node: ContainerDirective, source: string) {
25
+ const attrs = Schema.decodeUnknownSync(PromptFields, { onExcessProperty: "error" })(node.attributes ?? {});
26
+ const options = node.children.filter((child) => isContainer(child) && child.name === "option");
27
+ const description = node.children.filter((child) => !(isContainer(child) && child.name === "option"));
28
+ if (
29
+ description.some(hasControl)
30
+ || description.length === 0
31
+ || options.length > 32
32
+ || (attrs.select === "text" ? options.length > 0 : options.length === 0)
33
+ ) {
34
+ throw new Error("A question needs Markdown context and 1–32 direct options (or select=text without options), without nested questions.");
35
+ }
36
+ const choices = options.map((option) => {
37
+ if (!isContainer(option) || option.children.some(hasControl) || option.children.length === 0) {
38
+ throw new Error("Options need Markdown descriptions and cannot nest response controls.");
39
+ }
40
+ const fields = Schema.decodeUnknownSync(OptionFields, { onExcessProperty: "error" })(option.attributes ?? {});
41
+ return { id: fields.id, source: option.children.map((child) => sliceNode(source, child)).join("\n\n") };
42
+ });
43
+ if (new Set(choices.map((option) => option.id)).size !== choices.length) {
44
+ throw new Error("Option IDs must be unique within each question.");
45
+ }
46
+ return {
47
+ prompt: {
48
+ id: attrs.id,
49
+ mode: attrs.select ?? "one",
50
+ options: choices,
51
+ source: description.map((child) => sliceNode(source, child)).join("\n\n"),
52
+ } satisfies Prompt,
53
+ request: attrs.request,
54
+ };
55
+ }
@@ -0,0 +1,24 @@
1
+ import { Effect } from "effect";
2
+ import { type DraftInput, type Question, ResponseFailed } from "#browser/responses/schema.ts";
3
+ import type { Snapshot } from "#search/snapshot.ts";
4
+ import { questionFrom, responseFrom } from "./records.ts";
5
+
6
+ export function validateSupersedes(input: DraftInput, question: Question, data: Snapshot) {
7
+ if (input.supersedes === undefined) {
8
+ return Effect.void;
9
+ }
10
+ const matches = data.model.ids.get(input.supersedes);
11
+ const previous = matches?.length === 1 && matches[0] ? responseFrom(matches[0]) : undefined;
12
+ const context = previous && data.model.ids.get(previous.response.question);
13
+ const old = context?.length === 1 && context[0] ? questionFrom(context[0]) : undefined;
14
+ return old
15
+ && previous
16
+ && previous.response.reviewedRevision === old.question.reviewedRevision
17
+ && old.question.item === question.question.item
18
+ && old.question.request === question.question.request
19
+ && input.supersedes !== input.id
20
+ ? Effect.void
21
+ : Effect.fail(
22
+ new ResponseFailed({ code: "Conflict", message: "Supersedes must identify one recorded response to this item's attention request." }),
23
+ );
24
+ }
@@ -0,0 +1,45 @@
1
+ import { answerBody } from "#browser/responses/answerBody.ts";
2
+ import type { Prompt } from "#browser/responses/Prompt.ts";
3
+ import type { DraftInput, PromptAnswer } from "#browser/responses/schema.ts";
4
+ import { questionTemplate } from "./template.ts";
5
+
6
+ function answerFor(prompt: Prompt, answer: PromptAnswer | undefined, required: boolean) {
7
+ if (!answer || answer.prompt !== prompt.id) {
8
+ throw new Error("Submit one answer per question, in template order.");
9
+ }
10
+ if (new Set(answer.selected).size !== answer.selected.length || (prompt.mode === "one" && answer.selected.length > 1)) {
11
+ throw new Error("The selected options do not match the question's selection mode.");
12
+ }
13
+ if (answer.selected.some((id) => !prompt.options.some((option) => option.id === id))) {
14
+ throw new Error("A selected option is not in the reviewed template.");
15
+ }
16
+ if (required && answer.selected.length === 0 && !answer.text.trim()) {
17
+ throw new Error("Answer every question with a selection or additional text, or use Clarify / Not now.");
18
+ }
19
+ }
20
+ export function validatedInput(input: DraftInput, context: string, request: string, file?: string): DraftInput {
21
+ const template = questionTemplate(context, request, file);
22
+ if (input.answers === undefined) {
23
+ if (template.prompts.length > 0 && input.type === "answer") {
24
+ throw new Error("This question packet requires answers for all prompts.");
25
+ }
26
+ if (!input.body.trim()) {
27
+ throw new Error("A response needs text or answers to the question packet.");
28
+ }
29
+ return input;
30
+ }
31
+ if (input.answers.length !== template.prompts.length || template.prompts.length === 0) {
32
+ throw new Error("The answer packet does not match the reviewed questions.");
33
+ }
34
+ template.prompts.forEach((prompt, index) => {
35
+ answerFor(prompt, input.answers?.[index], input.type === "answer");
36
+ });
37
+ if (input.type !== "answer" && !input.body.trim() && !input.answers.some((answer) => answer.text.trim())) {
38
+ throw new Error("Clarify / Not now needs explanatory text.");
39
+ }
40
+ const body = answerBody(template.prompts, input.answers, input.body);
41
+ if (body.length > 32_768) {
42
+ throw new Error("The complete recorded response exceeds 32768 characters. Shorten the response or split the source request.");
43
+ }
44
+ return { ...input, body };
45
+ }
@@ -0,0 +1,58 @@
1
+ import * as Clock from "effect/Clock";
2
+ import * as Effect from "effect/Effect";
3
+ import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
4
+ import { ResponseFailed, type WaitReading } from "#browser/responses/schema.ts";
5
+
6
+ function resultFor(reading: WaitReading) {
7
+ if (reading.response) {
8
+ return Effect.succeed({ kind: "response", question: reading.question, response: reading.response } as const);
9
+ }
10
+ if (reading.expired) {
11
+ return Effect.fail(
12
+ new ResponseFailed({
13
+ code: "Unanswered",
14
+ message:
15
+ "The human left this unanswered for 48 hours. Check in with them; they may have forgotten or want to postpone. The request and late replies remain available.",
16
+ }),
17
+ );
18
+ }
19
+ return Effect.succeed(undefined);
20
+ }
21
+
22
+ function retryable(error: ResponseFailed | RpcClientError) {
23
+ return error._tag !== "ResponseFailed" || error.code === "Unavailable";
24
+ }
25
+
26
+ export const waitForResponse = Effect.fn("WorkBoard.waitForResponse")(function* (
27
+ read: () => Effect.Effect<WaitReading, ResponseFailed | RpcClientError>,
28
+ diagnostic: (text: string) => void,
29
+ ) {
30
+ let known: WaitReading | undefined;
31
+ const leaseEnd = (yield* Clock.currentTimeMillis) + 48 * 60 * 60 * 1000;
32
+ let unavailable = false;
33
+ while (true) {
34
+ const result = yield* read().pipe(Effect.result);
35
+ if (result._tag === "Failure") {
36
+ if (!retryable(result.failure)) {
37
+ return yield* Effect.fail(result.failure);
38
+ }
39
+ if ((yield* Clock.currentTimeMillis) >= (known?.question.question.deadline ?? leaseEnd)) {
40
+ return yield* Effect.fail(
41
+ new Error("The service is unavailable at the deadline; unanswered status could not be verified. Re-read this question when it returns."),
42
+ );
43
+ }
44
+ if (!unavailable) {
45
+ diagnostic("Local service unavailable; reconnecting to the same question.");
46
+ }
47
+ unavailable = true;
48
+ yield* Effect.sleep("2 seconds");
49
+ continue;
50
+ }
51
+ known = result.success;
52
+ unavailable = false;
53
+ const complete = yield* resultFor(known);
54
+ if (complete) {
55
+ return complete;
56
+ }
57
+ }
58
+ });
@@ -4,6 +4,7 @@ import { contract } from "@shivaedev/effect-contract/contract.ts";
4
4
  import { LiveHint } from "@shivaedev/effect-contract/live.ts";
5
5
  import { query } from "@shivaedev/effect-contract/operation.ts";
6
6
  import { HistoryInput, HistoryOutput } from "#history/schema.ts";
7
+ import { responseContract } from "#rpc/responseContract.ts";
7
8
  import { index, navigation, watcher, workspace } from "./keys.ts";
8
9
  import { pageKeys } from "./pageKeys.ts";
9
10
 
@@ -18,7 +19,7 @@ const PageUrl = Schema.String.check(
18
19
  const path = new URL(value, "http://127.0.0.1").pathname;
19
20
  return (
20
21
  path === "/"
21
- || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start"].includes(path)
22
+ || ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond"].includes(path)
22
23
  || /^\/_board\/item\/[^/]+\/$/u.exec(path) !== null
23
24
  || /\.md$/iu.exec(path) !== null
24
25
  || "Expected a local reading URL"
@@ -59,4 +60,4 @@ const ReadWatcher = query("watcher", {
59
60
  });
60
61
  export const workContract = contract("work-board", { queries: [ReadPage, ReadNavigation, ReadSearch, ReadHistory, ReadWatcher] });
61
62
  export const Subscribe = Rpc.make("work-board.subscribe", { error: ReadFailed, stream: true, success: LiveHint });
62
- export const workRpcs = workContract.add(Subscribe);
63
+ export const workRpcs = workContract.add(Subscribe).merge(responseContract);
@@ -0,0 +1,46 @@
1
+ import { Schema } from "effect";
2
+ import { contract } from "@shivaedev/effect-contract/contract.ts";
3
+ import { command, query } from "@shivaedev/effect-contract/operation.ts";
4
+ import {
5
+ DraftInput,
6
+ Question,
7
+ QuestionPreview,
8
+ Reading,
9
+ RecordedResponse,
10
+ ResponseFailed,
11
+ Revision,
12
+ WaitReading,
13
+ } from "#browser/responses/schema.ts";
14
+ import { index, workspace } from "#rpc/keys.ts";
15
+
16
+ const locate = query("question", {
17
+ payload: { item: Schema.String, request: Schema.String },
18
+ reads: () => [workspace.list, index.list],
19
+ rejections: { ResponseFailed },
20
+ success: QuestionPreview,
21
+ });
22
+ const awaitResponse = query("awaitResponse", {
23
+ payload: { after: Schema.optional(Schema.String), question: Schema.String },
24
+ reads: () => [workspace.list, index.list],
25
+ rejections: { ResponseFailed },
26
+ success: WaitReading,
27
+ });
28
+ const read = query("responses", {
29
+ payload: { question: Schema.String },
30
+ reads: () => [workspace.list, index.list],
31
+ rejections: { ResponseFailed },
32
+ success: Reading,
33
+ });
34
+ const register = command("registerQuestion", {
35
+ invalidates: () => [workspace.list, index.list],
36
+ payload: { item: Schema.String, request: Schema.String, revision: Revision },
37
+ rejections: { ResponseFailed },
38
+ success: Question,
39
+ });
40
+ const respond = command("recordResponse", {
41
+ invalidates: () => [workspace.list, index.list],
42
+ payload: DraftInput,
43
+ rejections: { ResponseFailed },
44
+ success: RecordedResponse,
45
+ });
46
+ export const responseContract = contract("work-board", { commands: [register, respond], queries: [locate, read, awaitResponse] });
package/src/rpc/server.ts CHANGED
@@ -6,6 +6,7 @@ import { MAX_BYTES } from "#history/limits.ts";
6
6
  import { historyResponse } from "#history/response.ts";
7
7
  import { HEADERS } from "#http/respond.ts";
8
8
  import { navHtml } from "#page/nav.ts";
9
+ import type { responseService } from "#responses/service.ts";
9
10
  import { searchMatch } from "#search/match.ts";
10
11
  import type { Snapshot } from "#search/snapshot.ts";
11
12
  import { ReadFailed, workRpcs } from "./contract.ts";
@@ -16,10 +17,12 @@ export interface ServerOptions {
16
17
  readonly home: string | undefined;
17
18
  readonly index: Effect.Effect<Snapshot, unknown>;
18
19
  readonly page: (url: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown>;
20
+ readonly responses: Effect.Success<ReturnType<typeof responseService>>;
19
21
  }
20
22
 
21
- export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page }: ServerOptions) {
23
+ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page, responses }: ServerOptions) {
22
24
  const handlers = workRpcs.toLayer({
25
+ "work-board.awaitResponse": responses.awaitResponse,
23
26
  "work-board.history": ({ action, baseline }) =>
24
27
  Effect.gen(function* () {
25
28
  const now = yield* Clock.currentTimeMillis;
@@ -40,6 +43,10 @@ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home,
40
43
  }),
41
44
  Effect.catch((error) => Effect.fail(error instanceof ReadFailed ? error : new ReadFailed({ operation: "page", status: 500 }))),
42
45
  ),
46
+ "work-board.question": ({ item, request }) => responses.locate(item, request),
47
+ "work-board.recordResponse": responses.record,
48
+ "work-board.registerQuestion": responses.register,
49
+ "work-board.responses": ({ question }) => responses.read(question),
43
50
  "work-board.search": ({ query }) =>
44
51
  Effect.map(index, (snapshot) => ({ ...searchMatch(snapshot.entries, query), unavailable: snapshot.unavailable })).pipe(
45
52
  Effect.catch(() => Effect.fail(new ReadFailed({ operation: "search", status: 500 }))),
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { Effect, Layer, Option, Path } from "effect";
4
+ import { Argument, Command, Flag } from "effect/unstable/cli";
5
+ import { HOST, serveBoard } from "./serve.ts";
6
+
7
+ const VERSION = "0.9.0";
8
+
9
+ const serve = Effect.fn("WorkBoard.serve")(function* (input: {
10
+ readonly dir: string;
11
+ readonly port: number;
12
+ readonly responses: boolean;
13
+ readonly home: Option.Option<string>;
14
+ }) {
15
+ const path = yield* Path.Path;
16
+ return yield* Layer.launch(
17
+ serveBoard({ home: Option.getOrUndefined(input.home), port: input.port, responses: input.responses, root: path.resolve(input.dir) }),
18
+ );
19
+ });
20
+
21
+ const workBoard = Command.make(
22
+ "work-board",
23
+ {
24
+ dir: Argument.directory("dir", { mustExist: true }).pipe(Argument.withDescription("The folder of markdown files to serve")),
25
+ home: Flag.string("home").pipe(Flag.optional, Flag.withDescription("The file shown as a board at /, relative to the folder")),
26
+ port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
27
+ responses: Flag.boolean("responses").pipe(
28
+ Flag.withDefault(false),
29
+ Flag.withDescription("Explicitly enable local durable question and response writes (Linux)"),
30
+ ),
31
+ },
32
+ serve,
33
+ ).pipe(Command.withDescription("Serve a folder of markdown files as a live page that updates in place when a file changes."));
34
+
35
+ export function runServe() {
36
+ Command.run(workBoard, { version: VERSION }).pipe(Effect.provide(NodeServices.layer), NodeRuntime.runMain);
37
+ }