ff-serv 0.1.10 → 0.1.11

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 (52) hide show
  1. package/dist/adapter-DLhjFlOu.d.cts +31 -0
  2. package/dist/adapter-DLhjFlOu.d.ts +31 -0
  3. package/dist/cli.js +47 -47
  4. package/dist/exports/cache-bun-redis.cjs +50 -0
  5. package/dist/exports/cache-bun-redis.cjs.map +1 -0
  6. package/dist/exports/cache-bun-redis.d.cts +11 -0
  7. package/dist/exports/cache-bun-redis.d.ts +11 -0
  8. package/dist/exports/cache-bun-redis.js +23 -0
  9. package/dist/exports/cache-bun-redis.js.map +1 -0
  10. package/dist/exports/cache-ioredis.cjs +51 -0
  11. package/dist/exports/cache-ioredis.cjs.map +1 -0
  12. package/dist/exports/cache-ioredis.d.cts +11 -0
  13. package/dist/exports/cache-ioredis.d.ts +11 -0
  14. package/dist/exports/cache-ioredis.js +24 -0
  15. package/dist/exports/cache-ioredis.js.map +1 -0
  16. package/dist/exports/cache.cjs +220 -0
  17. package/dist/exports/cache.cjs.map +1 -0
  18. package/dist/exports/cache.d.cts +30 -0
  19. package/dist/exports/cache.d.ts +30 -0
  20. package/dist/exports/cache.js +199 -0
  21. package/dist/exports/cache.js.map +1 -0
  22. package/dist/exports/orpc.cjs +15 -10
  23. package/dist/exports/orpc.cjs.map +1 -1
  24. package/dist/exports/orpc.js +16 -11
  25. package/dist/exports/orpc.js.map +1 -1
  26. package/dist/index.cjs +15 -10
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +10 -7
  29. package/dist/index.d.ts +10 -7
  30. package/dist/index.js +17 -12
  31. package/dist/index.js.map +1 -1
  32. package/package.json +26 -2
  33. package/src/cache/AGENTS.md +8 -0
  34. package/src/cache/adapter.test.ts +181 -0
  35. package/src/cache/adapter.ts +120 -0
  36. package/src/cache/adapters/bun-redis.ts +29 -0
  37. package/src/cache/adapters/ioredis.ts +30 -0
  38. package/src/cache/cache.test.ts +461 -0
  39. package/src/cache/cache.ts +184 -0
  40. package/src/cli/commands/db/pull.ts +5 -15
  41. package/src/cli/commands/db/shared.ts +7 -7
  42. package/src/cli/config/index.ts +1 -3
  43. package/src/cli/index.ts +1 -1
  44. package/src/cli/utils/database-source.ts +1 -4
  45. package/src/exports/cache-bun-redis.ts +1 -0
  46. package/src/exports/cache-ioredis.ts +1 -0
  47. package/src/exports/cache.ts +6 -0
  48. package/src/exports/orpc.ts +1 -1
  49. package/src/http/fetch-handler.test.ts +1 -1
  50. package/src/index.ts +1 -0
  51. package/src/logger.test.ts +168 -0
  52. package/src/logger.ts +41 -19
@@ -59,12 +59,9 @@ export const dumpToFile = (databaseUrl: string, filePath: string) =>
59
59
  const bytesSinceLastCheck = Number(stat.size - lastSize);
60
60
  const rateMB =
61
61
  timeSinceLastCheck > 0
62
- ? (
63
- bytesSinceLastCheck /
64
- 1024 /
65
- 1024 /
66
- timeSinceLastCheck
67
- ).toFixed(2)
62
+ ? (bytesSinceLastCheck / 1024 / 1024 / timeSinceLastCheck).toFixed(
63
+ 2,
64
+ )
68
65
  : '0';
69
66
 
70
67
  bar.update(elapsed, {
@@ -87,7 +84,10 @@ export const dumpToFile = (databaseUrl: string, filePath: string) =>
87
84
 
88
85
  export const resolveDatabaseSource = (
89
86
  sourceConfig?: DatabaseSourceConfig,
90
- ): Effect.Effect<DatabaseSource, Effect.Effect.Error<typeof promptDatabaseSourceType>> =>
87
+ ): Effect.Effect<
88
+ DatabaseSource,
89
+ Effect.Effect.Error<typeof promptDatabaseSourceType>
90
+ > =>
91
91
  Effect.gen(function* () {
92
92
  if (sourceConfig) {
93
93
  if (sourceConfig.type === 'railway') {
@@ -62,9 +62,7 @@ export const loadConfig = (
62
62
  const pathsToTry = customConfigPath
63
63
  ? [customConfigPath]
64
64
  : [
65
- ...(process.env.FF_SERV_CONFIG
66
- ? [process.env.FF_SERV_CONFIG]
67
- : []),
65
+ ...(process.env.FF_SERV_CONFIG ? [process.env.FF_SERV_CONFIG] : []),
68
66
  ...DEFAULT_CONFIG_PATHS,
69
67
  ];
70
68
 
package/src/cli/index.ts CHANGED
@@ -3,8 +3,8 @@ import * as cli from '@effect/cli';
3
3
  import * as BunContext from '@effect/platform-bun/BunContext';
4
4
  import * as BunRuntime from '@effect/platform-bun/BunRuntime';
5
5
  import { Effect } from 'effect';
6
- import { dbCommand } from './commands/db/index.js';
7
6
  import pkg from '../../package.json' with { type: 'json' };
7
+ import { dbCommand } from './commands/db/index.js';
8
8
 
9
9
  const rootCommand = cli.Command.make('ff-serv', {}, () =>
10
10
  Effect.log('ff-serv CLI - Use --help for available commands'),
@@ -23,10 +23,7 @@ export const createRailwaySource = (config: {
23
23
  `--project=${config.projectId}`,
24
24
  `--environment=${config.environmentId}`,
25
25
  `--service=${config.serviceId}`,
26
- ).pipe(
27
- platform.Command.stdout('inherit'),
28
- platform.Command.exitCode,
29
- );
26
+ ).pipe(platform.Command.stdout('inherit'), platform.Command.exitCode);
30
27
 
31
28
  const output = yield* platform.Command.make(
32
29
  'railway',
@@ -0,0 +1 @@
1
+ export { bunRedis } from '../cache/adapters/bun-redis.js';
@@ -0,0 +1 @@
1
+ export { ioredis } from '../cache/adapters/ioredis.js';
@@ -0,0 +1,6 @@
1
+ export {
2
+ CacheAdapter,
3
+ type CacheEntry,
4
+ type RedisClient,
5
+ } from '../cache/adapter.js';
6
+ export { Cache, type CacheInstance } from '../cache/cache.js';
@@ -1 +1 @@
1
- export { oRPCHandler } from '../http/orpc.js';
1
+ export { oRPCHandler } from '../http/orpc.js';
@@ -75,4 +75,4 @@ layer(
75
75
  }),
76
76
  }),
77
77
  );
78
- });
78
+ });
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './http/index.js';
2
+ export type { SyncLogger } from './logger.js';
2
3
  export { Logger } from './logger.js';
3
4
  export { getPort } from './port.js';
@@ -0,0 +1,168 @@
1
+ import { it } from '@effect/vitest';
2
+ import {
3
+ Effect,
4
+ Array as EffectArray,
5
+ Logger as EffectLogger,
6
+ Layer,
7
+ LogLevel,
8
+ } from 'effect';
9
+ import { describe, expect } from 'vitest';
10
+ import { Logger } from './logger.js';
11
+
12
+ type CapturedEntry = {
13
+ message: string;
14
+ logLevel: LogLevel.LogLevel;
15
+ annotations: Record<string, unknown>;
16
+ };
17
+
18
+ function makeTestLogger() {
19
+ const entries: Array<CapturedEntry> = [];
20
+
21
+ const logger = EffectLogger.make((options) => {
22
+ const annotations: Record<string, unknown> = {};
23
+ for (const [key, value] of options.annotations) {
24
+ annotations[key] = value;
25
+ }
26
+ entries.push({
27
+ message: EffectArray.ensure(options.message).join(' '),
28
+ logLevel: options.logLevel,
29
+ annotations,
30
+ });
31
+ });
32
+
33
+ const layer = Layer.merge(
34
+ EffectLogger.replace(EffectLogger.defaultLogger, logger),
35
+ EffectLogger.minimumLogLevel(LogLevel.All),
36
+ );
37
+
38
+ return { entries, layer };
39
+ }
40
+
41
+ describe('Logger.sync', () => {
42
+ it.effect('logs without annotations', () =>
43
+ Effect.gen(function* () {
44
+ const { entries, layer } = makeTestLogger();
45
+ const log = yield* Logger.sync().pipe(Effect.provide(layer));
46
+ log.info('hello');
47
+ yield* Effect.yieldNow();
48
+ expect(entries).toHaveLength(1);
49
+ expect(entries[0].message).toBe('hello');
50
+ expect(entries[0].annotations).toEqual({});
51
+ }),
52
+ );
53
+
54
+ it.effect('initial annotations from sync()', () =>
55
+ Effect.gen(function* () {
56
+ const { entries, layer } = makeTestLogger();
57
+ const log = yield* Logger.sync({ service: 'api' }).pipe(
58
+ Effect.provide(layer),
59
+ );
60
+ log.info('started');
61
+ yield* Effect.yieldNow();
62
+ expect(entries[0].annotations).toEqual({ service: 'api' });
63
+ }),
64
+ );
65
+
66
+ it.effect('.child() adds annotations', () =>
67
+ Effect.gen(function* () {
68
+ const { entries, layer } = makeTestLogger();
69
+ const log = yield* Logger.sync().pipe(Effect.provide(layer));
70
+ const child = log.child({ requestId: '123' });
71
+ child.info('handled');
72
+ yield* Effect.yieldNow();
73
+ expect(entries[0].annotations).toEqual({ requestId: '123' });
74
+ }),
75
+ );
76
+
77
+ it.effect('.child() merges parent + child annotations', () =>
78
+ Effect.gen(function* () {
79
+ const { entries, layer } = makeTestLogger();
80
+ const log = yield* Logger.sync({ service: 'api' }).pipe(
81
+ Effect.provide(layer),
82
+ );
83
+ const child = log.child({ requestId: '123' });
84
+ child.info('handled');
85
+ yield* Effect.yieldNow();
86
+ expect(entries[0].annotations).toEqual({
87
+ service: 'api',
88
+ requestId: '123',
89
+ });
90
+ }),
91
+ );
92
+
93
+ it.effect('.child() overrides parent on key conflict', () =>
94
+ Effect.gen(function* () {
95
+ const { entries, layer } = makeTestLogger();
96
+ const log = yield* Logger.sync({ env: 'dev' }).pipe(
97
+ Effect.provide(layer),
98
+ );
99
+ const child = log.child({ env: 'prod' });
100
+ child.info('test');
101
+ yield* Effect.yieldNow();
102
+ expect(entries[0].annotations).toEqual({ env: 'prod' });
103
+ }),
104
+ );
105
+
106
+ it.effect('chained .child().child() accumulates annotations', () =>
107
+ Effect.gen(function* () {
108
+ const { entries, layer } = makeTestLogger();
109
+ const log = yield* Logger.sync({ a: 1 }).pipe(Effect.provide(layer));
110
+ const grandchild = log.child({ b: 2 }).child({ c: 3 });
111
+ grandchild.info('deep');
112
+ yield* Effect.yieldNow();
113
+ expect(entries[0].annotations).toEqual({ a: 1, b: 2, c: 3 });
114
+ }),
115
+ );
116
+
117
+ it.effect('parent unaffected by child creation', () =>
118
+ Effect.gen(function* () {
119
+ const { entries, layer } = makeTestLogger();
120
+ const parent = yield* Logger.sync({ service: 'api' }).pipe(
121
+ Effect.provide(layer),
122
+ );
123
+ parent.child({ requestId: '123' });
124
+ parent.info('still parent');
125
+ yield* Effect.yieldNow();
126
+ expect(entries[0].annotations).toEqual({ service: 'api' });
127
+ }),
128
+ );
129
+
130
+ it.effect('all log levels work on child', () =>
131
+ Effect.gen(function* () {
132
+ const { entries, layer } = makeTestLogger();
133
+ const log = yield* Logger.sync().pipe(Effect.provide(layer));
134
+ const child = log.child({ ctx: 'test' });
135
+ child.info('i');
136
+ child.debug('d');
137
+ child.warn('w');
138
+ child.error('e');
139
+ yield* Effect.yieldNow();
140
+ expect(entries).toHaveLength(4);
141
+ expect(entries[0].logLevel).toBe(LogLevel.Info);
142
+ expect(entries[1].logLevel).toBe(LogLevel.Debug);
143
+ expect(entries[2].logLevel).toBe(LogLevel.Warning);
144
+ expect(entries[3].logLevel).toBe(LogLevel.Error);
145
+ for (const entry of entries) {
146
+ expect(entry.annotations).toEqual({ ctx: 'test' });
147
+ }
148
+ }),
149
+ );
150
+
151
+ it.effect('per-call attributes merge with persistent annotations', () =>
152
+ Effect.gen(function* () {
153
+ const { entries, layer } = makeTestLogger();
154
+ const log = yield* Logger.sync({ service: 'api' }).pipe(
155
+ Effect.provide(layer),
156
+ );
157
+ const child = log.child({ requestId: '123' });
158
+ child.info({ extra: 'val' }, 'with attrs');
159
+ yield* Effect.yieldNow();
160
+ expect(entries[0].message).toBe('with attrs');
161
+ expect(entries[0].annotations).toEqual({
162
+ service: 'api',
163
+ requestId: '123',
164
+ extra: 'val',
165
+ });
166
+ }),
167
+ );
168
+ });
package/src/logger.ts CHANGED
@@ -1,34 +1,56 @@
1
- import { Effect, FiberSet, Runtime } from 'effect';
1
+ import { Effect, Runtime } from 'effect';
2
2
 
3
3
  type LogParams = [obj: unknown, msg?: string];
4
4
  function extractParams(...[obj, msg]: LogParams) {
5
5
  if (typeof obj === 'string') {
6
6
  return { message: obj };
7
7
  }
8
+ // biome-ignore lint/suspicious/noExplicitAny: log attributes are unstructured
8
9
  return { message: msg, attributes: obj as Record<string, any> };
9
10
  }
10
11
 
12
+ // biome-ignore lint/suspicious/noExplicitAny: log annotations are unstructured
13
+ type LogAnnotations = Record<string, any>;
14
+
15
+ export type SyncLogger = {
16
+ info: (...params: Parameters<typeof Logger.info>) => void;
17
+ debug: (...params: Parameters<typeof Logger.debug>) => void;
18
+ warn: (...params: Parameters<typeof Logger.warn>) => void;
19
+ error: (...params: Parameters<typeof Logger.error>) => void;
20
+ child: (annotations: LogAnnotations) => SyncLogger;
21
+ };
22
+
23
+ function makeSyncLogger(
24
+ runtime: Runtime.Runtime<never>,
25
+ annotations: LogAnnotations,
26
+ ): SyncLogger {
27
+ const run = (e: Effect.Effect<void, never, never>) => {
28
+ const annotated =
29
+ Object.keys(annotations).length > 0
30
+ ? e.pipe(Effect.annotateLogs(annotations))
31
+ : e;
32
+ void Runtime.runPromise(runtime)(annotated);
33
+ };
34
+
35
+ return {
36
+ info: (...params: Parameters<typeof Logger.info>) =>
37
+ run(Logger.info(...params)),
38
+ debug: (...params: Parameters<typeof Logger.debug>) =>
39
+ run(Logger.debug(...params)),
40
+ warn: (...params: Parameters<typeof Logger.warn>) =>
41
+ run(Logger.warn(...params)),
42
+ error: (...params: Parameters<typeof Logger.error>) =>
43
+ run(Logger.error(...params)),
44
+ child: (childAnnotations: LogAnnotations) =>
45
+ makeSyncLogger(runtime, { ...annotations, ...childAnnotations }),
46
+ };
47
+ }
48
+
11
49
  export namespace Logger {
12
- export const sync = () =>
50
+ export const sync = (annotations?: LogAnnotations) =>
13
51
  Effect.gen(function* () {
14
52
  const runtime = yield* Effect.runtime();
15
-
16
- const run = (e: Effect.Effect<void, never, never>) => {
17
- // Intentionally ignoring the await here
18
- // void runPromise(e);
19
- void Runtime.runPromise(runtime)(e);
20
- };
21
-
22
- return {
23
- info: (...params: Parameters<typeof Logger.info>) =>
24
- run(Logger.info(...params)),
25
- debug: (...params: Parameters<typeof Logger.debug>) =>
26
- run(Logger.debug(...params)),
27
- warn: (...params: Parameters<typeof Logger.warn>) =>
28
- run(Logger.warn(...params)),
29
- error: (...params: Parameters<typeof Logger.error>) =>
30
- run(Logger.error(...params)),
31
- };
53
+ return makeSyncLogger(runtime, annotations ?? {});
32
54
  });
33
55
 
34
56
  // --