npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@marteye/studiojs

v1.2.1

Published

MartEye Studio JavaScript SDK

Readme

MartEye Studio JavaScript SDK (@marteye/studiojs)

This library is a JavaScript SDK that allows you to easily integrate with MartEye Studio's API. It provides methods to interact with various resources, manage data, and leverage the powerful features offered by MartEye Studio.

Installation

You can install the library using npm or yarn:

Using npm:

npm install @marteye/studiojs

Using yarn:

yarn add @marteye/studiojs

Usage

Importing the Library

Using ES Modules:

import StudioJS from "@marteye/studiojs";

Or using CommonJS:

let StudioJS = require("@marteye/studiojs");

Initializing the SDK

Before using the library, initialize it with your API key. For more information on obtaining an API key, please contact your MartEye representative.

let studio = StudioJS({
  apiKey: "YOUR_API_KEY_HERE",
});

Handling errors

Failed requests throw a StudioApiError. It extends Error, so existing try/catch code keeps working, but it also carries the fields you need to decide what to do next instead of matching on message text.

import { StudioApiError } from "@marteye/studiojs";

try {
  await studio.lots.update(marketId, saleId, lotId, { remarks: "sold as seen" });
} catch (e) {
  if (StudioApiError.isStudioApiError(e)) {
    e.status; // HTTP status, or 0 for a client-side timeout
    e.code; // "LOT_SAVE_PRECONDITION_FAILED", "NOT_FOUND", …
    e.fields; // paths that failed, e.g. ["lotNumber", "attributes.weight"]
    e.retryable; // whether replaying the identical request could ever succeed
  }
  throw e;
}

retryable is the important one for queued work: only a transient server fault is worth replaying unchanged. Everything else needs the caller to do something different first, so a queue that retries on those will never drain.

Offline-friendly mutations

These options exist so a client can queue mutations while offline and drain them later without creating duplicates or clobbering newer edits.

Supply your own lot ID

Generate the ID client-side and a retried create keeps its identity. Replaying the same ID returns the stored lot with 200 instead of overwriting it, so a retry carrying a drifted payload cannot rewrite what already landed.

await studio.lots.create(marketId, saleId, {
  lotId: locallyGeneratedId,
  lotNumber: "42",
});

Guard an update against concurrent edits

Send the values you started from. If any of them have moved, the update is refused and nothing is written.

try {
  await studio.lots.update(marketId, saleId, lotId, {
    remarks: "edited on the phone",
    expectedValues: { productCode: "SHEEP", lotNumber: "42" },
  });
} catch (e) {
  if (e.code === "LOT_SAVE_PRECONDITION_FAILED") {
    // Someone changed e.fields since this edit began — refetch and re-apply.
  }
}

Only the keys you supply are compared. An absent key means "no expectation"; an explicit null means "I expected this to be empty".

Handle a lot number that is already taken

This is the common offline collision: the office built the catalogue while the device was in the pen, so lot 42 already exists — with a different id and with a seller and product code the device knows nothing about.

A create never silently becomes an update of that lot. The device has no expectedValues to send (it believed the lot didn't exist), so nothing would protect the existing seller or price from being overwritten by a payload that was never aware of them. Instead the conflict names the lot:

try {
  await studio.lots.create(marketId, saleId, {
    lotId: locallyGeneratedId,
    lotNumber: "42",
  });
} catch (e) {
  if (e.code === "LOT_NUMBER_CONFLICT") {
    e.existingLotId; // the lot already holding number 42
  }
}

Deciding what happens next belongs to the client, which knows things the server doesn't — whether the operator meant this lot, and what local work is attached to it. Prefer asking. To merge scanned work onto the existing lot, sync the tags across rather than updating the lot wholesale:

await studio.eartags.sync(marketId, saleId, {
  parentId: e.existingLotId,
  removedEartags: [],
  // Superseded by scannedItemsAdded, but still required on the wire.
  scannedEartagsAdded: [],
  scannedItemsAdded: locallyScannedItems,
  missingEartagsAdded: 0,
  faultyEarTagsAdded: 0,
  expectedValues: /* … derived from a fresh read of that lot … */,
});

That path is guarded: if the existing lot has drifted since you read it, the merge is refused rather than silently applied.

Delete without racing yourself

idempotent makes deleting an already-deleted lot succeed, so a replayed delete clears the queue. Invoice and auction protections still reject the delete — only the missing-resource outcome converts to success.

await studio.lots.delete(marketId, saleId, lotId, { idempotent: true });

Files and media uploads

StudioJS includes file upload helpers on studio.files. They reuse the same Studio API key or Firebase ID token that initialized the SDK, so callers do not pass a second upload token.

import Studio from "@marteye/studiojs";

const studio = Studio({
  apiKey: "YOUR_STUDIO_TOKEN_OR_API_KEY",
});

Select production or staging uploads

The SDK picks the correct Studio upload endpoint from your project environment. You do not configure upload service URLs yourself.

Resolution priority:

  1. EXPO_PUBLIC_STUDIO_ENV
  2. NEXT_PUBLIC_STUDIO_ENV
  3. STUDIO_ENV
  4. NODE_ENV

Supported values:

  • production or prod
  • staging, stage, development, or dev

Examples:

# Next.js browser/server bundle
NEXT_PUBLIC_STUDIO_ENV=staging

# Expo browser/native bundle
EXPO_PUBLIC_STUDIO_ENV=staging

# Node.js
STUDIO_ENV=staging

Upload a lot file

Pass a File or Blob-like object. The SDK chooses a single or multipart upload automatically; large files are uploaded as multipart without changing this API.

await studio.files.upload({
  marketId: "market_123",
  saleId: "sale_123",
  lotId: "lot_123",
  attributeId: "media",
  file,
});

Override the destination file name when needed:

await studio.files.upload({
  marketId: "market_123",
  saleId: "sale_123",
  lotId: "lot_123",
  attributeId: "media",
  file,
  fileName: "front-view.jpg",
});

Force multipart only when you need to:

await studio.files.upload({
  marketId: "market_123",
  saleId: "sale_123",
  lotId: "lot_123",
  attributeId: "media",
  file,
  strategy: "multipart",
  onProgress(progress) {
    console.log(progress.bytesUploaded, progress.totalBytes);
  },
});

Upload from a URL

Pass sourceUrl instead of file. The URL must be directly fetchable; redirecting URLs are rejected.

await studio.files.upload({
  marketId: "market_123",
  saleId: "sale_123",
  lotId: "lot_123",
  attributeId: "media",
  sourceUrl: "https://example.com/media/photo.jpg",
});

Optional fileName overrides the name inferred from the URL. Pass a file name when you want to immediately set the uploaded file on a lot attribute.

await studio.files.upload({
  marketId: "market_123",
  saleId: "sale_123",
  lotId: "lot_123",
  attributeId: "media",
  sourceUrl: "https://example.com/media/photo.jpg",
  fileName: "catalogue-photo.jpg",
});

Set the uploaded file on a lot

upload() stores the file and returns its asset id and URLs. Attach it to a lot by updating the attribute afterwards:

const upload = await studio.files.upload({
  marketId,
  saleId,
  lotId,
  attributeId: "media",
  file,
});

await studio.lots.update(marketId, saleId, lotId, {
  attributes: {
    media: {
      id: upload.assetId,
      fileName: file.name,
      fileType: file.type,
      url: upload.expected.original?.url ?? "",
    },
  },
});

Upload to a custom path

For non-lot files, pass a market-prefixed path and choose transform or direct mode.

await studio.files.upload({
  marketId: "market_123",
  mode: "transform",
  path: "market_123/users/user_123/avatar.jpg",
  file,
});

URL uploads support lot and transform uploads:

await studio.files.upload({
  marketId: "market_123",
  mode: "transform",
  path: "market_123/users/user_123",
  sourceUrl: "https://example.com/avatar.jpg",
  fileName: "avatar.jpg",
});

Private customer documents

Private customer documents use Firebase user authentication rather than a Studio API key. Create the standalone client with the current Firebase ID token:

import { createCustomerDocumentsClient } from "@marteye/studiojs";

const customerDocuments = createCustomerDocumentsClient({
  authToken: await firebaseUser.getIdToken(),
});

The token is not refreshed automatically. After expiry or a 401, obtain a new ID token and create a new client. Do not persist or log the token.

The client exposes:

  • listDocuments, createDocument, getDocument, and updateDocument.
  • uploadDocument, getUploadStatus, and cancelUpload.
  • viewFile, which returns an authenticated streaming Response from Studio.
  • deleteFile, which returns 202 while deletion is pending or 204 once the Studio attachment is absent.

Uploads accept a Blob, ArrayBuffer, or typed-array view up to 25 MiB. Images are converted by Media Crate; PDFs remain direct. viewFile and deleteFile always go through Studio, so the SDK never receives private Media Crate action tokens, R2 paths, credentials, or signed URLs.

Each returned file may include createdBy, the Firebase UID that originally uploaded it. This field is set from Media Crate's trusted callback and cannot be provided or changed by SDK create/update requests.

Contributing

Contributions are welcome! Please open an issue or submit a pull request if you have suggestions or improvements.

License

MIT