@thomasfosterau/pulpit 0.0.0 → 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.
- package/LICENSE +21 -0
- package/README.md +1271 -1
- package/dist/blobs.d.ts +249 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +193 -0
- package/dist/blobs.js.map +1 -0
- package/dist/budgets.d.ts +255 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +211 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cache.d.ts +154 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +118 -0
- package/dist/cache.js.map +1 -0
- package/dist/collections.d.ts +242 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/comments.d.ts +212 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +192 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +231 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +204 -0
- package/dist/content.js.map +1 -0
- package/dist/documents.d.ts +184 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +190 -0
- package/dist/documents.js.map +1 -0
- package/dist/edges.d.ts +645 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +558 -0
- package/dist/edges.js.map +1 -0
- package/dist/filter.d.ts +461 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +387 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/filter-columns.d.ts +8 -0
- package/dist/internal/filter-columns.d.ts.map +1 -0
- package/dist/internal/filter-columns.js +43 -0
- package/dist/internal/filter-columns.js.map +1 -0
- package/dist/internal/list-order.d.ts +26 -0
- package/dist/internal/list-order.d.ts.map +1 -0
- package/dist/internal/list-order.js +92 -0
- package/dist/internal/list-order.js.map +1 -0
- package/dist/internal/owned-columns.d.ts +93 -0
- package/dist/internal/owned-columns.d.ts.map +1 -0
- package/dist/internal/owned-columns.js +98 -0
- package/dist/internal/owned-columns.js.map +1 -0
- package/dist/internal/plain-rows.d.ts +32 -0
- package/dist/internal/plain-rows.d.ts.map +1 -0
- package/dist/internal/plain-rows.js +32 -0
- package/dist/internal/plain-rows.js.map +1 -0
- package/dist/internal/reference-columns.d.ts +80 -0
- package/dist/internal/reference-columns.d.ts.map +1 -0
- package/dist/internal/reference-columns.js +102 -0
- package/dist/internal/reference-columns.js.map +1 -0
- package/dist/moderation.d.ts +392 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +375 -0
- package/dist/moderation.js.map +1 -0
- package/dist/paths.d.ts +204 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +165 -0
- package/dist/paths.js.map +1 -0
- package/dist/proposals.d.ts +523 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +505 -0
- package/dist/proposals.js.map +1 -0
- package/dist/publication.d.ts +148 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +183 -0
- package/dist/publication.js.map +1 -0
- package/dist/purge.d.ts +224 -0
- package/dist/purge.d.ts.map +1 -0
- package/dist/purge.js +151 -0
- package/dist/purge.js.map +1 -0
- package/dist/reactions.d.ts +246 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +183 -0
- package/dist/reactions.js.map +1 -0
- package/dist/registry.d.ts +316 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +305 -0
- package/dist/registry.js.map +1 -0
- package/dist/search.d.ts +366 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +273 -0
- package/dist/search.js.map +1 -0
- package/dist/social.d.ts +311 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +205 -0
- package/dist/social.js.map +1 -0
- package/dist/subtype.d.ts +1613 -0
- package/dist/subtype.d.ts.map +1 -0
- package/dist/subtype.js +1300 -0
- package/dist/subtype.js.map +1 -0
- package/dist/testing.d.ts +675 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1022 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-chain.d.ts +702 -0
- package/dist/version-chain.d.ts.map +1 -0
- package/dist/version-chain.js +489 -0
- package/dist/version-chain.js.map +1 -0
- package/dist/working-copy.d.ts +116 -0
- package/dist/working-copy.d.ts.map +1 -0
- package/dist/working-copy.js +95 -0
- package/dist/working-copy.js.map +1 -0
- package/package.json +138 -4
- package/src/blobs.ts +384 -0
- package/src/budgets.ts +338 -0
- package/src/cache.ts +218 -0
- package/src/collections.ts +427 -0
- package/src/comments.ts +392 -0
- package/src/content.ts +238 -0
- package/src/documents.ts +284 -0
- package/src/edges.ts +1185 -0
- package/src/filter.ts +523 -0
- package/src/index.ts +153 -0
- package/src/internal/filter-columns.ts +50 -0
- package/src/internal/list-order.ts +117 -0
- package/src/internal/owned-columns.ts +140 -0
- package/src/internal/plain-rows.ts +54 -0
- package/src/internal/reference-columns.ts +156 -0
- package/src/moderation.ts +728 -0
- package/src/paths.ts +267 -0
- package/src/proposals.ts +1014 -0
- package/src/publication.ts +225 -0
- package/src/purge.ts +313 -0
- package/src/reactions.ts +347 -0
- package/src/registry.ts +524 -0
- package/src/search.ts +540 -0
- package/src/social.ts +469 -0
- package/src/subtype.ts +3095 -0
- package/src/testing.ts +1873 -0
- package/src/version-chain.ts +1093 -0
- 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
|
+
};
|