Configuration Guide โ
Tempo provides a flexible, multi-tiered configuration system. Settings are applied in a specific order of precedence, allowing you to set broad defaults that can be refined at the application or instance level.
Precedence Hierarchy โ
Settings are loaded in the following order (where later stages override earlier ones):
- Library Defaults: Sensible out-of-the-box baseline.
- Persistent Storage: Sticky user preferences (which merge into Defaults).
- Global Discovery: Enterprise-level setup discovered via
Symbol.for('$Tempo'). - Library Extension: Dynamic feature registration via
Tempo.extend(). - Explicit Initialization: Baseline configuration via
Tempo.init(). - Instance Constructor: Specific overrides for a single
new Tempo()call.
๐ Best Practice: The tempo.config.ts Pattern โ
Rather than scattering Tempo.init() or Tempo.extend() calls throughout your application, the recommended best practice is to centralize your environment setup into a single tempo.config.ts (or .js) file.
This mirrors modern ecosystem standards (like vite.config.ts or tailwind.config.js) and ensures that plugins, timezones, and custom aliases are consistently applied before any domain logic executes.
INFO
Target Environment: This automatic configuration discovery pattern relies on Node.js file system capabilities and is designed for Server, Fullstack, or Bundled environments (like Vite or Webpack). If you are using Tempo via a <script> tag in a pure Browser environment, skip to Explicit Initialization to configure Tempo synchronously!
// tempo.config.ts
import { defineConfig } from '@magmacomputing/tempo';
import { CronModule } from '@magmacomputing/tempo-plugin-cron';
import { SLAModule } from '@magmacomputing/tempo-plugin-sla';
export default defineConfig({
timeZone: 'Australia/Sydney', // Set your baseline timezone
license: 'eyJhbGciOiJIUzI1...', // JWT Commercial License for Premium Plugins
plugins: [CronModule, SLAModule], // Register enterprise plugins
registry: {
periods: {
'market-open': '09:30',
'market-close': '16:00'
}
}
});You can then bootstrap this environment at the very top of your application's entry point (e.g., main.ts or index.js) to guarantee the configuration is locked in before any other files run:
// main.ts
import { Tempo } from '@magmacomputing/tempo';
// Automatically discovers and loads 'tempo.config.ts'
await Tempo.bootstrap();
// Dynamic import ensures domain logic loads ONLY AFTER configuration is complete
const { App } = await import('./app.js');
// ...Benefits vs. Drawbacks โ
Using tempo.config.ts is the modern standard, but it introduces specific architectural tradeoffs due to Node.js ES Module constraints.
๐ Benefits โ
- TypeScript Autocomplete: Using
defineConfigprovides instant IDE intellisense and type-safety for all configuration options. - Plugin Execution: You can import and instantiate plugins directly inside the configuration file, keeping your application logic clean.
- Dynamic Configuration: Enables runtime logic (e.g.,
debug: process.env.NODE_ENV !== 'production') that strict JSON cannot provide.
โ ๏ธ Drawbacks โ
- Asynchronous Requirement: Because
tempo.config.tsis evaluated as an ES Module, the JavaScript engine forces it to be loaded asynchronously via dynamicimport(). This means you must useawait Tempo.bootstrap()instead of the synchronousTempo.init().
๐ Risks โ
- Node.js Environment Only: The
bootstrap()automatic file discovery relies on Node.js (fs,path). If you are running strictly in a browser (e.g., via CDN without a bundler), automatic discovery will safely abort, andbootstrap()will simply act as a pass-through toTempo.init(). In these environments, you must bundle your config or manually pass your options toTempo.init(options). - Floating Promises: You must ensure you actually
awaitthe bootstrap call. If you forget theawaitkeyword, your application will continue booting before Tempo finishes reading your config file, leading to race conditions where early instances use default settings.
TIP
Looking to configure Internationalization?
Tempo offers deep integration with native Intl APIs for both parsing and formatting foreign languages out-of-the-box. See The Role of Locale for a general guide, and the Internationalized Parsing and Format Modifiers & Localization guides for configuration details.
1. Persistent Configuration ($Tempo) โ
The first layer Tempo checks after its own internal defaults is persistent storage. This is ideal for "sticky" settings like a user's preferred timezone or locale that should persist across sessions without a database.
// Write a preference to localStorage under the default key ('$Tempo')
Tempo.writeStore({ timeZone:'Australia/Sydney' });
// Write a preference to localStorage under the key 'mySettings'
Tempo.writeStore({ timeZone: 'America/New_York' }, 'mySettings');
// Later, or in another file, initialize Tempo pointing to that key
// It will automatically read 'America/New_York' and apply it
Tempo.init({ store: 'mySettings' });2. Global Discovery โ
To facilitate configuration in micro-frontend architectures or script-first bootstraps, Tempo can discover a Discovery object from globalThis during Tempo.init().
The intended flow is:
- Write a Discovery object into
globalThisunder the configured discovery symbol key. - Import a module containing
Tempo. Tempoclass static initialization runsTempo.init().Tempo.init()reads the global discovery slot and merges it.
By default, the key is Symbol.for('$Tempo').
Pre-Bootstrap Discovery (globalThis) โ
// Must run before the first Tempo module is evaluated
globalThis[Symbol.for('$Tempo')] = Object.freeze({
options: { timeZone: 'Europe/Paris' },
timeZones: { MYTZ: 'Asia/Dubai' },
registry: { formats: { myFormat: '{dd}!!{mm}!!{yyyy}' } },
terms: [myCustomTermPlugin]
});
// Load Tempo after the discovery object is in place
const { Tempo } = await import('@magmacomputing/tempo');INFO
With static ESM imports, import evaluation happens before module body execution. If you need discovery to apply on first load, assign globalThis in an earlier script/module, or use dynamic import() as shown above.
Explicit Runtime Registration (Not Global Discovery) โ
Using Tempo.extend(...) is explicit registration after Tempo is loaded. It is ergonomic and strongly recommended for normal application code, but it is a different mechanism from pre-bootstrap global discovery.
import { Tempo } from '@magmacomputing/tempo';
Tempo.extend({
options: { timeZone: 'Europe/Paris' },
timeZones: { MYTZ: 'Asia/Dubai' },
registry: { formats: { myFormat: '{dd}!!{mm}!!{yyyy}' } },
terms: [myCustomTermPlugin]
});Security and Ergonomics Notes โ
- Tamper Prevention: When utilizing Global Discovery in a shared environment (like micro-frontends), it is highly recommended to
Object.freeze()your configuration. Tempo only reads from this object, so freezing it prevents third-party scripts from injecting unauthorized plugins before Tempo boots up. - Global Discovery is convenient for host-controlled bootstraps and cross-bundle handoff.
Tempo.extend(...)is usually safer in app code because configuration is explicit, local, and easier to trace.- Use Global Discovery when you must configure
Tempobefore the firstTempoimport executes.
Discovery Contract โ
Tempo looks for the following structure:
| Property | Type | Description |
|---|---|---|
options | Options | (() => Options) | Configuration options merged into global state. |
intl | IntlOptions | Internationalization configuration grouping relativeTimeFormat, numberFormat, durationFormat, and dateTimeFormat. |
plugins | Plugin | Plugin[] | Modular plugin(s) (including TermPlugins) to be extended onto Tempo automatically. |
timeZones | Record<string, string> | Custom timezone aliases to be merged. |
numbers | Record<string, number> | Custom number-word aliases merged into the NUMBER registry. |
registry | { formats?, locales?, events?, periods?, snippets?, layouts?, ignores?, modifiers?, tokens? } | Custom configuration for internal dictionary registries. |
3. Explicit Initialization (Tempo.init) โ
This is the Standard Developer Tier. Most applications should call Tempo.init() during startup to establish a predictable baseline for all instances.
import { Tempo } from '@magmacomputing/tempo';
Tempo.init({
timeZone: 'Australia/Sydney',
locale: 'en-AU',
pivot: 80,
debug: 0
});Available Options โ
| Option | Type | Default | Description |
|---|---|---|---|
timeZone | string | System Zone | Default IANA time zone or alias. |
locale | string | System Locale | Default BCP 47 language tag. used in .since() method |
calendar | string | 'iso8601' | Default calendar system. |
pivot | number | 75 | Cutoff for parsing two-digit years. |
monthDay | MonthDay | boolean | undefined | Regional date-parsing configuration (grouped). Includes active, locales, layouts, and timezones. |
timeStamp | 'ss' | 'ms' | 'us' | 'ns' | 'ms' | Precision for numeric inputs and the .ts property. |
sphere | 'north' | 'south' | Auto-inferred | Hemisphere for seasonal plugins. |
intl | IntlOptions | undefined | Internationalization configuration grouping relativeTimeFormat, numberFormat, and durationFormat. |
registry | { formats?, locales?, events?, periods?, snippets?, layouts?, ignores?, modifiers? } | Built-in registries | Custom data augmentation registries (e.g., format aliases, parsing logic, localization). |
plugins | Plugin | Plugin[] | [] | Plugins/modules to extend during initialization. Unlike registry options, these values are not merged into internal state via extendState; Tempo.init() applies each plugin with Tempo.extend(p), so plugin authors should treat them as instance/class augmentations rather than internal-state merges. |
store | string | '$Tempo' | Persistent storage key used by readStore/writeStore. |
discovery | string | symbol | '$Tempo' symbol key | Discovery slot used to resolve global discovery config. |
debug | number | string | 'info' | Controls log verbosity via direct LOG levels (0=Off ... 5=Trace) or string labels ('trace', 'info', etc). |
catch | boolean | false | If true, invalid inputs return a Void instance instead of throwing. |
mode | 'auto' | 'strict' | 'defer' | 'auto' | Controls the hydration strategy (e.g., defer for Zero-Cost creation). |
silent | boolean | false | Suppresses console output. Combined with catch: true for silent failover. |
planner | PlannerOptions | undefined | Grouped configuration for layoutOrder and preFilter. |
INFO
debug accepts numeric level values (0 through 5) or lowercase string labels ('off', 'error', 'warn', 'info', 'debug', 'trace').
4. Instance-Level Overrides โ
The final layer of precedence is the constructor itself. You can override any global setting for a specific calculation without affecting the rest of your application.
// This instance uses UTC regardless of any global configuration
const t = new Tempo('now', { timeZone: 'UTC' });5. Advanced Parsing Rules โ
Beyond basic settings, Tempo's parsing engine can be extended with custom rules and behaviors to handle specialized natural language or high-volume processing requirements.
๐ 5.1 Custom Events and Periods โ
You can extend Tempo's intelligence by supplying custom Events (date aliases) and Periods (time aliases) at any global configuration tier.
Tempo.init({
registry: {
events: {
'launch date': '2026-05-20',
'deadline': function () { return this.add({ days: 30 }) }
},
periods: {
'tea time': '15:00',
'mid[ -]?after[ -]?noon': '16:00', // regex-like key for 'mid after noon' or 'mid-after-noon' etc
}
}
})
const delivery = new Tempo('deadline'); // Parsed using your custom logic, adds 30-days to current-dateโก 5.2 Deferring Initialization (mode: 'defer') โ
By default (mode: 'auto'), Tempo uses the Master Guard to determine if a string can be lazily evaluated. For exceptionally high-volume scenarios where you may be creating thousands of Tempo instances but only using them for calculations (not formats or Terms), you can force a standard lazy behavior using mode: 'defer'.
When mode: 'defer' is set, the registry-discovery logic is deferred until the first time you access a property on t.fmt or t.term.
// Optimized for mass-creation
const t = new Tempo('now', { mode: 'defer' });
console.log(t.format('{yyyy}')); // Discovery triggers NOW, only once.When initialized this way, no registries are built upfront. The constructor returns in O(1) time.
TIP
Zero-Cost Constructor: Combining the Master Guard (automatic) and the defer mode allows Tempo to satisfy the "Zero-Cost Constructor" requirement for mass-processing applications.
๐งน 5.3 Noise Word Filtering (ignore) โ
Tempo allows you to specify "noise words" that should be ignored during natural language parsing. This is particularly useful for handling human-readable strings that contain connectors or filler words.
By default, Tempo ignores the word "at" (e.g., "Friday at 3pm" becomes "Friday 3pm" internally).
// Extend globally via Tempo.init()
// This adds 'the' and 'o-clock' to the existing default list (['at'])
Tempo.init({ registry: { ignores: ['the', 'o-clock'] } });
// Use in a specific instance via the Tempo constructor (new Tempo(...))
// This instance will ignore 'at', 'the', and 'o-clock'
const t = new Tempo('next Friday at 3 o-clock', {
registry: { ignores: 'o-clock' }
});
console.log(t.toString()); // Resolved correctly (noise words stripped)TIP
Registry Structure: The ignore registry accepts a String or an Array of strings. These are converted to a high-performance internal format to support efficient prototype-based shadowing. Note that values provided via Tempo.init() or the new Tempo() constructor merge with the default ignore list rather than replacing it.
๐ 5.4 Parse Planner & Pre-filtering โ
For high-performance applications, you can enable the Parse Planner to optimize the pattern-matching loop.
planner.preFilter (Boolean) โ
When enabled, Tempo performs a fast upfront classification of the input string (detecting digits, letters, colons, etc.) and skips layouts that cannot possibly match.
- Purely numeric inputs: Skips
event,period,wkd, andrellayouts. - Alpha-only inputs: Skips time-heavy layouts like
hmsoroff. - Colon detected: Prioritizes time-based layouts (
tm,dtm) to find a match faster.
Tempo.init({
planner: { preFilter: true }
});planner.layoutOrder (Array) โ
You can manually define the order in which layouts are attempted. This is useful if you know your data primarily uses a specific format (e.g., ISO dates) and want to avoid checking other layouts first.
Tempo.init({
planner: { layoutOrder: ['ymd', 'dt', 'tm', 'rel'] }
});TIP
Observability: Set debug: 'debug' along with planner.preFilter to see a detailed "Planner summary" in the console, showing how many layouts were skipped for a given input.
๐ Summary of Tiers โ
| Tier | Precedence | Best For... |
|---|---|---|
| Defaults | ๐ Baseline | Out-of-the-box reasonable settings. |
| Persistence | ๐ Low (Default) | Sticky user preferences (merges into baseline). |
| Discovery | ๐ฅ Medium | Micro-frontends and third-party integrations. |
| Global Init | ๐ฅ High | Standard baseline for the whole application. |
| Instance | ๐ฅ Highest | Ad-hoc overrides for specific calculations. |
TIP
Observability: When debug: 'debug' is set, Tempo logs its discovery path to the console (e.g., "Global Discovery found via Symbol"), making it easy to trace exactly where a setting originated.
INFO
Hidden Keys: The tempo.config getter excludes internal properties like anchor and input-only properties like value to keep the public API clean. These properties are still used internally for relative date resolution and instance hydration.