Gruber

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) {}
})

See examples/node-fs-migrator

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": {}
      }
    }
  }
}