Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 57 additions & 16 deletions sdk_v2/js/native/src/catalog.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "addon_data.h"
#include "errors.h"
#include "model.h"
#include "manager.h"

#include <foundry_local/foundry_local_cpp.h>

Expand All @@ -16,10 +17,24 @@ namespace foundry_local_node {

namespace {

std::shared_ptr<foundry_local::Manager> LockManagerOrThrow(
const std::weak_ptr<foundry_local::Manager>& manager_keepalive,
const std::shared_ptr<ManagerLifecycle>& lifecycle) {
if (!lifecycle || lifecycle->disposed.load(std::memory_order_acquire)) {
throw foundry_local::Error("Manager has been disposed", FOUNDRY_LOCAL_ERROR_INVALID_USAGE);
}
auto manager = manager_keepalive.lock();
if (!manager) {
throw foundry_local::Error("Manager has been disposed", FOUNDRY_LOCAL_ERROR_INVALID_USAGE);
}
return manager;
}

// Wrap a ModelList (rvalue) into a JS array of Model handles, each pinning the
// passed-in manager reference.
Napi::Value WrapModelList(Napi::Env env, foundry_local::ModelList ml,
Napi::ObjectReference manager) {
Napi::Value WrapModelList(Napi::Env env, foundry_local::ModelList ml, Napi::ObjectReference manager,
std::weak_ptr<foundry_local::Manager> manager_keepalive,
std::shared_ptr<ManagerLifecycle> lifecycle) {
auto list = std::make_shared<foundry_local::ModelList>(std::move(ml));
auto models = list->Models();
Napi::Array arr = Napi::Array::New(env, models.size());
Expand All @@ -28,14 +43,18 @@ Napi::Value WrapModelList(Napi::Env env, foundry_local::ModelList ml,
token.impl = models[i].get();
token.keepalive = list;
token.manager = Napi::Reference<Napi::Object>::New(manager.Value(), 1);
token.manager_keepalive = manager_keepalive;
token.lifecycle = lifecycle;
arr.Set(static_cast<uint32_t>(i), Model::NewInstance(env, std::move(token)));
}
return arr;
}

// Wrap an owning unique_ptr<IModel> into a JS Model (or undefined when null).
Napi::Value WrapOwnedModelOrUndefined(Napi::Env env, std::unique_ptr<foundry_local::IModel> owned,
Napi::ObjectReference manager) {
Napi::ObjectReference manager,
std::weak_ptr<foundry_local::Manager> manager_keepalive,
std::shared_ptr<ManagerLifecycle> lifecycle) {
if (!owned) {
return env.Undefined();
}
Expand All @@ -46,11 +65,13 @@ Napi::Value WrapOwnedModelOrUndefined(Napi::Env env, std::unique_ptr<foundry_loc
auto holder = std::make_shared<std::unique_ptr<foundry_local::IModel>>(std::move(owned));
token.keepalive = holder;
token.manager = std::move(manager);
token.manager_keepalive = std::move(manager_keepalive);
token.lifecycle = std::move(lifecycle);
return Model::NewInstance(env, std::move(token));
}

// Extract IModel* from a JS Model arg, or return nullptr if not a Model.
foundry_local::IModel* ExtractIModel(const Napi::Value& v) {
// Extract Model* from a JS Model arg, or return nullptr if not a Model.
Model* ExtractModel(const Napi::Value& v) {
if (!v.IsObject()) {
return nullptr;
}
Expand All @@ -63,8 +84,7 @@ foundry_local::IModel* ExtractIModel(const Napi::Value& v) {
if (!obj.InstanceOf(ctor)) {
return nullptr;
}
Model* m = Napi::ObjectWrap<Model>::Unwrap(obj);
return m != nullptr ? m->native_impl() : nullptr;
return Napi::ObjectWrap<Model>::Unwrap(obj);
}

Napi::ObjectReference CloneManager(const Napi::ObjectReference& mgr) {
Expand Down Expand Up @@ -109,11 +129,15 @@ Catalog::Catalog(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Catalog>(inf
}
impl_ = token->impl;
manager_ = std::move(token->manager);
manager_keepalive_ = std::move(token->manager_keepalive);
lifecycle_ = std::move(token->lifecycle);
}

Napi::Value Catalog::GetName(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
std::string_view name = impl_->GetName();
return Napi::String::New(env, std::string(name));
});
Expand All @@ -125,22 +149,30 @@ Napi::Value Catalog::GetModels(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(
env, [&]() -> Napi::Value { return WrapModelList(env, impl_->GetModels(), std::move(mgr)); });
env, [&]() -> Napi::Value {
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
return WrapModelList(env, impl_->GetModels(), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Napi::Value Catalog::GetCachedModels(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
return WrapModelList(env, impl_->GetCachedModels(), std::move(mgr));
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
return WrapModelList(env, impl_->GetCachedModels(), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Napi::Value Catalog::GetLoadedModels(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
return WrapModelList(env, impl_->GetLoadedModels(), std::move(mgr));
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
return WrapModelList(env, impl_->GetLoadedModels(), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Expand All @@ -155,8 +187,10 @@ Napi::Value Catalog::GetModel(const Napi::CallbackInfo& info) {
std::string alias = info[0].As<Napi::String>();
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
auto owned = impl_->GetModel(alias);
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr));
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Expand All @@ -169,8 +203,10 @@ Napi::Value Catalog::GetModelVariant(const Napi::CallbackInfo& info) {
std::string model_id = info[0].As<Napi::String>();
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
(void)manager_alive;
auto owned = impl_->GetModelVariant(model_id);
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr));
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Expand All @@ -180,15 +216,20 @@ Napi::Value Catalog::GetLatestVersion(const Napi::CallbackInfo& info) {
Napi::TypeError::New(env, "getLatestVersion(model: Model)").ThrowAsJavaScriptException();
return env.Undefined();
}
foundry_local::IModel* arg = ExtractIModel(info[0]);
if (arg == nullptr) {
Model* arg = ExtractModel(info[0]);
if (arg == nullptr || arg->native_impl() == nullptr) {
Napi::TypeError::New(env, "getLatestVersion: argument must be a Model").ThrowAsJavaScriptException();
return env.Undefined();
}
Napi::ObjectReference mgr = CloneManager(manager_);
return CallChecked<Napi::Value>(env, [&]() -> Napi::Value {
auto owned = impl_->GetLatestVersion(*arg);
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr));
auto manager_alive = LockManagerOrThrow(manager_keepalive_, lifecycle_);
if (arg->manager_disposed() || !arg->manager_keepalive()) {
throw foundry_local::Error("Manager has been disposed", FOUNDRY_LOCAL_ERROR_INVALID_USAGE);
}
(void)manager_alive;
auto owned = impl_->GetLatestVersion(*arg->native_impl());
return WrapOwnedModelOrUndefined(env, std::move(owned), std::move(mgr), manager_keepalive_, lifecycle_);
});
}

Expand Down
7 changes: 7 additions & 0 deletions sdk_v2/js/native/src/catalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@

#include <foundry_local/foundry_local_cpp.h>

#include <memory>
#include <utility>

namespace foundry_local_node {

struct ManagerLifecycle;

struct CatalogCtorToken {
foundry_local::ICatalog* impl = nullptr;
Napi::ObjectReference manager; // pins the owning Manager
std::weak_ptr<foundry_local::Manager> manager_keepalive;
std::shared_ptr<ManagerLifecycle> lifecycle;
};

class Catalog : public Napi::ObjectWrap<Catalog> {
Expand All @@ -43,6 +48,8 @@ class Catalog : public Napi::ObjectWrap<Catalog> {

foundry_local::ICatalog* impl_ = nullptr;
Napi::ObjectReference manager_;
std::weak_ptr<foundry_local::Manager> manager_keepalive_;
std::shared_ptr<ManagerLifecycle> lifecycle_;
};

} // namespace foundry_local_node
60 changes: 55 additions & 5 deletions sdk_v2/js/native/src/manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ namespace foundry_local_node {

namespace {

struct WorkerLease {
explicit WorkerLease(std::shared_ptr<ManagerLifecycle> lifecycle) : lifecycle_(std::move(lifecycle)) {
if (lifecycle_) {
lifecycle_->active_workers.fetch_add(1, std::memory_order_acq_rel);
}
}
~WorkerLease() {
if (lifecycle_) {
lifecycle_->active_workers.fetch_sub(1, std::memory_order_acq_rel);
}
}
std::shared_ptr<ManagerLifecycle> lifecycle_;
};

std::shared_ptr<void> MakeWorkerLease(std::shared_ptr<ManagerLifecycle> lifecycle) {
return std::make_shared<WorkerLease>(std::move(lifecycle));
}

Napi::Value ConvertEndpoints(Napi::Env env, std::vector<std::string>& endpoints) {
Napi::Array out = Napi::Array::New(env, endpoints.size());
for (size_t i = 0; i < endpoints.size(); ++i) {
Expand Down Expand Up @@ -88,6 +106,21 @@ Manager::Manager(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Manager>(inf
return true;
};

auto read_optional_bool = [&](const char* key, bool& out, bool& has) -> bool {
if (opts.Has(key) && !opts.Get(key).IsUndefined() && !opts.Get(key).IsNull()) {
if (!opts.Get(key).IsBoolean()) {
std::string msg = "options.";
msg += key;
msg += " must be a boolean";
Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
return false;
}
out = opts.Get(key).As<Napi::Boolean>();
has = true;
}
return true;
};

std::string model_cache_dir;
bool has_model_cache_dir = false;
if (!read_optional_string("modelCacheDir", model_cache_dir, has_model_cache_dir)) return;
Expand Down Expand Up @@ -193,7 +226,7 @@ Manager::Manager(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Manager>(inf
}
config.SetAdditionalOptions(kvp);
}
impl_ = std::make_unique<foundry_local::Manager>(std::move(config));
impl_ = std::make_shared<foundry_local::Manager>(std::move(config));
});
}

Expand Down Expand Up @@ -227,13 +260,26 @@ Napi::Value Manager::GetCatalog(const Napi::CallbackInfo& info) {
CatalogCtorToken token;
token.impl = &cat;
token.manager = std::move(owner);
token.manager_keepalive = impl_;
token.lifecycle = lifecycle_;
return Catalog::NewInstance(env, std::move(token));
});
}

Napi::Value Manager::Dispose(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// Idempotent — releasing an already-null unique_ptr is a no-op.
if (lifecycle_->active_sessions.load(std::memory_order_acquire) > 0) {
ThrowFoundryLocalError(env, FOUNDRY_LOCAL_ERROR_INVALID_USAGE,
"Manager has active sessions; dispose sessions before disposing the manager");
return env.Undefined();
}
if (lifecycle_->active_workers.load(std::memory_order_acquire) > 0) {
ThrowFoundryLocalError(env, FOUNDRY_LOCAL_ERROR_INVALID_USAGE,
"Manager has active native workers; await them before disposing the manager");
return env.Undefined();
}
lifecycle_->disposed.store(true, std::memory_order_release);
impl_.reset();
return env.Undefined();
}
Expand Down Expand Up @@ -288,11 +334,13 @@ namespace {
// progress callback. Mirrors the pattern in model.cc's DownloadWorker.
class EpDownloadWorker : public Napi::AsyncWorker {
public:
EpDownloadWorker(Napi::Env env, foundry_local::Manager* impl, std::vector<std::string> ep_names,
Napi::ObjectReference owner, Napi::ThreadSafeFunction tsfn)
EpDownloadWorker(Napi::Env env, std::shared_ptr<foundry_local::Manager> impl, std::shared_ptr<void> worker_lease,
std::vector<std::string> ep_names, Napi::ObjectReference owner,
Napi::ThreadSafeFunction tsfn)
: Napi::AsyncWorker(env),
deferred_(Napi::Promise::Deferred::New(env)),
impl_(impl),
worker_lease_(std::move(worker_lease)),
ep_names_(std::move(ep_names)),
owner_(std::move(owner)),
tsfn_(std::move(tsfn)) {}
Expand Down Expand Up @@ -356,7 +404,8 @@ class EpDownloadWorker : public Napi::AsyncWorker {
}

Napi::Promise::Deferred deferred_;
foundry_local::Manager* impl_;
std::shared_ptr<foundry_local::Manager> impl_;
std::shared_ptr<void> worker_lease_;
std::vector<std::string> ep_names_;
Napi::ObjectReference owner_;
Napi::ThreadSafeFunction tsfn_;
Expand Down Expand Up @@ -413,7 +462,8 @@ Napi::Value Manager::DownloadAndRegisterEps(const Napi::CallbackInfo& info) {
}

Napi::ObjectReference owner = Napi::Reference<Napi::Object>::New(info.This().As<Napi::Object>(), 1);
auto* w = new EpDownloadWorker(env, impl_.get(), std::move(ep_names), std::move(owner), std::move(tsfn));
auto* w = new EpDownloadWorker(env, impl_, MakeWorkerLease(lifecycle_), std::move(ep_names), std::move(owner),
std::move(tsfn));
Napi::Promise p = w->Promise();
w->Queue();
return p;
Expand Down
12 changes: 10 additions & 2 deletions sdk_v2/js/native/src/manager.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Napi::ObjectWrap<Manager> over std::unique_ptr<foundry_local::Manager>.
// Napi::ObjectWrap<Manager> over std::shared_ptr<foundry_local::Manager>.
//
// Surface:
// - ctor accepts { appName, modelCacheDir?, serviceEndpoint? }
Expand All @@ -15,10 +15,17 @@

#include <foundry_local/foundry_local_cpp.h>

#include <atomic>
#include <memory>

namespace foundry_local_node {

struct ManagerLifecycle {
std::atomic<bool> disposed{false};
std::atomic<int> active_sessions{0};
std::atomic<int> active_workers{0};
};

class Manager : public Napi::ObjectWrap<Manager> {
public:
static Napi::Function Init(Napi::Env env);
Expand Down Expand Up @@ -55,7 +62,8 @@ class Manager : public Napi::ObjectWrap<Manager> {
// on env and returns true. Callers should return env.Undefined() when true.
bool ThrowIfDisposed(Napi::Env env);

std::unique_ptr<foundry_local::Manager> impl_;
std::shared_ptr<foundry_local::Manager> impl_;
std::shared_ptr<ManagerLifecycle> lifecycle_ = std::make_shared<ManagerLifecycle>();
};

} // namespace foundry_local_node
Loading
Loading