diff --git a/src/main.zig b/src/main.zig index 50587e6..ea14d60 100644 --- a/src/main.zig +++ b/src/main.zig @@ -464,10 +464,22 @@ fn upperDupe(allocator: std.mem.Allocator, s: []const u8) ![]u8 { return d; } -fn printRateLimitWait(svc: *zfin.DataService, data_type: zfin.cache.DataType, stdout: *std.Io.Writer) !void { +/// Print an inline rate-limit estimate tag like "[~14s] " before the +/// next fetch of `data_type`, then flush so the tag is visible before +/// the (possibly blocking) fetch runs. An interactive caller sees the +/// estimate, then the pause, then the result land on a single line; a +/// cron log just gets the tag inline. No-op when a token is available +/// (estimate 0) or the provider isn't instantiated yet. +/// +/// The number is the pause a *live* fetch would incur right now -- not +/// a promise that we will wait. A cache hit consumes no token and skips +/// the wait entirely (see the one-time legend at the top of a refresh +/// run). The estimate is read before the fetch, so it reflects the +/// pre-fetch bucket state. +fn printRateLimitTag(svc: *zfin.DataService, data_type: zfin.cache.DataType, stdout: *std.Io.Writer) !void { if (svc.estimateWaitSeconds(data_type)) |wait| { if (wait > 0) { - try stdout.print("\n (rate limit -- waiting {d}s)\n ", .{wait}); + try stdout.print("[~{d}s] ", .{wait}); try stdout.flush(); } } @@ -600,6 +612,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process try stdout.print("zfin-server {s}\n", .{version}); try stdout.print("Refreshing {d} symbols from {s}\n", .{ symbols.count(), portfolio_path }); + try stdout.print("note: [~Ns] = est. pause before the next live fetch (bucket empty); cache hits skip it\n", .{}); try stdout.flush(); var success_count: u32 = 0; @@ -613,7 +626,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process // handlers to serve. Per-symbol `getEtfMetrics` calls below // also rely on these maps being loaded. { - try printRateLimitWait(&svc, .tickers_funds, stdout); + try printRateLimitTag(&svc, .tickers_funds, stdout); if (svc.loadMutualFundTickerMap(.{})) |mut_map| { var m = mut_map; m.deinit(); @@ -621,7 +634,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process } else |err| { try stdout.print("EDGAR mutual-fund ticker map FAILED ({t})\n", .{err}); } - try printRateLimitWait(&svc, .tickers_companies, stdout); + try printRateLimitTag(&svc, .tickers_companies, stdout); if (svc.loadCompanyTickerMap(.{})) |co_map| { var m = co_map; m.deinit(); @@ -641,7 +654,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process var sym_ok = true; // Candles - try printRateLimitWait(&svc, .candles_daily, stdout); + try printRateLimitTag(&svc, .candles_daily, stdout); if (svc.getCandles(sym, .{})) |result| { defer result.deinit(); try stdout.print("candles ok ({s})", .{@tagName(result.source)}); @@ -679,32 +692,35 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process } // Dividends - try printRateLimitWait(&svc, .dividends, stdout); + try stdout.print(", ", .{}); + try printRateLimitTag(&svc, .dividends, stdout); if (svc.getDividends(sym, .{})) |result| { defer result.deinit(); - try stdout.print(", dividends ok ({s})", .{@tagName(result.source)}); + try stdout.print("dividends ok ({s})", .{@tagName(result.source)}); } else |err| { - try stdout.print(", dividends FAILED ({s})", .{@errorName(err)}); + try stdout.print("dividends FAILED ({s})", .{@errorName(err)}); sym_ok = false; } // Splits - try printRateLimitWait(&svc, .splits, stdout); + try stdout.print(", ", .{}); + try printRateLimitTag(&svc, .splits, stdout); if (svc.getSplits(sym, .{})) |result| { defer result.deinit(); - try stdout.print(", splits ok ({s})", .{@tagName(result.source)}); + try stdout.print("splits ok ({s})", .{@tagName(result.source)}); } else |err| { - try stdout.print(", splits FAILED ({s})", .{@errorName(err)}); + try stdout.print("splits FAILED ({s})", .{@errorName(err)}); sym_ok = false; } // Earnings - try printRateLimitWait(&svc, .earnings, stdout); + try stdout.print(", ", .{}); + try printRateLimitTag(&svc, .earnings, stdout); if (svc.getEarnings(sym, .{})) |result| { defer result.deinit(); - try stdout.print(", earnings ok ({s})", .{@tagName(result.source)}); + try stdout.print("earnings ok ({s})", .{@tagName(result.source)}); } else |err| { - try stdout.print(", earnings FAILED ({s})", .{@errorName(err)}); + try stdout.print("earnings FAILED ({s})", .{@errorName(err)}); sym_ok = false; } @@ -715,7 +731,8 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process var cik_buf: ?[]u8 = null; defer if (cik_buf) |b| allocator.free(b); var is_etf = false; - try printRateLimitWait(&svc, .classification, stdout); + try stdout.print(", ", .{}); + try printRateLimitTag(&svc, .classification, stdout); if (svc.getClassification(sym, .{})) |result| { defer result.deinit(); if (result.data.len > 0) { @@ -724,11 +741,11 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process } is_etf = result.data[0].is_etf; } - try stdout.print(", classification ok ({s})", .{@tagName(result.source)}); + try stdout.print("classification ok ({s})", .{@tagName(result.source)}); } else |err| switch (err) { - zfin.DataError.NotFound => try stdout.print(", classification n/a", .{}), + zfin.DataError.NotFound => try stdout.print("classification n/a", .{}), else => { - try stdout.print(", classification FAILED ({t})", .{err}); + try stdout.print("classification FAILED ({t})", .{err}); sym_ok = false; }, } @@ -737,14 +754,15 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process // non-funds (NPORT-P only exists for funds + UITs); a // negative-cache entry suppresses retries. Logged as // `n/a` and doesn't flip sym_ok. - try printRateLimitWait(&svc, .etf_metrics, stdout); + try stdout.print(", ", .{}); + try printRateLimitTag(&svc, .etf_metrics, stdout); if (svc.getEtfMetrics(sym, .{})) |result| { defer result.deinit(); - try stdout.print(", etf_metrics ok ({s})", .{@tagName(result.source)}); + try stdout.print("etf_metrics ok ({s})", .{@tagName(result.source)}); } else |err| switch (err) { - zfin.DataError.NotFound => try stdout.print(", etf_metrics n/a", .{}), + zfin.DataError.NotFound => try stdout.print("etf_metrics n/a", .{}), else => { - try stdout.print(", etf_metrics FAILED ({t})", .{err}); + try stdout.print("etf_metrics FAILED ({t})", .{err}); sym_ok = false; }, } @@ -756,17 +774,18 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process // concepts entity_facts looks for; calling EDGAR for // them is guaranteed-404 noise. Skip them up front. if (cik_buf) |cik| { + try stdout.print(", ", .{}); if (is_etf) { - try stdout.print(", entity_facts n/a (ETF)", .{}); + try stdout.print("entity_facts n/a (ETF)", .{}); } else { - try printRateLimitWait(&svc, .entity_facts, stdout); + try printRateLimitTag(&svc, .entity_facts, stdout); if (svc.getEntityFacts(cik, .{})) |result| { defer result.deinit(); - try stdout.print(", entity_facts ok ({s})", .{@tagName(result.source)}); + try stdout.print("entity_facts ok ({s})", .{@tagName(result.source)}); } else |err| switch (err) { - zfin.DataError.NotFound => try stdout.print(", entity_facts n/a", .{}), + zfin.DataError.NotFound => try stdout.print("entity_facts n/a", .{}), else => { - try stdout.print(", entity_facts FAILED ({t})", .{err}); + try stdout.print("entity_facts FAILED ({t})", .{err}); sym_ok = false; }, }