Core
At the centre of Gruber are a set of agnostic modules to help create platform-independent JavaScript servers. They are roughly organised as:
- Migrator is a helper for structuring project migrations that run up or down to manage the state of something like a database.
- Store is an abstraction for interacting with asynchonous key-value storage
- Terminator is a utility for ensuring graceful shutdown of your apps, especially when behind a load-balancer
- Tokens are an abstraction around signing access to a user to be consumed by other parts of the application
- Container is a dependency management utility to capture code dependencies, provide them on demand and allow them to be replaced during testing.
- Miscellaneous contains various things that are often useful
Miscellaneous
Container
unstable
Container holds a set of dependencies that are lazily computed and provides a system to override those dependencies during testing
const container = new Container({
message: () => 'hello there',
store: useStore
})
// Retrieve a dependency
console.log(container.get('message')) // outputs "hello there"
// Override dependencies
container.override({
store: new MemoryStore()
})
// get the overridden store
let store = container.get('store') // MemoryStore
// attempt to get the message
container.get('message') // throws Error('unmet dependency')
// restore the container back to the original dependencies
container.reset()
get
Get a dependency. First checking overrides, then previously computed or finaly use the dependency factory
override
Override the dependencies within the container or create unmet dependencies for those not-provided
// Replace the store with an in-memory one
container.override({ store: new MemoryStore() })
proxy
Create a proxy around an object that injects our dependencies
const container = new Container({ message: () => 'hello there' })
const proxy = container.proxy({ count: 7 })
proxy.message // 'hello there'
proxy.count // 7
// or with object destructuring
const { message, count } = container.proxy({ count: 7 })
reset
Clear any overrides on the dependencies
container.reset()
unwrap
internal
Compute a dependency from it's factory
const message = container.unwrap('message')
RandomService
type
RandomService provices an abstraction around generating random values
const random // RandomService
// Pick a number between 4 & 7 inclusively
let number = random.number(4, 7)
// Generate a UUID
let uuid = random.uuid()
// Pick an element from an array
let element = random.element([1, 2, 3, 4, 5])
useRandom
A standard implementation of RandomService using Math.random + crypto.randomUUID()
const random = useRandom()
let number = random.number(4, 7)
let uuid = random.uuid()
let element = random.element([1, 2, 3, 4, 5])
formatMarkdownTable
Given a set of records with known columns, format them into a pretty markdown table using the order from columns.
If a record does not have a specified value (it is null or undefined) it will be replaced with the fallback value.
const table = formatMarkdownTable(
[
{ name: 'Geoff Testington', age: 42 },
{ name: "Jess Smith", age: 32 },
{ name: "Tyler Rockwell" },
],
['name', 'age'],
'~'
)
Which will generate:
| name | age |
| ---------------- | --- |
| Geoff Testington | 42 |
| Jess Smith | 32 |
| Tyler Rockwell | ~ |
loader
unstable
loader let's you memoize the result of a function to create a singleton from it.
It works synchronously or with promises.
let index = 1
const useMessage = loader(() = 'hello there ${i++}')
useMessage() // hello there 1
useMessage() // hello there 1
useMessage() // hello there 1
trimIndentation
internal
trimIndentation takes a template literal (with values) and takes out the common whitespace.
Very heavily based on dedent
import { trimIndentation } from "gruber";
console.log(
trimIndentation`
Hello there!
My name is Geoff
`,
);
Which will output this, without any extra whitespace:
Hello there!
My name is Geoff
reconstructTemplateString
internal
Turn arguments from a string template literal back into a string
// 'I have 2 dogs'
reconstructTemplateString(['I have ', ' dogs'], 2)
or via template tags
// 'I have 2 dogs'
reconstructTemplateString`I have ${2} dogs`
preventExtraction
unstable
Take steps to prevent an object from being extracted from the app, inspired by crypto.subtle.importKey's extractable parameter.
This will:
- throw an error if the value are passed to JSON.stringify
- it recursively applies to nested objects, arrays and items within arrays
- seal and freeze the value and all nested objects & arrays
const config = preventExtraction({
name: "Geoff Testington",
pets: [
{ name: "Hugo" },
{ name: "Helga" },
],
favourite: {
mountain: "Cheviot"
}
})
// Any attempt to JSON-ify will result in an error
console.log(JSON.stringify(config)) // throws a TypeError
console.log(JSON.stringify(config.pets)) // throws a TypeError
console.log(JSON.stringify(config.pets[0])) // throws a TypeError
console.log(JSON.stringify(config.pets[1])) // throws a TypeError
console.log(JSON.stringify(config.favourite)) // throws a TypeError
The value will also be frozen and sealed, so any properties cannot be added, removed or modified.
dangerouslyExpose
unstable
DANGER undo a preventExtraction to allow values to be exposed.
This removes all of the precations that preventExtraction add.
console.log(
JSON.stringify(
dangerouslyExpose(appConfig.meta)
)
)
PromiseList
internal
A dynamic list of promises that are automatically removed when they resolve
const list = new PromiseList()
// Add a promise that waits for 5 seconds
list.push(async () => {
await new Promise(r => setTimeout(r, 5_000))
// Add dependant promises too
list.push(async () => {
await somethingElse()
})
})
// Wait for all promises and dependants to resolve in one go
await promises.all()
all
Wait for all promises to be resolved using Promise.all.
If new promises are added as a result of waiting, they are also awaited.
await list.all()
length
Get the current number of promises in the list
list.length // 5
push
Add a promise to the list using a factory method,
the factory just needs to return a promise
list.push(async () => {
// ...
})
Migrator
defineMigration
Define a generic migration, this is a wrapper around creating a MigrationOptions
which within TypeScript means you can specify the <T> once, rather than for each action.
const migration = defineMigration({
up () {},
down () {},
})
loadMigration
Attempt to load a migration from a file using import.
It combines the name and directory to get a file path, attempts to import-it and convert the default export into a MigrationDefinition. You can also force the <T> parameter onto the definition.
It will throw errors if the file does not exist or if the default export doesn't look like a MigrationOptions.
const migration = await loadMigration(
'001-create-users.js',
new URL('./migrations/', import.meta.url)
)
migration.name // "001-create-users.js"
migration.up // function
migration.down // function
MigratorOptions
type
MigratorOptions lets your create your own migrator that performs migrations in different ways. For instance you could create one that loads a JSON "migrations" file from the filesystem.
execute
Perform or reverse a migration and update any required state
function execute(definition, direction) {
console.log('running', definition.name, direction)
if (direction === 'up') definition.up()
if (direction === 'down') definition.down()
}
getDefinitions
Get or generate the all migration definitions
function getDefinitions () {
return { name: 001-something.js', up() {}, down() {} }
}
getRecords
Query which migrations have already been performed
function getRecords () {
return [{ name: '001-something.js' }]
}
Migrator
Migrator provides methods for running a specific type of migrations. The idea is that different platforms/integrations can create a migrator that works with a specific feature they want to add migrations around, e.g. a Postgres database.
const migrator = new Migrator({
async getRecords() {},
async getDefinitions() {},
async execute(definition, direction) {}
})
down
Run any "down" migrations for migrations that have already been performed
It would be cool to specify a number here so you could run just 1 but I haven't needed this so it hasn't been properly designed yet
await migrator.up()
up
Run any pending "up" migrations
It would be cool to specify a number here so you could run just 1 but I haven't needed this so it hasn't been properly designed yet
await migrator.up()
Store
Store
type
Store is an async abstraction around a key-value engine like Redis or a JavaScript Map with extra features for storing things for set-durations
Store implements Disposable so you can use Explicit Resource Management
async function main() {
await using store = new MemoryStore(…)
await store.set('users/geoff', …)
}
delete
Remove a value from the store
await store.remove("users/geoff")
dispose
Close the store
await store.dispose()
get
Retrieve the value from the store
const value = await store.get("users/geoff")
set
Put a value into the store
await store.set(
'users/geoff',
{ name: "Geoff Testington"},
)
// Store jess for 5 minutes
await store.set(
"users/jess",
{ name: "Jess Smith" },
{ maxAge: 5 * 60 * 1_000 }
)
MemoryStore
MemoryStore is a in-memory implementation of Store that puts values into a Map and uses timers to expire data. It was mainly made for automated testing.
const store = new MemoryStore()
Terminator
TerminatorOptions
internal
type
Options for creating a Terminator instance
const options = {
timeout: 5_000,
signals: ['SIGINT', 'SIGTERM'],
startListeners(signals, handler) {},
exitProcess(statusCode, error) {},
}
exitProcess
Exit the process with a given code and optionaly log an error
signals
Which OS signals to listen for
startListeners
Register each signal with the OS and call the handler
timeout
How long to wait in the terminating state so loadbalancers can process it (milliseconds)
Terminator
internal
Terminators let you add graceful shutdown to your applications, create one with TerminatorOptions
const arnie = new Terminator({
timeout: 5_000,
signals: ['SIGINT', 'SIGTERM'],
startListeners(signals, handler) {},
exitProcess(statusCode, error) {},
})
getResponse
Get a Fetch Response with the state of the terminator, probably for a load balancer.
If the terminator is running, it will return a http/200 otherwise it will return a http/503
const response = await arnie.getResponse()
start
Start the terminator and capture a block of code to close the server
arnie.start(async () => {
await store.dispose()
})
terminate
internal
Start the shutdown process
await arnie.terminate(async () => {
await store.dispose()
})
waitForSignals
unstable
Experimental, wait for a terminator with promises
using store = useStore()
using server = serveHTTP(…)
await arnie.waitForSignals()
// Automatic disposal!
Tokens
TokenService
unstable
type
A service for signing and verifying access tokens
let service // TokenService
// { userId: 42, scope: "user" }
const decoded = await service.verify("some-secret-token")
// "some-secret-token"
const token = await service.sign("user", { userId: 42 })
CompositeTokens
unstable
A TokenService with multiple verification methods and a single signer
debug
{
"Container": {
"entrypoint": "core/mod.ts",
"id": "Container",
"name": "Container",
"content": "Container holds a set of dependencies that are lazily computed\nand provides a system to override those dependencies during testing\n\n```js\nconst container = new Container({\n message: () => 'hello there',\n store: useStore\n})\n\n// Retrieve a dependency\nconsole.log(container.get('message')) // outputs \"hello there\"\n\n// Override dependencies\ncontainer.override({\n store: new MemoryStore()\n})\n\n// get the overridden store\nlet store = container.get('store') // MemoryStore\n\n// attempt to get the message\ncontainer.get('message') // throws Error('unmet dependency')\n\n// restore the container back to the original dependencies\ncontainer.reset()\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {
"override": {
"id": "Container#override",
"name": "override",
"content": "Override the dependencies within the container or create unmet dependencies for those not-provided\n\n```js\n// Replace the store with an in-memory one\ncontainer.override({ store: new MemoryStore() })\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"reset": {
"id": "Container#reset",
"name": "reset",
"content": "Clear any overrides on the dependencies\n\n```js\ncontainer.reset()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"get": {
"id": "Container#get",
"name": "get",
"content": "Get a dependency. First checking overrides, then previously computed or finaly use the dependency factory",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"unwrap": {
"id": "Container#unwrap",
"name": "unwrap",
"content": "Compute a dependency from it's factory\n\n```js\nconst message = container.unwrap('message')\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"proxy": {
"id": "Container#proxy",
"name": "proxy",
"content": "Create a proxy around an object that injects our dependencies\n\n```ts\nconst container = new Container({ message: () => 'hello there' })\n\nconst proxy = container.proxy({ count: 7 })\nproxy.message // 'hello there'\nproxy.count // 7\n\n// or with object destructuring\nconst { message, count } = container.proxy({ count: 7 })\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"defineMigration": {
"entrypoint": "core/mod.ts",
"id": "defineMigration",
"name": "defineMigration",
"content": "Define a generic migration, this is a wrapper around creating a `MigrationOptions`\nwhich within TypeScript means you can specify the `` once, rather than for each action.\n\n```js\nconst migration = defineMigration({\n up () {},\n down () {},\n})\n```",
"tags": {
"group": "Migrator"
},
"children": {}
},
"loadMigration": {
"entrypoint": "core/mod.ts",
"id": "loadMigration",
"name": "loadMigration",
"content": "Attempt to load a migration from a file using `import`.\n\nIt combines the `name` and `directory` to get a file path, attempts to `import`-it and convert the `default` export into a `MigrationDefinition`. You can also force the `` parameter onto the definition.\n\nIt will throw errors if the file does not exist or if the default export doesn't look like a `MigrationOptions`.\n\n\n```js\nconst migration = await loadMigration(\n '001-create-users.js',\n new URL('./migrations/', import.meta.url)\n)\n\nmigration.name // \"001-create-users.js\"\nmigration.up // function\nmigration.down // function\n```",
"tags": {
"group": "Migrator"
},
"children": {}
},
"MigratorOptions": {
"entrypoint": "core/mod.ts",
"id": "MigratorOptions",
"name": "MigratorOptions",
"content": "MigratorOptions lets your create your own migrator that performs migrations in different ways.\nFor instance you could create one that loads a JSON \"migrations\" file from the filesystem.",
"tags": {
"group": "Migrator",
"type": "true"
},
"children": {
"getDefinitions": {
"id": "MigratorOptions#getDefinitions",
"name": "getDefinitions",
"content": "Get or generate the all migration definitions\n\n```js\nfunction getDefinitions () {\n\treturn { name: 001-something.js', up() {}, down() {} }\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getRecords": {
"id": "MigratorOptions#getRecords",
"name": "getRecords",
"content": "Query which migrations have already been performed\n\n```js\nfunction getRecords () {\n\treturn [{ name: '001-something.js' }]\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"execute": {
"id": "MigratorOptions#execute",
"name": "execute",
"content": "Perform or reverse a migration and update any required state\n\n```js\nfunction execute(definition, direction) {\n\tconsole.log('running', definition.name, direction)\n\tif (direction === 'up') definition.up()\n\tif (direction === 'down') definition.down()\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Migrator": {
"entrypoint": "core/mod.ts",
"id": "Migrator",
"name": "Migrator",
"content": "Migrator provides methods for running a specific type of migrations.\nThe idea is that different platforms/integrations can create a migrator that\nworks with a specific feature they want to add migrations around, e.g. a Postgres database.\n\n```js\nconst migrator = new Migrator({\n\tasync getRecords() {},\n\tasync getDefinitions() {},\n\tasync execute(definition, direction) {}\n})\n```\n\nSee [examples/node-fs-migrator](https://github.com/robb-j/gruber/tree/main/examples/node-fs-migrator)",
"tags": {
"group": "Migrator"
},
"children": {
"up": {
"id": "Migrator#up",
"name": "up",
"content": "Run any pending \"up\" migrations\n\n> It would be cool to specify a number here so you could run just 1 but\n> I haven't needed this so it hasn't been properly designed yet\n\n```js\nawait migrator.up()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"down": {
"id": "Migrator#down",
"name": "down",
"content": "Run any \"down\" migrations for migrations that have already been performed\n\n> It would be cool to specify a number here so you could run just 1 but\n> I haven't needed this so it hasn't been properly designed yet\n\n```js\nawait migrator.up()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"RandomService": {
"entrypoint": "core/mod.ts",
"id": "RandomService",
"name": "RandomService",
"content": "RandomService provices an abstraction around generating random values\n\n```js\nconst random // RandomService\n\n// Pick a number between 4 & 7 inclusively\nlet number = random.number(4, 7)\n\n// Generate a UUID\nlet uuid = random.uuid()\n\n// Pick an element from an array\nlet element = random.element([1, 2, 3, 4, 5])\n```",
"tags": {
"group": "Miscellaneous",
"type": "true"
},
"children": {}
},
"useRandom": {
"entrypoint": "core/mod.ts",
"id": "useRandom",
"name": "useRandom",
"content": "A standard implementation of `RandomService` using Math.random + crypto.randomUUID()\n\n```js\nconst random = useRandom()\nlet number = random.number(4, 7)\nlet uuid = random.uuid()\nlet element = random.element([1, 2, 3, 4, 5])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Store": {
"entrypoint": "core/mod.ts",
"id": "Store",
"name": "Store",
"content": "Store is an async abstraction around a key-value engine like Redis or a JavaScript Map\nwith extra features for storing things for set-durations\n\nStore implements Disposable so you can use Explicit Resource Management\n\n```js\nasync function main() {\n await using store = new MemoryStore(…)\n\n await store.set('users/geoff', …)\n}\n```",
"tags": {
"group": "Store",
"type": "true"
},
"children": {
"get": {
"id": "Store#get",
"name": "get",
"content": "Retrieve the value from the store\n\n```js\nconst value = await store.get(\"users/geoff\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"set": {
"id": "Store#set",
"name": "set",
"content": "Put a value into the store\n\n```js\nawait store.set(\n 'users/geoff',\n { name: \"Geoff Testington\"},\n)\n\n// Store jess for 5 minutes\nawait store.set(\n \"users/jess\",\n { name: \"Jess Smith\" },\n { maxAge: 5 * 60 * 1_000 }\n)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"delete": {
"id": "Store#delete",
"name": "delete",
"content": "Remove a value from the store\n\n```js\nawait store.remove(\"users/geoff\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"dispose": {
"id": "Store#dispose",
"name": "dispose",
"content": "Close the store\n\n```js\nawait store.dispose()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"MemoryStore": {
"entrypoint": "core/mod.ts",
"id": "MemoryStore",
"name": "MemoryStore",
"content": "MemoryStore is a in-memory implementation of [Store](#store) that puts values into a Map and uses timers to expire data.\nIt was mainly made for automated testing.\n\n```js\nconst store = new MemoryStore()\n```",
"tags": {
"group": "Store"
},
"children": {}
},
"TerminatorOptions": {
"entrypoint": "core/mod.ts",
"id": "TerminatorOptions",
"name": "TerminatorOptions",
"content": "Options for creating a [Terminator](#terminator) instance\n\n```js\nconst options = {\n timeout: 5_000,\n signals: ['SIGINT', 'SIGTERM'],\n startListeners(signals, handler) {},\n exitProcess(statusCode, error) {},\n}\n```",
"tags": {
"internal": "true",
"group": "Terminator",
"type": "true"
},
"children": {
"timeout": {
"id": "TerminatorOptions#timeout",
"name": "timeout",
"content": "How long to wait in the terminating state so loadbalancers can process it (milliseconds)",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"signals": {
"id": "TerminatorOptions#signals",
"name": "signals",
"content": "Which OS signals to listen for",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"startListeners": {
"id": "TerminatorOptions#startListeners",
"name": "startListeners",
"content": "Register each signal with the OS and call the handler",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"exitProcess": {
"id": "TerminatorOptions#exitProcess",
"name": "exitProcess",
"content": "Exit the process with a given code and optionaly log an error",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
},
"Terminator": {
"entrypoint": "core/mod.ts",
"id": "Terminator",
"name": "Terminator",
"content": "Terminators let you add graceful shutdown to your applications,\ncreate one with [TerminatorOptions](#terminatoroptions)\n\n```js\nconst arnie = new Terminator({\n timeout: 5_000,\n signals: ['SIGINT', 'SIGTERM'],\n startListeners(signals, handler) {},\n exitProcess(statusCode, error) {},\n})\n```",
"tags": {
"internal": "true",
"group": "Terminator"
},
"children": {
"start": {
"id": "Terminator#start",
"name": "start",
"content": "Start the terminator and capture a block of code to close the server\n\n```js\narnie.start(async () => {\n await store.dispose()\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"terminate": {
"id": "Terminator#terminate",
"name": "terminate",
"content": "Start the shutdown process\n\n```js\nawait arnie.terminate(async () => {\n await store.dispose()\n})\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"getResponse": {
"id": "Terminator#getResponse",
"name": "getResponse",
"content": "Get a Fetch Response with the state of the terminator, probably for a load balancer.\n\nIf the terminator is running, it will return a http/200\notherwise it will return a http/503\n\n```js\nconst response = await arnie.getResponse()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"waitForSignals": {
"id": "Terminator#waitForSignals",
"name": "waitForSignals",
"content": "Experimental, wait for a terminator with promises\n\n```js\nusing store = useStore()\nusing server = serveHTTP(…)\n\nawait arnie.waitForSignals()\n\n// Automatic disposal!\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
}
}
},
"TokenService": {
"entrypoint": "core/mod.ts",
"id": "TokenService",
"name": "TokenService",
"content": "A service for signing and verifying access tokens\n\n```js\nlet service // TokenService\n\n// { userId: 42, scope: \"user\" }\nconst decoded = await service.verify(\"some-secret-token\")\n\n// \"some-secret-token\"\nconst token = await service.sign(\"user\", { userId: 42 })\n```",
"tags": {
"unstable": "true",
"group": "Tokens",
"type": "true"
},
"children": {}
},
"CompositeTokens": {
"entrypoint": "core/mod.ts",
"id": "CompositeTokens",
"name": "CompositeTokens",
"content": "A TokenService with multiple verification methods and a single signer",
"tags": {
"unstable": "true",
"group": "Tokens"
},
"children": {}
},
"formatMarkdownTable": {
"entrypoint": "core/mod.ts",
"id": "formatMarkdownTable",
"name": "formatMarkdownTable",
"content": "Given a set of `records` with known `columns`, format them into a pretty markdown table using the order from `columns`.\nIf a record does not have a specified value (it is null or undefined) it will be replaced with the `fallback` value.\n\n```js\nconst table = formatMarkdownTable(\n\t[\n\t\t{ name: 'Geoff Testington', age: 42 },\n\t\t{ name: \"Jess Smith\", age: 32 },\n\t\t{ name: \"Tyler Rockwell\" },\n\t],\n\t['name', 'age'],\n\t'~'\n)\n```\n\nWhich will generate:\n\n```\n| name | age |\n| ---------------- | --- |\n| Geoff Testington | 42 |\n| Jess Smith | 32 |\n| Tyler Rockwell | ~ |\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"loader": {
"entrypoint": "core/mod.ts",
"id": "loader",
"name": "loader",
"content": "`loader` let's you memoize the result of a function to create a singleton from it.\nIt works synchronously or with promises.\n\n```js\nlet index = 1\nconst useMessage = loader(() = 'hello there ${i++}')\n\nuseMessage() // hello there 1\nuseMessage() // hello there 1\nuseMessage() // hello there 1\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"trimIndentation": {
"entrypoint": "core/mod.ts",
"id": "trimIndentation",
"name": "trimIndentation",
"content": "`trimIndentation` takes a template literal (with values) and takes out the common whitespace.\nVery heavily based on [dedent](https://github.com/dmnd/dedent/tree/main)\n\n```js\nimport { trimIndentation } from \"gruber\";\n\nconsole.log(\n\ttrimIndentation`\n\t\tHello there!\n\t\tMy name is Geoff\n\t`,\n);\n```\n\nWhich will output this, without any extra whitespace:\n\n```\nHello there!\nMy name is Geoff\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"reconstructTemplateString": {
"entrypoint": "core/mod.ts",
"id": "reconstructTemplateString",
"name": "reconstructTemplateString",
"content": "Turn arguments from a string template literal back into a string\n\n```js\n// 'I have 2 dogs'\nreconstructTemplateString(['I have ', ' dogs'], 2)\n```\n\nor via template tags\n\n```js\n// 'I have 2 dogs'\nreconstructTemplateString`I have ${2} dogs`\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {}
},
"preventExtraction": {
"entrypoint": "core/mod.ts",
"id": "preventExtraction",
"name": "preventExtraction",
"content": "Take steps to prevent an object from being extracted from the app,\ninspired by crypto.subtle.importKey's [extractable](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#extractable) parameter.\n\nThis will:\n- throw an error if the value are passed to JSON.stringify\n- it recursively applies to nested objects, arrays and items within arrays\n- [seal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal) and [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) the value and all nested objects & arrays\n\n```js\nconst config = preventExtraction({\n\tname: \"Geoff Testington\",\n\tpets: [\n\t\t{ name: \"Hugo\" },\n\t\t{ name: \"Helga\" },\n\t],\n favourite: {\n\t\tmountain: \"Cheviot\"\n\t}\n})\n\n// Any attempt to JSON-ify will result in an error\nconsole.log(JSON.stringify(config)) // throws a TypeError\nconsole.log(JSON.stringify(config.pets)) // throws a TypeError\nconsole.log(JSON.stringify(config.pets[0])) // throws a TypeError\nconsole.log(JSON.stringify(config.pets[1])) // throws a TypeError\nconsole.log(JSON.stringify(config.favourite)) // throws a TypeError\n```\n\nThe value will also be frozen and sealed, so any properties cannot be added, removed or modified.",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"dangerouslyExpose": {
"entrypoint": "core/mod.ts",
"id": "dangerouslyExpose",
"name": "dangerouslyExpose",
"content": "**DANGER** undo a [preventExtraction](#preventextraction) to allow values to be exposed.\nThis removes all of the precations that `preventExtraction` add.\n\n```js\nconsole.log(\n\tJSON.stringify(\n\t\tdangerouslyExpose(appConfig.meta)\n\t)\n)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"PromiseList": {
"entrypoint": "core/mod.ts",
"id": "PromiseList",
"name": "PromiseList",
"content": "A dynamic list of promises that are automatically removed when they resolve\n\n```js\nconst list = new PromiseList()\n\n// Add a promise that waits for 5 seconds\nlist.push(async () => {\n\tawait new Promise(r => setTimeout(r, 5_000))\n\n\t// Add dependant promises too\n\tlist.push(async () => {\n\t\tawait somethingElse()\n\t})\n})\n\n// Wait for all promises and dependants to resolve in one go\nawait promises.all()\n\n```",
"tags": {
"internal": "true",
"group": "Miscellaneous"
},
"children": {
"push": {
"id": "PromiseList#push",
"name": "push",
"content": "Add a promise to the list using a factory method,\nthe `factory` just needs to return a promise\n\n```js\nlist.push(async () => {\n // ...\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"all": {
"id": "PromiseList#all",
"name": "all",
"content": "Wait for all promises to be resolved using `Promise.all`.\nIf new promises are added as a result of waiting, they are also awaited.\n\n```js\nawait list.all()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"length": {
"id": "PromiseList#length",
"name": "length",
"content": "Get the current number of promises in the list\n\n```js\nlist.length // 5\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
}
}