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

@twinfinity/twin

v6.2.0

Published

Twinfinity Twin Service client

Readme

@twinfinity/twin

TypeScript client for the Twinfinity Twin Service API. Provides typed access to twins, assets, and twin files with optimistic concurrency control and structured error handling.

Installation

bun add @twinfinity/twin

Peer Dependencies

  • @twinfinity/authentication - provides TwinfinityHttpClient for authenticated HTTP requests
  • @twinfinity/t8-client-model - provides T8 client model types used by loadTwin / loadGeometryInfo
  • @twinfinity/entity-reference - provides the EntityReference type returned by referenceOf and the entity reference builders, and format to turn one into a string

Usage

Creating a Client

import { TwinClient } from '@twinfinity/twin';

const client = new TwinClient(baseUrl, httpClient);

The baseUrl is the API root (e.g. https://api.twinfinity.com). The client appends /_ps/twins internally.

Twin Operations

// List twins (paginated)
const page = await client.listTwins();

// List all twins (auto-paginates)
const allTwins = await client.listAllTwins();

// Get a specific twin
const twin = await client.getTwin('twin-id');

// Get a specific version
const twinV2 = await client.getTwin({ id: 'twin-id', version: 2 });

// Create or update
const updated = await client.createOrUpdateTwin('twin-id', {
  relatedContainerIds: ['container-1'],
  rootEntity: '0Site00000000000000000',
  entities: {
    sites: {
      '0Site00000000000000000': { id: '0Site00000000000000000', name: 'My site' }
    }
  },
  etag: twin.etag // required for updates
});

// Soft-delete and restore
const deleted = await client.deleteTwin('twin-id', twin.etag);
const restored = await client.restoreTwin('twin-id', deleted.etag);

Nesting twins

A twin can nest other twins as children. Declare each child in entities.twinReferences and place it under a site via that site's aggregatedTwinReferences:

await client.createOrUpdateTwin('parent-id', {
  relatedContainerIds: ['container-1'],
  rootEntity: '0Site00000000000000000',
  entities: {
    sites: {
      '0Site00000000000000000': {
        id: '0Site00000000000000000',
        name: 'Campus site',
        aggregatedTwinReferences: ['ref-a']
      }
    },
    twinReferences: {
      'ref-a': { twin: 'child-id' } // id of the nested twin
    }
  }
  // Omit `etag` when creating; supply the current twin's etag only when updating.
});

// The parent/child edge is computed and read back on the twin:
const parent = await client.getTwin('parent-id');
parent.aggregatedTwins; // ['child-id'] — twins this twin parents
const child = await client.getTwin('child-id');
child.parentTwin; // 'parent-id' — the twin that references this one

aggregatedTwins and parentTwin are computed by the service, not authored — sending them in a write has no effect. A twin may have at most one parent, may not reference itself, and references may not form a cycle. Adding a reference requires View on the referenced twin (in addition to Edit on the twin being written); a referenced twin you cannot read is reported as not found. See Twin Management → Nested Twins for the full rules.

Loading Twin Models

The client can download and deserialize processed twin content (T8CM/T8GI formats):

// Load client model with default types
const { id, version, model } = await client.loadTwin('twin-id');

// Load with custom factory functions for domain-specific types
const custom = await client.loadTwinWithFactories(
  'twin-id',
  myProjectFactory,
  myProductFactory,
  myEntityTypeFactory,
  myUnitFactory,
  myConnectProject,
  myConnectProduct
);

// Load geometry info
const geometry = await client.loadGeometryInfo('twin-id');

// Load client model with geometry combined
const withGeometry = await client.loadTwinWithGeometry('twin-id');

When no version is specified, the client searches versions (newest first) for one that has the requested content format.

Entity References

Preview. Entity references are released to try out and may change. In particular the twin~twin:<id>.entity:<gid> spelling is expected to change once platform responses carry their own self reference.

An entity reference names a twin, or an entity in it, in the form other Twinfinity services accept. referenceOf gives the entity reference of an object in a loaded twin: the twin itself for the model's project, an entity of the twin for one of its products. It makes no request.

import { format } from '@twinfinity/entity-reference';

const twin = await client.loadTwin('550e8400-e29b-41d4-a716-446655440000');

for (const product of twin.model?.products ?? []) {
  const reference = client.referenceOf(product);
  if (reference !== undefined) {
    format(reference); // 'twin~twin:550e8400-e29b-41d4-a716-446655440000.entity:3WoDmit2L9H8xguu5dNQPk'
  }
}

The client records which twin each object was loaded for, so several twins can be loaded at once, through one client or several, and any client answers for any loaded object. entityReferenceOf(object) is the same function without a client.

The result is undefined, never an exception, when:

  • the object was not loaded as the project or a product of a twin by a loadTwin… call. That includes an entity type, a model read without TwinClient, and a product added to model.products after the load;
  • the product's globalId is empty or not a canonical IFC GlobalId, which occurs for DWG-derived objects and for twins stored before the Twin service required specification entity ids to be IFC GlobalIds;
  • the twin id is not a lowercase uuid. The id is used as you passed it to loadTwin, so load with the lowercase spelling.

When you hold ids instead of a loaded object, use the builders. Each returns undefined under the same id rules, and none takes a version:

| Builder | Entity reference | | ------------------------------------------------------ | ------------------------------------------------- | | entityReferenceOfTwin(twinId) | twin~twin:<twinId> | | entityReferenceOfTwinEntity(twinId, gid) | twin~twin:<twinId>.entity:<gid> | | entityReferenceOfTwinFile(twinId, fileId) | twin~twin:<twinId>.file:<fileId> | | entityReferenceOfTwinFileEntity(twinId, fileId, gid) | twin~twin:<twinId>.file:<fileId>/entities/<gid> |

A gid is unique only within a twin's merged model, so the merged-model and per-file spellings are separate functions and you choose between them. The per-file spelling is the one a twin specification's representedBy takes.

format and the EntityReference type come from @twinfinity/entity-reference, a peer dependency that must be installed alongside this package. See Entity References for the rules.

Asset Operations

// List assets in a twin
const assets = await client.listAssets('twin-id');

// Include soft-deleted assets
const allAssets = await client.listAssets('twin-id', 0, 100, { includeDeleted: true });

// Create assets
const created = await client.createAssets('twin-id', [{ name: 'Pump A', category: 'HVAC', type: 'Pump' }]);

// Update with optimistic concurrency
const updated = await client.updateAsset('twin-id', 'asset-id', {
  etag: asset.etag,
  name: 'Pump A (Updated)'
});

// Batch operations (atomic - all succeed or all roll back)
const result = await client.batchAssets('twin-id', [
  { operation: 'upsert', id: 'asset-1', item: { name: 'New Asset' } },
  { operation: 'delete', id: 'asset-2', item: { etag: 'etag-2' } },
  { operation: 'restore', id: 'asset-3', item: { etag: 'etag-3' } }
]);

// Batch get multiple assets by ID
const fetched = await client.batchGetAssets('twin-id', ['id-1', 'id-2']);

Twin File Operations

// List files (with filtering)
const files = await client.listTwinFiles('twin-id', { checkedIn: true });

// Create or update a file
const file = await client.createOrUpdateTwinFile('twin-id', 'file-id', {
  source: { changeId: 'doc-id', changeVersion: 1, sourceType: 'DocumentChange' },
  name: 'model.ifc',
  quality: 100
});

// Download file content
const blob = await client.getTwinFileContent('twin-id', 'file-id');

Error Handling

All methods throw typed errors for specific HTTP failure codes. Unexpected status codes throw a generic Error.

Error Classes

| Error | HTTP Status | Context Attached | | ------------------------- | ----------- | ------------------------------ | | TwinValidationError | 400 | errorBody?: TwinErrorBody | | TwinNotFoundError | 404 | errorBody?: TwinErrorBody | | TwinConflictError | 409 | currentTwin: TwinGet | | AssetValidationError | 400 | errorBody?: TwinErrorBody | | AssetForbiddenError | 403 | - | | AssetNotFoundError | 404 | - | | AssetConflictError | 409 | currentAsset: AssetGet | | AssetBatchError | 404, 409 | response: AssetBatchResponse | | TwinFileValidationError | 400 | errorBody?: TwinErrorBody | | TwinFileNotFoundError | 404 | - | | TwinFileConflictError | 409 | currentTwinFile: TwinFileGet | | ProcessorNotFoundError | 404 | - |

Twin-scoped permissions (/twins/{twinId}/permissions, /twins/{twinId}/groups):

| Error | HTTP Status | Context Attached | | --------------------------------------- | ----------- | --------------------------------------------------------- | | TwinPermissionsForbiddenError | 403 | errorBody?: TwinErrorBody | | TwinPrincipalNotFoundError | 404 | errorBody?: TwinErrorBody | | TwinPermissionsConflictError | 409 | conflictCause, currentState?, errorBody | | TwinTemporarilyUnavailableError | 503 | errorBody?: TwinErrorBody, retryAfterSeconds?: number |

TwinPermissionsConflictError names the conflict on conflictCause rather than on cause, leaving the standard Error.cause free: EtagMismatch is retried by re-reading, EtagRequired by reading at all, and ScopesNotProvisioned is not retryable. currentState carries the entry as the server holds it, including the etag to retry with.

Asset-domain permissions (/twins/permissions, /twins/groups) reuse the errors above, except that a 409 arrives as AssetPermissionsConflictError — the same shape, but its currentState is an AssetPrincipalPermissionsGet rather than a twin's entry, and ScopesNotProvisioned never occurs because the domain's scope is seeded once per customer rather than provisioned per resource.

Conflict Handling

Conflict errors (409) carry the current server-side state, enabling retry logic:

import { TwinConflictError } from '@twinfinity/twin';

try {
  await client.createOrUpdateTwin('twin-id', twinData);
} catch (e) {
  if (e instanceof TwinConflictError) {
    // e.currentTwin contains the server's current version
    // Use it to merge changes and retry
    const merged = mergeChanges(twinData, e.currentTwin);
    await client.createOrUpdateTwin('twin-id', {
      ...merged,
      etag: e.currentTwin.etag
    });
  }
}

Validation Error Body

TwinValidationError, TwinFileValidationError, and AssetValidationError may include a structured error body:

interface TwinErrorBody {
  detail: string;
  cause: string;
  item?: {
    kind: string;
    id: string;
    version?: number;
    field?: string;
  };
}

Batch Error Handling

AssetBatchError carries the full batch response with per-item results, including which operations were reverted:

import { AssetBatchError } from '@twinfinity/twin';

try {
  await client.batchAssets('twin-id', operations);
} catch (e) {
  if (e instanceof AssetBatchError) {
    for (const result of e.response.results) {
      if (result.status === 'conflicted') {
        console.log(`Item ${result.index} conflicted: ${result.error?.detail}`);
      }
    }
  }
}

Exports

The package has two export paths:

  • @twinfinity/twin - Public API: TwinClient, ITwinClient, all types, all error classes
  • @twinfinity/twin/internal/* - Internal modules (no stability guarantees)