@q32/core 0.1.5 → 0.1.7

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/src/testing.ts ADDED
@@ -0,0 +1,119 @@
1
+ export type SentQueueMessage<T> = {
2
+ body: T;
3
+ options?: QueueSendOptions;
4
+ };
5
+
6
+ export type MemoryQueue<T = unknown> = Pick<Queue<T>, "send" | "sendBatch"> & {
7
+ sent: SentQueueMessage<T>[];
8
+ clear(): void;
9
+ };
10
+
11
+ export function createMemoryQueue<T = unknown>(): MemoryQueue<T> {
12
+ const sent: SentQueueMessage<T>[] = [];
13
+ return {
14
+ sent,
15
+ async send(body: T, options?: QueueSendOptions): Promise<QueueSendResponse> {
16
+ sent.push({ body, options });
17
+ return queueSendResponse();
18
+ },
19
+ async sendBatch(messages: Iterable<MessageSendRequest<T>>): Promise<QueueSendBatchResponse> {
20
+ for (const message of messages) sent.push({ body: message.body, options: message });
21
+ return queueSendBatchResponse();
22
+ },
23
+ clear(): void {
24
+ sent.length = 0;
25
+ },
26
+ };
27
+ }
28
+
29
+ export type MemoryR2Object = {
30
+ key: string;
31
+ body: Uint8Array;
32
+ httpMetadata?: R2HTTPMetadata;
33
+ customMetadata?: Record<string, string>;
34
+ text(): Promise<string>;
35
+ json<T = unknown>(): Promise<T>;
36
+ arrayBuffer(): Promise<ArrayBuffer>;
37
+ };
38
+
39
+ export type MemoryR2Bucket = Pick<R2Bucket, "put" | "get" | "delete"> & {
40
+ objects: Map<string, MemoryR2Object>;
41
+ clear(): void;
42
+ };
43
+
44
+ export function createMemoryR2Bucket(): MemoryR2Bucket {
45
+ const objects = new Map<string, MemoryR2Object>();
46
+ return {
47
+ objects,
48
+ async put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise<R2Object> {
49
+ const body = await toBytes(value);
50
+ const object = memoryR2Object(key, body, normalizeR2HttpMetadata(options?.httpMetadata), options?.customMetadata);
51
+ objects.set(key, object);
52
+ return object as unknown as R2Object;
53
+ },
54
+ async get(key: string): Promise<R2ObjectBody | null> {
55
+ return (objects.get(key) as unknown as R2ObjectBody | undefined) ?? null;
56
+ },
57
+ async delete(keys: string | string[]): Promise<void> {
58
+ for (const key of Array.isArray(keys) ? keys : [keys]) objects.delete(key);
59
+ },
60
+ clear(): void {
61
+ objects.clear();
62
+ },
63
+ };
64
+ }
65
+
66
+ export async function expectJsonResponse<T = unknown>(response: Response, status = 200): Promise<T> {
67
+ if (response.status !== status) throw new Error(`Expected response status ${status}, got ${response.status}.`);
68
+ const contentType = response.headers.get("content-type") ?? "";
69
+ if (!contentType.includes("application/json")) throw new Error(`Expected JSON response, got ${contentType || "no content-type"}.`);
70
+ return (await response.json()) as T;
71
+ }
72
+
73
+ async function toBytes(value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob): Promise<Uint8Array> {
74
+ if (value === null) return new Uint8Array();
75
+ if (typeof value === "string") return new TextEncoder().encode(value);
76
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
77
+ if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
78
+ if (value instanceof Blob) return new Uint8Array(await value.arrayBuffer());
79
+ return new Uint8Array(await new Response(value).arrayBuffer());
80
+ }
81
+
82
+ function queueSendResponse(): QueueSendResponse {
83
+ return { metadata: { metrics: { backlogCount: 0, backlogBytes: 0 } } };
84
+ }
85
+
86
+ function queueSendBatchResponse(): QueueSendBatchResponse {
87
+ return { metadata: { metrics: { backlogCount: 0, backlogBytes: 0 } } };
88
+ }
89
+
90
+ function normalizeR2HttpMetadata(value: R2HTTPMetadata | Headers | undefined): R2HTTPMetadata | undefined {
91
+ if (!value) return undefined;
92
+ if (!(value instanceof Headers)) return value;
93
+ const contentType = value.get("content-type") ?? undefined;
94
+ const cacheControl = value.get("cache-control") ?? undefined;
95
+ return { contentType, cacheControl };
96
+ }
97
+
98
+ function memoryR2Object(
99
+ key: string,
100
+ body: Uint8Array,
101
+ httpMetadata?: R2HTTPMetadata,
102
+ customMetadata?: Record<string, string>,
103
+ ): MemoryR2Object {
104
+ return {
105
+ key,
106
+ body,
107
+ httpMetadata,
108
+ customMetadata,
109
+ async text(): Promise<string> {
110
+ return new TextDecoder().decode(body);
111
+ },
112
+ async json<T = unknown>(): Promise<T> {
113
+ return JSON.parse(await this.text()) as T;
114
+ },
115
+ async arrayBuffer(): Promise<ArrayBuffer> {
116
+ return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer;
117
+ },
118
+ };
119
+ }
package/src/time.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function nowIso(date = new Date()): string {
2
+ return date.toISOString();
3
+ }
4
+
5
+ export function epochSeconds(date = new Date()): number {
6
+ return Math.floor(date.getTime() / 1000);
7
+ }
8
+
9
+ export function addSeconds(date: Date | string, seconds: number): string {
10
+ const base = typeof date === "string" ? new Date(date) : date;
11
+ return new Date(base.getTime() + seconds * 1000).toISOString();
12
+ }
13
+
14
+ export function isFutureIso(value: string | null | undefined, now = new Date()): boolean {
15
+ if (!value) return false;
16
+ const timestamp = Date.parse(value);
17
+ return Number.isFinite(timestamp) && timestamp > now.getTime();
18
+ }