@ogygia/flags 0.11.0 → 0.13.0

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 CHANGED
@@ -31,4 +31,4 @@ hero(c); // 'control' | 'bold', sticky per visitor
31
31
 
32
32
  A native `flag()` works without the plugin. The plugin primes a `decide({ source })` (OpenFeature, OFREP, or your own function) one time per request, and drains the `exposure` sink at the end of each request. OpenFeature lives at `@ogygia/flags/openfeature`.
33
33
 
34
- Docs: [Flags & experiments](https://ogygiajs.dev/docs/app/experiments).
34
+ Docs: [@ogygia/flags](https://ogygiajs.dev/docs/flags/overview), with the full [Flags & experiments](https://ogygiajs.dev/docs/flags/guide) guide.
package/dist/index.d.ts CHANGED
@@ -58,14 +58,19 @@ export interface DecideOptions {
58
58
  * decide({ source: openfeature(client), overrides: (c) => !!c.visitor?.qa, exposure: send });
59
59
  */
60
60
  export declare function decide(opts: DecideOptions): void;
61
- /** Drain queued exposures to the sink. Called at each request's end (serverless-safe tail) and on
62
- * the size/age caps. A sink failure is contained — a lost batch is never a request's problem. */
63
- export declare function flush_exposures(): void;
61
+ /**
62
+ * Send the queued exposures to the sink now. Called at each request's end (the flags plugin's `done`)
63
+ * and by the size and age caps. Resolves when they have reached the sink (behind the batches before
64
+ * them); `undefined` when nothing is queued or being sent. A sink failure is contained: a lost batch
65
+ * is never a request's problem.
66
+ */
67
+ export declare function flush_exposures(): Promise<void> | undefined;
64
68
  /**
65
69
  * Resolve the source ONCE per request over every registered flag, caching decisions on the
66
70
  * Request so reads stay sync. Awaited by the flags plugin's `request` stage (flags/server.ts), above
67
71
  * the page / hole split. Idempotent; a no-op without a source. Never throws — a source failure
68
- * degrades every flag to its native rule, it does not take the request down.
72
+ * degrades every flag to its native rule, it does not take the request down, and is said
73
+ * (`flags_source_failed`, once per failing streak).
69
74
  */
70
75
  export declare function prime_flags(c: CtxLike): Promise<void>;
71
76
  /** A per-request component chooser for `page()` — `page(hero.pick({ control: A, bold: B }))`.
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { flags_app } from "./app.js";
2
+ import { warn } from "./messages.js";
3
+ import { source_answered, source_failed, source_mark, source_reset } from "./source-health.js";
2
4
  import { BROWSER, DEV } from "esm-env";
3
5
  import { fnv1a32 } from "@ogygia/core/plugin/utils";
6
+ import { machine, outbox, policy } from "@ogygia/machine";
4
7
  // src/index.ts
5
8
  /**
6
9
  * `flag()` — the ONE assignment primitive. A kill switch, a rollout, a targeting rule, and an
@@ -20,8 +23,10 @@ import { fnv1a32 } from "@ogygia/core/plugin/utils";
20
23
  */
21
24
  /** Written by an upstream shell's identity hop (the fragment feature); absent = the tier is skipped. */
22
25
  const CLAIMS = Symbol.for("ogygia.claims.v1");
23
- /** Per-request source resolution, cached on the Request (GC'd with it). */
24
- const RESOLVED = Symbol.for("ogygia.flags.resolved.v1");
26
+ /** Per-request source resolution, cached on the Request (GC'd with it): its {@link Prime}. */
27
+ const RESOLVED = Symbol.for("ogygia.flags.resolved.v2");
28
+ const prime_of = (c) => c.request[RESOLVED];
29
+ const set_prime = (c, p) => c.request[RESOLVED] = p;
25
30
  /** 0..9999 — sticky hash bucket for (salt, visitor). Uniform + deterministic, not cryptographic;
26
31
  * the shared pure hash keeps this module safe in ANY bundle (runtime + every island entry). */
27
32
  const pct = (s) => fnv1a32(s) % 1e4;
@@ -52,8 +57,6 @@ let _batch = {
52
57
  max: 25,
53
58
  ms: 2e3
54
59
  };
55
- let _queue = [];
56
- let _timer = null;
57
60
  /**
58
61
  * Configure flags for this server, once (hooks.server.ts / any server module). Everything is
59
62
  * optional — with no `decide()` at all, flags are native, sticky, and zero-JS.
@@ -61,7 +64,10 @@ let _timer = null;
61
64
  * decide({ source: openfeature(client), overrides: (c) => !!c.visitor?.qa, exposure: send });
62
65
  */
63
66
  function decide(opts) {
64
- if ("source" in opts) _source = opts.source ?? null;
67
+ if ("source" in opts) {
68
+ _source = opts.source ?? null;
69
+ source_reset();
70
+ }
65
71
  if (opts.overrides) _override_gate = opts.overrides;
66
72
  if ("exposure" in opts) _exposure = opts.exposure ?? null;
67
73
  if (opts.batch) _batch = {
@@ -69,23 +75,75 @@ function decide(opts) {
69
75
  ms: opts.batch.ms ?? 2e3
70
76
  };
71
77
  }
78
+ const _exposures = /* @__PURE__ */ (/* @__PURE__ */ machine()({
79
+ name: "flags exposures",
80
+ initial: "idle",
81
+ steps: {
82
+ idle: ["buffering"],
83
+ buffering: ["idle"]
84
+ }
85
+ })).create();
86
+ /** What the next batch takes. */
87
+ let _queue = [];
88
+ /** The last batch was given up: the next one given up is not said again until one is taken. */
89
+ let _sink_failing = false;
90
+ /** The last batch put, until it settles: the request's end waits for it (the outbox sends in
91
+ * order, so every batch before it has settled by then). */
92
+ let _last_sent = null;
93
+ /** The batches on their way to the sink: kept alive past the response, one at a time, in order. */
94
+ const _batches = /* @__PURE__ */ outbox({
95
+ name: "flags exposures",
96
+ send: async (batch) => {
97
+ await _exposure?.(batch);
98
+ _sink_failing = false;
99
+ },
100
+ retry: /* @__PURE__ */ policy({
101
+ attempts: 3,
102
+ backoff: [250, 1e3],
103
+ jitter: .2,
104
+ deadline: 1e4
105
+ }),
106
+ inOrder: true,
107
+ giveUp: (_batch, error) => {
108
+ if (_sink_failing) return;
109
+ _sink_failing = true;
110
+ warn("flags_exposure_failed", error);
111
+ },
112
+ waitUntil: (work) => flags_app()?.keepAlive(work)
113
+ });
72
114
  function emit_exposure(e) {
73
- if (!_exposure) return;
115
+ if (BROWSER || !_exposure) return;
74
116
  _queue.push(e);
75
- if (_queue.length >= _batch.max) flush_exposures();
76
- else if (!_timer) _timer = setTimeout(flush_exposures, _batch.ms);
117
+ if (_queue.length >= _batch.max) {
118
+ send();
119
+ return;
120
+ }
121
+ if (_exposures.now.kind === "buffering") return;
122
+ _exposures.go("buffering");
123
+ const timer = setTimeout(() => void send(), _batch.ms);
124
+ _exposures.scope.defer(() => clearTimeout(timer));
77
125
  }
78
- /** Drain queued exposures to the sink. Called at each request's end (serverless-safe tail) and on
79
- * the size/age caps. A sink failure is contained — a lost batch is never a request's problem. */
80
- function flush_exposures() {
81
- if (_timer) clearTimeout(_timer);
82
- _timer = null;
83
- if (_queue.length === 0) return;
126
+ /** Everything queued, as one batch into the outbox now (buffering → idle). Settles once the sink
127
+ * took it or it was given up; never rejects. */
128
+ function send() {
84
129
  const batch = _queue;
85
130
  _queue = [];
86
- try {
87
- _exposure?.(batch);
88
- } catch {}
131
+ _exposures.move("buffering", "idle");
132
+ const sent = _batches.put(batch).then(() => {
133
+ if (_last_sent === sent) _last_sent = null;
134
+ });
135
+ _last_sent = sent;
136
+ return sent;
137
+ }
138
+ /**
139
+ * Send the queued exposures to the sink now. Called at each request's end (the flags plugin's `done`)
140
+ * and by the size and age caps. Resolves when they have reached the sink (behind the batches before
141
+ * them); `undefined` when nothing is queued or being sent. A sink failure is contained: a lost batch
142
+ * is never a request's problem.
143
+ */
144
+ function flush_exposures() {
145
+ if (_queue.length > 0) return send();
146
+ return _last_sent ?? void 0;
89
147
  }
90
148
  /** Every constructed flag self-registers here — the complete name list a source is asked for, and
91
149
  * the set federation auto-carries. Import your flags (route modules do this at startup; on plain
@@ -95,15 +153,32 @@ const _registry = /* @__PURE__ */ new Map();
95
153
  * Resolve the source ONCE per request over every registered flag, caching decisions on the
96
154
  * Request so reads stay sync. Awaited by the flags plugin's `request` stage (flags/server.ts), above
97
155
  * the page / hole split. Idempotent; a no-op without a source. Never throws — a source failure
98
- * degrades every flag to its native rule, it does not take the request down.
156
+ * degrades every flag to its native rule, it does not take the request down, and is said
157
+ * (`flags_source_failed`, once per failing streak).
99
158
  */
100
- async function prime_flags(c) {
101
- const req = c.request;
102
- if (!_source || req[RESOLVED] !== void 0) return;
103
- req[RESOLVED] = {};
104
- if (_registry.size === 0) return;
159
+ function prime_flags(c) {
160
+ if (!_source) return Promise.resolve();
161
+ const now = prime_of(c);
162
+ if (now !== void 0) return now.kind === "priming" ? now.done : Promise.resolve();
163
+ if (_registry.size === 0) {
164
+ set_prime(c, {
165
+ kind: "resolved",
166
+ map: {}
167
+ });
168
+ return Promise.resolve();
169
+ }
170
+ const claim = {
171
+ kind: "priming",
172
+ done: Promise.resolve()
173
+ };
174
+ set_prime(c, claim);
175
+ claim.done = ask_source(c, _source);
176
+ return claim.done;
177
+ }
178
+ async function ask_source(c, source) {
179
+ const mark = source_mark();
105
180
  try {
106
- const out = await _source([..._registry.values()], {
181
+ const out = await source([..._registry.values()], {
107
182
  ...c,
108
183
  visitor: visitor_of(c)
109
184
  });
@@ -114,18 +189,26 @@ async function prime_flags(c) {
114
189
  const norm = typeof raw === "boolean" ? { variant: raw ? "on" : "off" } : typeof raw === "string" ? { variant: raw } : raw;
115
190
  if (norm && q.variants.includes(norm.variant)) map[q.name] = norm;
116
191
  }
117
- req[RESOLVED] = map;
118
- } catch {
119
- req[RESOLVED] = {};
192
+ set_prime(c, {
193
+ kind: "resolved",
194
+ map
195
+ });
196
+ source_answered(mark);
197
+ } catch (error) {
198
+ set_prime(c, {
199
+ kind: "resolved",
200
+ map: {}
201
+ });
202
+ source_failed(error);
120
203
  }
121
204
  }
122
205
  function resolved_for(c, name) {
123
- const map = c.request[RESOLVED];
124
- if (map === void 0 && _source && !_unprimed_warned) {
206
+ const p = prime_of(c);
207
+ if (p === void 0 && _source && !_unprimed_warned) {
125
208
  _unprimed_warned = true;
126
- console.warn("[ogygia] flags: `decide({ source })` is set but no request was primed — list `flags()` beside `ogygia()` in vite.config `plugins`.");
209
+ warn("flags_source_unprimed");
127
210
  }
128
- return map?.[name];
211
+ return p?.kind === "resolved" ? p.map[name] : void 0;
129
212
  }
130
213
  let _unprimed_warned = false;
131
214
  let _unstarted_warned = false;
@@ -204,7 +287,7 @@ function flag(name, shape, opts = {}) {
204
287
  if (facts.perVisitor === null) facts.perVisitor = `flag:${name}`;
205
288
  } else if (!BROWSER && !_unstarted_warned) {
206
289
  _unstarted_warned = true;
207
- console.warn("[ogygia] flags: a flag was read on the server but the flags plugin is not running — list `flags()` (from `@ogygia/flags/vite`) beside `ogygia()` in vite.config `plugins`. Until then flag reads are not reported to the request, and a stored page could keep one variant.");
290
+ warn("flags_plugin_not_running");
208
291
  }
209
292
  const cached = memo.get(c.request);
210
293
  if (cached !== void 0) return cached;
@@ -0,0 +1,29 @@
1
+ import { MessagesOf } from "@ogygia/core/internal/messages";
2
+ /** The errors: `throw new OgygiaError(code, args)`. */
3
+ export declare const FLAGS_ERRORS: {};
4
+ /** The warnings: `warn(code, args, ...rest)`. */
5
+ export declare const FLAGS_WARNINGS: {
6
+ flags_source_unprimed: {
7
+ text: () => string;
8
+ details: () => string;
9
+ };
10
+ flags_exposure_failed: {
11
+ text: () => string;
12
+ details: () => string;
13
+ };
14
+ flags_source_failed: {
15
+ text: () => string;
16
+ details: () => string;
17
+ };
18
+ flags_plugin_not_running: {
19
+ text: () => string;
20
+ details: () => string;
21
+ };
22
+ };
23
+ type Own = MessagesOf<typeof FLAGS_ERRORS, typeof FLAGS_WARNINGS>;
24
+ /** `throw new OgygiaError(code, args)`; `warn(code, args, ...rest)` logs; `line(code, args)` is the
25
+ * line as a string. Wording and link in dev and while prerendering, the link alone otherwise. */
26
+ export declare const OgygiaError: Own["OgygiaError"];
27
+ export declare const warn: Own["warn"];
28
+ export declare const line: Own["line"];
29
+ export {};
@@ -0,0 +1,84 @@
1
+ import { BROWSER, DEV } from "esm-env";
2
+ import { define_messages, dev_or_prerender, prose } from "@ogygia/core/internal/messages";
3
+ // src/messages.ts
4
+ /**
5
+ * EVERY MESSAGE THE FLAGS PACKAGE PRINTS, in one place: its code, its wording, what to do about it
6
+ * (core's `@ogygia/core/internal/messages` says how the system works).
7
+ *
8
+ * warn('flags_plugin_not_running');
9
+ *
10
+ * Flag reads run in the app's server and its pages, so the wording is only passed in dev: a
11
+ * production build prints the code's page alone.
12
+ */
13
+ /** The errors: `throw new OgygiaError(code, args)`. */
14
+ const FLAGS_ERRORS = {};
15
+ /** The warnings: `warn(code, args, ...rest)`. */
16
+ const FLAGS_WARNINGS = {
17
+ flags_source_unprimed: {
18
+ text: () => prose`
19
+ flags: \`decide({ source })\` is set but no request was primed — list \`flags()\` beside
20
+ \`ogygia()\` in vite.config \`plugins\`.
21
+ `,
22
+ details: () => prose`
23
+ A flag has a \`source\`, but the request it was read in was never primed by the flags
24
+ plugin, so every read ignores the source and falls back to the flag's own decision. Said
25
+ once per server.
26
+
27
+ List \`flags()\` (from \`@ogygia/flags/vite\`) beside \`ogygia()\` in the \`plugins\` of
28
+ vite.config.
29
+ `
30
+ },
31
+ flags_exposure_failed: {
32
+ text: () => prose`
33
+ flags: the exposure sink (\`decide({ exposure })\`) failed, and the batch it was sent is lost.
34
+ The error follows.
35
+ `,
36
+ details: () => prose`
37
+ The function given to \`decide({ exposure })\` threw, or the promise it returned rejected (a
38
+ send that failed), so the exposures in that batch did not reach your analytics. The request
39
+ that read the flags is not affected. Said once, then again only after a send has worked:
40
+ a sink that stays down does not fill the log.
41
+
42
+ Check the sink's endpoint and credentials. To keep a batch through an outage, retry or
43
+ queue inside the sink.
44
+ `
45
+ },
46
+ flags_source_failed: {
47
+ text: () => prose`
48
+ flags: the flag source (\`decide({ source })\`) failed, so its flags fell back to their own
49
+ rule. The error follows.
50
+ `,
51
+ details: () => prose`
52
+ The source given to \`decide({ source })\` threw, rejected, or (with \`ofrep()\`) its
53
+ endpoint answered an error or could not be reached in time; with \`openfeature()\`, one
54
+ flag's evaluation threw. The request is not affected: each flag the source did not answer
55
+ is decided by its own rollout, as if there were no source. Said once, then again only
56
+ after a request the source served worked: a source that stays down does not fill the log.
57
+
58
+ Check the vendor's status, the endpoint and its credentials, and \`ofrep({ timeout })\`.
59
+ `
60
+ },
61
+ flags_plugin_not_running: {
62
+ text: () => prose`
63
+ flags: a flag was read on the server but the flags plugin is not running — list
64
+ \`flags()\` (from \`@ogygia/flags/vite\`) beside \`ogygia()\` in vite.config \`plugins\`.
65
+ Until then flag reads are not reported to the request, and a stored page could keep one
66
+ variant.
67
+ `,
68
+ details: () => prose`
69
+ A flag was read on the server, but the flags plugin never started. Nothing tells the
70
+ request that its page depends on a flag, so a store (a postrender cache) could keep one
71
+ variant of an A/B-tested page and serve it to everyone. Said once per server.
72
+
73
+ List \`flags()\` (from \`@ogygia/flags/vite\`) beside \`ogygia()\` in the \`plugins\` of
74
+ vite.config.
75
+ `
76
+ }
77
+ };
78
+ const own = /* @__PURE__ */ define_messages(DEV || !BROWSER ? FLAGS_ERRORS : null, DEV || !BROWSER ? FLAGS_WARNINGS : null, dev_or_prerender);
79
+ /** `throw new OgygiaError(code, args)`; `warn(code, args, ...rest)` logs; `line(code, args)` is the
80
+ * line as a string. Wording and link in dev and while prerendering, the link alone otherwise. */
81
+ const OgygiaError = own.OgygiaError;
82
+ const warn = own.warn;
83
+ const line = own.line;
84
+ export { FLAGS_ERRORS, FLAGS_WARNINGS, OgygiaError, line, warn };
package/dist/ofrep.js CHANGED
@@ -1,4 +1,15 @@
1
+ import { request_scope } from "@ogygia/core/internal/server";
1
2
  // src/ofrep.ts
3
+ /**
4
+ * `@ogygia/flags/openfeature` (OFREP) — speak the OpenFeature Remote Evaluation Protocol directly, with
5
+ * ZERO vendor SDK. Any OFREP endpoint works (flagd, the OpenFeature operator, a gateway). One
6
+ * bulk POST per request over the app's declared flags; decisions come back as variant names +
7
+ * optional payloads, indistinguishable from native flags after the await.
8
+ *
9
+ * import { decide } from '@ogygia/flags';
10
+ * import { ofrep } from '@ogygia/flags/openfeature';
11
+ * decide({ source: ofrep({ url: 'http://flagd:8016' }) });
12
+ */
2
13
  const TRAILING_SLASH_RE = /\/$/;
3
14
  function default_context(c) {
4
15
  const sub = c.visitor?.sub;
@@ -14,6 +25,7 @@ function ofrep(opts) {
14
25
  const timeout = opts.timeout ?? 800;
15
26
  return async (queries, c) => {
16
27
  const out = {};
28
+ const ended = request_scope(c.request).signal;
17
29
  try {
18
30
  const res = await f(endpoint, {
19
31
  method: "POST",
@@ -22,9 +34,9 @@ function ofrep(opts) {
22
34
  ...opts.headers
23
35
  },
24
36
  body: JSON.stringify({ context: ctx_of(c) }),
25
- signal: AbortSignal.timeout(timeout)
37
+ signal: AbortSignal.any([AbortSignal.timeout(timeout), ended])
26
38
  });
27
- if (!res.ok) return out;
39
+ if (!res.ok) throw new Error(`OFREP ${endpoint} answered ${res.status}`);
28
40
  const body = await res.json();
29
41
  const want = new Map(queries.map((q) => [q.name, q]));
30
42
  for (const f of body.flags ?? []) {
@@ -37,7 +49,10 @@ function ofrep(opts) {
37
49
  value: f.value
38
50
  } : variant;
39
51
  }
40
- } catch {}
52
+ } catch (error) {
53
+ if (ended.aborted) return out;
54
+ throw error;
55
+ }
41
56
  return out;
42
57
  };
43
58
  }
@@ -1,3 +1,4 @@
1
+ import { source_failed } from "./source-health.js";
1
2
  import { ofrep } from "./ofrep.js";
2
3
  // src/openfeature.ts
3
4
  function default_context(c) {
@@ -31,7 +32,9 @@ function openfeature(client, opts = {}) {
31
32
  };
32
33
  }
33
34
  }
34
- } catch {}
35
+ } catch (error) {
36
+ source_failed(error);
37
+ }
35
38
  }));
36
39
  return out;
37
40
  };
package/dist/server.js CHANGED
@@ -5,7 +5,9 @@ import { defineServer } from "@ogygia/core/plugin";
5
5
  /**
6
6
  * The flags plugin's server realm (named by `flags()`).
7
7
  * `request`: prime the decision source once, above the page / hole split, so every read stays sync.
8
- * `done`: drain queued exposures at the request's end, always (the serverless-safe tail).
8
+ * `done`: drain queued exposures at the request's end, always (the serverless-safe tail): the send
9
+ * is returned, so the request's end hands it to the platform's `waitUntil` when there is one (#243);
10
+ * the response never waits for it.
9
11
  */
10
12
  const server = defineServer({
11
13
  setup(app) {
@@ -19,7 +21,7 @@ const server = defineServer({
19
21
  });
20
22
  },
21
23
  done() {
22
- flush_exposures();
24
+ return flush_exposures();
23
25
  }
24
26
  });
25
27
  export { server as default };
@@ -0,0 +1,8 @@
1
+ /** A failure of the source (all of it, or one flag): said unless its streak was already said. */
2
+ export declare function source_failed(error: unknown): void;
3
+ /** Before a call of the source: what `source_answered` compares against. */
4
+ export declare function source_mark(): number;
5
+ /** The source answered: with no failure since `mark`, its streak ends. */
6
+ export declare function source_answered(mark: number): void;
7
+ /** A new source: no streak. */
8
+ export declare function source_reset(): void;
@@ -0,0 +1,29 @@
1
+ import { warn } from "./messages.js";
2
+ // src/source-health.ts
3
+ /**
4
+ * Whether the flag source is failing: a failure is said (`flags_source_failed`) once per failing
5
+ * streak, and the streak ends with a request the source served with no failure, like the exposure
6
+ * sink's. `openfeature()` says a failure of one flag here while it still answers the others.
7
+ */
8
+ let failing = false;
9
+ let failures = 0;
10
+ /** A failure of the source (all of it, or one flag): said unless its streak was already said. */
11
+ function source_failed(error) {
12
+ failures++;
13
+ if (failing) return;
14
+ failing = true;
15
+ warn("flags_source_failed", error);
16
+ }
17
+ /** Before a call of the source: what `source_answered` compares against. */
18
+ function source_mark() {
19
+ return failures;
20
+ }
21
+ /** The source answered: with no failure since `mark`, its streak ends. */
22
+ function source_answered(mark) {
23
+ if (failures === mark) failing = false;
24
+ }
25
+ /** A new source: no streak. */
26
+ function source_reset() {
27
+ failing = false;
28
+ }
29
+ export { source_answered, source_failed, source_mark, source_reset };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogygia/flags",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Feature flags and A/B tests for ogygia: one flag() primitive, decided server-side, zero JS",
5
5
  "keywords": [
6
6
  "ab-testing",
@@ -22,6 +22,7 @@
22
22
  "README.md"
23
23
  ],
24
24
  "type": "module",
25
+ "sideEffects": false,
25
26
  "exports": {
26
27
  ".": {
27
28
  "types": "./dist/index.d.ts",
@@ -34,24 +35,29 @@
34
35
  "./vite": {
35
36
  "types": "./dist/vite.d.ts",
36
37
  "default": "./dist/vite.js"
38
+ },
39
+ "./internal/messages": {
40
+ "types": "./dist/messages.d.ts",
41
+ "default": "./dist/messages.js"
37
42
  }
38
43
  },
39
44
  "publishConfig": {
40
45
  "access": "public"
41
46
  },
42
47
  "dependencies": {
48
+ "@ogygia/machine": "^0.13.0",
43
49
  "esm-env": "^1.2.2"
44
50
  },
45
51
  "devDependencies": {
52
+ "@ogygia/config": "0.0.0",
53
+ "@ogygia/core": "0.13.0",
46
54
  "@typescript/native": "npm:typescript@^7.0.2",
47
55
  "tsdown": "^0.23.4",
48
56
  "typescript": "^6.0.3",
49
- "vitest": "^4.1.12",
50
- "@ogygia/core": "0.11.0",
51
- "@ogygia/tooling": "0.0.0"
57
+ "vitest": "^4.1.12"
52
58
  },
53
59
  "peerDependencies": {
54
- "@ogygia/core": "^0.11.0"
60
+ "@ogygia/core": "^0.13.0"
55
61
  },
56
62
  "scripts": {
57
63
  "build": "tsdown",