@fabrika/platform 0.3.3 → 0.4.1
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/package.json +1 -1
- package/src/index.ts +13 -1
- package/src/ports.ts +59 -0
- package/src/testing.ts +69 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
// Runtime-specific implementations live with the workers that use them, or in @fabrika/platform-node.
|
|
10
10
|
|
|
11
11
|
export { SqlDeployLocks, type SqlDeployLocksOptions } from './deploy-locks-sql'
|
|
12
|
-
export
|
|
12
|
+
export { ObjectExistsError, ObjectTooLargeError } from './ports'
|
|
13
|
+
export type {
|
|
14
|
+
AssetServer,
|
|
15
|
+
BlobStore,
|
|
16
|
+
DeployLocks,
|
|
17
|
+
FileObjectStore,
|
|
18
|
+
HttpService,
|
|
19
|
+
JobQueue,
|
|
20
|
+
ObjectMetadata,
|
|
21
|
+
ObjectWriteOptions,
|
|
22
|
+
StoredObject,
|
|
23
|
+
WaitUntil,
|
|
24
|
+
} from './ports'
|
|
13
25
|
export type { SqlDatabase, SqlQueryResult, SqlRunResult, SqlStatement } from './sql'
|
|
14
26
|
export { uuidv7 } from './uuid'
|
package/src/ports.ts
CHANGED
|
@@ -16,6 +16,65 @@ export interface BlobStore {
|
|
|
16
16
|
delete(key: string): Promise<void>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** What the store recorded for an object. `contentType` is what the writer declared, or null. */
|
|
20
|
+
export interface ObjectMetadata {
|
|
21
|
+
readonly size: number
|
|
22
|
+
readonly contentType: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StoredObject extends ObjectMetadata {
|
|
26
|
+
/** One stream, consumable once. */
|
|
27
|
+
readonly body: ReadableStream<Uint8Array>
|
|
28
|
+
/** These exact bytes' version, from the same answer as the body: an S3 ETag. It changes when the object is replaced. */
|
|
29
|
+
readonly version: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ObjectWriteOptions {
|
|
33
|
+
readonly contentType: string
|
|
34
|
+
/** The most bytes the object may have. The write stops reading `body` once more arrive. */
|
|
35
|
+
readonly maxBytes: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A write named a key that already holds an object: a file's object is written once. Read by `name`. */
|
|
39
|
+
export class ObjectExistsError extends Error {
|
|
40
|
+
override readonly name = 'ObjectExistsError'
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super('an object is already stored under this key')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The body of a write was longer than its `maxBytes`. Read by `name`, never by a cross-package `instanceof`. */
|
|
48
|
+
export class ObjectTooLargeError extends Error {
|
|
49
|
+
override readonly name = 'ObjectTooLargeError'
|
|
50
|
+
readonly maxBytes: number
|
|
51
|
+
|
|
52
|
+
constructor(maxBytes: number) {
|
|
53
|
+
super(`the object is larger than ${maxBytes} bytes`)
|
|
54
|
+
this.maxBytes = maxBytes
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The objects of an application's file collections (S3/MinIO). Separate from `BlobStore` because its callers
|
|
60
|
+
* need a size bound enforced while the bytes stream and the metadata read back; an R2 adapter that ignored
|
|
61
|
+
* either would silently drop a limit.
|
|
62
|
+
*/
|
|
63
|
+
export interface FileObjectStore {
|
|
64
|
+
/**
|
|
65
|
+
* Streams `body` to a new `key`. A key that already holds an object is refused with `ObjectExistsError`, and nothing
|
|
66
|
+
* is written. Past `options.maxBytes` it stops reading, keeps nothing under `key` and rejects with
|
|
67
|
+
* `ObjectTooLargeError`. Any other failure also leaves nothing stored.
|
|
68
|
+
*/
|
|
69
|
+
write(key: string, body: ReadableStream<Uint8Array>, options: ObjectWriteOptions): Promise<ObjectMetadata>
|
|
70
|
+
/** The object with its metadata and version, in one answer, or `null` for a missing key. `signal` stops it. */
|
|
71
|
+
read(key: string, signal?: AbortSignal): Promise<StoredObject | null>
|
|
72
|
+
/** The object only while it is still `version`, in one answer; `null` when it is missing or was replaced. */
|
|
73
|
+
readVersion(key: string, version: string, signal?: AbortSignal): Promise<StoredObject | null>
|
|
74
|
+
/** Idempotent: a missing key is not an error. */
|
|
75
|
+
delete(key: string): Promise<void>
|
|
76
|
+
}
|
|
77
|
+
|
|
19
78
|
/** Enqueue a deploy job. The CONSUMER side is platform-specific and deliberately not modelled here. */
|
|
20
79
|
export interface JobQueue<T> {
|
|
21
80
|
send(message: T, options?: { delaySeconds?: number }): Promise<void>
|
package/src/testing.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type BlobStore,
|
|
3
|
+
type FileObjectStore,
|
|
4
|
+
type JobQueue,
|
|
5
|
+
ObjectExistsError,
|
|
6
|
+
type ObjectMetadata,
|
|
7
|
+
ObjectTooLargeError,
|
|
8
|
+
type ObjectWriteOptions,
|
|
9
|
+
type StoredObject,
|
|
10
|
+
} from './ports'
|
|
2
11
|
import type { SqlDatabase } from './sql'
|
|
3
12
|
|
|
4
13
|
/** A blob store that keeps string values in memory. */
|
|
@@ -23,6 +32,65 @@ export class MemoryBlobs implements BlobStore {
|
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
/** A file object store in memory, with the port's size bound enforced while the body streams. */
|
|
36
|
+
export class MemoryFileObjects implements FileObjectStore {
|
|
37
|
+
readonly objects = new Map<string, { readonly bytes: Uint8Array<ArrayBuffer>; readonly contentType: string; readonly version: string }>()
|
|
38
|
+
#writes = 0
|
|
39
|
+
|
|
40
|
+
async write(key: string, body: ReadableStream<Uint8Array>, options: ObjectWriteOptions): Promise<ObjectMetadata> {
|
|
41
|
+
const reader = body.getReader()
|
|
42
|
+
if (this.objects.has(key)) {
|
|
43
|
+
await reader.cancel()
|
|
44
|
+
throw new ObjectExistsError()
|
|
45
|
+
}
|
|
46
|
+
const chunks: Uint8Array[] = []
|
|
47
|
+
let size = 0
|
|
48
|
+
for (let next = await reader.read(); !next.done; next = await reader.read()) {
|
|
49
|
+
size += next.value.byteLength
|
|
50
|
+
if (size > options.maxBytes) {
|
|
51
|
+
await reader.cancel()
|
|
52
|
+
throw new ObjectTooLargeError(options.maxBytes)
|
|
53
|
+
}
|
|
54
|
+
chunks.push(next.value)
|
|
55
|
+
}
|
|
56
|
+
const bytes = new Uint8Array(size)
|
|
57
|
+
let offset = 0
|
|
58
|
+
for (const chunk of chunks) {
|
|
59
|
+
bytes.set(chunk, offset)
|
|
60
|
+
offset += chunk.byteLength
|
|
61
|
+
}
|
|
62
|
+
this.replace(key, bytes, options.contentType)
|
|
63
|
+
return { size, contentType: options.contentType }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Puts `bytes` under `key` whatever it held, under a new version: what a write outside the upload path does. */
|
|
67
|
+
replace(key: string, bytes: Uint8Array<ArrayBuffer>, contentType = 'application/octet-stream'): void {
|
|
68
|
+
this.#writes++
|
|
69
|
+
this.objects.set(key, { bytes, contentType, version: `"memory-${this.#writes}"` })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
read(key: string): Promise<StoredObject | null> {
|
|
73
|
+
const object = this.objects.get(key)
|
|
74
|
+
if (object === undefined) return Promise.resolve(null)
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
size: object.bytes.byteLength,
|
|
77
|
+
contentType: object.contentType,
|
|
78
|
+
version: object.version,
|
|
79
|
+
body: new Blob([object.bytes]).stream(),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async readVersion(key: string, version: string): Promise<StoredObject | null> {
|
|
84
|
+
const object = await this.read(key)
|
|
85
|
+
return object?.version === version ? object : null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
delete(key: string): Promise<void> {
|
|
89
|
+
this.objects.delete(key)
|
|
90
|
+
return Promise.resolve()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
26
94
|
/** A queue that records every sent message. */
|
|
27
95
|
export class MemoryQueue<T> implements JobQueue<T> {
|
|
28
96
|
readonly messages: T[] = []
|