Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/bundler/src/helpers/azion-local-polyfills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default {
['Azion.env', `${externalPolyfillsPath}/env-vars/env-vars.polyfills.js`],
['Azion.networkList', `${externalPolyfillsPath}/network-list/network-list.polyfills.js`],
['Azion.Storage', `${externalPolyfillsPath}/storage/storage.polyfills.js`],
['Azion.KV', `${externalPolyfillsPath}/kv/kv.polyfills.js`],
]),
};
3 changes: 3 additions & 0 deletions packages/bundler/src/polyfills/azion/kv/context/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import KVContext from './kv.context.js';

export default { KVContext };
149 changes: 149 additions & 0 deletions packages/bundler/src/polyfills/azion/kv/context/kv.context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import fs from 'fs';
import path from 'path';
import { Readable } from 'stream';

// KVContext Simulate KV Storage API with file system

class KVContext {
#pathKV;
#metadataPrefix;

constructor(kvName, pathKV) {
this.kvName = kvName;
this.#pathKV = pathKV || `.edge/kv/${kvName}`;
this.#metadataPrefix = `.metadata-${kvName}.json`;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async get(key, returnType = 'text', options = {}) {
try {
const item = await fs.promises.readFile(`${this.#pathKV}/${key}`);
const metadata = await KVContext.getMetadata(this.#pathKV, key, this.#metadataPrefix);

if (metadata?.expiration) {
const now = Math.floor(Date.now() / 1000);
if (now >= metadata.expiration) {
await this.delete(key);
return { value: null, metadata: null };
}
}

return {
value: await this.#convertValue(item, returnType),
metadata: metadata?.metadata || null,
};
} catch {
return { value: null, metadata: null };
}
}

async getMultiple(keys, returnType = 'text', options = {}) {
const results = new Map();

for (const key of keys) {
const result = await this.get(key, returnType, options);
results.set(key, result);
}

return results;
}

async put(key, value, options = {}) {
const prefix = path.dirname(key);
await fs.promises.mkdir(`${this.#pathKV}/${prefix}`, {
recursive: true,
});

let fileContent;
if (value instanceof ReadableStream) {
const reader = value.getReader();
const chunks = [];
while (true) {
const { done, value: chunk } = await reader.read();
if (done) break;
chunks.push(chunk);
}
fileContent = Buffer.concat(chunks);
} else if (value instanceof ArrayBuffer) {
fileContent = Buffer.from(value);
} else if (typeof value === 'object') {
fileContent = JSON.stringify(value);
} else {
fileContent = String(value);
}

await fs.promises.writeFile(`${this.#pathKV}/${key}`, fileContent);
await KVContext.putMetadata(this.#pathKV, key, options, this.#metadataPrefix);

if (options.expirationTtl) {
setTimeout(async () => {
try {
await this.delete(key);
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty
} catch (error) {}
}, options.expirationTtl * 1000);
} else if (options.expiration) {
const now = Math.floor(Date.now() / 1000);
const ttl = options.expiration - now;
if (ttl > 0) {
setTimeout(async () => {
try {
await this.delete(key);
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty
} catch (error) {}
}, ttl * 1000);
}
}
}

async delete(key) {
try {
await fs.promises.rm(`${this.#pathKV}/${key}`);
try {
await fs.promises.rm(`${this.#pathKV}/${key}${this.#metadataPrefix}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty
} catch (error) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty
} catch (error) {}
}

async #convertValue(buffer, returnType) {
switch (returnType) {
case 'json':
return JSON.parse(buffer.toString());
case 'arrayBuffer':
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
case 'stream':
return Readable.from(buffer);
case 'text':
default:
return buffer.toString();
}
}

static async putMetadata(pathKV, key, options, metadataPrefix) {
const bodyMetadata = {
metadata: options?.metadata || null,
expiration: options?.expiration || null,
expirationTtl: options?.expirationTtl || null,
};
await fs.promises.writeFile(`${pathKV}/${key}${metadataPrefix}`, JSON.stringify(bodyMetadata, undefined, 2));
return bodyMetadata;
}

static async getMetadata(pathKV, key, metadataPrefix) {
try {
let data = await fs.promises.readFile(`${pathKV}/${key}${metadataPrefix}`);
data = JSON.parse(data.toString());
return {
metadata: data?.metadata || null,
expiration: data?.expiration || null,
expirationTtl: data?.expirationTtl || null,
};
} catch {
return { metadata: null };
}
}
}

export default KVContext;
4 changes: 4 additions & 0 deletions packages/bundler/src/polyfills/azion/kv/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import KVContext from './context/kv.context.js';
import KV from './kv.polyfills.js';

export { KV, KVContext };
83 changes: 83 additions & 0 deletions packages/bundler/src/polyfills/azion/kv/kv.polyfills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable */

/**
* KV Storage API Polyfill
* This polyfill is referenced in #build/bundlers/polyfills/polyfills-manager.js
*
* @example
*
* const kv = new KV("mystore");
* const value = await kv.get("mykey");
*/

class KV {
#kvName;

constructor(kvName) {
if (typeof kvName !== 'string') {
throw new Error('kvName must be a string');
}
this.#kvName = kvName;
}

static async open(name) {
if (typeof name !== 'string') {
throw new Error('name must be a string');
}
return new KV(name);
}

async get(key, returnType, options) {
if (Array.isArray(key)) {
return this.#getMultiple(key, returnType, options);
}

const actualReturnType = options?.type || returnType || 'text';
const result = await new KV_CONTEXT(this.#kvName).get(key, actualReturnType, options);
return result.value;
}

async getWithMetadata(key, returnType, options) {
if (Array.isArray(key)) {
return this.#getMultipleWithMetadata(key, returnType, options);
}

const actualReturnType = options?.type || returnType || 'text';
return await new KV_CONTEXT(this.#kvName).get(key, actualReturnType, options);
}

async #getMultiple(keys, returnType, options) {
const actualReturnType = options?.type || returnType || 'text';
const results = await new KV_CONTEXT(this.#kvName).getMultiple(keys, actualReturnType, options);

const valueMap = new Map();
for (const [key, result] of results.entries()) {
valueMap.set(key, result.value);
}
return valueMap;
}

async #getMultipleWithMetadata(keys, returnType, options) {
const actualReturnType = options?.type || returnType || 'text';
return await new KV_CONTEXT(this.#kvName).getMultiple(keys, actualReturnType, options);
}

async put(key, value, options) {
if (typeof key !== 'string') {
throw new Error('key must be a string');
}
return await new KV_CONTEXT(this.#kvName).put(key, value, options || {});
}

async delete(key) {
if (typeof key !== 'string') {
throw new Error('key must be a string');
}
return await new KV_CONTEXT(this.#kvName).delete(key);
}
}

export default KV;

globalThis.Azion = globalThis.Azion || {};
globalThis.Azion.KV = KV;
1 change: 1 addition & 0 deletions packages/bundler/src/polyfills/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ declare module 'azion/bundler/polyfills' {
export const streamContext: any;
export const cryptoContext: any;
export const promisesContext: any;
export const KVContext: any;
}
2 changes: 2 additions & 0 deletions packages/bundler/src/polyfills/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import EnvVarsContext from './azion/env-vars/index.js';
import FetchEventContext from './azion/fetch-event/index.js';
import fetchContext from './azion/fetch/index.js';
import FirewallEventContext from './azion/firewall-event/index.js';
import { KVContext } from './azion/kv/index.js';
import NetworkListContext from './azion/network-list/index.js';
import { StorageContext } from './azion/storage/index.js';
import cryptoContext from './crypto/index.js';
Expand All @@ -20,6 +21,7 @@ export {
FetchEventContext,
FirewallEventContext,
fsContext,
KVContext,
NetworkListContext,
promisesContext,
StorageContext,
Expand Down