@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.
- package/LICENSE +21 -0
- package/README.md +1272 -2
- 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 +145 -3
- 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
package/dist/blobs.d.ts
ADDED
|
@@ -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":"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"}
|
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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"}
|