Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cache and renderComponentShadow #69

Merged
merged 1 commit into from
Apr 11, 2024
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v1.0.4] - 2024-04-11

### Added
- Add basic key/value cache
- Add `renderComponentShadow` function to `custom-elements.js` module

## [v1.0.3] - 2024-04-05

### Added
Expand Down
79 changes: 79 additions & 0 deletions Cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const keyMap = {};

let supportsSymbolKeys = true;

try {
new WeakMap([[Symbol('test'), null]]);
} catch {
supportsSymbolKeys = false;
}

const convertKey = supportsSymbolKeys ? key => key : (key) => {
if (supportsSymbolKeys || typeof key === 'object') {
return key;
} else if (typeof key !== 'symbol' || typeof Symbol.keyFor(key) !== 'undefined') {
throw new TypeError(`Cache key "${key.toString()}" must be an object or an unregistered symbol.`);
} else if (keyMap.hasOwnProperty(key)) {
return keyMap[key];
} else {
const mapped = Object.freeze({ key });
keyMap[key] = mapped;
return mapped;
}
};

export class Cache {
#cache;

constructor() {
this.#cache = new WeakMap();
}

has(key, requireFresh = false) {
if (requireFresh) {
const result = this.#cache.get(convertKey(key));
return typeof result === 'object' && (
typeof result.expires !== 'number'
|| Date.now() < result.expires
);
} else {
return this.#cache.has(convertKey(key));
}
}

delete(key) {
return this.#cache.delete(convertKey(key));
}

set(key, value, { expires = undefined } = {}) {
if (value === undefined) {
throw new TypeError('Cannot cache undefined values.');
} else if (expires instanceof Date) {
this.set(key, value, { expires: expires.getTime() });
} else if (typeof expires === 'string') {
this.set(key, value, { expires: new Date(expires).getTime() });
} else if (typeof expires === 'number' && Date.now() > expires) {
throw new TypeError('Attempting to set already expired value.');
} else if (Number.isNaN(expires)) {
throw new TypeError('Expires is NaN.');
} else if (expires !== undefined && typeof expires !== 'number') {
throw new TypeError(`${typeof expires} is not a valid type for cache expires.`);
} else {
this.#cache.set(convertKey(key), Object.freeze({ value, expires, updated: Date.now() }));
}
}

get(key, fallback = undefined) {
const result = this.#cache.get(convertKey(key));

if (result === undefined) {
return fallback;
} else if (typeof result.expires === 'number' && result.expires < Date.now()) {
this.delete(key);
console.info(`${key.toString()} is expired and has been deleted.`);
return fallback;
} else {
return result.value;
}
}
}
84 changes: 84 additions & 0 deletions custom-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
*/
import { HTML } from '@shgysk8zer0/consts/mimes.js';
import { registerComponent as reg } from '@aegisjsproject/core/componentRegistry.js';
import { whenIntersecting } from './intersect.js';
import { Cache } from './Cache.js';

const componentCache = new Cache();

export const supported = globalThis.customElements instanceof Object;

Expand Down Expand Up @@ -88,3 +92,83 @@ export async function getTemplate(src, opts, {
throw new Error(`${resp.url} [${resp.status} ${resp.statusText}]`);
}
}

export async function renderComponentShadow(shadow, {
template,
styles,
content,
loading = 'eager',
cache = componentCache,
key,
expires,
sanitizer: {
elements,
attributes,
comments = false,
dataAttributes = true,
...sanitizer
}
}) {
if (! (shadow instanceof ShadowRoot)) {
throw new TypeError('Shadow expected to be a `ShadowRoot`.');
}

if (typeof content === 'string') {
shadow.host.setHTML(content, { elements, attributes, comments, dataAttributes, ...sanitizer });
} else if (content instanceof HTMLTemplateElement) {
shadow.host.append(content.content.cloneNode(true));
} else if (content instanceof Node) {
shadow.host.append(content);
} else if (Array.isArray(content)) {
shadow.host.append(...content);
}

const cacheKey = typeof key === 'undefined' ? shadow.host.constructor : key;

if (loading === 'lazy') {
await whenIntersecting(shadow.host);
}

if (cache.has(cacheKey, expires !== undefined)) {
const { frag, sheets } = await cache.get(cacheKey);
shadow.adoptedStyleSheets = sheets;
shadow.append(frag.cloneNode(true));
} else {
cache.set(key, new Promise(async (resolve, reject) => {
const frag = document.createDocumentFragment();

if (typeof template === 'string') {
frag.setHTML(template, { elements, attributes, comments, dataAttributes, ...sanitizer });
} else if (template instanceof HTMLTemplateElement) {
frag.append(template.content.cloneNode(true));
} else if (template instanceof Node) {
frag.append(template.cloneNode(true));
} else {
reject(new TypeError('Template must be a string or a Node.'));
}

if (Array.isArray(styles)) {
const sheets = await Array.fromAsync(
styles,
async sheet => sheet instanceof CSSStyleSheet ? sheet : new CSSStyleSheet().replace(sheet)
);

resolve({ frag, sheets });
shadow.adoptedStyleSheets = sheets;
shadow.append(frag.cloneNode(true));
} else if (styles instanceof CSSStyleSheet) {
resolve({ frag, sheets: [styles] });
shadow.adoptedStyleSheets = [styles];
shadow.append(frag.cloneNode(true));
} else if (typeof styles === 'string') {
const sheet = await new CSSStyleSheet().replace(styles);
resolve({ frag, sheets: [sheet] });
shadow.adoptedStyleSheets = [sheet];
shadow.append(frag.cloneNode(true));
}
}).catch(err => {
console.error(err);
cache.delete(cacheKey);
}), expires);
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shgysk8zer0/kazoo",
"version": "1.0.3",
"version": "1.0.4",
"private": false,
"type": "module",
"description": "A JavaScript monorepo for all the things!",
Expand Down
2 changes: 1 addition & 1 deletion test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
}
}
</script>
<script referrerpolicy="no-referrer" crossorigin="anonymous" integrity="sha384-CjIJ66gyWso3qhYfO5C5XoJ8xGecllSUxI0IrVqzJnDZNT7+IN7fh3xfKnnwgORo" src="https://unpkg.com/@shgysk8zer0/[email protected].9/all.min.js" fetchpriority="high" defer=""></script>
<script type="application/javascript" defer="" referrerpolicy="no-referrer" crossorigin="anonymous" integrity="sha384-cDZDMlFHMB71kkyJ24QLYvj1kTEbLJmmQevX8HSVwFM+d5oXdAudsvLoec99AUiW" src="https://unpkg.com/@shgysk8zer0/[email protected].10/all.min.js" fetchpriority="high"></script>
<script type="module" src="./js/policy.js" referrerpolicy="no-referrer"></script>
<script type="module" src="./js/index.js" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="./css/index.css" referrerpolicy="no-referrer" media="all" />
Expand Down
Loading