@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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1271 -1
  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 +138 -4
  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,255 @@
1
+ /**
2
+ * **Write budgets** (pulpit#199): a declared ceiling on how many writes of
3
+ * one kind an account may make in a rolling window, so a family that takes
4
+ * user content (comments, proposals, reports, posts) throttles it without
5
+ * every application re-adding the check.
6
+ *
7
+ * - **Declared once per family and action.** A {@link WriteBudget} names a
8
+ * limit and a window; {@link bindBudget} binds it to a family's type, a
9
+ * {@link BudgetCounter} and the access policy, and checks the declaration
10
+ * (a malformed one throws {@link BudgetDeclarationError} when it is bound).
11
+ * - **Spent inside the write.** A chain write spends through
12
+ * {@link budgetHook}, a before hook, so a write over budget appends
13
+ * nothing; `./proposals`' `submit` and `./moderation`'s report `file` take
14
+ * the bound budget as an opt-in and spend it before their insert. Either
15
+ * way the refusal is `WriteRejected` with the reason {@link RATE_LIMITED},
16
+ * which {@link isRateLimited} tells apart from every other refusal (the
17
+ * wire's 429).
18
+ * - **Exempt above a rung.** An account the budget's `exempt` rung admits
19
+ * (the moderate rung, by default) spends nothing.
20
+ * - **One counter seam.** The owner's decision on #199: count within the
21
+ * write's transaction, with the seam shaped so an atomic counter can be
22
+ * plugged in later. {@link countingCounter} is the counting
23
+ * implementation, one indexed `count(*)` over the family's own rows; a
24
+ * counter that holds its own state (a Durable Object, a locked row)
25
+ * implements {@link BudgetCounter.take} directly.
26
+ *
27
+ * @copyright Thomas Foster
28
+ * @module pulpit/budgets
29
+ * @since 0.1.0
30
+ */
31
+ import * as Effect from "effect/Effect";
32
+ import * as Schema from "effect/Schema";
33
+ import { WriteRejected, type AccessPolicy, type BeforeWriteHook, type Viewer, type ViewerAccount } from "./subtype.ts";
34
+ /**
35
+ * The `WriteRejected` reason a write over its budget carries, so the wire
36
+ * can answer 429 rather than 422.
37
+ *
38
+ * @since 0.1.0
39
+ */
40
+ export declare const RATE_LIMITED = "rate-limited";
41
+ /**
42
+ * Whether a refusal is a write budget's ({@link RATE_LIMITED}), not another
43
+ * hook's.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import { strictEqual } from "node:assert";
48
+ * import { isRateLimited, RATE_LIMITED } from "@thomasfosterau/pulpit/budgets";
49
+ * import { WriteRejected } from "@thomasfosterau/pulpit/subtype";
50
+ *
51
+ * const refusal = new WriteRejected({ type: "comments", id: "c-1", action: "create", reason: RATE_LIMITED });
52
+ * strictEqual(isRateLimited(refusal), true);
53
+ * ```
54
+ *
55
+ * @since 0.1.0
56
+ */
57
+ export declare const isRateLimited: (error: WriteRejected) => boolean;
58
+ /**
59
+ * A write budget: at most `limit` writes per account in any window of
60
+ * `windowSeconds`, for every account the `exempt` rung doesn't admit.
61
+ *
62
+ * @since 0.1.0
63
+ */
64
+ export interface WriteBudget {
65
+ /** The most writes one account may make in a window: a positive safe integer. */
66
+ readonly limit: number;
67
+ /** The window's length, in seconds: a positive safe integer. */
68
+ readonly windowSeconds: number;
69
+ /**
70
+ * The rung that spends nothing, read from the access policy (as
71
+ * `./edges`' `rungGate` reads one). Absent, the moderate rung.
72
+ */
73
+ readonly exempt?: ((policy: AccessPolicy) => (account: ViewerAccount) => boolean) | undefined;
74
+ }
75
+ declare const BudgetDeclarationError_base: Schema.Class<BudgetDeclarationError, Schema.TaggedStruct<"BudgetDeclarationError", {
76
+ readonly type: Schema.String;
77
+ readonly reason: Schema.String;
78
+ }>, import("effect/Cause").YieldableError & {
79
+ readonly brand: unique symbol;
80
+ }>;
81
+ /**
82
+ * A budget declaration that can never admit a write as intended (a limit or
83
+ * window that is not a positive safe integer), thrown by {@link bindBudget}
84
+ * when the budget is bound, never as an Effect failure.
85
+ *
86
+ * @since 0.1.0
87
+ */
88
+ export declare class BudgetDeclarationError extends BudgetDeclarationError_base {
89
+ }
90
+ /**
91
+ * What a {@link BudgetCounter} is asked: whether the account may make one
92
+ * more write of `action` on the family `type`, given the window that
93
+ * started at `since` and the budget's `limit`.
94
+ *
95
+ * @since 0.1.0
96
+ */
97
+ export interface BudgetRequest {
98
+ readonly type: string;
99
+ readonly action: string;
100
+ readonly accountId: string;
101
+ readonly since: Date;
102
+ readonly now: Date;
103
+ readonly limit: number;
104
+ }
105
+ /**
106
+ * The counter seam: spend one write against the budget, answering whether
107
+ * it was within it. Runs in the caller's transaction. A counting
108
+ * implementation ({@link countingCounter}) reads the writes already made,
109
+ * which the write itself then adds to; an atomic one (a Durable Object, a
110
+ * row taken `FOR UPDATE`) records the spend itself, and must spend nothing
111
+ * when it answers `false`. See the package README → Seams.
112
+ *
113
+ * @since 0.1.0
114
+ */
115
+ export interface BudgetCounter<E, R> {
116
+ readonly take: (request: BudgetRequest) => Effect.Effect<boolean, E, R>;
117
+ }
118
+ /**
119
+ * The read {@link countingCounter} counts with: how many writes of `action`
120
+ * on `type` the account made at or after `since`, as one indexed
121
+ * `count(*)` over the family's own rows (`author_id`, `created_at`).
122
+ *
123
+ * @since 0.1.0
124
+ */
125
+ export interface WriteCountStore<E, R> {
126
+ readonly countSince: (query: {
127
+ readonly type: string;
128
+ readonly action: string;
129
+ readonly accountId: string;
130
+ readonly since: Date;
131
+ }) => Effect.Effect<number, E, R>;
132
+ }
133
+ /**
134
+ * The count-within-the-transaction counter (gardeneria's
135
+ * `assertNotRateLimitedE`): a write is within budget while fewer than
136
+ * `limit` writes were made in the window. It spends nothing itself: the
137
+ * write's own row is the spend, and a write that rolls back took none.
138
+ * **It is exact but not atomic:** two concurrent transactions can both
139
+ * count `limit - 1` and both write. A family that must hold its limit under
140
+ * concurrency fills {@link BudgetCounter} with an atomic counter instead.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import { strictEqual } from "node:assert";
145
+ * import * as Effect from "effect/Effect";
146
+ * import { countingCounter } from "@thomasfosterau/pulpit/budgets";
147
+ *
148
+ * const counter = countingCounter({ countSince: () => Effect.succeed(2) });
149
+ * const request = {
150
+ * type: "comments",
151
+ * action: "create",
152
+ * accountId: "u-1",
153
+ * since: new Date(0),
154
+ * now: new Date(60_000),
155
+ * limit: 3,
156
+ * };
157
+ * strictEqual(Effect.runSync(counter.take(request)), true);
158
+ * strictEqual(Effect.runSync(counter.take({ ...request, limit: 2 })), false);
159
+ * ```
160
+ *
161
+ * @since 0.1.0
162
+ */
163
+ export declare const countingCounter: <E, R>(store: WriteCountStore<E, R>) => BudgetCounter<E, R>;
164
+ /**
165
+ * A budget bound to its family: {@link BoundBudget.spend} spends one write.
166
+ *
167
+ * @since 0.1.0
168
+ */
169
+ export interface BoundBudget<E, R> {
170
+ readonly type: string;
171
+ readonly budget: WriteBudget;
172
+ /**
173
+ * Spend one write of `action` by `viewer` on the node `id`. An anonymous
174
+ * viewer spends nothing (the write's own gate refuses it), nor does one
175
+ * the exempt rung admits. Over budget it fails `WriteRejected` with
176
+ * {@link RATE_LIMITED}.
177
+ */
178
+ readonly spend: (viewer: Viewer, action: string, id: string) => Effect.Effect<void, WriteRejected | E, R>;
179
+ }
180
+ /**
181
+ * Bind a {@link WriteBudget} to a family's type, its counter and the access
182
+ * policy. Throws {@link BudgetDeclarationError} for a limit or window that
183
+ * is not a positive safe integer.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { strictEqual } from "node:assert";
188
+ * import * as Effect from "effect/Effect";
189
+ * import * as Exit from "effect/Exit";
190
+ * import * as Option from "effect/Option";
191
+ * import { bindBudget } from "@thomasfosterau/pulpit/budgets";
192
+ * import { makeInMemoryBudgetCounter } from "@thomasfosterau/pulpit/testing";
193
+ *
194
+ * const policy = {
195
+ * canViewPrivate: () => false,
196
+ * canViewDeleted: () => false,
197
+ * canModerate: () => false,
198
+ * canWriteCorpus: () => false,
199
+ * };
200
+ * const comments = bindBudget({
201
+ * type: "comments",
202
+ * budget: { limit: 1, windowSeconds: 60 },
203
+ * counter: makeInMemoryBudgetCounter(),
204
+ * policy,
205
+ * });
206
+ * const member = Option.some({ id: "u-1", role: "member" });
207
+ * const twice = Effect.gen(function* () {
208
+ * yield* comments.spend(member, "create", "c-1");
209
+ * return yield* Effect.exit(comments.spend(member, "create", "c-2"));
210
+ * });
211
+ * strictEqual(Exit.isFailure(Effect.runSync(twice)), true);
212
+ * ```
213
+ *
214
+ * @since 0.1.0
215
+ */
216
+ export declare const bindBudget: <E, R>(definition: {
217
+ readonly type: string;
218
+ readonly budget: WriteBudget;
219
+ readonly counter: BudgetCounter<E, R>;
220
+ readonly policy: AccessPolicy;
221
+ }) => BoundBudget<E, R>;
222
+ /**
223
+ * A bound budget as a before hook for a chain write (`./subtype`'s
224
+ * `hooks.before`), spending one write on each of the `actions` it lists
225
+ * (`create`, by default) and passing the patch through. A localised write
226
+ * spends once: the hook's `locales` pass spends nothing.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * import { strictEqual } from "node:assert";
231
+ * import { bindBudget, budgetHook } from "@thomasfosterau/pulpit/budgets";
232
+ * import { makeInMemoryBudgetCounter } from "@thomasfosterau/pulpit/testing";
233
+ *
234
+ * const policy = {
235
+ * canViewPrivate: () => false,
236
+ * canViewDeleted: () => false,
237
+ * canModerate: () => false,
238
+ * canWriteCorpus: () => false,
239
+ * };
240
+ * const hook = budgetHook(
241
+ * bindBudget({
242
+ * type: "comments",
243
+ * budget: { limit: 10, windowSeconds: 3600 },
244
+ * counter: makeInMemoryBudgetCounter(),
245
+ * policy,
246
+ * }),
247
+ * );
248
+ * strictEqual(hook.name, "comments.budget");
249
+ * ```
250
+ *
251
+ * @since 0.1.0
252
+ */
253
+ export declare const budgetHook: <E, R>(bound: BoundBudget<E, R>, actions?: ReadonlyArray<string>) => BeforeWriteHook<E, R>;
254
+ export {};
255
+ //# sourceMappingURL=budgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,UAAW,aAAa,KAAG,OAAwC,CAAC;AAE9F;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IAEH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,YAAY,KAAK,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;CAC/F;;;;;oBAWmB,OAAO,MAAM;;AATjC;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,2BAGiC;CAAG;AAEhF;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,EAAE,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACzE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE;QAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;KACtB,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,CAAC,SAAS,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,aAAa,CAAC,CAAC,EAAE,CAAC,CAWrF,CAAC;AAEH;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,KACP,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;CAChD;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,cAAc;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,KAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAwCnB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,SACtB,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,YACf,aAAa,CAAC,MAAM,CAAC,KAC7B,eAAe,CAAC,CAAC,EAAE,CAAC,CAOrB,CAAC"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * **Write budgets** (pulpit#199): a declared ceiling on how many writes of
3
+ * one kind an account may make in a rolling window, so a family that takes
4
+ * user content (comments, proposals, reports, posts) throttles it without
5
+ * every application re-adding the check.
6
+ *
7
+ * - **Declared once per family and action.** A {@link WriteBudget} names a
8
+ * limit and a window; {@link bindBudget} binds it to a family's type, a
9
+ * {@link BudgetCounter} and the access policy, and checks the declaration
10
+ * (a malformed one throws {@link BudgetDeclarationError} when it is bound).
11
+ * - **Spent inside the write.** A chain write spends through
12
+ * {@link budgetHook}, a before hook, so a write over budget appends
13
+ * nothing; `./proposals`' `submit` and `./moderation`'s report `file` take
14
+ * the bound budget as an opt-in and spend it before their insert. Either
15
+ * way the refusal is `WriteRejected` with the reason {@link RATE_LIMITED},
16
+ * which {@link isRateLimited} tells apart from every other refusal (the
17
+ * wire's 429).
18
+ * - **Exempt above a rung.** An account the budget's `exempt` rung admits
19
+ * (the moderate rung, by default) spends nothing.
20
+ * - **One counter seam.** The owner's decision on #199: count within the
21
+ * write's transaction, with the seam shaped so an atomic counter can be
22
+ * plugged in later. {@link countingCounter} is the counting
23
+ * implementation, one indexed `count(*)` over the family's own rows; a
24
+ * counter that holds its own state (a Durable Object, a locked row)
25
+ * implements {@link BudgetCounter.take} directly.
26
+ *
27
+ * @copyright Thomas Foster
28
+ * @module pulpit/budgets
29
+ * @since 0.1.0
30
+ */
31
+ import * as Clock from "effect/Clock";
32
+ import * as DateTime from "effect/DateTime";
33
+ import * as Effect from "effect/Effect";
34
+ import * as Option from "effect/Option";
35
+ import * as Schema from "effect/Schema";
36
+ import { WriteRejected, } from "./subtype.js";
37
+ /**
38
+ * The `WriteRejected` reason a write over its budget carries, so the wire
39
+ * can answer 429 rather than 422.
40
+ *
41
+ * @since 0.1.0
42
+ */
43
+ export const RATE_LIMITED = "rate-limited";
44
+ /**
45
+ * Whether a refusal is a write budget's ({@link RATE_LIMITED}), not another
46
+ * hook's.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * import { strictEqual } from "node:assert";
51
+ * import { isRateLimited, RATE_LIMITED } from "@thomasfosterau/pulpit/budgets";
52
+ * import { WriteRejected } from "@thomasfosterau/pulpit/subtype";
53
+ *
54
+ * const refusal = new WriteRejected({ type: "comments", id: "c-1", action: "create", reason: RATE_LIMITED });
55
+ * strictEqual(isRateLimited(refusal), true);
56
+ * ```
57
+ *
58
+ * @since 0.1.0
59
+ */
60
+ export const isRateLimited = (error) => error.reason === RATE_LIMITED;
61
+ /**
62
+ * A budget declaration that can never admit a write as intended (a limit or
63
+ * window that is not a positive safe integer), thrown by {@link bindBudget}
64
+ * when the budget is bound, never as an Effect failure.
65
+ *
66
+ * @since 0.1.0
67
+ */
68
+ export class BudgetDeclarationError extends Schema.TaggedError()("BudgetDeclarationError", { type: Schema.String, reason: Schema.String }) {
69
+ }
70
+ /**
71
+ * The count-within-the-transaction counter (gardeneria's
72
+ * `assertNotRateLimitedE`): a write is within budget while fewer than
73
+ * `limit` writes were made in the window. It spends nothing itself: the
74
+ * write's own row is the spend, and a write that rolls back took none.
75
+ * **It is exact but not atomic:** two concurrent transactions can both
76
+ * count `limit - 1` and both write. A family that must hold its limit under
77
+ * concurrency fills {@link BudgetCounter} with an atomic counter instead.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * import { strictEqual } from "node:assert";
82
+ * import * as Effect from "effect/Effect";
83
+ * import { countingCounter } from "@thomasfosterau/pulpit/budgets";
84
+ *
85
+ * const counter = countingCounter({ countSince: () => Effect.succeed(2) });
86
+ * const request = {
87
+ * type: "comments",
88
+ * action: "create",
89
+ * accountId: "u-1",
90
+ * since: new Date(0),
91
+ * now: new Date(60_000),
92
+ * limit: 3,
93
+ * };
94
+ * strictEqual(Effect.runSync(counter.take(request)), true);
95
+ * strictEqual(Effect.runSync(counter.take({ ...request, limit: 2 })), false);
96
+ * ```
97
+ *
98
+ * @since 0.1.0
99
+ */
100
+ export const countingCounter = (store) => ({
101
+ take: (request) => Effect.map(store.countSince({
102
+ type: request.type,
103
+ action: request.action,
104
+ accountId: request.accountId,
105
+ since: request.since,
106
+ }), (count) => count < request.limit),
107
+ });
108
+ const positiveSafeInteger = (value) => Number.isSafeInteger(value) && value > 0;
109
+ /**
110
+ * Bind a {@link WriteBudget} to a family's type, its counter and the access
111
+ * policy. Throws {@link BudgetDeclarationError} for a limit or window that
112
+ * is not a positive safe integer.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { strictEqual } from "node:assert";
117
+ * import * as Effect from "effect/Effect";
118
+ * import * as Exit from "effect/Exit";
119
+ * import * as Option from "effect/Option";
120
+ * import { bindBudget } from "@thomasfosterau/pulpit/budgets";
121
+ * import { makeInMemoryBudgetCounter } from "@thomasfosterau/pulpit/testing";
122
+ *
123
+ * const policy = {
124
+ * canViewPrivate: () => false,
125
+ * canViewDeleted: () => false,
126
+ * canModerate: () => false,
127
+ * canWriteCorpus: () => false,
128
+ * };
129
+ * const comments = bindBudget({
130
+ * type: "comments",
131
+ * budget: { limit: 1, windowSeconds: 60 },
132
+ * counter: makeInMemoryBudgetCounter(),
133
+ * policy,
134
+ * });
135
+ * const member = Option.some({ id: "u-1", role: "member" });
136
+ * const twice = Effect.gen(function* () {
137
+ * yield* comments.spend(member, "create", "c-1");
138
+ * return yield* Effect.exit(comments.spend(member, "create", "c-2"));
139
+ * });
140
+ * strictEqual(Exit.isFailure(Effect.runSync(twice)), true);
141
+ * ```
142
+ *
143
+ * @since 0.1.0
144
+ */
145
+ export const bindBudget = (definition) => {
146
+ const { type, budget, counter, policy } = definition;
147
+ if (!positiveSafeInteger(budget.limit)) {
148
+ throw new BudgetDeclarationError({ type, reason: "limit must be a positive safe integer" });
149
+ }
150
+ if (!positiveSafeInteger(budget.windowSeconds)) {
151
+ throw new BudgetDeclarationError({
152
+ type,
153
+ reason: "windowSeconds must be a positive safe integer",
154
+ });
155
+ }
156
+ const exempt = (budget.exempt ?? ((rungs) => rungs.canModerate))(policy);
157
+ const windowMillis = budget.windowSeconds * 1000;
158
+ const spend = (viewer, action, id) => Option.match(Option.filter(viewer, (account) => !exempt(account)), {
159
+ onNone: () => Effect.void,
160
+ onSome: (account) => Effect.flatMap(Clock.currentTimeMillis, (millis) => Effect.flatMap(counter.take({
161
+ type,
162
+ action,
163
+ accountId: account.id,
164
+ since: DateTime.toDate(DateTime.makeUnsafe(millis - windowMillis)),
165
+ now: DateTime.toDate(DateTime.makeUnsafe(millis)),
166
+ limit: budget.limit,
167
+ }), (within) => within
168
+ ? Effect.void
169
+ : Effect.fail(new WriteRejected({ type, id, action, reason: RATE_LIMITED })))),
170
+ });
171
+ return { type, budget, spend };
172
+ };
173
+ /**
174
+ * A bound budget as a before hook for a chain write (`./subtype`'s
175
+ * `hooks.before`), spending one write on each of the `actions` it lists
176
+ * (`create`, by default) and passing the patch through. A localised write
177
+ * spends once: the hook's `locales` pass spends nothing.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * import { strictEqual } from "node:assert";
182
+ * import { bindBudget, budgetHook } from "@thomasfosterau/pulpit/budgets";
183
+ * import { makeInMemoryBudgetCounter } from "@thomasfosterau/pulpit/testing";
184
+ *
185
+ * const policy = {
186
+ * canViewPrivate: () => false,
187
+ * canViewDeleted: () => false,
188
+ * canModerate: () => false,
189
+ * canWriteCorpus: () => false,
190
+ * };
191
+ * const hook = budgetHook(
192
+ * bindBudget({
193
+ * type: "comments",
194
+ * budget: { limit: 10, windowSeconds: 3600 },
195
+ * counter: makeInMemoryBudgetCounter(),
196
+ * policy,
197
+ * }),
198
+ * );
199
+ * strictEqual(hook.name, "comments.budget");
200
+ * ```
201
+ *
202
+ * @since 0.1.0
203
+ */
204
+ export const budgetHook = (bound, actions = ["create"]) => ({
205
+ name: `${bound.type}.budget`,
206
+ run: (event) => actions.includes(event.action)
207
+ ? Effect.as(bound.spend(event.viewer, event.action, event.id), event.patch)
208
+ : Effect.succeed(event.patch),
209
+ locales: (event) => Effect.succeed(event.locales ?? {}),
210
+ });
211
+ //# sourceMappingURL=budgets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgets.js","sourceRoot":"","sources":["../src/budgets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * **The reference resource cache** (pulpit#205) — `./subtype`'s
3
+ * {@link ResourceCacheSeam}, with a read-through `get`, over a minimal
4
+ * versioned key-value seam ({@link VersionedKv}).
5
+ *
6
+ * The behavioural spec is the four apps' own caches (website's, gardeneria's,
7
+ * preferenceflow's and in-the-dark's `apps/api/src/cache.ts`), which this
8
+ * replaces: one implementation of the contract the kernel already states.
9
+ *
10
+ * - **Compare-on-set** (pulpit#121). Every entry carries the version it
11
+ * speaks for, and the KV seam's one conditional write keeps the highest:
12
+ * a `set` or `delete` below the stored version changes nothing, and an
13
+ * eviction raises the version, so a racing writer's late write-through can
14
+ * never re-cache a resource older than one already replaced or evicted.
15
+ * - **An eviction wins a tie.** A tombstone outranks a value at the same
16
+ * version: only a strictly newer write clears it, so a read-through that
17
+ * loaded the row before an unpublish at that version can't re-cache it.
18
+ * The cache stores a rank, `2·version` for a value and `2·version + 1` for
19
+ * a tombstone, as the entry's `version`, so the seam still compares one
20
+ * number.
21
+ * - **Tombstones outlive values.** A tombstone is kept for
22
+ * `tombstoneTtlSeconds` (by default a day, and never less than the value
23
+ * TTL), which must exceed the longest a write can be in flight: a late
24
+ * write-through that arrives after its tombstone has expired is no longer
25
+ * ordered against it.
26
+ * - **Serveability on both sides** (`CACHE-AUTH-005`). `set` refuses (and
27
+ * evicts) a resource the `serveable` policy refuses, and `get` checks it
28
+ * again, so a policy change can't serve a stale cached private row.
29
+ * - **Decode or evict.** `get` decodes the stored value with the resource's
30
+ * schema; a value that no longer decodes (the schema moved) is evicted and
31
+ * read as a miss.
32
+ * - **A TTL backstop.** Every write carries the configured TTL, so an entry
33
+ * no write ever evicted still expires.
34
+ * - **Never fails.** A KV failure reads as a miss and skips a write: the
35
+ * cache is an optimisation, and the store stays the source of truth.
36
+ *
37
+ * The KV seam is the application's (a Durable Object, Redis, a SQL table):
38
+ * nothing here imports a platform. Workers KV alone has no conditional
39
+ * write, so it can't fill {@link VersionedKv.putIfNotBehind}.
40
+ *
41
+ * @copyright Thomas Foster
42
+ * @module pulpit/cache
43
+ * @since 0.1.0
44
+ */
45
+ import * as Effect from "effect/Effect";
46
+ import * as Option from "effect/Option";
47
+ import * as Schema from "effect/Schema";
48
+ import type { ResourceCacheSeam } from "./subtype.ts";
49
+ /**
50
+ * One stored entry: the rank it is ordered by, and the encoded resource, or
51
+ * none for an eviction (a tombstone that keeps the rank). `makeResourceCache`
52
+ * stores `2·version` for a value and `2·version + 1` for a tombstone, so a
53
+ * tombstone outranks a value at the same node version.
54
+ *
55
+ * @since 0.1.0
56
+ */
57
+ export interface KvEntry {
58
+ readonly version: bigint;
59
+ readonly value: Option.Option<Schema.Json>;
60
+ }
61
+ /**
62
+ * The minimal versioned key-value store an application supplies. A store
63
+ * seam: no policy, fails only with its `E`. See the package README → Seams.
64
+ *
65
+ * @since 0.1.0
66
+ */
67
+ export interface VersionedKv<E, R> {
68
+ /** The entry at `key`, or none (never written, or expired). */
69
+ readonly get: (key: string) => Effect.Effect<Option.Option<KvEntry>, E, R>;
70
+ /**
71
+ * Write `entry` at `key` with a TTL of `ttlSeconds`, **in one atomic
72
+ * step**, unless the stored entry's version is above `entry.version`;
73
+ * whether it wrote. A store with no conditional write cannot fill this
74
+ * seam.
75
+ */
76
+ readonly putIfNotBehind: (key: string, entry: KvEntry, ttlSeconds: number) => Effect.Effect<boolean, E, R>;
77
+ }
78
+ /**
79
+ * {@link makeResourceCache}'s configuration.
80
+ *
81
+ * @since 0.1.0
82
+ */
83
+ export interface ResourceCacheOptions<Resource, E, R> {
84
+ readonly kv: VersionedKv<E, R>;
85
+ /** The resource's schema: its JSON encoding in the store, decoded on every read. */
86
+ readonly schema: Schema.Codec<Resource, Schema.Json>;
87
+ /** The `CACHE-AUTH-005` serveability policy (typically `./publication`'s `serveablePolicy`). */
88
+ readonly serveable: (resource: Resource) => boolean;
89
+ /** The key prefix, typically the leaf's type and a schema generation (`posts:v3:`). */
90
+ readonly prefix: string;
91
+ /** The TTL backstop every value write carries, in seconds (clamped to at least 1). */
92
+ readonly ttlSeconds: number;
93
+ /**
94
+ * How long an eviction's tombstone is kept, in seconds: it must exceed the
95
+ * longest a write-through or read-through fill can be in flight, since a
96
+ * stale write that lands after its tombstone expired is no longer refused.
97
+ * Defaults to a day ({@link DEFAULT_TOMBSTONE_TTL_SECONDS}), and is never
98
+ * less than `ttlSeconds`.
99
+ */
100
+ readonly tombstoneTtlSeconds?: number | undefined;
101
+ }
102
+ /**
103
+ * The default time an eviction's tombstone is kept: one day, far longer than
104
+ * any request can be in flight.
105
+ *
106
+ * @since 0.1.0
107
+ */
108
+ export declare const DEFAULT_TOMBSTONE_TTL_SECONDS = 86400;
109
+ /**
110
+ * A resource cache: the kernel's write-through seam plus the read-through
111
+ * `get`.
112
+ *
113
+ * @since 0.1.0
114
+ */
115
+ export interface ResourceCache<Resource, R> extends ResourceCacheSeam<Resource, R> {
116
+ /**
117
+ * The cached resource for `id`: none on a miss, an eviction, a value that
118
+ * no longer decodes (evicted at its version) or one the policy no longer
119
+ * serves (evicted likewise). Never fails.
120
+ */
121
+ readonly get: (id: string) => Effect.Effect<Option.Option<Resource>, never, R>;
122
+ }
123
+ /**
124
+ * The reference {@link ResourceCacheSeam} over a {@link VersionedKv}.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { deepStrictEqual } from "node:assert";
129
+ * import * as Effect from "effect/Effect";
130
+ * import * as Option from "effect/Option";
131
+ * import * as Schema from "effect/Schema";
132
+ * import { makeResourceCache } from "@thomasfosterau/pulpit/cache";
133
+ * import { makeInMemoryVersionedKv } from "@thomasfosterau/pulpit/testing";
134
+ *
135
+ * const Post = Schema.Struct({ id: Schema.String, visibility: Schema.String });
136
+ * const cache = makeResourceCache({
137
+ * kv: makeInMemoryVersionedKv(),
138
+ * schema: Post,
139
+ * serveable: (post) => post.visibility === "public",
140
+ * prefix: "posts:v1:",
141
+ * ttlSeconds: 3600,
142
+ * });
143
+ * const program = Effect.gen(function* () {
144
+ * yield* cache.set("p-1", { id: "p-1", visibility: "public" }, 1n);
145
+ * yield* cache.set("p-1", { id: "p-1", visibility: "private" }, 0n); // behind: no-op
146
+ * return yield* cache.get("p-1");
147
+ * });
148
+ * deepStrictEqual(Effect.runSync(program), Option.some({ id: "p-1", visibility: "public" }));
149
+ * ```
150
+ *
151
+ * @since 0.1.0
152
+ */
153
+ export declare const makeResourceCache: <Resource, E, R>(options: ResourceCacheOptions<Resource, E, R>) => ResourceCache<Resource, R>;
154
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD;;;;;;;GAOG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,EAAE,CAAC;IAC/B,+DAA+D;IAC/D,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3E;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,EAAE,CACvB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,MAAM,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACnC;AAID;;;;GAIG;AACH,MAAM,WAAW,oBAAoB,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC;IAClD,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IACrD,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC;IACpD,uFAAuF;IACvF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IAEH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,aAAa,CAAC,QAAQ,EAAE,CAAC,CAAE,SAAQ,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChF;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;CAChF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,EAAE,CAAC,EAAE,CAAC,WACrC,oBAAoB,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,KAC5C,aAAa,CAAC,QAAQ,EAAE,CAAC,CA+C3B,CAAC"}