@arki/dot 0.1.2 → 0.1.4
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.
- package/README.md +370 -18
- package/dist/kernel/app-instance.d.ts +1 -1
- package/dist/kernel/app-instance.d.ts.map +1 -1
- package/dist/kernel/app-instance.js +131 -23
- package/dist/kernel/app-instance.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +2 -0
- package/dist/lifecycle.js.map +1 -1
- package/dist/pip-contract.d.ts +15 -2
- package/dist/pip-contract.d.ts.map +1 -1
- package/dist/pip-contract.js.map +1 -1
- package/package.json +1 -1
- package/src/kernel/app-instance.ts +134 -23
- package/src/lifecycle.ts +2 -0
- package/src/pip-contract.ts +19 -2
package/README.md
CHANGED
|
@@ -3,26 +3,31 @@
|
|
|
3
3
|
> TypeScript-first application composition framework for the ARKI package family.
|
|
4
4
|
|
|
5
5
|
`@arki/dot` is the kernel that wires **pips**, lifecycle hooks, dependency
|
|
6
|
-
|
|
7
|
-
authors a stable contract for declaring how their package
|
|
8
|
-
app, and gives app developers a single place to wire
|
|
6
|
+
injection, and diagnostics into a deterministic application boot. It gives
|
|
7
|
+
library authors a stable contract for declaring how their package
|
|
8
|
+
participates in an app, and gives app developers a single place to wire
|
|
9
|
+
those packages together — with the type checker verifying the wiring before
|
|
10
|
+
anything runs.
|
|
9
11
|
|
|
10
12
|
## What is a pip?
|
|
11
13
|
|
|
12
14
|
A **pip** is the unit a DOT app is built from — one self-describing,
|
|
13
15
|
lifecycle-aware piece of an application. Each pip:
|
|
14
16
|
|
|
15
|
-
- declares a **name**, **version**, and the
|
|
17
|
+
- declares a **name**, **version**, and the services it **needs** as a shape
|
|
18
|
+
of type witnesses;
|
|
19
|
+
- **provides** services by returning them from its `boot` hook — provides
|
|
20
|
+
are *inferred from the return type*, never declared separately;
|
|
16
21
|
- runs a **5-hook lifecycle** — `configure` → `boot` → `start` → `stop` → `dispose`;
|
|
17
|
-
- **publishes typed services** to a shared, type-safe registry that later
|
|
18
|
-
can read from;
|
|
19
|
-
- **composes deterministically** — pips boot in declaration order and
|
|
20
|
-
in reverse.
|
|
22
|
+
- **publishes typed services** to a shared, type-safe registry that later
|
|
23
|
+
pips can read from;
|
|
24
|
+
- **composes deterministically** — pips boot in declaration order and
|
|
25
|
+
dispose in reverse.
|
|
21
26
|
|
|
22
27
|
The name comes from the small dots on dice, dominoes, and music notation:
|
|
23
|
-
each pip is one small mark, and the *combination* of pips is what gives the
|
|
24
|
-
its value. Two pips on a die make a value of two; six pips make six. The
|
|
25
|
-
**are** the app — not optional add-ons to a hidden core.
|
|
28
|
+
each pip is one small mark, and the *combination* of pips is what gives the
|
|
29
|
+
app its value. Two pips on a die make a value of two; six pips make six. The
|
|
30
|
+
pips **are** the app — not optional add-ons to a hidden core.
|
|
26
31
|
|
|
27
32
|
```ts
|
|
28
33
|
import { defineApp } from '@arki/dot';
|
|
@@ -61,6 +66,19 @@ bun add @arki/dot
|
|
|
61
66
|
```ts
|
|
62
67
|
import { defineApp, pip, service } from '@arki/dot';
|
|
63
68
|
|
|
69
|
+
type Db = { query(sql: string): Promise<unknown[]> };
|
|
70
|
+
|
|
71
|
+
const dbPip = pip({
|
|
72
|
+
name: 'database',
|
|
73
|
+
async boot() {
|
|
74
|
+
const db = await openDb(process.env.DATABASE_URL);
|
|
75
|
+
return { db }; // ← this IS the provides declaration
|
|
76
|
+
},
|
|
77
|
+
async dispose({ db }) {
|
|
78
|
+
await db.close();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
64
82
|
const billingPip = pip({
|
|
65
83
|
name: 'billing',
|
|
66
84
|
version: '1.0.0',
|
|
@@ -94,6 +112,319 @@ await app.stop();
|
|
|
94
112
|
await app.dispose();
|
|
95
113
|
```
|
|
96
114
|
|
|
115
|
+
There is no registry to configure, no decorators, no reflection. The `needs`
|
|
116
|
+
shape is the injection contract; the `boot` return type is the provision
|
|
117
|
+
contract; the builder's type-level guard connects the two.
|
|
118
|
+
|
|
119
|
+
## Dependency injection
|
|
120
|
+
|
|
121
|
+
### Needs are witnesses, provides are inferred
|
|
122
|
+
|
|
123
|
+
`service<T>()` creates a phantom **type witness** — a runtime no-op that
|
|
124
|
+
carries `T` at the type level. The property name you give it is your local
|
|
125
|
+
alias *and* (for anonymous witnesses) the wire key:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const search = pip({
|
|
129
|
+
name: 'search',
|
|
130
|
+
needs: {
|
|
131
|
+
db: service<Db>(), // wire key 'db', local alias 'db'
|
|
132
|
+
log: service<Logger>(), // wire key 'log'
|
|
133
|
+
},
|
|
134
|
+
async boot({ db, log, $app }) { // $app/$pip/$config: kernel context
|
|
135
|
+
log.info(`indexing for ${$app}`);
|
|
136
|
+
return { search: await buildIndex(db) };
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`start`, `stop`, and `dispose` additionally receive the pip's **own
|
|
142
|
+
provides** — and because teardown runs in reverse declaration order, a pip's
|
|
143
|
+
needs are still alive in its `dispose`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
async dispose({ search, db }) { // own provide + still-live need
|
|
147
|
+
await search.flush(db);
|
|
148
|
+
},
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Wrong wiring does not compile
|
|
152
|
+
|
|
153
|
+
Declaration order IS boot order. The `.use()` guard makes unsatisfied needs,
|
|
154
|
+
type mismatches, and key collisions **compile errors at the call site**:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
defineApp('shop')
|
|
158
|
+
.use(billing) // ❌ "Expected 2 arguments, but got 1."
|
|
159
|
+
.use(database); // billing's `db` need has no earlier provider
|
|
160
|
+
|
|
161
|
+
defineApp('shop')
|
|
162
|
+
.use(database)
|
|
163
|
+
.use(database2); // ❌ collision: two providers of wire key 'db'
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Reordering the first example — or `rename`-ing one provider in the second —
|
|
167
|
+
makes both compile. The kernel re-validates at runtime with coded errors
|
|
168
|
+
(`E012` unsatisfied need, `E013` collision, `E014` reserved key) for
|
|
169
|
+
erased/dynamic composition, with full rollback of already-booted pips.
|
|
170
|
+
|
|
171
|
+
There is no dependency graph to debug and **cycles are unrepresentable**:
|
|
172
|
+
services flow strictly forward through the `.use()` chain. If two pips need
|
|
173
|
+
each other's services, that's a design smell the type system surfaces
|
|
174
|
+
immediately — merge them, or extract the shared piece into a third pip both
|
|
175
|
+
consume.
|
|
176
|
+
|
|
177
|
+
### Tokens — cross-package service contracts
|
|
178
|
+
|
|
179
|
+
An anonymous `service<T>()` couples consumer and provider by property name.
|
|
180
|
+
When a contract spans packages, a **token** owns the wire key and the local
|
|
181
|
+
alias becomes free choice:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// The package that owns the contract exports the token:
|
|
185
|
+
export const Db = token<NodePgDatabase<Schema>>()('arki.db');
|
|
186
|
+
|
|
187
|
+
// A provider publishes under the token's key:
|
|
188
|
+
const database = pip({
|
|
189
|
+
name: 'database',
|
|
190
|
+
async boot() {
|
|
191
|
+
return provide(Db, await connect()); // → { 'arki.db': NodePgDatabase }
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Any consumer, any package, any local alias:
|
|
196
|
+
const reports = pip({
|
|
197
|
+
name: 'reports',
|
|
198
|
+
needs: { warehouse: Db }, // wire key 'arki.db', alias yours
|
|
199
|
+
async boot({ warehouse }) { /* ... */ },
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Multi-instance with `rename`
|
|
204
|
+
|
|
205
|
+
Mounting an adapter twice would collide on its wire keys — loudly, at
|
|
206
|
+
compile time. `rename` is the multi-instance primitive, and
|
|
207
|
+
`Token.instance()` derives the matching contract:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const ReportsDb = Db.instance('reports'); // token for key 'arki.db#reports'
|
|
211
|
+
|
|
212
|
+
const app = await defineApp('shop')
|
|
213
|
+
.use(database({ url: PRIMARY_URL }))
|
|
214
|
+
.use(rename(
|
|
215
|
+
database({ url: REPLICA_URL }),
|
|
216
|
+
{ 'arki.db': ReportsDb.key }, // republish under the derived key
|
|
217
|
+
'reports-db', // distinct pip name
|
|
218
|
+
))
|
|
219
|
+
.use(pip({
|
|
220
|
+
name: 'analytics',
|
|
221
|
+
needs: { primary: Db, replica: ReportsDb },
|
|
222
|
+
async boot({ primary, replica }) { /* two live instances, both typed */ },
|
|
223
|
+
}))
|
|
224
|
+
.boot();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Renames compose left-to-right and are applied at publish time — collision
|
|
228
|
+
checks see the final keys.
|
|
229
|
+
|
|
230
|
+
### Lazy services
|
|
231
|
+
|
|
232
|
+
`lazy(init, { dispose })` publishes a **handle** instead of an open
|
|
233
|
+
resource. Initialization runs on first `get()` (memoized, single-flight;
|
|
234
|
+
failed attempts retry). Never-touched handles never initialize — and the
|
|
235
|
+
kernel auto-disposes initialized ones during teardown, *after* the
|
|
236
|
+
publishing pip's own `dispose` hook, even if that pip has none:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const search = pip({
|
|
240
|
+
name: 'search',
|
|
241
|
+
async boot() {
|
|
242
|
+
return {
|
|
243
|
+
search: lazy(() => connectToElastic(), {
|
|
244
|
+
dispose: client => client.close(),
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Consumers that shouldn't care whether a provider is eager or lazy declare a
|
|
252
|
+
**lifting witness** — `service.lazy<T>()` always delivers a `Lazy<T>`
|
|
253
|
+
handle, wrapping eager provides and passing lazy ones through by identity:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const suggestions = pip({
|
|
257
|
+
name: 'suggestions',
|
|
258
|
+
needs: { search: service.lazy<SearchClient>() },
|
|
259
|
+
async start({ search }) {
|
|
260
|
+
const client = await search.get(); // first access initializes
|
|
261
|
+
/* ... */
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Swapping the search provider between eager and lazy is invisible to every
|
|
267
|
+
consumer — the wiring guard accepts both shapes against the same witness.
|
|
268
|
+
|
|
269
|
+
### Kernel context — the reserved `$` namespace
|
|
270
|
+
|
|
271
|
+
Every service-carrying hook context includes `$app` (app name), `$pip` (pip
|
|
272
|
+
name), and `$config` (the `defineApp(name, { config })` bag). The `$` prefix
|
|
273
|
+
is enforced as reserved: `pip()` rejects `$`-prefixed needs aliases and
|
|
274
|
+
publish keys at compile time, and the kernel re-validates at runtime
|
|
275
|
+
(`DOT_LIFECYCLE_E014`) — kernel keys can never be shadowed.
|
|
276
|
+
|
|
277
|
+
## A complex setup
|
|
278
|
+
|
|
279
|
+
A distilled commerce platform showing everything above working together.
|
|
280
|
+
(The package's stress-test suite boots a 28-pip version of this and asserts
|
|
281
|
+
exact boot/teardown ordering.)
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { defineApp, lazy, pip, provide, rename, service, token } from '@arki/dot';
|
|
285
|
+
|
|
286
|
+
// ── contracts.ts — tokens owned by their respective packages ─────────
|
|
287
|
+
export const Env = token<AppEnv>()('shop.env');
|
|
288
|
+
export const Db = token<DbHandle>()('shop.db');
|
|
289
|
+
export const ReportsDb = Db.instance('reports');
|
|
290
|
+
export const Cache = token<CacheHandle>()('shop.cache');
|
|
291
|
+
export const Bus = token<MessageBus>()('shop.bus');
|
|
292
|
+
|
|
293
|
+
// ── infrastructure pips ───────────────────────────────────────────────
|
|
294
|
+
const env = pip({
|
|
295
|
+
name: 'env',
|
|
296
|
+
boot: ({ $config }) => provide(Env, parseEnv($config)),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const telemetry = pip({
|
|
300
|
+
name: 'telemetry',
|
|
301
|
+
needs: { env: Env },
|
|
302
|
+
boot: ({ env }) => ({ metrics: makeMetrics(env.OTEL_ENDPOINT) }),
|
|
303
|
+
dispose: ({ metrics }) => metrics.flush(),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const database = (url: string) =>
|
|
307
|
+
pip({
|
|
308
|
+
name: 'database',
|
|
309
|
+
needs: { metrics: service<Metrics>() },
|
|
310
|
+
async boot({ metrics }) {
|
|
311
|
+
return provide(Db, await connectPg(url, { metrics }));
|
|
312
|
+
},
|
|
313
|
+
async dispose(ctx) {
|
|
314
|
+
await ctx[Db.key].end(); // own provide, published under the token key
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const cache = pip({
|
|
319
|
+
name: 'cache',
|
|
320
|
+
needs: { env: Env },
|
|
321
|
+
boot: ({ env }) => provide(Cache, connectRedis(env.REDIS_URL)),
|
|
322
|
+
dispose: async ctx => ctx[Cache.key].quit(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const searchCluster = pip({
|
|
326
|
+
name: 'search-cluster',
|
|
327
|
+
needs: { env: Env },
|
|
328
|
+
boot: ({ env }) => ({
|
|
329
|
+
// Expensive external cluster — only opens if something get()s it.
|
|
330
|
+
search: lazy(() => connectElastic(env.ELASTIC_URL), {
|
|
331
|
+
dispose: client => client.close(),
|
|
332
|
+
}),
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const bus = pip({
|
|
337
|
+
name: 'bus',
|
|
338
|
+
boot: () => provide(Bus, makeInMemoryBus()),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ── domain pips ───────────────────────────────────────────────────────
|
|
342
|
+
const catalog = pip({
|
|
343
|
+
name: 'catalog',
|
|
344
|
+
needs: {
|
|
345
|
+
db: Db,
|
|
346
|
+
cache: Cache,
|
|
347
|
+
search: service.lazy<SearchClient>(), // eager or lazy provider — same code
|
|
348
|
+
},
|
|
349
|
+
boot: ({ db, cache, search }) => ({
|
|
350
|
+
catalog: makeCatalog({ db, cache, search }),
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const checkout = pip({
|
|
355
|
+
name: 'checkout',
|
|
356
|
+
needs: { db: Db, bus: Bus, catalog: service<Catalog>() },
|
|
357
|
+
boot: ({ db, bus, catalog }) => ({
|
|
358
|
+
checkout: makeCheckout({ db, bus, catalog }),
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const reporting = pip({
|
|
363
|
+
name: 'reporting',
|
|
364
|
+
needs: { replica: ReportsDb, catalog: service<Catalog>() },
|
|
365
|
+
boot: ({ replica, catalog }) => ({ reporting: makeReporting(replica, catalog) }),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ── process pips — real work happens in start/stop ───────────────────
|
|
369
|
+
const outboxWorker = pip({
|
|
370
|
+
name: 'outbox-worker',
|
|
371
|
+
needs: { db: Db, bus: Bus },
|
|
372
|
+
boot: ({ db, bus }) => ({ outbox: makeOutbox(db, bus) }),
|
|
373
|
+
start: ({ outbox }) => outbox.startPolling(),
|
|
374
|
+
stop: ({ outbox }) => outbox.drain(), // stop processing, keep resources
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const http = pip({
|
|
378
|
+
name: 'http',
|
|
379
|
+
needs: {
|
|
380
|
+
env: Env,
|
|
381
|
+
catalog: service<Catalog>(),
|
|
382
|
+
checkout: service<Checkout>(),
|
|
383
|
+
reporting: service<Reporting>(),
|
|
384
|
+
},
|
|
385
|
+
async boot({ env, catalog, checkout, reporting }) {
|
|
386
|
+
return { server: buildServer({ port: env.PORT, catalog, checkout, reporting }) };
|
|
387
|
+
},
|
|
388
|
+
start: ({ server }) => server.listen(),
|
|
389
|
+
stop: ({ server }) => server.close(),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ── composition — the order IS the architecture ──────────────────────
|
|
393
|
+
const app = await defineApp('shop', { config: process.env })
|
|
394
|
+
.use(env)
|
|
395
|
+
.use(telemetry)
|
|
396
|
+
.use(database(PRIMARY_URL))
|
|
397
|
+
.use(rename(database(REPLICA_URL), { 'shop.db': ReportsDb.key }, 'reports-db'))
|
|
398
|
+
.use(cache)
|
|
399
|
+
.use(searchCluster)
|
|
400
|
+
.use(bus)
|
|
401
|
+
.use(catalog)
|
|
402
|
+
.use(checkout)
|
|
403
|
+
.use(reporting)
|
|
404
|
+
.use(outboxWorker)
|
|
405
|
+
.use(http)
|
|
406
|
+
.start();
|
|
407
|
+
|
|
408
|
+
// SIGTERM → dispose() cascades stop() first, tears down in exact reverse:
|
|
409
|
+
// http, outbox (drained), reporting, checkout, catalog, bus,
|
|
410
|
+
// search cluster (only if it ever initialized), cache, reports-db,
|
|
411
|
+
// primary db, telemetry, env.
|
|
412
|
+
process.on('SIGTERM', () => void app.dispose());
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
What the type checker is holding for you in that chain:
|
|
416
|
+
|
|
417
|
+
- move `.use(catalog)` above `.use(cache)` → compile error at that line;
|
|
418
|
+
- delete `.use(bus)` → compile errors at `checkout` and `outboxWorker`;
|
|
419
|
+
- mount the second `database(...)` without `rename` → collision error;
|
|
420
|
+
- change `makeCatalog` to return something that isn't a `Catalog` →
|
|
421
|
+
every consumer's `.use()` flags the mismatch.
|
|
422
|
+
|
|
423
|
+
Concurrent lifecycle calls are safe: transitions are serialized on one
|
|
424
|
+
queue, same-phase calls coalesce onto one in-flight promise, and a
|
|
425
|
+
`dispose()` racing a slow `start()` always runs after it — the app ends
|
|
426
|
+
`disposed`, never resurrected.
|
|
427
|
+
|
|
97
428
|
## Pip authoring
|
|
98
429
|
|
|
99
430
|
`pip(config)` accepts a `needs` shape plus five lifecycle hooks. Hook
|
|
@@ -106,7 +437,7 @@ contexts carry the needed services (typed, under your local aliases) and
|
|
|
106
437
|
| `boot` | Open connections; the returned record is what the pip provides. |
|
|
107
438
|
| `start` | Begin processing (workers, subscribers, schedulers). |
|
|
108
439
|
| `stop` | Stop processing in reverse declaration order. |
|
|
109
|
-
| `dispose` | Free resources after `stop
|
|
440
|
+
| `dispose` | Free resources after `stop`; lazy handles auto-clean afterwards. |
|
|
110
441
|
|
|
111
442
|
Wiring is compile-time checked: `.use(pip)` fails to typecheck when the
|
|
112
443
|
pip's needs aren't satisfied by earlier `.use()` calls, or when its
|
|
@@ -122,7 +453,7 @@ order is boot order — same input, same order, every time.
|
|
|
122
453
|
`defineApp(name)` returns a builder. Calling `.use(pip)` accumulates
|
|
123
454
|
pips. The lifecycle then flows:
|
|
124
455
|
|
|
125
|
-
```
|
|
456
|
+
```text
|
|
126
457
|
defined ──configure()──▶ configured ──boot()──▶ booted ──start()──▶ started
|
|
127
458
|
│
|
|
128
459
|
disposed ◀──dispose()── stopped ◀──stop()───────┘
|
|
@@ -131,7 +462,19 @@ defined ──configure()──▶ configured ──boot()──▶ booted ─
|
|
|
131
462
|
`boot()` runs `configure()` implicitly if you skipped it. `start()` runs
|
|
132
463
|
`boot()` implicitly. `stop()` and `dispose()` always run in reverse
|
|
133
464
|
declaration order, even when an earlier hook failed — failure isolation is
|
|
134
|
-
part of the contract.
|
|
465
|
+
part of the contract. A boot failure rolls back every already-booted pip
|
|
466
|
+
before throwing.
|
|
467
|
+
|
|
468
|
+
Runtime failures carry stable codes (`DotLifecycleError.code`):
|
|
469
|
+
|
|
470
|
+
| Code | Meaning |
|
|
471
|
+
| -------------------- | ---------------------------------------------------- |
|
|
472
|
+
| `DOT_LIFECYCLE_E011` | Pip registered twice. |
|
|
473
|
+
| `DOT_LIFECYCLE_E012` | A need has no provider among earlier pips. |
|
|
474
|
+
| `DOT_LIFECYCLE_E013` | A published wire key collides with an earlier one. |
|
|
475
|
+
| `DOT_LIFECYCLE_E014` | A service key uses the reserved `$` (kernel) prefix. |
|
|
476
|
+
|
|
477
|
+
(Full table including hook-failure codes: [docs/lifecycle.md](./docs/lifecycle.md).)
|
|
135
478
|
|
|
136
479
|
## CLI
|
|
137
480
|
|
|
@@ -156,7 +499,9 @@ dot explain --app ./my-app.ts --json | jq '.data.pips'
|
|
|
156
499
|
```
|
|
157
500
|
|
|
158
501
|
The CLI emits the same envelope shape as the in-process diagnostics snapshot
|
|
159
|
-
(`app.diagnostics`), so the same downstream tools can consume either.
|
|
502
|
+
(`app.diagnostics`), so the same downstream tools can consume either. The
|
|
503
|
+
manifest's dependency edges are **observed, not declared** — the kernel
|
|
504
|
+
records which pip's published service satisfied which need during boot.
|
|
160
505
|
|
|
161
506
|
### `dot new <app-name>`
|
|
162
507
|
|
|
@@ -177,7 +522,14 @@ ship with the published tarball.
|
|
|
177
522
|
`@arki/dot` is intentionally small: it defines the contracts (pip shape,
|
|
178
523
|
lifecycle hooks, manifest schema, diagnostics envelope) and runs them. Adapters
|
|
179
524
|
that bridge databases, queues, auth providers, and HTTP routers live in their
|
|
180
|
-
own packages and consume `@arki/dot` as a peer dependency
|
|
525
|
+
own packages and consume `@arki/dot` as a peer dependency:
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { env } from '@arki/env/dot';
|
|
529
|
+
import { db } from '@arki/db/dot';
|
|
530
|
+
import { kv } from '@arki/kv/dot';
|
|
531
|
+
import { eventSourcing } from '@arki/event-sourcing/dot';
|
|
532
|
+
```
|
|
181
533
|
|
|
182
534
|
This keeps the kernel free of optional dependencies and lets each adapter
|
|
183
535
|
ship at its own cadence.
|
|
@@ -189,8 +541,8 @@ The full docs live in [`docs/`](./docs):
|
|
|
189
541
|
- [Principles](./docs/principles.md) — **read first.** The five rules every
|
|
190
542
|
API, error, and PR is measured against. Slightly playful, very precise.
|
|
191
543
|
- [Quickstart](./docs/quickstart.md) — boot your first app in five minutes.
|
|
192
|
-
- [Pip authoring](./docs/pip-authoring.md) — write your own
|
|
193
|
-
- [Lifecycle](./docs/lifecycle.md) — the 5-hook contract.
|
|
544
|
+
- [Pip authoring](./docs/pip-authoring.md) — write your own pip.
|
|
545
|
+
- [Lifecycle](./docs/lifecycle.md) — the 5-hook contract, idempotency, error codes.
|
|
194
546
|
- [Diagnostics](./docs/diagnostics.md) — `app.manifest`, `app.diagnostics`,
|
|
195
547
|
`dot explain`, `dot doctor`.
|
|
196
548
|
- [Adapter authoring](./docs/adapter-authoring.md) — expose your package as
|
|
@@ -49,7 +49,7 @@ export declare class DotAppImpl {
|
|
|
49
49
|
runConfigure(): void;
|
|
50
50
|
/** Public boot() — idempotent + concurrent-safe. */
|
|
51
51
|
boot(): Promise<void>;
|
|
52
|
-
/** Public start(). Boots first if needed. Idempotent. */
|
|
52
|
+
/** Public start(). Boots first if needed. Idempotent + concurrent-safe. */
|
|
53
53
|
start(): Promise<void>;
|
|
54
54
|
/** Public stop(). Idempotent + concurrent-safe. */
|
|
55
55
|
stop(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-instance.d.ts","sourceRoot":"","sources":["../../src/kernel/app-instance.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAEV,sBAAsB,EAKvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAqB,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACxF,OAAO,KAAK,EAA4C,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACnG,OAAO,KAAK,EAEV,cAAc,EAKf,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAA4C,MAAM,oBAAoB,CAAC;AAkE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7C,CAAC;AAEF;;;GAGG;AACH,qBAAa,UAAU;;
|
|
1
|
+
{"version":3,"file":"app-instance.d.ts","sourceRoot":"","sources":["../../src/kernel/app-instance.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAEV,sBAAsB,EAKvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAqB,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACxF,OAAO,KAAK,EAA4C,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACnG,OAAO,KAAK,EAEV,cAAc,EAKf,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAA4C,MAAM,oBAAoB,CAAC;AAkE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3C;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7C,CAAC;AAEF;;;GAGG;AACH,qBAAa,UAAU;;gBAmET,MAAM,EAAE,oBAAoB;IAoExC;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,IAAI;IAuIrD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,KAAK,IAAI,iBAAiB,CAE7B;IAED,IAAI,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEtC;IAED,IAAI,QAAQ,IAAI,cAAc,CAE7B;IAED,IAAI,WAAW,IAAI,sBAAsB,CAExC;IAED;;;;OAIG;IACH,YAAY,IAAI,IAAI;IA8KpB,oDAAoD;IAC9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiR3B,2EAA2E;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+H5B,mDAAmD;IAC7C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoH3B,sDAAsD;IAChD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA2T/B;AA0BD,kFAAkF;AAElF,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,KAAK,GAAG,EAAE,MAAM,oBAAoB,CAAC"}
|