> $catalog Facts active extensions declare. */ public static function value_of( ActionDescriptor $action, string $fact, array $catalog ): int|float|null { $type = $catalog[ $fact ]['type'] ?? null; $value = $action->facts[ $fact ] ?? null; $typed = 'money' === $type ? is_int( $value ) : 'number' === $type && ( is_int( $value ) || ( is_float( $value ) && is_finite( $value ) ) ); // A budget sums costs. A negative one would make room for others, // so it is not a value the budget can use. return $typed && $value >= 0 ? $value : null; } /** * Whether this action takes the agent over the budget, and if so how * long until enough of the window's spending ages out for it to fit; * null when the window cannot be read in full. * * The hint is null when the action alone exceeds the budget: no wait * lets it through. Otherwise it is never less than one second. Negative * stored values spend nothing. * * @param int $agent_id Acting agent. * @param array{fact: string, max: int|float, window: int} $budget The rule's budget. * @param int|float $value This action's value. * @param DateTimeImmutable $now Reference time. * @return array{over: bool, retry_after: int|null}|null */ public function measure( int $agent_id, array $budget, int|float $value, DateTimeImmutable $now ): ?array { $rows = $this->ledger->fact_values_since( $agent_id, $budget['fact'], $now->modify( sprintf( '-%d seconds', $budget['window'] ) ), self::MAX_ROWS ); if ( null === $rows ) { return null; } $spent = array_sum( array_map( static fn ( array $row ): int|float => max( 0, $row[1] ), $rows ) ); if ( $spent + $value <= $budget['max'] ) { return array( 'over' => false, 'retry_after' => null, ); } $retry_after = null; if ( $value <= $budget['max'] && array() !== $rows ) { // Rows leave the window oldest first, a row still counting // through its last second; the first whose leaving makes room is // when a retry fits. Once every row has left it surely fits, // whatever rounding the running total picked up. $last = $rows[ count( $rows ) - 1 ][0]; foreach ( $rows as list( $at, $amount ) ) { $spent -= max( 0, $amount ); if ( $spent + $value <= $budget['max'] ) { $last = $at; break; } } $retry_after = max( 1, $last + $budget['window'] + 1 - $now->getTimestamp() ); } return array( 'over' => true, 'retry_after' => $retry_after, ); } }