@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,249 @@
1
+ /**
2
+ * **File and media leaves** — the blob-store seam a file leaf's bytes live
3
+ * behind, the field spread a file version carries, the commit-ordered byte
4
+ * write, and the usage index as an edge family.
5
+ *
6
+ * The behavioural spec is gardeneria's `apps/api/src/operations/media.ts`
7
+ * (bytes written only after the transaction commits) and `apps/api/src/files.ts`
8
+ * (the content hash), and website's `apps/api/src/operations/file-chain.ts`.
9
+ *
10
+ * ## The model (owner decisions on pulpit#55)
11
+ *
12
+ * - **Bytes are keyed by their content hash** ({@link blobKey}), so a
13
+ * version's bytes are immutable: new bytes are a new hash and a new
14
+ * version, and `restoreVersion` restores the reference to the old bytes,
15
+ * which are still there. Equal bytes are stored once.
16
+ * - **Bytes are written after the chain commits.** {@link defineFileWrites}
17
+ * appends the version first, marked `bytesPending: true` unless the store
18
+ * already holds the bytes, puts the bytes once the transaction has
19
+ * returned, and then appends `bytesPending: false`. A byte write that
20
+ * fails leaves the node visibly incomplete: `bytesPending` stays `true`
21
+ * and {@link fileUrl} gives no URL, so no reader is ever pointed at
22
+ * missing bytes. `confirmBytes` finishes it later, once the bytes are
23
+ * there.
24
+ * - **Nothing garbage-collects bytes.** They stay for as long as any
25
+ * version references them; purge (pulpit#58) is the only removal path,
26
+ * which is what {@link BlobStore.delete} is for.
27
+ * - **"Where is this file used" is an edge family** ({@link fileUsageEnds}),
28
+ * so the endpoint-visibility gate (`EDGE-001`) applies: a "used by" list
29
+ * never reveals content the viewer can't see. Maintaining those edges is
30
+ * the referencing leaf's job (or an after hook's).
31
+ *
32
+ * The adapters (R2, S3), image transforms, EXIF extraction and video
33
+ * streaming are the application's.
34
+ *
35
+ * @copyright Thomas Foster
36
+ * @module pulpit/blobs
37
+ * @since 0.1.0
38
+ */
39
+ import { Resource } from "@thomasfosterau/effect-jsonapi";
40
+ import * as Effect from "effect/Effect";
41
+ import * as Option from "effect/Option";
42
+ import * as Schema from "effect/Schema";
43
+ import type { EdgeEnd, EdgeEnds } from "./edges.ts";
44
+ import type { Forbidden, SubtypeOps, Unauthorized, WriteRejected } from "./subtype.ts";
45
+ import type { ChainState, ChainWrite, NodeAlreadyExists, NodeNotFound, Precondition, StaleVersion, VersionConflict } from "./version-chain.ts";
46
+ /**
47
+ * A file's content hash: the SHA-256 digest of its bytes as 64 lowercase
48
+ * hexadecimal characters ({@link sha256Hex}).
49
+ *
50
+ * @since 0.1.0
51
+ */
52
+ export declare const ContentHash: Schema.String;
53
+ /**
54
+ * The {@link ContentHash} string.
55
+ *
56
+ * @since 0.1.0
57
+ */
58
+ export type ContentHash = typeof ContentHash.Type;
59
+ /**
60
+ * The SHA-256 content hash of `bytes`, through the platform's Web Crypto
61
+ * (`globalThis.crypto.subtle`, in browsers, Node 22+ and Workers alike).
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { strictEqual } from "node:assert";
66
+ * import * as Effect from "effect/Effect";
67
+ * import { sha256Hex } from "@thomasfosterau/pulpit/blobs";
68
+ *
69
+ * strictEqual(
70
+ * await Effect.runPromise(sha256Hex(new TextEncoder().encode("abc"))),
71
+ * "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
72
+ * );
73
+ * ```
74
+ *
75
+ * @since 0.1.0
76
+ */
77
+ export declare const sha256Hex: (bytes: Uint8Array) => Effect.Effect<ContentHash>;
78
+ /**
79
+ * The blob-store key for a content hash: `sha256/<hash>`. The key is
80
+ * derived from the bytes alone, never from a node id, so a node's history
81
+ * can point at every set of bytes it has ever had.
82
+ *
83
+ * @since 0.1.0
84
+ */
85
+ export declare const blobKey: (hash: ContentHash) => string;
86
+ /**
87
+ * What the blob store knows of one stored object.
88
+ *
89
+ * @since 0.1.0
90
+ */
91
+ export interface BlobHead {
92
+ readonly byteSize: number;
93
+ readonly mediaType: string;
94
+ }
95
+ /**
96
+ * The byte store an application supplies (R2, S3, a local directory). It
97
+ * applies no policy and fails only with its `E`. Keys come from
98
+ * {@link blobKey}; see the package README → Seams for the contract.
99
+ *
100
+ * @since 0.1.0
101
+ */
102
+ export interface BlobStore<E, R> {
103
+ /** Store `bytes` at `key`. Idempotent: the same key always holds the same bytes. */
104
+ readonly put: (key: string, bytes: Uint8Array, head: BlobHead) => Effect.Effect<void, E, R>;
105
+ /** The bytes at `key`, or none. */
106
+ readonly get: (key: string) => Effect.Effect<Option.Option<Uint8Array>, E, R>;
107
+ /** The object's size and media type, or none when nothing is stored at `key`. */
108
+ readonly head: (key: string) => Effect.Effect<Option.Option<BlobHead>, E, R>;
109
+ /** Remove the object at `key`. Purge's alone (pulpit#58): nothing else deletes bytes. */
110
+ readonly delete: (key: string) => Effect.Effect<void, E, R>;
111
+ /** The URL a reader fetches `key` from, when the store serves one. */
112
+ readonly publicUrl: (key: string) => Option.Option<string>;
113
+ /** A signed URL a client may upload `key`'s bytes to directly, when the store offers one. */
114
+ readonly signedUploadUrl: Option.Option<(key: string, head: BlobHead) => Effect.Effect<string, E, R>>;
115
+ }
116
+ /**
117
+ * The attributes every file version carries, all server-computed from the
118
+ * upload and present on the resource only (`readOnlyAttribute`): the
119
+ * `contentHash` its bytes are stored under, their `byteSize` and
120
+ * `mediaType`, the original `filename`, and `bytesPending`, `true` while the
121
+ * bytes are not yet confirmed in the blob store. Spread into a file leaf's
122
+ * `Resource.make({ attributes })`.
123
+ *
124
+ * @since 0.1.0
125
+ */
126
+ export declare const fileResourceFields: {
127
+ readonly contentHash: Resource.ReadOnlyAttribute<Schema.String>;
128
+ readonly byteSize: Resource.ReadOnlyAttribute<Schema.Finite>;
129
+ readonly mediaType: Resource.ReadOnlyAttribute<Schema.String>;
130
+ readonly filename: Resource.ReadOnlyAttribute<Schema.String>;
131
+ readonly bytesPending: Resource.ReadOnlyAttribute<Schema.Boolean>;
132
+ };
133
+ /**
134
+ * A file version's columns, as {@link defineFileWrites} writes them.
135
+ *
136
+ * @since 0.1.0
137
+ */
138
+ export interface FileColumns {
139
+ readonly contentHash: ContentHash;
140
+ readonly byteSize: number;
141
+ readonly mediaType: string;
142
+ readonly filename: string;
143
+ readonly bytesPending: boolean;
144
+ }
145
+ /**
146
+ * The URL a file version's bytes are read from: none while `bytesPending`
147
+ * (anything but an explicit `false`), and none when the store serves no
148
+ * URL, so no reader is pointed at bytes that may not be there.
149
+ *
150
+ * @since 0.1.0
151
+ */
152
+ export declare const fileUrl: <E, R>(file: {
153
+ readonly contentHash: ContentHash;
154
+ readonly bytesPending: boolean;
155
+ }, blobs: Pick<BlobStore<E, R>, "publicUrl">) => Option.Option<string>;
156
+ /**
157
+ * An upload: the bytes and what the client said they are.
158
+ *
159
+ * @since 0.1.0
160
+ */
161
+ export interface Upload {
162
+ readonly bytes: Uint8Array;
163
+ readonly mediaType: string;
164
+ readonly filename: string;
165
+ }
166
+ /**
167
+ * What a file write reports: the version it landed at, its content hash,
168
+ * and whether its bytes are still pending (the post-commit byte write
169
+ * failed, or its confirmation did).
170
+ *
171
+ * @since 0.1.0
172
+ */
173
+ export interface FileWrite extends ChainWrite {
174
+ readonly contentHash: ContentHash;
175
+ readonly bytesPending: boolean;
176
+ }
177
+ type WriteFailure = Unauthorized | Forbidden | WriteRejected | VersionConflict;
178
+ /**
179
+ * The operations {@link defineFileWrites} produces.
180
+ *
181
+ * @since 0.1.0
182
+ */
183
+ export interface FileWriteOps<E, R> {
184
+ /**
185
+ * Create a file node from an upload, with the leaf's other columns in
186
+ * `state`. Fails as the leaf's `create` does; a failure to store the
187
+ * bytes after the commit does not fail it (the node is left
188
+ * `bytesPending`).
189
+ */
190
+ readonly create: (input: {
191
+ readonly id: Option.Option<string>;
192
+ readonly state: ChainState;
193
+ readonly upload: Upload;
194
+ }) => Effect.Effect<FileWrite, WriteFailure | NodeAlreadyExists | E, R>;
195
+ /**
196
+ * Replace a file node's bytes with a new version. Fails as the leaf's
197
+ * `update` does (`StaleVersion` against `expectedVersion`, `NodeNotFound`
198
+ * for a missing or tombstoned node).
199
+ */
200
+ readonly replace: (input: {
201
+ readonly id: string;
202
+ readonly upload: Upload;
203
+ } & Precondition) => Effect.Effect<FileWrite, WriteFailure | NodeNotFound | StaleVersion | E, R>;
204
+ /**
205
+ * Finish a pending version (`version`, whose bytes `contentHash` names,
206
+ * as a {@link FileWrite} reports them): when the store now holds those
207
+ * bytes, append `bytesPending: false` over exactly that version and
208
+ * report `false`; otherwise write nothing and report `true`. Gated as the
209
+ * leaf's `update` is; `StaleVersion` when the node has moved past
210
+ * `version` (its head names other bytes).
211
+ */
212
+ readonly confirmBytes: (input: {
213
+ readonly id: string;
214
+ readonly version: bigint;
215
+ readonly contentHash: ContentHash;
216
+ }) => Effect.Effect<boolean, WriteFailure | NodeNotFound | StaleVersion | E, R>;
217
+ }
218
+ /**
219
+ * Bind a file leaf's commit-ordered writes: the version is appended (in
220
+ * `transact`, the caller's write boundary) before any byte is written, and
221
+ * the bytes go to the blob store only once it has returned. Each version
222
+ * write goes through the leaf's own operations, so their gates,
223
+ * preconditions and before hooks apply; the cache write-through and after
224
+ * hooks are `subtypeHandlers`', which an application that caches the leaf
225
+ * runs itself.
226
+ *
227
+ * @since 0.1.0
228
+ */
229
+ export declare const defineFileWrites: <Resource, E, R>(definition: {
230
+ readonly leaf: SubtypeOps<Resource, E, R>;
231
+ readonly blobs: BlobStore<E, R>;
232
+ readonly transact: <A, E2>(effect: Effect.Effect<A, E2, R>) => Effect.Effect<A, E2, R>;
233
+ }) => FileWriteOps<E, R>;
234
+ /**
235
+ * The ends of a file-usage edge family: `user` (the content node that uses
236
+ * the file) and `file` (the file node), each declared with its own leaf's
237
+ * access and view, for `./edges`' `defineEdgeOps`. The family's edge table
238
+ * keys `(user_id, file_id)` uniquely, so a re-asserted use is a no-op under
239
+ * `onConflict: "ignore"`. A "used by" list is then an edge list at the file
240
+ * end, and `EDGE-001` hides every use whose content the viewer can't read.
241
+ *
242
+ * @since 0.1.0
243
+ */
244
+ export declare const fileUsageEnds: <E, R>(ends: {
245
+ readonly user: Omit<EdgeEnd<E, R>, "column">;
246
+ readonly file: Omit<EdgeEnd<E, R>, "column">;
247
+ }) => EdgeEnds<E, R>;
248
+ export {};
249
+ //# sourceMappingURL=blobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAE1D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACvF,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAI5B;;;;;GAKG;AACH,eAAO,MAAM,WAAW,eAIvB,CAAC;AACF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAC;AAElD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,SAAS,UAAW,UAAU,KAAG,MAAM,CAAC,MAAM,CAAC,WAAW,CASpE,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,SAAU,WAAW,KAAG,MAA0B,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAID;;;;;;GAMG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5F,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3D,6FAA6F;IAC7F,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CACrC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAC7D,CAAC;CACH;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;aAC7B,WAAW;aACX,QAAQ;aACR,SAAS;aACT,QAAQ;aACR,YAAY;CACJ,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,QACpB;IAAE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAAE,SACpE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,KACxC,MAAM,CAAC,MAAM,CAAC,MAAM,CAC0D,CAAC;AAIlF;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,KAAK,YAAY,GAAG,YAAY,GAAG,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC;AAE/E;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,EAAE,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACzB,KAAK,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,iBAAiB,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACxE;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAChB,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,YAAY,KACnE,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACjF;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE;QAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;KACnC,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;CACjF;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,EAAE,CAAC,EAAE,CAAC,cAAc;IAC3D,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;CACxF,KAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAoFpB,CAAC;AAIF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,QAAQ;IACxC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC7C,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;CAC9C,KAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAGf,CAAC"}
package/dist/blobs.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * **File and media leaves** — the blob-store seam a file leaf's bytes live
3
+ * behind, the field spread a file version carries, the commit-ordered byte
4
+ * write, and the usage index as an edge family.
5
+ *
6
+ * The behavioural spec is gardeneria's `apps/api/src/operations/media.ts`
7
+ * (bytes written only after the transaction commits) and `apps/api/src/files.ts`
8
+ * (the content hash), and website's `apps/api/src/operations/file-chain.ts`.
9
+ *
10
+ * ## The model (owner decisions on pulpit#55)
11
+ *
12
+ * - **Bytes are keyed by their content hash** ({@link blobKey}), so a
13
+ * version's bytes are immutable: new bytes are a new hash and a new
14
+ * version, and `restoreVersion` restores the reference to the old bytes,
15
+ * which are still there. Equal bytes are stored once.
16
+ * - **Bytes are written after the chain commits.** {@link defineFileWrites}
17
+ * appends the version first, marked `bytesPending: true` unless the store
18
+ * already holds the bytes, puts the bytes once the transaction has
19
+ * returned, and then appends `bytesPending: false`. A byte write that
20
+ * fails leaves the node visibly incomplete: `bytesPending` stays `true`
21
+ * and {@link fileUrl} gives no URL, so no reader is ever pointed at
22
+ * missing bytes. `confirmBytes` finishes it later, once the bytes are
23
+ * there.
24
+ * - **Nothing garbage-collects bytes.** They stay for as long as any
25
+ * version references them; purge (pulpit#58) is the only removal path,
26
+ * which is what {@link BlobStore.delete} is for.
27
+ * - **"Where is this file used" is an edge family** ({@link fileUsageEnds}),
28
+ * so the endpoint-visibility gate (`EDGE-001`) applies: a "used by" list
29
+ * never reveals content the viewer can't see. Maintaining those edges is
30
+ * the referencing leaf's job (or an after hook's).
31
+ *
32
+ * The adapters (R2, S3), image transforms, EXIF extraction and video
33
+ * streaming are the application's.
34
+ *
35
+ * @copyright Thomas Foster
36
+ * @module pulpit/blobs
37
+ * @since 0.1.0
38
+ */
39
+ import { Resource } from "@thomasfosterau/effect-jsonapi";
40
+ import * as Arr from "effect/Array";
41
+ import * as Effect from "effect/Effect";
42
+ import * as Exit from "effect/Exit";
43
+ import * as Option from "effect/Option";
44
+ import * as Schema from "effect/Schema";
45
+ // ── Vocabulary ────────────────────────────────────────────────────────
46
+ /**
47
+ * A file's content hash: the SHA-256 digest of its bytes as 64 lowercase
48
+ * hexadecimal characters ({@link sha256Hex}).
49
+ *
50
+ * @since 0.1.0
51
+ */
52
+ export const ContentHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/, {
53
+ description: "a SHA-256 content hash as 64 lowercase hexadecimal characters",
54
+ }));
55
+ /**
56
+ * The SHA-256 content hash of `bytes`, through the platform's Web Crypto
57
+ * (`globalThis.crypto.subtle`, in browsers, Node 22+ and Workers alike).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { strictEqual } from "node:assert";
62
+ * import * as Effect from "effect/Effect";
63
+ * import { sha256Hex } from "@thomasfosterau/pulpit/blobs";
64
+ *
65
+ * strictEqual(
66
+ * await Effect.runPromise(sha256Hex(new TextEncoder().encode("abc"))),
67
+ * "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
68
+ * );
69
+ * ```
70
+ *
71
+ * @since 0.1.0
72
+ */
73
+ export const sha256Hex = (bytes) => Effect.tryPromise(() => globalThis.crypto.subtle.digest("SHA-256", Uint8Array.from(bytes))).pipe(
74
+ // Web Crypto's SHA-256 over bytes has no failure a caller could act on.
75
+ Effect.orDie, Effect.map((digest) => Arr.map(Arr.fromIterable(new Uint8Array(digest)), (byte) => byte.toString(16).padStart(2, "0")).join("")));
76
+ /**
77
+ * The blob-store key for a content hash: `sha256/<hash>`. The key is
78
+ * derived from the bytes alone, never from a node id, so a node's history
79
+ * can point at every set of bytes it has ever had.
80
+ *
81
+ * @since 0.1.0
82
+ */
83
+ export const blobKey = (hash) => `sha256/${hash}`;
84
+ // ── The field spread ──────────────────────────────────────────────────
85
+ /**
86
+ * The attributes every file version carries, all server-computed from the
87
+ * upload and present on the resource only (`readOnlyAttribute`): the
88
+ * `contentHash` its bytes are stored under, their `byteSize` and
89
+ * `mediaType`, the original `filename`, and `bytesPending`, `true` while the
90
+ * bytes are not yet confirmed in the blob store. Spread into a file leaf's
91
+ * `Resource.make({ attributes })`.
92
+ *
93
+ * @since 0.1.0
94
+ */
95
+ export const fileResourceFields = {
96
+ contentHash: Resource.readOnlyAttribute(ContentHash),
97
+ byteSize: Resource.readOnlyAttribute(Schema.Finite),
98
+ mediaType: Resource.readOnlyAttribute(Schema.String),
99
+ filename: Resource.readOnlyAttribute(Schema.String),
100
+ bytesPending: Resource.readOnlyAttribute(Schema.Boolean),
101
+ };
102
+ /**
103
+ * The URL a file version's bytes are read from: none while `bytesPending`
104
+ * (anything but an explicit `false`), and none when the store serves no
105
+ * URL, so no reader is pointed at bytes that may not be there.
106
+ *
107
+ * @since 0.1.0
108
+ */
109
+ export const fileUrl = (file, blobs) => !file.bytesPending ? blobs.publicUrl(blobKey(file.contentHash)) : Option.none();
110
+ /**
111
+ * Bind a file leaf's commit-ordered writes: the version is appended (in
112
+ * `transact`, the caller's write boundary) before any byte is written, and
113
+ * the bytes go to the blob store only once it has returned. Each version
114
+ * write goes through the leaf's own operations, so their gates,
115
+ * preconditions and before hooks apply; the cache write-through and after
116
+ * hooks are `subtypeHandlers`', which an application that caches the leaf
117
+ * runs itself.
118
+ *
119
+ * @since 0.1.0
120
+ */
121
+ export const defineFileWrites = (definition) => {
122
+ const { leaf, blobs, transact } = definition;
123
+ /** The upload's columns, pending unless the store already holds its bytes. */
124
+ const columnsOf = (upload) => Effect.gen(function* () {
125
+ const contentHash = yield* sha256Hex(upload.bytes);
126
+ const stored = yield* blobs.head(blobKey(contentHash));
127
+ const columns = {
128
+ contentHash,
129
+ byteSize: upload.bytes.byteLength,
130
+ mediaType: upload.mediaType,
131
+ filename: upload.filename,
132
+ bytesPending: Option.isNone(stored),
133
+ };
134
+ return columns;
135
+ });
136
+ /**
137
+ * `bytesPending: false`, appended in its own transaction over exactly the
138
+ * pending version: if another write has landed since, it names other
139
+ * bytes, so the confirmation fails `StaleVersion` and confirms nothing.
140
+ */
141
+ const confirm = (id, pending) => transact(leaf.update({ id, patch: { bytesPending: false }, expectedVersion: pending }));
142
+ /**
143
+ * After the commit: put the bytes and confirm them. A failure of either
144
+ * leaves the node pending, never failing the write that committed.
145
+ */
146
+ const afterCommit = (written, columns, upload) => Effect.gen(function* () {
147
+ if (!columns.bytesPending) {
148
+ return { ...written, contentHash: columns.contentHash, bytesPending: false };
149
+ }
150
+ const confirmed = yield* Effect.exit(Effect.andThen(blobs.put(blobKey(columns.contentHash), upload.bytes, {
151
+ byteSize: columns.byteSize,
152
+ mediaType: columns.mediaType,
153
+ }), confirm(written.id, written.version)));
154
+ if (Exit.isFailure(confirmed)) {
155
+ yield* Effect.logWarning(`${leaf.type} ${written.id}: bytes left pending`, confirmed.cause);
156
+ return { ...written, contentHash: columns.contentHash, bytesPending: true };
157
+ }
158
+ return { ...confirmed.value, contentHash: columns.contentHash, bytesPending: false };
159
+ });
160
+ const create = Effect.fn(`${leaf.type}.files.create`)(function* (input) {
161
+ const columns = yield* columnsOf(input.upload);
162
+ const written = yield* transact(leaf.create({ id: input.id, state: { ...input.state, ...columns } }));
163
+ return yield* afterCommit(written, columns, input.upload);
164
+ });
165
+ const replace = Effect.fn(`${leaf.type}.files.replace`)(function* (input) {
166
+ const columns = yield* columnsOf(input.upload);
167
+ const written = yield* transact(leaf.update({ id: input.id, patch: { ...columns }, expectedVersion: input.expectedVersion }));
168
+ return yield* afterCommit(written, columns, input.upload);
169
+ });
170
+ const confirmBytes = Effect.fn(`${leaf.type}.files.confirmBytes`)(function* (input) {
171
+ if (Option.isNone(yield* blobs.head(blobKey(input.contentHash))))
172
+ return true;
173
+ yield* confirm(input.id, input.version);
174
+ return false;
175
+ });
176
+ return { create, replace, confirmBytes };
177
+ };
178
+ // ── The usage index ───────────────────────────────────────────────────
179
+ /**
180
+ * The ends of a file-usage edge family: `user` (the content node that uses
181
+ * the file) and `file` (the file node), each declared with its own leaf's
182
+ * access and view, for `./edges`' `defineEdgeOps`. The family's edge table
183
+ * keys `(user_id, file_id)` uniquely, so a re-asserted use is a no-op under
184
+ * `onConflict: "ignore"`. A "used by" list is then an edge list at the file
185
+ * end, and `EDGE-001` hides every use whose content the viewer can't read.
186
+ *
187
+ * @since 0.1.0
188
+ */
189
+ export const fileUsageEnds = (ends) => ({
190
+ user: { ...ends.user, column: "userId" },
191
+ file: { ...ends.file, column: "fileId" },
192
+ });
193
+ //# sourceMappingURL=blobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blobs.js","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"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"}