@thomasfosterau/pulpit 0.0.0-stage → 0.1.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1272 -2
  3. package/dist/blobs.d.ts +249 -0
  4. package/dist/blobs.d.ts.map +1 -0
  5. package/dist/blobs.js +193 -0
  6. package/dist/blobs.js.map +1 -0
  7. package/dist/budgets.d.ts +255 -0
  8. package/dist/budgets.d.ts.map +1 -0
  9. package/dist/budgets.js +211 -0
  10. package/dist/budgets.js.map +1 -0
  11. package/dist/cache.d.ts +154 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +118 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/collections.d.ts +242 -0
  16. package/dist/collections.d.ts.map +1 -0
  17. package/dist/collections.js +215 -0
  18. package/dist/collections.js.map +1 -0
  19. package/dist/comments.d.ts +212 -0
  20. package/dist/comments.d.ts.map +1 -0
  21. package/dist/comments.js +192 -0
  22. package/dist/comments.js.map +1 -0
  23. package/dist/content.d.ts +231 -0
  24. package/dist/content.d.ts.map +1 -0
  25. package/dist/content.js +204 -0
  26. package/dist/content.js.map +1 -0
  27. package/dist/documents.d.ts +184 -0
  28. package/dist/documents.d.ts.map +1 -0
  29. package/dist/documents.js +190 -0
  30. package/dist/documents.js.map +1 -0
  31. package/dist/edges.d.ts +645 -0
  32. package/dist/edges.d.ts.map +1 -0
  33. package/dist/edges.js +558 -0
  34. package/dist/edges.js.map +1 -0
  35. package/dist/filter.d.ts +461 -0
  36. package/dist/filter.d.ts.map +1 -0
  37. package/dist/filter.js +387 -0
  38. package/dist/filter.js.map +1 -0
  39. package/dist/index.d.ts +133 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +133 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/internal/filter-columns.d.ts +8 -0
  44. package/dist/internal/filter-columns.d.ts.map +1 -0
  45. package/dist/internal/filter-columns.js +43 -0
  46. package/dist/internal/filter-columns.js.map +1 -0
  47. package/dist/internal/list-order.d.ts +26 -0
  48. package/dist/internal/list-order.d.ts.map +1 -0
  49. package/dist/internal/list-order.js +92 -0
  50. package/dist/internal/list-order.js.map +1 -0
  51. package/dist/internal/owned-columns.d.ts +93 -0
  52. package/dist/internal/owned-columns.d.ts.map +1 -0
  53. package/dist/internal/owned-columns.js +98 -0
  54. package/dist/internal/owned-columns.js.map +1 -0
  55. package/dist/internal/plain-rows.d.ts +32 -0
  56. package/dist/internal/plain-rows.d.ts.map +1 -0
  57. package/dist/internal/plain-rows.js +32 -0
  58. package/dist/internal/plain-rows.js.map +1 -0
  59. package/dist/internal/reference-columns.d.ts +80 -0
  60. package/dist/internal/reference-columns.d.ts.map +1 -0
  61. package/dist/internal/reference-columns.js +102 -0
  62. package/dist/internal/reference-columns.js.map +1 -0
  63. package/dist/moderation.d.ts +392 -0
  64. package/dist/moderation.d.ts.map +1 -0
  65. package/dist/moderation.js +375 -0
  66. package/dist/moderation.js.map +1 -0
  67. package/dist/paths.d.ts +204 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +165 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/proposals.d.ts +523 -0
  72. package/dist/proposals.d.ts.map +1 -0
  73. package/dist/proposals.js +505 -0
  74. package/dist/proposals.js.map +1 -0
  75. package/dist/publication.d.ts +148 -0
  76. package/dist/publication.d.ts.map +1 -0
  77. package/dist/publication.js +183 -0
  78. package/dist/publication.js.map +1 -0
  79. package/dist/purge.d.ts +224 -0
  80. package/dist/purge.d.ts.map +1 -0
  81. package/dist/purge.js +151 -0
  82. package/dist/purge.js.map +1 -0
  83. package/dist/reactions.d.ts +246 -0
  84. package/dist/reactions.d.ts.map +1 -0
  85. package/dist/reactions.js +183 -0
  86. package/dist/reactions.js.map +1 -0
  87. package/dist/registry.d.ts +316 -0
  88. package/dist/registry.d.ts.map +1 -0
  89. package/dist/registry.js +305 -0
  90. package/dist/registry.js.map +1 -0
  91. package/dist/search.d.ts +366 -0
  92. package/dist/search.d.ts.map +1 -0
  93. package/dist/search.js +273 -0
  94. package/dist/search.js.map +1 -0
  95. package/dist/social.d.ts +311 -0
  96. package/dist/social.d.ts.map +1 -0
  97. package/dist/social.js +205 -0
  98. package/dist/social.js.map +1 -0
  99. package/dist/subtype.d.ts +1613 -0
  100. package/dist/subtype.d.ts.map +1 -0
  101. package/dist/subtype.js +1300 -0
  102. package/dist/subtype.js.map +1 -0
  103. package/dist/testing.d.ts +675 -0
  104. package/dist/testing.d.ts.map +1 -0
  105. package/dist/testing.js +1022 -0
  106. package/dist/testing.js.map +1 -0
  107. package/dist/version-chain.d.ts +702 -0
  108. package/dist/version-chain.d.ts.map +1 -0
  109. package/dist/version-chain.js +489 -0
  110. package/dist/version-chain.js.map +1 -0
  111. package/dist/working-copy.d.ts +116 -0
  112. package/dist/working-copy.d.ts.map +1 -0
  113. package/dist/working-copy.js +95 -0
  114. package/dist/working-copy.js.map +1 -0
  115. package/package.json +145 -3
  116. package/src/blobs.ts +384 -0
  117. package/src/budgets.ts +338 -0
  118. package/src/cache.ts +218 -0
  119. package/src/collections.ts +427 -0
  120. package/src/comments.ts +392 -0
  121. package/src/content.ts +238 -0
  122. package/src/documents.ts +284 -0
  123. package/src/edges.ts +1185 -0
  124. package/src/filter.ts +523 -0
  125. package/src/index.ts +153 -0
  126. package/src/internal/filter-columns.ts +50 -0
  127. package/src/internal/list-order.ts +117 -0
  128. package/src/internal/owned-columns.ts +140 -0
  129. package/src/internal/plain-rows.ts +54 -0
  130. package/src/internal/reference-columns.ts +156 -0
  131. package/src/moderation.ts +728 -0
  132. package/src/paths.ts +267 -0
  133. package/src/proposals.ts +1014 -0
  134. package/src/publication.ts +225 -0
  135. package/src/purge.ts +313 -0
  136. package/src/reactions.ts +347 -0
  137. package/src/registry.ts +524 -0
  138. package/src/search.ts +540 -0
  139. package/src/social.ts +469 -0
  140. package/src/subtype.ts +3095 -0
  141. package/src/testing.ts +1873 -0
  142. package/src/version-chain.ts +1093 -0
  143. package/src/working-copy.ts +192 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * **The working copy** — an editor's autosave buffer, kept outside the
3
+ * version chain and overwritten freely, which an explicit save appends as
4
+ * one ordinary `update` version.
5
+ *
6
+ * ## The model (owner decisions on pulpit#46)
7
+ *
8
+ * - **Nothing in a working copy is a version**: saving it writes only the
9
+ * {@link WorkingCopyStore}, so `VERSION-CHAIN-001` is untouched and a
10
+ * hundred autosaves leave no trace in history.
11
+ * - **One copy per (node, editor)**, so two editors never overwrite each
12
+ * other's unsaved work.
13
+ * - **`commit` appends the copy** through the leaf's own `update`, expecting
14
+ * the head the copy was first based on: an edit landing in between fails
15
+ * `StaleVersion` (and keeps the copy), so a commit never silently
16
+ * overwrites work its editor didn't see. A committed copy is cleared.
17
+ * - **Every operation is gated like a write** (the leaf's `authorize`):
18
+ * reading a copy shows unpublished state, so it is gated like the head.
19
+ *
20
+ * @copyright Thomas Foster
21
+ * @module pulpit/working-copy
22
+ * @since 0.1.0
23
+ */
24
+
25
+ import * as Clock from "effect/Clock";
26
+ import * as DateTime from "effect/DateTime";
27
+ import * as Effect from "effect/Effect";
28
+ import * as Option from "effect/Option";
29
+ import * as Record from "effect/Record";
30
+ import * as Schema from "effect/Schema";
31
+ import type { Forbidden, SubtypeOps, Unauthorized, WriteRejected } from "./subtype.ts";
32
+ import {
33
+ NodeNotFound,
34
+ type ChainPatch,
35
+ type ChainWrite,
36
+ type StaleVersion,
37
+ type VersionChainStore,
38
+ type VersionConflict,
39
+ } from "./version-chain.ts";
40
+
41
+ /**
42
+ * One editor's working copy of one node: the tri-state patch over the head
43
+ * it was based on, and when it was last saved.
44
+ *
45
+ * @since 0.1.0
46
+ */
47
+ export interface WorkingCopy {
48
+ readonly patch: ChainPatch;
49
+ /** The head the copy was first based on; `commit` expects it. */
50
+ readonly baseVersion: bigint;
51
+ readonly savedAt: Date;
52
+ }
53
+
54
+ /**
55
+ * Whose copy of which node.
56
+ *
57
+ * @since 0.1.0
58
+ */
59
+ export interface WorkingCopyKey {
60
+ readonly id: string;
61
+ readonly userId: string;
62
+ }
63
+
64
+ /**
65
+ * The overwritable buffer an application supplies (a table keyed by
66
+ * `(node_id, user_id)`, or a KV namespace). Applies no policy and fails
67
+ * only with its `E` (README → Seams).
68
+ *
69
+ * @since 0.1.0
70
+ */
71
+ export interface WorkingCopyStore<E, R> {
72
+ /** Write the copy, replacing any earlier one under the key. */
73
+ readonly put: (key: WorkingCopyKey, copy: WorkingCopy) => Effect.Effect<void, E, R>;
74
+ readonly get: (key: WorkingCopyKey) => Effect.Effect<Option.Option<WorkingCopy>, E, R>;
75
+ /** Remove the copy; idempotent. */
76
+ readonly discard: (key: WorkingCopyKey) => Effect.Effect<void, E, R>;
77
+ }
78
+
79
+ /**
80
+ * No working copy to commit: the wire's 404.
81
+ *
82
+ * @since 0.1.0
83
+ */
84
+ export class WorkingCopyNotFound extends Schema.TaggedError<
85
+ WorkingCopyNotFound,
86
+ { readonly brand: unique symbol }
87
+ >()("WorkingCopyNotFound", { type: Schema.String, id: Schema.String }) {}
88
+
89
+ type Gate = Unauthorized | Forbidden | NodeNotFound;
90
+
91
+ /**
92
+ * The operations {@link defineWorkingCopy} produces, each acting on the
93
+ * calling editor's own copy.
94
+ *
95
+ * @since 0.1.0
96
+ */
97
+ export interface WorkingCopyOps<E, R> {
98
+ /**
99
+ * Save `patch` over the editor's copy (a value sets, `null` clears, an
100
+ * absent key keeps the copy's), based on the head when there was no
101
+ * copy. Fails at the leaf's write gate, and `NodeNotFound` for a missing
102
+ * or tombstoned node.
103
+ */
104
+ readonly save: (id: string, patch: ChainPatch) => Effect.Effect<WorkingCopy, Gate | E, R>;
105
+ /** The editor's copy, or none. Gated like a write. */
106
+ readonly get: (id: string) => Effect.Effect<Option.Option<WorkingCopy>, Gate | E, R>;
107
+ /** Throw the editor's copy away. Gated like a write. */
108
+ readonly discard: (id: string) => Effect.Effect<void, Gate | E, R>;
109
+ /**
110
+ * Append the copy as one `update` version, expecting its base head, and
111
+ * clear it. Fails `WorkingCopyNotFound` without a copy, and as the
112
+ * leaf's `update` does (`StaleVersion` when the head moved since the
113
+ * copy's base, keeping the copy; `WriteRejected` from its hooks).
114
+ */
115
+ readonly commit: (
116
+ id: string,
117
+ ) => Effect.Effect<
118
+ ChainWrite,
119
+ Gate | WorkingCopyNotFound | StaleVersion | VersionConflict | WriteRejected | E,
120
+ R
121
+ >;
122
+ }
123
+
124
+ const nowDate: Effect.Effect<Date> = Effect.map(Clock.currentTimeMillis, (millis) =>
125
+ DateTime.toDate(DateTime.makeUnsafe(millis)),
126
+ );
127
+
128
+ /**
129
+ * Bind a leaf's working copies: `leaf` gates and commits, `chain` reads
130
+ * the head a new copy is based on, `store` keeps the copies.
131
+ *
132
+ * @since 0.1.0
133
+ */
134
+ export const defineWorkingCopy = <Resource, E, R>(definition: {
135
+ readonly leaf: SubtypeOps<Resource, E, R>;
136
+ readonly chain: VersionChainStore<E, R>;
137
+ readonly store: WorkingCopyStore<E, R>;
138
+ }): WorkingCopyOps<E, R> => {
139
+ const { leaf, chain, store } = definition;
140
+ const type = leaf.type;
141
+ const keyFor = (id: string) =>
142
+ Effect.map(leaf.authorize(id), (account) => ({ id, userId: account.id }));
143
+
144
+ const save = Effect.fn(`${type}.workingCopy.save`)(function* (id: string, patch: ChainPatch) {
145
+ const key = yield* keyFor(id);
146
+ const head = yield* chain.head(id);
147
+ if (Option.isNone(head) || head.value.action === "delete") {
148
+ return yield* new NodeNotFound({ type, id });
149
+ }
150
+ const earlier = yield* store.get(key);
151
+ const copy: WorkingCopy = {
152
+ patch: Option.match(earlier, {
153
+ onNone: () => patch,
154
+ // An absent (or `undefined`) key in the new patch keeps the copy's own.
155
+ onSome: (existing): ChainPatch => ({
156
+ ...existing.patch,
157
+ ...Record.filter(patch, (value) => value !== undefined),
158
+ }),
159
+ }),
160
+ baseVersion: Option.match(earlier, {
161
+ onNone: () => head.value.version,
162
+ onSome: (existing) => existing.baseVersion,
163
+ }),
164
+ savedAt: yield* nowDate,
165
+ };
166
+ yield* store.put(key, copy);
167
+ return copy;
168
+ });
169
+
170
+ const get = Effect.fn(`${type}.workingCopy.get`)(function* (id: string) {
171
+ return yield* store.get(yield* keyFor(id));
172
+ });
173
+
174
+ const discard = Effect.fn(`${type}.workingCopy.discard`)(function* (id: string) {
175
+ yield* store.discard(yield* keyFor(id));
176
+ });
177
+
178
+ const commit = Effect.fn(`${type}.workingCopy.commit`)(function* (id: string) {
179
+ const key = yield* keyFor(id);
180
+ const copy = yield* store.get(key);
181
+ if (Option.isNone(copy)) return yield* new WorkingCopyNotFound({ type, id });
182
+ const written = yield* leaf.update({
183
+ id,
184
+ patch: copy.value.patch,
185
+ expectedVersion: copy.value.baseVersion,
186
+ });
187
+ yield* store.discard(key);
188
+ return written;
189
+ });
190
+
191
+ return { save, get, discard, commit };
192
+ };