Browser
EXPERIMENTAL
This module is the entrypoint for using Gruber in the browser. It's a slimmed down export of the modules that might be useful client-side.
Install
Gruber is available through NPM
npm install gruber
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)
)
)
pickProperties
Create a subset of an object by picking off specific keys
const object = {
name: "Geoff Testington",
age: 42,
pets: ["Hugo", "Florence"]
}
pickProperties(object, ["name", "age"])
getOrInsert
Polyfil for Map#getOrInsert
let preferences = new Map()
let darkMode = getOrInsert(preferences, "use_dark_mode", true)
let groups = new Map()
for (let value of array) {
getOrInsert(groups, value.theme, []).push(value)
}
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
Structure
Structure
Structure is a composable primative for processing values to make sure they are what you expect them to be, optionally coercing the value into something else. It's also strongly-typed so values that are validated have the correct TypeScript type too.
The Structure class also supports StandardSchema v1 so you can use it anywhere that supports that standard.
getFullSchema
Get a JSON schema from the structure, where equivalent fields are available.
process
Execute the structure by passing it a value and getting back the result if it is successful, otherwise a Structure.Error is thrown
Structure.any
Define a Structure that lets any value through
Structure.any()
Structure.array
Define a list of values that each match the same structure.
// An array of strings
Structure.array(
Structure.string()
)
// An array of objects
Structure.array(
Structure.object({
name: Structure.string(),
age: Structure.number()
})
)
Structure.boolean
Define a boolean value with an optional fallback.
Structure.boolean()
Structure.boolean(false)
Structure.date
Creates a Structure that validates dates or values that can be turned into dates through the Date constructor.
Structure.date()
Structure.enum
Creates a Structure that validates a value is one of a set of literals
Structure.enum(['a string', 42, false])
Structure.fromJSONSchema
unstable
Attempts to create a Structure from a parsed JSON Schema value. This is implemented on a as-needed bases, currently it supports:
- "const" →
Structure.literal - type=string →
Structure.string - type=number →
Structure.number - type=boolean →
Structure.boolean - type=array → "items" are recursively parsed and put into a
Structure.array - type=object → "properties" are recursively parsed and put into a
Structure.object - anyOf →
Structure.union
Structure.fromJSONSchema({ type: "string" })
Structure.fromJSONSchema({ type: "number" })
Structure.fromJSONSchema({ type: "boolean" })
Structure.fromJSONSchema({
type: "object",
properties: {
name: { type:"string" },
age: { type: "number" }
},
required: ["name"]
})
Structure.fromJSONSchema({ type: "array", items: { type: "string" } })
Structure.fromJSONSchema({
anyOf: [
{ type: "string" },
{ type: "number" }
]
});
notes
- array "prefixItems" are not supported, maybe they could be mapped to tuples?
Structure.literal
Define a specific value that must be exactly equal.
Structure.literal("click_event")
Structure.literal(42)
Structure.literal(true)
Structure.null
Define a Structure to validate the value is null
Structure.null()
Structure.nullable
Creates a Structure that validates a value is either another structure or a null value
Structure.nullable(Structure.string())
Structure.number
Define a number-based value with an optional fallback,
it will also try to parse floating-point values from strings.
Structure.number()
Structure.number("Geoff Testington")
Structure.object
Define a group of structures under an object. Each field needs to matched their respective Structures and no additionaly fields are allowed.
Structure.object({
name: Structure.string(),
age: Structure.number(),
})
Structure.optional
Creates a Structure that validates another structure or is not defined
Structure.optional(Structure.string())
Structure.partial
Create a Structure that validates an object where some or none of the fields match their respective Structures. Only fields specified may be set, nothing additional.
Structure.partial({
name: Structure.string(),
age: Structure.number()
})
Structure.pick
unstable
Define an interface over an object that plucks off known values and ignores the rest.
Structure.type({
name: Structure.string(),
age: Structure.number(),
})
Structure.record
unstable
Creates a Structure for objects that map a key to a common type of value
Structure.record(Structure.string(), Structure.number())
Structure.record(
Structure.string(),
Structure.object({ name: Structure.string() })
)
Structure.record(
Structure.enum(["name", "address", "emailAddress"]),
Structure.string()
)
Structure.string
Define a string-based value with an optional fallback.
Structure.string()
Structure.string("Geoff Testington")
Structure.tuple
unstable
Creates a Structure for arrays where each index has a different validation
Structure.tuple([Structure.string(), Structure.number()])
Structure.undefined
Creates a Structure that validates a value is the undefined value
Structure.undefined()
Structure.union
Define a Structure that must match one of a set of Structures
Structure.union([
Structure.object({
type: Structure.literal("click"),
element: Structure.string()
}),
Structure.object({
type: Structure.literal("login"),
}),
])
Structure.url
Define a URL value with an optional fallback,
that will be coerced into a URL.
Structure.url()
Structure.url("http://example.com")
Structure.url(new URL("http://example.com"))
debug
{
"Container": {
"entrypoint": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": "browser/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": {}
},
"pickProperties": {
"entrypoint": "browser/mod.ts",
"id": "pickProperties",
"name": "pickProperties",
"content": "Create a subset of an object by picking off specific keys\n\n```js\nconst object = {\n name: \"Geoff Testington\",\n age: 42,\n pets: [\"Hugo\", \"Florence\"]\n}\npickProperties(object, [\"name\", \"age\"])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getOrInsert": {
"entrypoint": "browser/mod.ts",
"id": "getOrInsert",
"name": "getOrInsert",
"content": "Polyfil for [Map#getOrInsert](https://github.com/tc39/proposal-upsert)\n\n```js\nlet preferences = new Map()\nlet darkMode = getOrInsert(preferences, \"use_dark_mode\", true)\n\nlet groups = new Map()\nfor (let value of array) {\n getOrInsert(groups, value.theme, []).push(value)\n}\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"PromiseList": {
"entrypoint": "browser/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": {}
}
}
},
"Structure": {
"entrypoint": "browser/mod.ts",
"id": "Structure",
"name": "Structure",
"content": "**Structure** is a composable primative for processing values to make sure they are what you expect them to be, optionally coercing the value into something else. It's also strongly-typed so values that are validated have the correct TypeScript type too.\n\nThe Structure class also supports [StandardSchema v1](https://standardschema.dev) so you can use it anywhere that supports that standard.",
"tags": {
"group": "Structure"
},
"children": {
"process": {
"id": "Structure#process",
"name": "process",
"content": "Execute the structure by passing it a value and getting back the result if it is successful, otherwise a [Structure.Error](#structure-error) is thrown",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"getFullSchema": {
"id": "Structure#getFullSchema",
"name": "getFullSchema",
"content": "Get a JSON schema from the structure, where equivalent fields are available.",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.string": {
"id": "Structure.string",
"name": "string",
"content": "Define a string-based value with an optional `fallback`.\n\n```js\nStructure.string()\nStructure.string(\"Geoff Testington\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.number": {
"id": "Structure.number",
"name": "number",
"content": "Define a number-based value with an optional `fallback`,\nit will also try to parse floating-point values from strings.\n\n```js\nStructure.number()\nStructure.number(\"Geoff Testington\")\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.boolean": {
"id": "Structure.boolean",
"name": "boolean",
"content": "Define a boolean value with an optional `fallback`.\n\n```js\nStructure.boolean()\nStructure.boolean(false)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.url": {
"id": "Structure.url",
"name": "url",
"content": "Define a URL value with an optional `fallback`,\nthat will be coerced into a `URL`.\n\n```js\nStructure.url()\nStructure.url(\"http://example.com\")\nStructure.url(new URL(\"http://example.com\"))\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.object": {
"id": "Structure.object",
"name": "object",
"content": "Define a group of structures under an object.\nEach field needs to matched their respective Structures and no additionaly fields are allowed.\n\n```js\nStructure.object({\n\tname: Structure.string(),\n\tage: Structure.number(),\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.pick": {
"id": "Structure.pick",
"name": "pick",
"content": "Define an interface over an object that plucks off known values and ignores the rest.\n\n```js\nStructure.type({\n\tname: Structure.string(),\n\tage: Structure.number(),\n})\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.array": {
"id": "Structure.array",
"name": "array",
"content": "Define a list of values that each match the same structure.\n\n```js\n// An array of strings\nStructure.array(\n\tStructure.string()\n)\n\n// An array of objects\nStructure.array(\n\tStructure.object({\n\t\tname: Structure.string(),\n\t\tage: Structure.number()\n\t})\n)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.literal": {
"id": "Structure.literal",
"name": "literal",
"content": "Define a specific value that must be exactly equal.\n\n\n```js\nStructure.literal(\"click_event\")\nStructure.literal(42)\nStructure.literal(true)\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.union": {
"id": "Structure.union",
"name": "union",
"content": "Define a Structure that must match one of a set of Structures\n\n```js\nStructure.union([\n\tStructure.object({\n\t\ttype: Structure.literal(\"click\"),\n\t\telement: Structure.string()\n\t}),\n\tStructure.object({\n\t\ttype: Structure.literal(\"login\"),\n\t}),\n])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.fromJSONSchema": {
"id": "Structure.fromJSONSchema",
"name": "fromJSONSchema",
"content": "Attempts to create a Structure from a parsed [JSON Schema](https://json-schema.org/specification) value.\nThis is implemented on a as-needed bases, currently it supports:\n- \"const\" → `Structure.literal`\n- type=string → `Structure.string`\n- type=number → `Structure.number`\n- type=boolean → `Structure.boolean`\n- type=array → \"items\" are recursively parsed and put into a `Structure.array`\n- type=object → \"properties\" are recursively parsed and put into a `Structure.object`\n- anyOf → `Structure.union`\n\n```js\nStructure.fromJSONSchema({ type: \"string\" })\nStructure.fromJSONSchema({ type: \"number\" })\nStructure.fromJSONSchema({ type: \"boolean\" })\nStructure.fromJSONSchema({\n type: \"object\",\n properties: {\n name: { type:\"string\" },\n age: { type: \"number\" }\n },\n required: [\"name\"]\n})\nStructure.fromJSONSchema({ type: \"array\", items: { type: \"string\" } })\nStructure.fromJSONSchema({\n anyOf: [\n { type: \"string\" },\n { type: \"number\" }\n ]\n});\n```\n\nnotes\n- array \"prefixItems\" are not supported, maybe they could be mapped to tuples?",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.tuple": {
"id": "Structure.tuple",
"name": "tuple",
"content": "Creates a Structure for arrays where each index has a different validation\n\n```js\nStructure.tuple([Structure.string(), Structure.number()])\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.record": {
"id": "Structure.record",
"name": "record",
"content": "Creates a Structure for objects that map a key to a common type of value\n\n```js\nStructure.record(Structure.string(), Structure.number())\nStructure.record(\n Structure.string(),\n Structure.object({ name: Structure.string() })\n)\nStructure.record(\n Structure.enum([\"name\", \"address\", \"emailAddress\"]),\n Structure.string()\n)\n```",
"tags": {
"unstable": "true",
"group": "Miscellaneous"
},
"children": {}
},
"Structure.null": {
"id": "Structure.null",
"name": "null",
"content": "Define a Structure to validate the value is `null`\n\n```js\nStructure.null()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.any": {
"id": "Structure.any",
"name": "any",
"content": "Define a Structure that lets any value through\n\n```js\nStructure.any()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.partial": {
"id": "Structure.partial",
"name": "partial",
"content": "Create a Structure that validates an object where some or none of the fields match their respective Structures.\nOnly fields specified may be set, nothing additional.\n\n```js\nStructure.partial({\n\tname: Structure.string(),\n\tage: Structure.number()\n})\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.date": {
"id": "Structure.date",
"name": "date",
"content": "Creates a Structure that validates dates or values that can be turned into dates\nthrough the [Date constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date).\n\n```js\nStructure.date()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.nullable": {
"id": "Structure.nullable",
"name": "nullable",
"content": "Creates a Structure that validates a value is either another structure or a null value\n\n```js\nStructure.nullable(Structure.string())\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.enum": {
"id": "Structure.enum",
"name": "enum",
"content": "Creates a Structure that validates a value is one of a set of literals\n\n```js\nStructure.enum(['a string', 42, false])\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.undefined": {
"id": "Structure.undefined",
"name": "undefined",
"content": "Creates a Structure that validates a value is the `undefined` value\n\n```js\nStructure.undefined()\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
},
"Structure.optional": {
"id": "Structure.optional",
"name": "optional",
"content": "Creates a Structure that validates another structure or is not defined\n\n```js\nStructure.optional(Structure.string())\n```",
"tags": {
"group": "Miscellaneous"
},
"children": {}
}
}
}
}