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

@krknet/suitesyncer

v2.0.0

Published

NetSuite Restlet API

Readme

SuiteSyncer

Oracle NetSuite Restlet API Connector

Requirements

  • Node.js 22 or newer
  • @krknet/profiler 1.5 or newer as a peer dependency

Usage

const { SuiteSyncer } = require('@krknet/suitesyncer')
const suiteSyncer = new SuiteSyncer({
  isSyncEnabled: true,
  queue: {
    captureException: Sentry.captureException
  },
  restlet: {
    accountID: '000000_SB1',
    scriptNr: 1,
    deployNr: 1,
    consumerID: 'consumerID',
    consumerSecret: 'consumerSecret',
    tokenID: 'tokenID',
    tokenSecret: 'tokenSecret'
  },
  syncers: [
    {
      key: 'getTest',
      isActive: true,
      isAutostarting: false,
      interval: 5, // Minutes
      runner: async (endpoint, profiler) => {
        try {
          const result = await endpoint.get({
            mode: 'test',
            before: Date.now()
          })
          profiler.succeed(`${result.length} results`)
        } catch (err) {
          profiler.fail(err.message)
        }
      }
    },
    {
      key: 'pushTest',
      action: async (endpoint, profiler, payload = {}) => {
        try {
          const answer = await endpoint.post({ mode: 'test', ...payload })
          profiler.succeed()
          return answer
        } catch (err) {
          profiler.fail(err.message)
        }
      }
    }
  ]
})

await suiteSyncer.start()
console.log(await suiteSyncer.call('pushTest', { test: 2 }))
await suiteSyncer.stop()

Options

{
  isSyncEnabled: false,
  queue: { // completely optional
    captureException: console.error // Gracefull Error Handling
  },
  restlet: { // required
    accountID: null, // required
    scriptNr: 0, // required
    deployNr: 0, // required
    consumerID: null, // required
    consumerSecret: null, // required
    tokenID: null, // required
    tokenSecret: null, // required
    timeout: 60000 // optional, milliseconds
  },
  syncers: null // folder holding Syncers, or Array of Syncers
}

Functions

async start ()

get state ()      // 'idle' | 'running' | 'stopped'
get isActive ()   // true only while running
get isSyncEnabled ()

async call (action, payload)

async stop ()

Runtime behavior

Lifecycle and shutdown

Create one SuiteSyncer instance, call start() once before submitting actions, and call stop() once during shutdown. When isSyncEnabled is false, start() does not activate either queue and the instance stays idle.

An instance is idle, running, or stopped. isActive is true only while running; use isSyncEnabled to read the configured flag. Calling start() on an instance that is already running or stopped throws — a stopped instance is not restartable. stop() is idempotent.

stop() disables both queues and clears recurring runner intervals as well as pending autostart timers. It does not wait for an operation that is already running: an in-flight call() still settles normally. A call() that was queued but had not started is rejected, so no caller is left waiting on a promise that can never settle. Pending runner jobs remain paused and are not processed after shutdown.

Syncer configuration

syncers is either an array of syncers or a path to a folder holding one module per syncer. A relative path resolves against the working directory. In a folder, every .js file except index.js becomes a syncer keyed by its filename; in an array, each entry supplies its own key.

Every syncer is validated while the instance is constructed, so a misconfiguration is reported where it was made rather than at the moment a runner or a call() would have reached for it. A syncer must carry a key, at least one of runner and action, an interval that is a positive number of minutes, and an isAutostarting that is either a boolean or a delay in milliseconds. interval and isAutostarting are coerced as they are checked. A syncer marked isActive: false is skipped rather than validated, so a half written one can be parked in place. A module that fails to load is reported with its filename.

Concurrency

Runner jobs are processed serially by one queue, while calls submitted through call() are processed serially by a separate queue. Consequently, at most one runner and one call can execute concurrently.

Response envelopes

A Restlet that answers in an { isSuccess, result } envelope reports its own failures through it, at HTTP 200. When the parsed body carries a boolean isSuccess, the envelope is unwrapped:

// the Restlet answers { isSuccess: true, result: [ ... ] }
const orders = await endpoint.get({ call: 'getSalesOrders' })

// the Restlet answers { isSuccess: false, result: { msg: 'RCRD_DSNT_EXIST: ...' } }
// -> rejects with a RestletError of kind 'netsuite'

A body without a boolean isSuccess is returned untouched, so a Restlet that answers with a plain payload is unaffected.

Errors and timeouts

call() rejects when the instance is not running, when the requested action does not exist, when the named syncer has no action, and when a shutdown cancels it before it starts. Action failures reject the returned promise. Runner failures are passed to queue.captureException, which defaults to console.error.

A runner is deduplicated by its syncer key: while one of its jobs is queued or running, the autostart job and any interval tick for that same syncer are ignored rather than piling up behind it.

Every REST failure rejects with a RestletError, so a caller discriminates on kind rather than on the shape of cause:

| kind | Raised when | Also set | | --- | --- | --- | | transport | The request never completed | code (ECONNRESET, ENOTFOUND, ...) | | timeout | The request outlived its timeout | — | | http | The Restlet answered with a status of 400 or greater | statusCode, body | | redirect | The Restlet answered with a redirect, which is never followed | statusCode, body | | payload | The response body was not valid JSON | body | | netsuite | The Restlet reported a failure | code, function, parameters, body |

Redirects are never followed, so a 3xx is a failure rather than a silently empty answer: NetSuite redirects an unauthenticated request to its login page, which would otherwise arrive as an empty body and read as success.

All of them carry method and url, and the original error or NetSuite error object as cause. A failure raised inside a runner also carries syncer, the key of the syncer it belongs to, which toSentryContext reads to keep issues apart. It is null on a failure raised anywhere else. Request headers are deliberately never attached, because they carry the OAuth signature and the token key.

A netsuite failure keeps NetSuite's own server-side stack trace as the error's stack, so the trace points at the SuiteScript rather than at this library. The stack it replaces stays reachable as localStack, which points at the frame that issued the request.

try {
  await endpoint.get({ mode: 'test' })
} catch (err) {
  if (err.kind === 'timeout' || err.kind === 'transport') retryLater()
  else if (err.code === 'RCRD_DSNT_EXIST') skip()
  else profiler.fail(`${err.kind}: ${err.message}`)
}

The class itself is exported for an instanceof check, and carries the taxonomy as RestletError.KIND so a caller does not have to spell the strings:

const { SuiteSyncer, RestletError } = require('@krknet/suitesyncer')

if (err instanceof RestletError && err.kind === RestletError.KIND.TIMEOUT) retryLater()

A helper that rethrows a failure can fold context into it with annotate(), which toSentryContext reads as extra. It returns the error, so annotating and rethrowing is one statement:

catch (err) {
  throw err.annotate({ fetchMode: 'updated', locationCount: locations.length })
}

Each request is aborted after restlet.timeout milliseconds, defaulting to 60000 (one minute). A timeout rejects with a RestletError whose kind is timeout; the underlying TimeoutError remains available as cause. Because both queues run serially, this also bounds how long one stuck request can block everything queued behind it.

Every endpoint method takes an optional second argument to override the timeout for that request alone, which is useful for a long-running saved search or a bulk write that legitimately exceeds the instance default:

await endpoint.get({ mode: 'test' }, { timeout: 5000 })    // tighter than the default
await endpoint.post({ mode: 'bulk' }, { timeout: 300000 }) // looser, for a slow write

The override applies only to that request; the instance default is unchanged.