Skip to content

Commit bdf45fa

Browse files
Add experimental link subscriptions (#3886)
* Add link subscriptions * Multiplex link subscriptions * Extract shared SubscriptionWebsocket for object set and link subscriptions * Use shared websocket for subscriptions * Address link subscription review * Update link subscription API * Format mock websocket * Wait for object set revalidation --------- Co-authored-by: jack busa <jbusa@palantir.com>
1 parent c742209 commit bdf45fa

16 files changed

Lines changed: 1366 additions & 455 deletions

.changeset/link-subscriptions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@osdk/api": minor
3+
"@osdk/client": minor
4+
---
5+
6+
Add experimental subscriptions for directed link changes between selected objects.

etc/client.report.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export interface Client extends SharedClient, OldSharedClient {
112112
// (undocumented)
113113
<Q extends QueryDefinition<any>>(o: Q): QuerySignatureFromDef<Q>;
114114
// (undocumented)
115-
<Q extends Experiment<"2.0.8"> | Experiment<"2.1.0"> | Experiment<"2.2.0"> | Experiment<"2.8.0"> | Experiment<"2.19.0">>(experiment: Q): ExperimentFns<Q>;
115+
<Q extends Experiment<"2.0.8"> | Experiment<"2.1.0"> | Experiment<"2.59.0"> | Experiment<"2.2.0"> | Experiment<"2.8.0"> | Experiment<"2.19.0">>(experiment: Q): ExperimentFns<Q>;
116116
fetchMetadata<Q extends ObjectTypeDefinition | InterfaceDefinition | ActionDefinition<any> | QueryDefinition<any>>(o: Q): Promise<Q extends ObjectTypeDefinition ? ObjectMetadata : Q extends InterfaceDefinition ? InterfaceMetadata : Q extends ActionDefinition<any> ? ActionMetadata : Q extends QueryDefinition<any> ? QueryMetadata : never>;
117117
}
118118

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, expectTypeOf, it } from "vitest";
18+
19+
import type { ObjectIdentifiers } from "../OsdkBase.js";
20+
import type { Osdk } from "../OsdkObjectFrom.js";
21+
import { EmployeeApiTest } from "../test/EmployeeApiTest.js";
22+
import type { ExperimentFns } from "./Experiment.js";
23+
import type { __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions } from "./subscribeToLinks.js";
24+
25+
type SubscribeToLinks = ExperimentFns<
26+
typeof __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions
27+
>["subscribeToLinks"];
28+
29+
describe("subscribeToLinks", () => {
30+
const employeeOne = {} as Osdk.Instance<EmployeeApiTest>;
31+
const employeeTwo = {} as Osdk.Instance<EmployeeApiTest>;
32+
const subscribeToLinks: SubscribeToLinks = () => ({
33+
unsubscribe: () => {},
34+
});
35+
36+
it("infers subscription types from the object type and links", () => {
37+
subscribeToLinks(EmployeeApiTest, {
38+
links: ["lead", "peeps"],
39+
listener: {
40+
onChange: (linkUpdate) => {
41+
expectTypeOf(linkUpdate.linkType).toEqualTypeOf<"lead" | "peeps">();
42+
expectTypeOf(linkUpdate.source).toEqualTypeOf<
43+
ObjectIdentifiers<EmployeeApiTest>
44+
>();
45+
expectTypeOf(linkUpdate.target).toEqualTypeOf<
46+
ObjectIdentifiers<EmployeeApiTest>
47+
>();
48+
expectTypeOf(linkUpdate.state).toEqualTypeOf<"ADDED" | "REMOVED">();
49+
},
50+
},
51+
objects: [employeeOne, employeeTwo, 3],
52+
});
53+
});
54+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2026 Palantir Technologies, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type {
18+
LinkTypeApiNamesFor,
19+
LinkedObjectType,
20+
} from "../objectSet/ObjectSetLinks.js";
21+
import type { ObjectTypeDefinition } from "../ontology/ObjectTypeDefinition.js";
22+
import type { ObjectIdentifiers } from "../OsdkBase.js";
23+
import type { OsdkObjectPrimaryKeyType } from "../OsdkObjectPrimaryKeyType.js";
24+
import type { Experiment } from "./Experiment.js";
25+
26+
export namespace LinkSubscription {
27+
export interface Args<
28+
Q extends ObjectTypeDefinition,
29+
L extends LinkTypeApiNamesFor<Q>,
30+
> {
31+
readonly links: ReadonlyArray<L>;
32+
readonly listener: Listener<NoInfer<Q>, NoInfer<L>>;
33+
readonly objects: ReadonlyArray<
34+
ObjectIdentifiers<NoInfer<Q>> | OsdkObjectPrimaryKeyType<NoInfer<Q>>
35+
>;
36+
}
37+
38+
export interface Listener<
39+
Q extends ObjectTypeDefinition,
40+
L extends LinkTypeApiNamesFor<Q>,
41+
> {
42+
readonly onChange?: (linkUpdate: LinkUpdate<Q, L>) => void;
43+
readonly onError?: (error: {
44+
readonly error: unknown;
45+
readonly subscriptionClosed: boolean;
46+
}) => void;
47+
readonly onOutOfDate?: (event: OutOfDateEvent<L>) => void;
48+
readonly onSuccessfulSubscription?: () => void;
49+
}
50+
51+
export type LinkUpdate<
52+
Q extends ObjectTypeDefinition,
53+
L extends LinkTypeApiNamesFor<Q>,
54+
> =
55+
L extends LinkTypeApiNamesFor<Q>
56+
? {
57+
readonly linkType: L;
58+
readonly source: ObjectIdentifiers<Q>;
59+
readonly state: "ADDED" | "REMOVED";
60+
readonly target: ObjectIdentifiers<LinkedObjectType<Q, L>>;
61+
}
62+
: never;
63+
64+
export interface OutOfDateEvent<L extends string> {
65+
readonly links: ReadonlyArray<L>;
66+
}
67+
}
68+
69+
type SubscribeToLinks = <
70+
const Q extends ObjectTypeDefinition,
71+
const L extends LinkTypeApiNamesFor<NoInfer<Q>>,
72+
>(
73+
objectType: Q,
74+
args: LinkSubscription.Args<Q, L>,
75+
) => { readonly unsubscribe: () => void };
76+
77+
export const __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions: Experiment<
78+
"2.59.0",
79+
"__EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions",
80+
{ readonly subscribeToLinks: SubscribeToLinks }
81+
> = {
82+
name: "__EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions",
83+
type: "experiment",
84+
version: "2.59.0",
85+
};

packages/api/src/objectSet/ObjectSetLinks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { ObjectIdentifiers } from "../OsdkBase.js";
2121
export type LinkTypeApiNamesFor<Q extends ObjectOrInterfaceDefinition> =
2222
Extract<keyof CompileTimeMetadata<Q>["links"], string>;
2323

24-
type LinkedObjectType<
24+
export type LinkedObjectType<
2525
Q extends ObjectOrInterfaceDefinition,
2626
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>,
2727
> = NonNullable<

packages/api/src/public/unstable.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export {
2323
type FetchPageByRidPayload,
2424
} from "../experimental/fetchPageByRid.js";
2525
export { __EXPERIMENTAL__NOT_SUPPORTED_YET__getBulkLinks } from "../experimental/getBulkLinks.js";
26+
export {
27+
__EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions,
28+
type LinkSubscription,
29+
} from "../experimental/subscribeToLinks.js";
2630
export type {
2731
AnnotateGeometry,
2832
Annotation,

packages/client/src/Client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export interface Client extends SharedClient, OldSharedClient {
125125
Q extends
126126
| Experiment<"2.0.8">
127127
| Experiment<"2.1.0">
128+
| Experiment<"2.59.0">
128129
| Experiment<"2.2.0">
129130
| Experiment<"2.8.0">
130131
| Experiment<"2.19.0">,

packages/client/src/createClient.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ActionDefinition,
1919
FetchPageArgs,
2020
InterfaceDefinition,
21+
LinkTypeApiNamesFor,
2122
Logger,
2223
Media,
2324
NullabilityAdherence,
@@ -34,6 +35,7 @@ import type {
3435
import type {
3536
Experiment,
3637
ExperimentFns,
38+
LinkSubscription,
3739
MediaTransformation,
3840
MinimalObjectSet,
3941
TransformOptions,
@@ -43,6 +45,7 @@ import {
4345
__EXPERIMENTAL__NOT_SUPPORTED_YET__fetchOneByRid,
4446
__EXPERIMENTAL__NOT_SUPPORTED_YET__fetchPageByRid,
4547
__EXPERIMENTAL__NOT_SUPPORTED_YET__getBulkLinks,
48+
__EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions,
4649
__EXPERIMENTAL__NOT_SUPPORTED_YET__subscribeToNoTypeObjectSet,
4750
transformAndWait,
4851
} from "@osdk/api/unstable";
@@ -165,6 +168,7 @@ export function createClientFromContext(clientCtx: MinimalClient) {
165168
| QueryDefinition<any>
166169
| Experiment<"2.0.8">
167170
| Experiment<"2.1.0">
171+
| Experiment<"2.59.0">
168172
| Experiment<"2.8.0">
169173
| Experiment<"2.19.0">,
170174
>(
@@ -180,6 +184,7 @@ export function createClientFromContext(clientCtx: MinimalClient) {
180184
: T extends
181185
| Experiment<"2.0.8">
182186
| Experiment<"2.1.0">
187+
| Experiment<"2.59.0">
183188
| Experiment<"2.8.0">
184189
| Experiment<"2.19.0">
185190
? { invoke: ExperimentFns<T> }
@@ -222,6 +227,28 @@ export function createClientFromContext(clientCtx: MinimalClient) {
222227
);
223228
},
224229
} as any;
230+
case __EXPERIMENTAL__NOT_SUPPORTED_YET__linkSubscriptions.name:
231+
return {
232+
subscribeToLinks: (
233+
objectType: ObjectTypeDefinition,
234+
args: LinkSubscription.Args<
235+
ObjectTypeDefinition,
236+
LinkTypeApiNamesFor<ObjectTypeDefinition>
237+
>,
238+
) => {
239+
const pendingSubscription =
240+
import("./objectSet/LinkSubscriptionWebsocket.js").then(
241+
({ LinkSubscriptionWebsocket }) =>
242+
LinkSubscriptionWebsocket.getInstance(clientCtx).subscribe(
243+
objectType,
244+
args,
245+
),
246+
);
247+
return {
248+
unsubscribe: async () => (await pendingSubscription)(),
249+
};
250+
},
251+
} as any;
225252
case __EXPERIMENTAL__NOT_SUPPORTED_YET__fetchOneByRid.name:
226253
return {
227254
fetchOneByRid: async <

0 commit comments

Comments
 (0)