@raubjo/architect 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +860 -0
  2. package/package.json +121 -0
  3. package/src/cache/cache.ts +46 -0
  4. package/src/cache/contract.ts +9 -0
  5. package/src/cache/manager.ts +110 -0
  6. package/src/cache/provider.ts +11 -0
  7. package/src/config/contract.ts +63 -0
  8. package/src/config/discovery.ts +99 -0
  9. package/src/config/env.global.d.ts +6 -0
  10. package/src/config/env.ts +68 -0
  11. package/src/config/index.ts +5 -0
  12. package/src/config/provider.ts +17 -0
  13. package/src/config/repository.ts +164 -0
  14. package/src/container/adapters/builtin.ts +323 -0
  15. package/src/container/contract.ts +43 -0
  16. package/src/container/runtime.ts +29 -0
  17. package/src/events/bus.ts +174 -0
  18. package/src/events/concerns/dispatchable.ts +10 -0
  19. package/src/events/provider.ts +9 -0
  20. package/src/events/types.ts +9 -0
  21. package/src/foundation/application.ts +136 -0
  22. package/src/foundation/current-application.ts +20 -0
  23. package/src/index.ts +58 -0
  24. package/src/log/contract.ts +21 -0
  25. package/src/log/drivers/console.ts +54 -0
  26. package/src/log/drivers/null.ts +23 -0
  27. package/src/log/drivers/stack.ts +46 -0
  28. package/src/log/manager.ts +76 -0
  29. package/src/log/provider.ts +11 -0
  30. package/src/react.ts +2 -0
  31. package/src/renderers/adapters/react.tsx +25 -0
  32. package/src/renderers/adapters/solid.tsx +26 -0
  33. package/src/renderers/adapters/svelte.ts +73 -0
  34. package/src/renderers/adapters/vue.ts +22 -0
  35. package/src/renderers/contract.ts +12 -0
  36. package/src/runtimes/react.tsx +81 -0
  37. package/src/runtimes/solid.tsx +47 -0
  38. package/src/runtimes/svelte.ts +17 -0
  39. package/src/runtimes/vue.ts +34 -0
  40. package/src/solid.ts +2 -0
  41. package/src/store/adapters/contract.ts +11 -0
  42. package/src/store/adapters/indexed-db.ts +187 -0
  43. package/src/store/adapters/local-storage.ts +48 -0
  44. package/src/store/adapters/memory.ts +35 -0
  45. package/src/store/manager.ts +68 -0
  46. package/src/store/provider.ts +10 -0
  47. package/src/store/store.ts +1 -0
  48. package/src/support/arr.ts +372 -0
  49. package/src/support/collection.ts +889 -0
  50. package/src/support/facades/cache.ts +6 -0
  51. package/src/support/facades/config.ts +6 -0
  52. package/src/support/facades/event.ts +6 -0
  53. package/src/support/facades/facade.ts +146 -0
  54. package/src/support/facades/index.ts +5 -0
  55. package/src/support/facades/log.ts +6 -0
  56. package/src/support/facades/store.ts +6 -0
  57. package/src/support/fluent.ts +56 -0
  58. package/src/support/globals.ts +8 -0
  59. package/src/support/lazy-collection.ts +341 -0
  60. package/src/support/manager.ts +53 -0
  61. package/src/support/num.ts +50 -0
  62. package/src/support/pipeline.ts +29 -0
  63. package/src/support/service-provider.ts +19 -0
  64. package/src/support/str.ts +682 -0
  65. package/src/svelte.ts +2 -0
  66. package/src/types/peer-deps.d.ts +10 -0
  67. package/src/vue.ts +2 -0
  68. package/tsconfig.json +15 -0
package/README.md ADDED
@@ -0,0 +1,860 @@
1
+ # @raubjo/architect
2
+
3
+ `@raubjo/architect` is a Laravel-inspired application container for frontend applications.
4
+
5
+ It gives you a predictable boot process, a shared dependency container, service providers, framework-specific runtime helpers, and small configuration/storage/cache primitives that can be used consistently across React, Solid, Svelte, and Vue projects.
6
+
7
+ The package is intentionally small. It does not try to replace your framework state tools. Instead, it gives you a structured place to register services and application infrastructure, then resolve those services from components and startup code.
8
+
9
+ The current runtime is browser-oriented. `Application.run()` expects `window`, and renderer adapters expect a DOM mount node.
10
+
11
+ ## Table of contents
12
+
13
+ - [Why this package exists](#why-this-package-exists)
14
+ - [What it includes](#what-it-includes)
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [Core mental model](#core-mental-model)
18
+ - [Application lifecycle](#application-lifecycle)
19
+ - [Container adapters](#container-adapters)
20
+ - [Working with service providers](#working-with-service-providers)
21
+ - [Framework integrations](#framework-integrations)
22
+ - [Configuration](#configuration)
23
+ - [Cache and storage](#cache-and-storage)
24
+ - [Facades](#facades)
25
+ - [Low-level utilities](#low-level-utilities)
26
+ - [Public exports](#public-exports)
27
+ - [Examples](#examples)
28
+ - [Development](#development)
29
+ - [Notes and constraints](#notes-and-constraints)
30
+
31
+ ## Why this package exists
32
+
33
+ In small frontend applications, service wiring usually starts in components, top-level files, or ad hoc singleton modules. That works until:
34
+
35
+ - the same dependencies need to be created in multiple places
36
+ - startup and teardown logic become hard to reason about
37
+ - configuration is scattered across modules
38
+ - services need to be shared across framework components and non-UI code
39
+ - you want clearer separation between business logic and rendering
40
+
41
+ `@raubjo/architect` solves that by giving you:
42
+
43
+ - a central application object
44
+ - a container abstraction
45
+ - provider-based registration and boot hooks
46
+ - consistent setup and cleanup ordering
47
+ - runtime-specific `useService(...)` helpers
48
+ - optional facade-style access for config, cache, and storage
49
+
50
+ If you are familiar with Laravel service providers and container bindings, the design will feel familiar.
51
+
52
+ ## What it includes
53
+
54
+ The current codebase provides:
55
+
56
+ - `Application` orchestration with `register -> services -> boot -> startup -> render`
57
+ - a built-in dependency injection container
58
+ - an Inversify adapter behind the same container contract
59
+ - service providers with cleanup support
60
+ - runtime helpers for React, Solid, Svelte, and Vue
61
+ - renderer adapters for React, Solid, Svelte, and Vue
62
+ - `ConfigRepository` with dot-notation lookups and typed getters
63
+ - `StorageManager` with memory, local storage, and IndexedDB-backed adapters
64
+ - `CacheManager` built on the same adapter contract as storage
65
+ - facades for config, cache, and storage
66
+ - small utility exports such as `env`, `Str`, and filesystem/config helpers
67
+
68
+ ## Installation
69
+
70
+ ```sh
71
+ bun add @raubjo/architect reflect-metadata
72
+ ```
73
+
74
+ Framework integrations are imported from subpaths:
75
+
76
+ ```ts
77
+ import { ContextProvider, useService } from "@raubjo/architect/react";
78
+ ```
79
+
80
+ Optional peer dependencies depend on the runtime you use:
81
+
82
+ - `react` and `react-dom` for React
83
+ - `solid-js` for Solid
84
+ - `svelte` for Svelte
85
+ - `vue` for Vue
86
+ - `inversify` if you want the Inversify container adapter
87
+
88
+ If you use decorator-based constructor injection with the built-in container, or Inversify, load `reflect-metadata` before resolving those classes:
89
+
90
+ ```ts
91
+ import "reflect-metadata";
92
+ ```
93
+
94
+ ## Quick start
95
+
96
+ This is the smallest useful application shape:
97
+
98
+ ```ts
99
+ import "reflect-metadata";
100
+ import { Application, ServiceProvider, type ServiceProviderContext } from "@raubjo/architect";
101
+ import { ContextProvider, useService } from "@raubjo/architect/react";
102
+ import ReactDOM from "react-dom/client";
103
+ import { createElement } from "react";
104
+
105
+ class CounterService {
106
+ protected count = 0;
107
+
108
+ increment(): number {
109
+ this.count += 1;
110
+ return this.count;
111
+ }
112
+
113
+ current(): number {
114
+ return this.count;
115
+ }
116
+ }
117
+
118
+ class CounterProvider extends ServiceProvider {
119
+ register({ container }: ServiceProviderContext): void {
120
+ container.singleton(CounterService, CounterService);
121
+ }
122
+ }
123
+
124
+ function App() {
125
+ const counter = useService(CounterService);
126
+
127
+ return (
128
+ <button
129
+ onClick={() => {
130
+ counter.increment();
131
+ }}
132
+ >
133
+ Count: {counter.current()}
134
+ </button>
135
+ );
136
+ }
137
+
138
+ const application = Application.configure({
139
+ container: { adapter: "builtin" },
140
+ config: {
141
+ app: { name: "Architect Example" },
142
+ },
143
+ })
144
+ .withProviders([new CounterProvider()]);
145
+
146
+ const root = ReactDOM.createRoot(document.getElementById("root")!);
147
+ root.render(createElement(ContextProvider, { application }, createElement(App)));
148
+ ```
149
+
150
+ `Application.run()` returns:
151
+
152
+ ```ts
153
+ {
154
+ container,
155
+ stop,
156
+ }
157
+ ```
158
+
159
+ The returned `container` is the active runtime container. `stop()` runs cleanup callbacks in reverse order, clears facade caches, and flushes the container.
160
+
161
+ ## Core mental model
162
+
163
+ The package is built around a few simple ideas:
164
+
165
+ ### 1. The application owns startup
166
+
167
+ `Application.configure(...).run()` is the entry point. It creates the container, registers built-in services, runs provider hooks, runs startup callbacks, and mounts a renderer when one is configured.
168
+
169
+ ### 2. Providers own wiring
170
+
171
+ Register application services once in providers instead of rebuilding them inside components.
172
+
173
+ ### 3. Components resolve services, not construction rules
174
+
175
+ Framework code should usually call `useService(...)` or consume already-registered abstractions. Construction details stay in providers and service registrars.
176
+
177
+ ### 4. Reactivity is your service’s responsibility
178
+
179
+ `useService(...)` resolves an object from the container. It does not automatically subscribe a component to changes inside that object.
180
+
181
+ If a service exposes mutable state, you still need a framework-appropriate reactive mechanism such as:
182
+
183
+ - component state
184
+ - signals
185
+ - framework stores
186
+ - subscriptions
187
+ - Zustand or another state library
188
+
189
+ The examples in this repository show both simple imperative services and services that expose reactive state mechanisms.
190
+
191
+ ## Application lifecycle
192
+
193
+ `Application.run()` executes in this order:
194
+
195
+ 1. provider `register()`
196
+ 2. `.withServices(...)` callbacks
197
+ 3. provider `boot()`
198
+ 4. `.withStartup(...)` callbacks
199
+
200
+ Cleanup runs in reverse order when `stop()` is called:
201
+
202
+ 1. startup cleanup
203
+ 2. provider `boot()` cleanup
204
+ 3. service registrar cleanup
205
+ 4. provider `register()` cleanup
206
+
207
+ The application also registers a `beforeunload` listener and calls `stop()` once when the page unloads.
208
+
209
+ ### Built-in services registered by `Application`
210
+
211
+ Every application instance automatically registers:
212
+
213
+ - `"config"` and `ConfigRepository`
214
+ - `"storage"` and `StorageManager`
215
+ - `"cache"` and `CacheManager`
216
+
217
+ That means you can resolve them by string identifier or by class identifier.
218
+
219
+ ### Application builder methods
220
+
221
+ The main fluent API is:
222
+
223
+ - `Application.configure(basePath?: string)`
224
+ - `Application.configure(options?: { basePath?: string; container?: ...; config?: ... })`
225
+ - `.withProviders(providers)`
226
+ - `.withServices(registerServices)`
227
+ - `.withStartup(startupHandler)`
228
+ - `.run()`
229
+
230
+ Static resolution is also available:
231
+
232
+ ```ts
233
+ const config = Application.make("config");
234
+ ```
235
+
236
+ That only works after `run()`. Calling `Application.make(...)` before the app starts throws.
237
+
238
+ ## Container adapters
239
+
240
+ The package exposes a common `ContainerContract` and currently ships with two concrete adapters:
241
+
242
+ - built-in container
243
+ - Inversify container
244
+
245
+ ### Selecting an adapter
246
+
247
+ Container runtime options look like this:
248
+
249
+ ```ts
250
+ {
251
+ adapter?: "auto" | "builtin" | "inversify";
252
+ factory?: (() => ContainerContract) | null;
253
+ }
254
+ ```
255
+
256
+ Resolution rules are:
257
+
258
+ - if `factory` is provided, it wins
259
+ - if `adapter` is `"builtin"`, the built-in container is used
260
+ - if `adapter` is `"inversify"`, the runtime expects an Inversify factory to be registered
261
+ - if `adapter` is `"auto"`, the runtime prefers Inversify when it detects an `inversify` dependency and a factory is available; otherwise it falls back to the built-in container
262
+
263
+ ### Built-in container
264
+
265
+ The built-in container supports:
266
+
267
+ - `bind(...).to(...)`
268
+ - `bind(...).toConstantValue(...)`
269
+ - `singleton(identifier, concrete)`
270
+ - `bind(identifier, concrete)`
271
+ - `instance(identifier, value)`
272
+ - `make(...)` and `get(...)`
273
+ - `bound(...)` and `has(...)`
274
+ - `unbind(...)`, `unbindAll()`, and `flush()`
275
+
276
+ Constructor injection is supported through `design:paramtypes` metadata. Parameter-level token overrides are supported through the exported `injectDependency(...)` decorator.
277
+
278
+ Example:
279
+
280
+ ```ts
281
+ import "reflect-metadata";
282
+ import {
283
+ Application,
284
+ injectDependency,
285
+ } from "@raubjo/architect";
286
+
287
+ class Logger {
288
+ log(message: string) {
289
+ console.log(message);
290
+ }
291
+ }
292
+
293
+ class Greeter {
294
+ constructor(
295
+ public readonly logger: Logger,
296
+ @injectDependency("app.name") public readonly appName: string,
297
+ ) {}
298
+
299
+ greet() {
300
+ this.logger.log(`Hello from ${this.appName}`);
301
+ }
302
+ }
303
+
304
+ const { container } = Application.configure({
305
+ container: { adapter: "builtin" },
306
+ })
307
+ .withServices(({ container }) => {
308
+ container.singleton(Logger, Logger);
309
+ container.instance("app.name", "Architect");
310
+ container.bind(Greeter, Greeter);
311
+ })
312
+ .run();
313
+
314
+ container.make(Greeter).greet();
315
+ ```
316
+
317
+ Important built-in container behaviors:
318
+
319
+ - string, symbol, and class identifiers are supported
320
+ - unbound class identifiers can still be resolved directly
321
+ - circular constructor dependencies throw
322
+ - missing constructor metadata for a parameter throws
323
+ - singletons are cached after first resolution
324
+ - transient factories/classes resolve anew each time
325
+
326
+ ### Inversify adapter
327
+
328
+ If you want to use Inversify, install `inversify` and register an adapter factory on `globalThis.__iocContainerFactoryRegistry`:
329
+
330
+ ```ts
331
+ import "reflect-metadata";
332
+ import { Application } from "@raubjo/architect";
333
+ import InversifyContainer from "@raubjo/architect/container/adapters/inversify";
334
+
335
+ globalThis.__iocContainerFactoryRegistry = {
336
+ inversify: () => new InversifyContainer(),
337
+ };
338
+
339
+ Application.configure({
340
+ container: { adapter: "inversify" },
341
+ }).run();
342
+ ```
343
+
344
+ If you explicitly request `adapter: "inversify"` and no factory is registered, the runtime throws a configuration error.
345
+
346
+ ## Working with service providers
347
+
348
+ Providers are the main extension point for application wiring.
349
+
350
+ The base class is:
351
+
352
+ ```ts
353
+ class ServiceProvider {
354
+ register(context): void | Cleanup {}
355
+ boot(context): void | Cleanup {}
356
+ }
357
+ ```
358
+
359
+ Use `register()` for bindings and `boot()` for work that should happen after all providers and manual service registrations are complete.
360
+
361
+ Both methods may optionally return a cleanup callback.
362
+
363
+ `DeferrableServiceProvider` is also exported, but in the current implementation it is only a base class with a `provides()` method. The application runtime does not yet perform automatic deferred loading based on that contract.
364
+
365
+ Example:
366
+
367
+ ```ts
368
+ import {
369
+ ServiceProvider,
370
+ type Cleanup,
371
+ type ServiceProviderContext,
372
+ } from "@raubjo/architect";
373
+
374
+ class HeartbeatService {
375
+ protected intervalId: number | null = null;
376
+ protected ticksValue = 0;
377
+
378
+ ticks() {
379
+ return this.ticksValue;
380
+ }
381
+
382
+ start(): Cleanup {
383
+ if (this.intervalId !== null) {
384
+ return () => this.stop();
385
+ }
386
+
387
+ this.intervalId = window.setInterval(() => {
388
+ this.ticksValue += 1;
389
+ }, 1000);
390
+
391
+ return () => this.stop();
392
+ }
393
+
394
+ stop() {
395
+ if (this.intervalId === null) {
396
+ return;
397
+ }
398
+
399
+ window.clearInterval(this.intervalId);
400
+ this.intervalId = null;
401
+ }
402
+ }
403
+
404
+ class HeartbeatProvider extends ServiceProvider {
405
+ register({ container }: ServiceProviderContext): void {
406
+ container.singleton(HeartbeatService, HeartbeatService);
407
+ }
408
+
409
+ boot({ container }: ServiceProviderContext): Cleanup {
410
+ return container.get(HeartbeatService).start();
411
+ }
412
+ }
413
+ ```
414
+
415
+ You can mix providers with `.withServices(...)` and `.withStartup(...)` when a full provider class would be excessive.
416
+
417
+ ## Framework integrations
418
+
419
+ Each supported framework has:
420
+
421
+ - a runtime entrypoint with service resolution helpers
422
+ - a context/provider helper so you choose the context boundary
423
+ - an optional renderer adapter you can use directly if you want a turnkey mount
424
+
425
+ ### React
426
+
427
+ Import from `@raubjo/architect/react`:
428
+
429
+ - `ContextProvider`
430
+ - `ApplicationProvider`
431
+ - `Renderer`
432
+ - `useService`
433
+
434
+ `ContextProvider` runs the application and provides the container through React context, so you can decide where the application boundary lives in your component tree.
435
+
436
+ ```ts
437
+ import { useService } from "@raubjo/architect/react";
438
+
439
+ function App() {
440
+ const service = useService(MyService);
441
+ return <div>{service.value()}</div>;
442
+ }
443
+ ```
444
+
445
+ ### Solid
446
+
447
+ Import from `@raubjo/architect/solid`:
448
+
449
+ - `ContextProvider`
450
+ - `ApplicationProvider`
451
+ - `Renderer`
452
+ - `useService`
453
+
454
+ `ContextProvider` runs the application and provides the container through Solid context.
455
+
456
+ ```ts
457
+ import { useService } from "@raubjo/architect/solid";
458
+
459
+ function App() {
460
+ const service = useService(MyService);
461
+ return <div>{service.value()}</div>;
462
+ }
463
+ ```
464
+
465
+ ### Svelte
466
+
467
+ Import from `@raubjo/architect/svelte`:
468
+
469
+ - `Renderer`
470
+ - `containerKey`
471
+ - `provideContainer` (alias: `ContextProvider`)
472
+ - `useService`
473
+
474
+ The Svelte renderer passes `container` as a prop to the root component. Inside the component, call `provideContainer(container)` (or `ContextProvider(container)`) before using `useService(...)`.
475
+
476
+ ```svelte
477
+ <script lang="ts">
478
+ import { provideContainer, useService } from "@raubjo/architect/svelte";
479
+ import CounterService from "./counter/service";
480
+
481
+ export let container: unknown;
482
+
483
+ provideContainer(container as never);
484
+ const counter = useService(CounterService);
485
+ </script>
486
+ ```
487
+
488
+ ### Vue
489
+
490
+ Import from `@raubjo/architect/vue`:
491
+
492
+ - `ContextProvider`
493
+ - `Renderer`
494
+ - `useService`
495
+
496
+ `ContextProvider` runs the application, provides the container via Vue injection, and renders its slot content.
497
+
498
+ ```vue
499
+ <script setup lang="ts">
500
+ import { useService } from "@raubjo/architect/vue";
501
+
502
+ const service = useService(MyService);
503
+ </script>
504
+ ```
505
+
506
+ ### Renderer behavior
507
+
508
+ All renderers:
509
+
510
+ - look up a DOM mount node by `rootElementId`
511
+ - throw if the mount node does not exist
512
+ - return a cleanup function that unmounts the rendered tree
513
+
514
+ Renderers are not coupled to `Application`. If you want turnkey mounting, call `app.run()` yourself, then pass the returned `container` into a renderer (or into a framework `ContextProvider`).
515
+
516
+ ## Configuration
517
+
518
+ `Application` does not implicitly load config files during startup. The configuration source for an app instance is the object you pass into `Application.configure({ config })`.
519
+
520
+ Example:
521
+
522
+ ```ts
523
+ const running = Application.configure({
524
+ config: {
525
+ app: {
526
+ name: "Architect",
527
+ timezone: "UTC",
528
+ },
529
+ storage: {
530
+ driver: "memory",
531
+ },
532
+ cache: {
533
+ default: "memory",
534
+ },
535
+ },
536
+ }).run();
537
+ ```
538
+
539
+ That data is cloned per application instance before it is wrapped in `ConfigRepository`.
540
+
541
+ ### `ConfigRepository`
542
+
543
+ `ConfigRepository` supports:
544
+
545
+ - dot-notation reads
546
+ - `has(...)`
547
+ - `get(...)`
548
+ - `getMany(...)`
549
+ - typed accessors: `string`, `integer`, `float`, `boolean`, `array`
550
+ - writes via `set(...)`
551
+ - array mutation helpers via `prepend(...)` and `push(...)`
552
+ - offset-style helpers like `offsetGet(...)` and `offsetUnset(...)`
553
+
554
+ Example:
555
+
556
+ ```ts
557
+ import { ConfigRepository } from "@raubjo/architect";
558
+
559
+ const config = new ConfigRepository({
560
+ app: {
561
+ name: "Architect",
562
+ retries: 3,
563
+ tags: ["frontend"],
564
+ },
565
+ });
566
+
567
+ config.string("app.name"); // "Architect"
568
+ config.integer("app.retries"); // 3
569
+ config.push("app.tags", "ioc");
570
+ ```
571
+
572
+ ### `env(...)`
573
+
574
+ The exported `env(...)` helper reads from:
575
+
576
+ 1. `import.meta.env`
577
+ 2. `process.env`
578
+
579
+ It normalizes common string forms:
580
+
581
+ - `"true"` and `"(true)"` -> `true`
582
+ - `"false"` and `"(false)"` -> `false`
583
+ - `"null"` and `"(null)"` -> `null`
584
+ - `"empty"` and `"(empty)"` -> `""`
585
+
586
+ It is also registered globally as `env` when the module is loaded.
587
+
588
+ ## Cache and storage
589
+
590
+ The package ships with two manager abstractions:
591
+
592
+ - `StorageManager`
593
+ - `CacheManager`
594
+
595
+ Both use the same async adapter contract:
596
+
597
+ ```ts
598
+ interface Adapter {
599
+ get<T = unknown>(key: string): Promise<T | null>;
600
+ set<T = unknown>(key: string, value: T): Promise<void>;
601
+ has(key: string): Promise<boolean>;
602
+ delete(key: string): Promise<void>;
603
+ clear(): Promise<void>;
604
+ keys(): Promise<string[]>;
605
+ }
606
+ ```
607
+
608
+ ### Storage
609
+
610
+ `StorageManager` reads its active driver from `storage.driver`.
611
+
612
+ Supported built-in drivers:
613
+
614
+ - `memory`
615
+ - `local`
616
+ - `indexed`
617
+
618
+ Default behavior:
619
+
620
+ - `memory` uses an in-memory `Map`
621
+ - `local` uses `window.localStorage` when available, otherwise falls back to memory
622
+ - `indexed` uses `indexedDB` when available, otherwise falls back to memory
623
+
624
+ Example:
625
+
626
+ ```ts
627
+ const app = Application.configure({
628
+ config: {
629
+ storage: {
630
+ driver: "local",
631
+ },
632
+ },
633
+ }).run();
634
+
635
+ const storage = app.container.get("storage");
636
+ await storage.set("draft", { title: "Post" });
637
+ ```
638
+
639
+ You can also switch drivers manually:
640
+
641
+ ```ts
642
+ storage.use("memory");
643
+ ```
644
+
645
+ ### Cache
646
+
647
+ `CacheManager` is intentionally very similar, but it reads from a cache-specific configuration shape:
648
+
649
+ ```ts
650
+ {
651
+ cache: {
652
+ default: "memory",
653
+ stores: {
654
+ memory: { driver: "memory" },
655
+ persistent: { driver: "indexed" },
656
+ },
657
+ },
658
+ }
659
+ ```
660
+
661
+ `cache.default` selects the active store, and `cache.stores` maps store names to drivers.
662
+
663
+ If a configured store points to an unknown driver, the implementation falls back to the memory driver.
664
+
665
+ Example:
666
+
667
+ ```ts
668
+ const cache = app.container.get("cache");
669
+
670
+ await cache.set("session", { ok: true });
671
+ await cache.get("session");
672
+
673
+ cache.use("persistent");
674
+ ```
675
+
676
+ ### IndexedDB fallback behavior
677
+
678
+ The IndexedDB adapter is resilient by design:
679
+
680
+ - if IndexedDB is unavailable, it falls back to another adapter
681
+ - if database open or request operations fail, it falls back to the configured fallback adapter
682
+ - the default fallback is an in-memory adapter
683
+
684
+ That makes it safe to use in mixed browser or test environments without adding a separate compatibility layer.
685
+
686
+ ## Facades
687
+
688
+ Facade classes provide a static access pattern similar to Laravel:
689
+
690
+ - `Config`
691
+ - `Cache`
692
+ - `Store`
693
+
694
+ Example:
695
+
696
+ ```ts
697
+ import { Config, Cache, Store } from "@raubjo/architect";
698
+
699
+ const name = Config.get("app.name");
700
+ await Cache.set("token", "abc");
701
+ await Store.set("draft", { id: 1 });
702
+ ```
703
+
704
+ Facade resolution works through `Application.make(...)`, and resolved instances are cached per facade accessor until the application is stopped or facade caches are cleared.
705
+
706
+ That means facades only work correctly after the application has started.
707
+
708
+ ## Low-level utilities
709
+
710
+ The codebase also exports a few lower-level building blocks.
711
+
712
+ ### `FileSystem` and local config loading
713
+
714
+ `FileSystem` is a small wrapper around a `FileSystemAdapter`. The bundled `LocalAdapter` can discover config modules from `config/*` and `src/config/*` using `import.meta.glob(...)`.
715
+
716
+ This is not used automatically by `Application` today, but it is available if you want explicit config-module loading in your own bootstrap code.
717
+
718
+ ### `loadConfig(...)`
719
+
720
+ `@raubjo/architect/config/adapters/esm` exports `loadConfig(modules)`, which turns an ESM module map into a config object keyed by filename.
721
+
722
+ ### `Str`
723
+
724
+ `Str` provides a few common string helpers:
725
+
726
+ - `lower`
727
+ - `upper`
728
+ - `length`
729
+ - `contains`
730
+ - `startsWith`
731
+ - `endsWith`
732
+ - `replace`
733
+ - `snake`
734
+ - `kebab`
735
+ - `studly`
736
+ - `camel`
737
+ - `slug`
738
+
739
+ It is also registered globally as `Str` when the package is loaded.
740
+
741
+ ## Public exports
742
+
743
+ The main export surface is defined in `package.json`.
744
+
745
+ Important root exports include:
746
+
747
+ - `Application`
748
+ - `BuiltinContainer`
749
+ - `injectDependency`
750
+ - `ServiceProvider`
751
+ - `DeferrableServiceProvider`
752
+ - `ConfigRepository`
753
+ - `env`
754
+ - `Str`
755
+ - `Config`, `Cache`, `Store`
756
+ - `CacheManager`
757
+ - `StorageManager`
758
+ - `MemoryStorageAdapter`
759
+ - `LocalStorageAdapter`
760
+ - `IndexedDbAdapter`
761
+ - `FileSystem`
762
+ - `LocalAdapter`
763
+
764
+ Important subpath exports include:
765
+
766
+ - `@raubjo/architect/react`
767
+ - `@raubjo/architect/solid`
768
+ - `@raubjo/architect/svelte`
769
+ - `@raubjo/architect/vue`
770
+ - `@raubjo/architect/application`
771
+ - `@raubjo/architect/container/contract`
772
+ - `@raubjo/architect/container/adapters/inversify`
773
+ - `@raubjo/architect/container/adapters/builtin`
774
+ - `@raubjo/architect/config/adapters/esm`
775
+ - `@raubjo/architect/config/env`
776
+ - `@raubjo/architect/config/repository`
777
+ - `@raubjo/architect/cache/manager`
778
+ - `@raubjo/architect/storage/manager`
779
+ - `@raubjo/architect/filesystem/filesystem`
780
+ - renderer and facade subpaths
781
+
782
+ ## Examples
783
+
784
+ The repository contains runnable examples under `examples/`:
785
+
786
+ - `examples/react`
787
+ - `examples/solid`
788
+ - `examples/svelte`
789
+ - `examples/vue`
790
+
791
+ Each example shows:
792
+
793
+ - application bootstrap
794
+ - provider registration
795
+ - service resolution from components
796
+ - a user-driven counter service
797
+ - a heartbeat/background service with startup and cleanup behavior
798
+
799
+ To run one:
800
+
801
+ ```sh
802
+ cd examples/react
803
+ bun install
804
+ bun run dev
805
+ ```
806
+
807
+ The example applications are intentionally minimal and are the best place to inspect framework-specific bootstrapping details.
808
+
809
+ ## Development
810
+
811
+ This repository is written in TypeScript and tested with Bun.
812
+
813
+ Useful scripts:
814
+
815
+ ```sh
816
+ bun test
817
+ bun test --coverage
818
+ ```
819
+
820
+ Formatting:
821
+
822
+ ```sh
823
+ bun run fix
824
+ ```
825
+
826
+ The project uses:
827
+
828
+ - ESM modules
829
+ - `strict` TypeScript mode
830
+ - `moduleResolution: "Bundler"`
831
+ - JSX enabled for React-oriented source files
832
+
833
+ Tests cover:
834
+
835
+ - application lifecycle ordering and cleanup
836
+ - built-in container behavior
837
+ - Inversify adapter behavior
838
+ - runtime helpers for each supported framework
839
+ - configuration helpers
840
+ - storage adapters and fallbacks
841
+ - cache manager behavior
842
+ - facade resolution
843
+
844
+ ## Notes and constraints
845
+
846
+ The current implementation has a few important constraints worth knowing up front:
847
+
848
+ - `Application.configure(basePath)` is still supported, but startup configuration is driven by `configure({ config })`, not automatic filesystem discovery.
849
+ - `Application.clearConfigCache()` exists for compatibility but is currently a no-op.
850
+ - `useService(...)` resolves a dependency from the active container; it does not subscribe components to service internals automatically.
851
+ - `DeferrableServiceProvider` does not currently enable deferred provider loading by itself.
852
+ - Svelte root components are expected to receive `container` as a prop and call `provideContainer(...)` before using `useService(...)`.
853
+ - Explicit `adapter: "inversify"` requires a registered factory on `globalThis.__iocContainerFactoryRegistry`.
854
+ - If you set a root component without a renderer, or a renderer without a root component, the application throws.
855
+ - `Application.run()` is designed for browser/client execution and assumes `window` is available.
856
+ - All renderer adapters require the target mount node to exist in the DOM.
857
+
858
+ ## License
859
+
860
+ MIT