@noy-db/to-aws-s3 0.2.0-pre.12 → 0.2.0-pre.14
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/dist/index.cjs +92 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -9
- package/dist/index.d.ts +6 -9
- package/dist/index.js +93 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -111,8 +111,15 @@ function s3Bundle(options) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// src/index.ts
|
|
114
|
+
function isPreconditionFailed(err) {
|
|
115
|
+
if (!(err instanceof Error)) return false;
|
|
116
|
+
if (err.name === "PreconditionFailed") return true;
|
|
117
|
+
const meta = err.$metadata;
|
|
118
|
+
return meta?.httpStatusCode === 412;
|
|
119
|
+
}
|
|
114
120
|
function s3(options) {
|
|
115
121
|
const { bucket, prefix = "" } = options;
|
|
122
|
+
const clockUncertaintyMs = options.clockUncertaintyMs ?? 5e3;
|
|
116
123
|
const client = options.client ?? new import_client_s32.S3Client({
|
|
117
124
|
...options.region ? { region: options.region } : {}
|
|
118
125
|
});
|
|
@@ -127,8 +134,28 @@ function s3(options) {
|
|
|
127
134
|
function compPrefix(vault) {
|
|
128
135
|
return prefix ? `${prefix}/${vault}/` : `${vault}/`;
|
|
129
136
|
}
|
|
137
|
+
const clockKey = prefix ? `${prefix}/_noydb-clock` : "_noydb-clock";
|
|
130
138
|
return {
|
|
131
139
|
name: "s3",
|
|
140
|
+
capabilities: {
|
|
141
|
+
casAtomic: true,
|
|
142
|
+
serverWriteTime: true,
|
|
143
|
+
auth: { kind: "iam", required: true, flow: "static" }
|
|
144
|
+
},
|
|
145
|
+
async getStoreTime() {
|
|
146
|
+
await client.send(new import_client_s32.PutObjectCommand({
|
|
147
|
+
Bucket: bucket,
|
|
148
|
+
Key: clockKey,
|
|
149
|
+
Body: "",
|
|
150
|
+
ContentType: "text/plain"
|
|
151
|
+
}));
|
|
152
|
+
const head = await client.send(new import_client_s32.HeadObjectCommand({
|
|
153
|
+
Bucket: bucket,
|
|
154
|
+
Key: clockKey
|
|
155
|
+
}));
|
|
156
|
+
const serverMs = head.LastModified.getTime();
|
|
157
|
+
return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs };
|
|
158
|
+
},
|
|
132
159
|
async get(vault, collection, id) {
|
|
133
160
|
try {
|
|
134
161
|
const result = await client.send(new import_client_s32.GetObjectCommand({
|
|
@@ -146,15 +173,76 @@ function s3(options) {
|
|
|
146
173
|
}
|
|
147
174
|
},
|
|
148
175
|
async put(vault, collection, id, envelope, expectedVersion) {
|
|
176
|
+
const key = objectKey(vault, collection, id);
|
|
149
177
|
if (expectedVersion !== void 0) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
if (expectedVersion === 0) {
|
|
179
|
+
try {
|
|
180
|
+
await client.send(new import_client_s32.PutObjectCommand({
|
|
181
|
+
Bucket: bucket,
|
|
182
|
+
Key: key,
|
|
183
|
+
Body: JSON.stringify(envelope),
|
|
184
|
+
ContentType: "application/json",
|
|
185
|
+
IfNoneMatch: "*"
|
|
186
|
+
}));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (isPreconditionFailed(err)) {
|
|
189
|
+
try {
|
|
190
|
+
const r = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
191
|
+
const b = await r.Body.transformToString();
|
|
192
|
+
const cur = JSON.parse(b);
|
|
193
|
+
throw new import_hub2.ConflictError(cur._v, "Concurrent create: object already exists");
|
|
194
|
+
} catch (inner) {
|
|
195
|
+
if (inner instanceof import_hub2.ConflictError) throw inner;
|
|
196
|
+
}
|
|
197
|
+
throw new import_hub2.ConflictError(0, "Concurrent create: object already exists");
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let currentETag;
|
|
204
|
+
try {
|
|
205
|
+
const result = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
206
|
+
const body = await result.Body.transformToString();
|
|
207
|
+
const current = JSON.parse(body);
|
|
208
|
+
if (current._v !== expectedVersion) {
|
|
209
|
+
throw new import_hub2.ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
|
|
210
|
+
}
|
|
211
|
+
currentETag = result.ETag ?? "";
|
|
212
|
+
} catch (err) {
|
|
213
|
+
if (err instanceof import_hub2.ConflictError) throw err;
|
|
214
|
+
if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) {
|
|
215
|
+
throw new import_hub2.ConflictError(0, `Object not found, expected version ${expectedVersion}`);
|
|
216
|
+
}
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await client.send(new import_client_s32.PutObjectCommand({
|
|
221
|
+
Bucket: bucket,
|
|
222
|
+
Key: key,
|
|
223
|
+
Body: JSON.stringify(envelope),
|
|
224
|
+
ContentType: "application/json",
|
|
225
|
+
IfMatch: currentETag
|
|
226
|
+
}));
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (isPreconditionFailed(err)) {
|
|
229
|
+
try {
|
|
230
|
+
const r = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
231
|
+
const b = await r.Body.transformToString();
|
|
232
|
+
const latest = JSON.parse(b);
|
|
233
|
+
throw new import_hub2.ConflictError(latest._v, "Concurrent write detected");
|
|
234
|
+
} catch (inner) {
|
|
235
|
+
if (inner instanceof import_hub2.ConflictError) throw inner;
|
|
236
|
+
}
|
|
237
|
+
throw new import_hub2.ConflictError(0, "Concurrent write detected");
|
|
238
|
+
}
|
|
239
|
+
throw err;
|
|
153
240
|
}
|
|
241
|
+
return;
|
|
154
242
|
}
|
|
155
243
|
await client.send(new import_client_s32.PutObjectCommand({
|
|
156
244
|
Bucket: bucket,
|
|
157
|
-
Key:
|
|
245
|
+
Key: key,
|
|
158
246
|
Body: JSON.stringify(envelope),
|
|
159
247
|
ContentType: "application/json"
|
|
160
248
|
}));
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["import_hub","import_client_s3"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),\n * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel\n * object's `LastModified` timestamp — store-authoritative, not client wall clock.\n * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nfunction isPreconditionFailed(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n if (err.name === 'PreconditionFailed') return true\n const meta = (err as { $metadata?: { httpStatusCode?: number } }).$metadata\n return meta?.httpStatusCode === 412\n}\n\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n const clockUncertaintyMs = options.clockUncertaintyMs ?? 5_000\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n // Sentinel key used solely to sample the store's server clock.\n const clockKey = prefix ? `${prefix}/_noydb-clock` : '_noydb-clock'\n\n return {\n name: 's3',\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'iam', required: true, flow: 'static' },\n },\n\n async getStoreTime() {\n // Write a sentinel object so S3 assigns a server-authoritative LastModified.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n Body: '',\n ContentType: 'text/plain',\n }))\n const head = await client.send(new HeadObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n }))\n const serverMs = head.LastModified!.getTime()\n return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs }\n },\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const key = objectKey(vault, collection, id)\n\n if (expectedVersion !== undefined) {\n if (expectedVersion === 0) {\n // Create-only — IfNoneMatch: '*' is atomic at S3's write layer.\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfNoneMatch: '*',\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const cur = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(cur._v, 'Concurrent create: object already exists')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent create: object already exists')\n }\n throw err\n }\n return\n }\n\n // Update — GetObject captures ETag + verifies _v, then PutObject with\n // IfMatch: etag ensures no concurrent writer slipped in between.\n let currentETag: string\n try {\n const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const body = await result.Body!.transformToString()\n const current = JSON.parse(body) as EncryptedEnvelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n currentETag = result.ETag ?? ''\n } catch (err: unknown) {\n if (err instanceof ConflictError) throw err\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`)\n }\n throw err\n }\n\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfMatch: currentETag,\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const latest = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(latest._v, 'Concurrent write detected')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent write detected')\n }\n throw err\n }\n return\n }\n\n // Unconditional PUT.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["import_hub","import_client_s3"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -50,9 +50,6 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
|
|
|
50
50
|
*
|
|
51
51
|
* ## Limitations
|
|
52
52
|
*
|
|
53
|
-
* - **`casAtomic: false`** — S3 has no server-side conditional write on
|
|
54
|
-
* arbitrary metadata. Concurrent puts may result in last-write-wins.
|
|
55
|
-
* Use DynamoDB for records that need conflict-safe writes.
|
|
56
53
|
* - **`loadAll()` is O(N) requests** — listing + fetching every object in a
|
|
57
54
|
* vault. Suitable for vaults up to ~10K records; beyond that, prefer
|
|
58
55
|
* DynamoDB for indexed stores and S3 only for append-heavy blob storage.
|
|
@@ -76,8 +73,10 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
|
|
|
76
73
|
* vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only
|
|
77
74
|
* holds archived records.
|
|
78
75
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
76
|
+
* S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),
|
|
77
|
+
* enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel
|
|
78
|
+
* object's `LastModified` timestamp — store-authoritative, not client wall clock.
|
|
79
|
+
* ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).
|
|
81
80
|
*/
|
|
82
81
|
interface S3Options {
|
|
83
82
|
/** S3 bucket name. */
|
|
@@ -92,11 +91,9 @@ interface S3Options {
|
|
|
92
91
|
* to share a client across adapters or supply custom middleware.
|
|
93
92
|
*/
|
|
94
93
|
client?: S3Client;
|
|
94
|
+
/** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */
|
|
95
|
+
clockUncertaintyMs?: number;
|
|
95
96
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Create an S3 adapter.
|
|
98
|
-
* Key scheme: `{prefix}/{vault}/{collection}/{id}.json`
|
|
99
|
-
*/
|
|
100
97
|
declare function s3(options: S3Options): NoydbStore;
|
|
101
98
|
|
|
102
99
|
export { type S3BundleOptions, type S3Options, s3, s3Bundle };
|
package/dist/index.d.ts
CHANGED
|
@@ -50,9 +50,6 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
|
|
|
50
50
|
*
|
|
51
51
|
* ## Limitations
|
|
52
52
|
*
|
|
53
|
-
* - **`casAtomic: false`** — S3 has no server-side conditional write on
|
|
54
|
-
* arbitrary metadata. Concurrent puts may result in last-write-wins.
|
|
55
|
-
* Use DynamoDB for records that need conflict-safe writes.
|
|
56
53
|
* - **`loadAll()` is O(N) requests** — listing + fetching every object in a
|
|
57
54
|
* vault. Suitable for vaults up to ~10K records; beyond that, prefer
|
|
58
55
|
* DynamoDB for indexed stores and S3 only for append-heavy blob storage.
|
|
@@ -76,8 +73,10 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
|
|
|
76
73
|
* vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only
|
|
77
74
|
* holds archived records.
|
|
78
75
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
76
|
+
* S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),
|
|
77
|
+
* enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel
|
|
78
|
+
* object's `LastModified` timestamp — store-authoritative, not client wall clock.
|
|
79
|
+
* ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).
|
|
81
80
|
*/
|
|
82
81
|
interface S3Options {
|
|
83
82
|
/** S3 bucket name. */
|
|
@@ -92,11 +91,9 @@ interface S3Options {
|
|
|
92
91
|
* to share a client across adapters or supply custom middleware.
|
|
93
92
|
*/
|
|
94
93
|
client?: S3Client;
|
|
94
|
+
/** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */
|
|
95
|
+
clockUncertaintyMs?: number;
|
|
95
96
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Create an S3 adapter.
|
|
98
|
-
* Key scheme: `{prefix}/{vault}/{collection}/{id}.json`
|
|
99
|
-
*/
|
|
100
97
|
declare function s3(options: S3Options): NoydbStore;
|
|
101
98
|
|
|
102
99
|
export { type S3BundleOptions, type S3Options, s3, s3Bundle };
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
S3Client as S3Client2,
|
|
5
5
|
GetObjectCommand as GetObjectCommand2,
|
|
6
6
|
PutObjectCommand as PutObjectCommand2,
|
|
7
|
+
HeadObjectCommand as HeadObjectCommand2,
|
|
7
8
|
DeleteObjectCommand as DeleteObjectCommand2,
|
|
8
9
|
ListObjectsV2Command as ListObjectsV2Command2,
|
|
9
10
|
HeadBucketCommand
|
|
@@ -100,8 +101,15 @@ function s3Bundle(options) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// src/index.ts
|
|
104
|
+
function isPreconditionFailed(err) {
|
|
105
|
+
if (!(err instanceof Error)) return false;
|
|
106
|
+
if (err.name === "PreconditionFailed") return true;
|
|
107
|
+
const meta = err.$metadata;
|
|
108
|
+
return meta?.httpStatusCode === 412;
|
|
109
|
+
}
|
|
103
110
|
function s3(options) {
|
|
104
111
|
const { bucket, prefix = "" } = options;
|
|
112
|
+
const clockUncertaintyMs = options.clockUncertaintyMs ?? 5e3;
|
|
105
113
|
const client = options.client ?? new S3Client2({
|
|
106
114
|
...options.region ? { region: options.region } : {}
|
|
107
115
|
});
|
|
@@ -116,8 +124,28 @@ function s3(options) {
|
|
|
116
124
|
function compPrefix(vault) {
|
|
117
125
|
return prefix ? `${prefix}/${vault}/` : `${vault}/`;
|
|
118
126
|
}
|
|
127
|
+
const clockKey = prefix ? `${prefix}/_noydb-clock` : "_noydb-clock";
|
|
119
128
|
return {
|
|
120
129
|
name: "s3",
|
|
130
|
+
capabilities: {
|
|
131
|
+
casAtomic: true,
|
|
132
|
+
serverWriteTime: true,
|
|
133
|
+
auth: { kind: "iam", required: true, flow: "static" }
|
|
134
|
+
},
|
|
135
|
+
async getStoreTime() {
|
|
136
|
+
await client.send(new PutObjectCommand2({
|
|
137
|
+
Bucket: bucket,
|
|
138
|
+
Key: clockKey,
|
|
139
|
+
Body: "",
|
|
140
|
+
ContentType: "text/plain"
|
|
141
|
+
}));
|
|
142
|
+
const head = await client.send(new HeadObjectCommand2({
|
|
143
|
+
Bucket: bucket,
|
|
144
|
+
Key: clockKey
|
|
145
|
+
}));
|
|
146
|
+
const serverMs = head.LastModified.getTime();
|
|
147
|
+
return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs };
|
|
148
|
+
},
|
|
121
149
|
async get(vault, collection, id) {
|
|
122
150
|
try {
|
|
123
151
|
const result = await client.send(new GetObjectCommand2({
|
|
@@ -135,15 +163,76 @@ function s3(options) {
|
|
|
135
163
|
}
|
|
136
164
|
},
|
|
137
165
|
async put(vault, collection, id, envelope, expectedVersion) {
|
|
166
|
+
const key = objectKey(vault, collection, id);
|
|
138
167
|
if (expectedVersion !== void 0) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
if (expectedVersion === 0) {
|
|
169
|
+
try {
|
|
170
|
+
await client.send(new PutObjectCommand2({
|
|
171
|
+
Bucket: bucket,
|
|
172
|
+
Key: key,
|
|
173
|
+
Body: JSON.stringify(envelope),
|
|
174
|
+
ContentType: "application/json",
|
|
175
|
+
IfNoneMatch: "*"
|
|
176
|
+
}));
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (isPreconditionFailed(err)) {
|
|
179
|
+
try {
|
|
180
|
+
const r = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
|
|
181
|
+
const b = await r.Body.transformToString();
|
|
182
|
+
const cur = JSON.parse(b);
|
|
183
|
+
throw new ConflictError(cur._v, "Concurrent create: object already exists");
|
|
184
|
+
} catch (inner) {
|
|
185
|
+
if (inner instanceof ConflictError) throw inner;
|
|
186
|
+
}
|
|
187
|
+
throw new ConflictError(0, "Concurrent create: object already exists");
|
|
188
|
+
}
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let currentETag;
|
|
194
|
+
try {
|
|
195
|
+
const result = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
|
|
196
|
+
const body = await result.Body.transformToString();
|
|
197
|
+
const current = JSON.parse(body);
|
|
198
|
+
if (current._v !== expectedVersion) {
|
|
199
|
+
throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
|
|
200
|
+
}
|
|
201
|
+
currentETag = result.ETag ?? "";
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err instanceof ConflictError) throw err;
|
|
204
|
+
if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) {
|
|
205
|
+
throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await client.send(new PutObjectCommand2({
|
|
211
|
+
Bucket: bucket,
|
|
212
|
+
Key: key,
|
|
213
|
+
Body: JSON.stringify(envelope),
|
|
214
|
+
ContentType: "application/json",
|
|
215
|
+
IfMatch: currentETag
|
|
216
|
+
}));
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (isPreconditionFailed(err)) {
|
|
219
|
+
try {
|
|
220
|
+
const r = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
|
|
221
|
+
const b = await r.Body.transformToString();
|
|
222
|
+
const latest = JSON.parse(b);
|
|
223
|
+
throw new ConflictError(latest._v, "Concurrent write detected");
|
|
224
|
+
} catch (inner) {
|
|
225
|
+
if (inner instanceof ConflictError) throw inner;
|
|
226
|
+
}
|
|
227
|
+
throw new ConflictError(0, "Concurrent write detected");
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
142
230
|
}
|
|
231
|
+
return;
|
|
143
232
|
}
|
|
144
233
|
await client.send(new PutObjectCommand2({
|
|
145
234
|
Bucket: bucket,
|
|
146
|
-
Key:
|
|
235
|
+
Key: key,
|
|
147
236
|
Body: JSON.stringify(envelope),
|
|
148
237
|
ContentType: "application/json"
|
|
149
238
|
}));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command","S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),\n * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel\n * object's `LastModified` timestamp — store-authoritative, not client wall clock.\n * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nfunction isPreconditionFailed(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n if (err.name === 'PreconditionFailed') return true\n const meta = (err as { $metadata?: { httpStatusCode?: number } }).$metadata\n return meta?.httpStatusCode === 412\n}\n\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n const clockUncertaintyMs = options.clockUncertaintyMs ?? 5_000\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n // Sentinel key used solely to sample the store's server clock.\n const clockKey = prefix ? `${prefix}/_noydb-clock` : '_noydb-clock'\n\n return {\n name: 's3',\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'iam', required: true, flow: 'static' },\n },\n\n async getStoreTime() {\n // Write a sentinel object so S3 assigns a server-authoritative LastModified.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n Body: '',\n ContentType: 'text/plain',\n }))\n const head = await client.send(new HeadObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n }))\n const serverMs = head.LastModified!.getTime()\n return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs }\n },\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const key = objectKey(vault, collection, id)\n\n if (expectedVersion !== undefined) {\n if (expectedVersion === 0) {\n // Create-only — IfNoneMatch: '*' is atomic at S3's write layer.\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfNoneMatch: '*',\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const cur = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(cur._v, 'Concurrent create: object already exists')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent create: object already exists')\n }\n throw err\n }\n return\n }\n\n // Update — GetObject captures ETag + verifies _v, then PutObject with\n // IfMatch: etag ensures no concurrent writer slipped in between.\n let currentETag: string\n try {\n const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const body = await result.Body!.transformToString()\n const current = JSON.parse(body) as EncryptedEnvelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n currentETag = result.ETag ?? ''\n } catch (err: unknown) {\n if (err instanceof ConflictError) throw err\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`)\n }\n throw err\n }\n\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfMatch: currentETag,\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const latest = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(latest._v, 'Concurrent write detected')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent write detected')\n }\n throw err\n }\n return\n }\n\n // Unconditional PUT.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["S3Client","GetObjectCommand","PutObjectCommand","HeadObjectCommand","DeleteObjectCommand","ListObjectsV2Command","S3Client","PutObjectCommand","HeadObjectCommand","GetObjectCommand","DeleteObjectCommand","ListObjectsV2Command"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/to-aws-s3",
|
|
3
|
-
"version": "0.2.0-pre.
|
|
3
|
+
"version": "0.2.0-pre.14",
|
|
4
4
|
"description": "AWS S3 adapter for noy-db — encrypted object storage with zero-knowledge cloud sync",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
43
|
-
"@noy-db/hub": "0.2.0-pre.
|
|
43
|
+
"@noy-db/hub": "0.2.0-pre.14"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
47
|
-
"@noy-db/hub": "0.2.0-pre.
|
|
47
|
+
"@noy-db/hub": "0.2.0-pre.14"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"noy-db",
|