reuseOrInvalidate method

Future<EmbeddingModel?> reuseOrInvalidate(
  1. ActiveEmbedderParams requested, {
  2. required String label,
})

The cached embedder when it matches requested, else null for "build one".

A mismatch closes the cached model before returning, so the caller only ever has to handle "reuse this" or "build a new one".

Implementation

Future<EmbeddingModel?> reuseOrInvalidate(
  ActiveEmbedderParams requested, {
  required String label,
}) async {
  final cached = _cached;
  if (cached == null) return null;

  // Checked, not trusted. Eviction rides the close listener, so a model that
  // never fires one — or that was already closed when it was recorded, after
  // which `fireCloseListeners` has nothing left to call — would be handed to
  // every later caller, and every `generateEmbedding` on it throws. Only as
  // good as the model's own `isClosed`: the interface default is false, for
  // implementations that predate it.
  if (cached.model.isClosed) {
    gemmaLog('ℹ️  Cached embedder is closed; building a new one for $label');
    _cached = null;
    return null;
  }

  final changedParam = cached.params.firstDifference(requested);
  if (changedParam == null) {
    gemmaLog('ℹ️  Reusing existing embedding model instance for $label');
    return cached.model;
  }

  gemmaLog(
    '⚠️  Embedder config changed ($changedParam) for $label — rebuilding',
  );
  // Dropped BEFORE the await, not after: while a close is in flight the
  // cached model is no longer a valid answer to anybody.
  _cached = null;
  gemmaLog('🔄 Closing old embedding model and creating new one...');
  // Reported on its own terms, not as the new caller's failure. They asked for
  // a different embedder; handing them the old one's teardown error would name
  // neither model, and the rebuild they asked for would never happen. The
  // inference lane in the shells does the same (see `createModel`). Nothing
  // depends on this succeeding — the bookkeeping is already cleared above.
  try {
    await cached.model.close();
  } catch (e, st) {
    // `print`, not `gemmaLog`, for the reason `_warn` in
    // flutter_gemma_litertlm's litert_default_scope.dart already documents:
    // gemmaLog opens with `if (!kDebugMode) return`, so it is silent in
    // release — and release is the build where a leaked worker isolate gets
    // debugged. A teardown that throws leaves that isolate and its native
    // model alive, so this is worth a line that reaches logcat. It fires only
    // in an abnormal state, so it costs nothing in the normal case.
    // ignore: avoid_print
    print(
      '[flutter_gemma] WARNING: the old embedder\'s close() threw while '
      'rebuilding for $label; its worker isolate and native model may be '
      'leaked: $e\n$st',
    );
  }
  return null;
}