Skip to content

Add experimental link subscriptions - #3886

Merged
nihalbhatnagar merged 8 commits into
palantir:mainfrom
jbusa22:jbusa-osdk-link-subscriptions
Aug 27, 2026
Merged

Add experimental link subscriptions#3886
nihalbhatnagar merged 8 commits into
palantir:mainfrom
jbusa22:jbusa-osdk-link-subscriptions

Conversation

@jbusa22

@jbusa22 jbusa22 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an experimental subscribeToLinks API for subscribing to directed link changes from selected OSDK objects
  • infer the object type from the first object and require every remaining object to have that same concrete type
  • infer valid directed link API names and each update target type from the generated ontology types
  • deliver backend update batches over the ontology subscriptions WebSocket
  • reuse the existing object-set subscription retry and WebSocket test infrastructure

Usage

This example watches two managers for changes to their direct reports. fetchOne returns real Osdk.Instance<Employee> values, and peeps is the generated directed link API name from a manager to their reports.

import {
  __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions,
} from "@osdk/api/unstable";
import { Employee } from "./generated/ontology/objects/Employee.js";

const [managerOne, managerTwo] = await Promise.all([
  client(Employee).fetchOne(managerOneId),
  client(Employee).fetchOne(managerTwoId),
]);

# Still need to get the links from a separate service
const directReportsLink = "peeps"

const subscription = client(
  __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions,
).subscribeToLinks({
  objects: [managerOne, managerTwo],
  links: [directReportsLink],
  listener: {
    onSuccessfulSubscription() {
      console.log("Listening for direct-report changes");
    },
    onChange({ updates }) {
      for (const update of updates) {
        // source is the manager; target is the added or removed report.
        console.log(update.state, update.source, update.target);
      }
    },
    onOutOfDate({ links }) {
      // Re-fetch any local state derived from these links.
      console.log("Links need refreshing", links);
    },
    onError({ error, subscriptionClosed }) {
      console.error({ error, subscriptionClosed });
    },
  },
});

subscription.unsubscribe();


async #prepare(): Promise<void> {
try {
const [firstObject] = this.#args.objects;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ts should enforce that all objects are the same type so this is safe

Comment thread packages/client/src/createClient.ts Outdated
| QueryDefinition<any>
| Experiment<"2.0.8">
| Experiment<"2.1.0">
| Experiment<"2.17.0">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to be 2.59 and updated to the version at which we ship osdk/client. @ssanjay1 we def need to review this paradigm since we're shipping minor versions rapidly now

Comment thread pnpm-workspace.yaml Outdated
"@osdk/foundry.admin": 2.70.0
"@osdk/foundry.mediasets": 2.70.0
"@osdk/foundry.ontologies": 2.70.0
"@osdk/foundry.ontologies": 2.73.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets go ahead and bump the rest to keep in sync

const EXPONENTIAL_BACKOFF_JITTER_FACTOR = 0.3;
const WEBSOCKET_IDLE_DISCONNECT_DELAY_MS = 15000;
const WEBSOCKET_HEARTBEAT_INTERVAL_MS = 45 * 1000;
export const EXPONENTIAL_BACKOFF_INITIAL_DELAY_MS: number = 1000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should move these to a shared util file instead of exporting from here

}

/** @internal */
export class LinkSubscriptionWebsocket {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a super class that both of these websockets now use for websocket connection/retry/heartbeat logic? I'd imagine that we can basically reuse onMessage, onOpen, initiateSubscribe, all the way upto the specific message ser/de and request payloads?

@@ -810,9 +810,39 @@ export class ObjectSetListenerWebsocket {

/** @internal */
export function constructWebsocketUrl(baseUrl: string, ontologyRid: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think keeping these in the same utils file as above will be helpful

];
}

export interface ChangeEvent<

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe LinkUpdate to match ObjectSetUpdate?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we flatten this out too? We did the same for object set updates where youc an get multiple object updates back on one message, but the handler should be triggered once per specific update/object

Q extends ObjectTypeDefinition,
L extends LinkTypeApiNamesFor<Q>,
> {
readonly links: readonly [L, ...ReadonlyArray<NoInfer<L>>];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave this as a regular array? (NoInfer is fine). We generally try to avoid non-empty array type in this codebase

: never;
}

type SubscribeToLinks = <

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add javadoc for this method. Also, will this work for updates to one to many link types backed by a FK? If not, we should document that here

unsubscribe: () => {},
});

it("infers links and updates from the first object", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the type test

vi.useRealTimers();
});

it("multiplexes subscriptions and routes update batches", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make this description a bit more clear, like "uses one WebSocket for multiple subscriptions and sends updates to the correct listener", I was a bit confused by the wording

L extends LinkTypeApiNamesFor<Q>,
> {
readonly updates: ReadonlyArray<Update<Q, L>>;
readonly objects: ReadonlyArray<Osdk.Instance<Q>>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have this mimic the queries and actions API that take objects like here

| OsdkObjectPrimaryKeyType<T>;
, which probably means just using that same union

if (isDone(subscription)) return;
if (isSubscriptionDone(subscription)) return;
try {
subscription.listener.onError?.({ error, subscriptionClosed: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try catch this error call too?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitty but I think the API should be (Employee, {listener, objects,links})

nihalbhatnagar
nihalbhatnagar previously approved these changes Aug 26, 2026
@policy-bot
policy-bot Bot dismissed nihalbhatnagar’s stale review August 26, 2026 15:21

Invalidated by push of 5d34c94

nihalbhatnagar
nihalbhatnagar previously approved these changes Aug 26, 2026
@jbusa22
jbusa22 force-pushed the jbusa-osdk-link-subscriptions branch from 5d34c94 to 1e71fd5 Compare August 26, 2026 16:46
@policy-bot
policy-bot Bot dismissed nihalbhatnagar’s stale review August 26, 2026 16:47

Invalidated by push of 1e71fd5

nihalbhatnagar
nihalbhatnagar previously approved these changes Aug 26, 2026
@policy-bot
policy-bot Bot dismissed nihalbhatnagar’s stale review August 26, 2026 18:15

Invalidated by push of bd55880

nihalbhatnagar
nihalbhatnagar previously approved these changes Aug 26, 2026
@policy-bot
policy-bot Bot dismissed nihalbhatnagar’s stale review August 26, 2026 18:41

Invalidated by push of 1311a36

@nihalbhatnagar
nihalbhatnagar merged commit bdf45fa into palantir:main Aug 27, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants