Skip to content

Commit 8480f3b

Browse files
committed
feat: support un-scoping via scope(null | '')
1 parent 14cdebc commit 8480f3b

9 files changed

Lines changed: 50 additions & 5 deletions

File tree

‎packages/devframe/src/client/rpc.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,13 @@ export interface DevframeRpcClient {
120120
* shared-state key, and streaming channel with `my-plugin:`, and
121121
* exposes a typed top-level `settings` store. This is the preferred way
122122
* to consume the client from a single tool's UI code.
123+
*
124+
* Pass `null` or `''` to un-scope and get the base client.
123125
*/
124-
scope: <NS extends string>(namespace: NS) => DevframeScopedClientContext<NS, SettingsForNamespace<NS>>
126+
scope: {
127+
<NS extends string>(namespace: NS): DevframeScopedClientContext<NS, SettingsForNamespace<NS>>
128+
(namespace?: null | ''): DevframeRpcClient
129+
}
125130
}
126131

127132
export interface DevframeRpcClientMode {
@@ -324,7 +329,9 @@ export async function getDevframeRpcClient(
324329
// Namespace-scoped views are memoized per namespace so repeated
325330
// `client.scope('my-plugin')` calls return a stable object.
326331
const scopedCache = new Map<string, DevframeScopedClientContext<string>>()
327-
rpc.scope = ((namespace: string) => {
332+
rpc.scope = ((namespace?: string | null) => {
333+
if (!namespace)
334+
return rpc
328335
let scoped = scopedCache.get(namespace)
329336
if (!scoped) {
330337
scoped = createScopedClientContext(rpc, namespace)

‎packages/devframe/src/client/scope.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ describe('client.scope()', () => {
4545
expect(scoped.rpc.namespace).toBe('my-plugin')
4646
})
4747

48+
it('un-scopes and returns the base client when passed null or empty string', () => {
49+
const { rpc } = createMockClient()
50+
// To properly test this, we need the `scope` method added to `rpc`, similar
51+
// to what `getDevframeRpcClient` does in `rpc.ts`.
52+
rpc.scope = (ns?: string | null) => (ns ? createScopedClientContext(rpc, ns) : rpc) as any
53+
const scoped = rpc.scope('my-plugin')
54+
expect(rpc.scope('')).toBe(rpc)
55+
expect(rpc.scope(null)).toBe(rpc)
56+
expect(scoped.scope('')).toBe(rpc)
57+
expect(scoped.scope(null)).toBe(rpc)
58+
})
59+
4860
it('qualifies bare call/callEvent/callOptional method names', () => {
4961
const { rpc } = createMockClient()
5062
const scoped = createScopedClientContext(rpc, 'my-plugin')

‎packages/devframe/src/client/scope.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export interface DevframeScopedClientContext<NS extends string = string, Setting
8585
rpc: DevframeScopedClientRpc<NS>
8686
/** Persisted settings for this namespace (`global` + `project`). */
8787
settings: DevframeSettings<Settings>
88+
/** Derive another scope, or pass `null` / `''` to get the base client. */
89+
scope: DevframeRpcClient['scope']
8890
}
8991

9092
/**
@@ -126,5 +128,6 @@ export function createScopedClientContext<NS extends string = string>(
126128
base: rpc,
127129
rpc: scopedRpc,
128130
settings: createClientSettings(rpc, namespace),
131+
scope: rpc.scope,
129132
}
130133
}

‎packages/devframe/src/node/__tests__/scope.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ describe('ctx.scope()', () => {
3838
expect(ctx.scope('my-plugin')).not.toBe(ctx.scope('other'))
3939
})
4040

41+
it('un-scopes and returns the base context when passed null or empty string', async () => {
42+
const { ctx } = await createCtx()
43+
const scoped = ctx.scope('my-plugin')
44+
expect(ctx.scope('')).toBe(ctx)
45+
expect(ctx.scope(null)).toBe(ctx)
46+
expect(scoped.scope('')).toBe(ctx)
47+
expect(scoped.scope(null)).toBe(ctx)
48+
})
49+
4150
it('exposes the base context and read-only fields', async () => {
4251
const { ctx } = await createCtx()
4352
const scoped = ctx.scope('my-plugin')

‎packages/devframe/src/node/context.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ export async function createHostContext(options: CreateHostContextOptions): Prom
6161
// Namespace-scoped views are memoized per namespace so repeated
6262
// `ctx.scope('my-plugin')` calls return a stable object.
6363
const scopedCache = new Map<string, DevframeScopedNodeContext<string>>()
64-
context.scope = ((namespace: string) => {
64+
context.scope = ((namespace?: string | null) => {
65+
if (!namespace)
66+
return context
6567
let scoped = scopedCache.get(namespace)
6668
if (!scoped) {
6769
scoped = createScopedNodeContext(context, namespace)

‎packages/devframe/src/node/scope.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@ export function createScopedNodeContext<NS extends string = string>(
5656
views: context.views,
5757
diagnostics: context.diagnostics,
5858
agent: context.agent,
59+
scope: context.scope,
5960
}
6061
}

‎packages/devframe/src/types/context.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,13 @@ export interface DevframeNodeContext {
5858
* key, and streaming channel with `my-plugin:`, and exposes a typed
5959
* top-level `settings` store. This is the preferred way to consume the
6060
* context from a single tool's setup code.
61+
*
62+
* Pass `null` or `''` to un-scope and get the base context.
6163
*/
62-
scope: <NS extends string>(namespace: NS) => DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>
64+
scope: {
65+
<NS extends string>(namespace: NS): DevframeScopedNodeContext<NS, SettingsForNamespace<NS>>
66+
(namespace?: null | ''): DevframeNodeContext
67+
}
6368
}
6469

6570
export interface ConnectionMeta {

‎packages/devframe/src/types/scope.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,6 @@ export interface DevframeScopedNodeContext<NS extends string = string, Settings
208208
views: DevframeViewHost
209209
diagnostics: DevframeDiagnosticsHost
210210
agent: DevframeAgentHost
211+
/** Derive another scope, or pass `null` / `''` to get the base context. */
212+
scope: DevframeNodeContext['scope']
211213
}

‎tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ export interface DevframeRpcClient {
1616
sharedState: RpcSharedStateHost;
1717
streaming: RpcStreamingClientHost;
1818
cacheManager: RpcCacheManager;
19-
scope: <NS extends string>(_: NS) => DevframeScopedClientContext<NS, SettingsForNamespace<NS>>;
19+
scope: {
20+
<NS extends string>(_: NS): DevframeScopedClientContext<NS, SettingsForNamespace<NS>>;
21+
(_?: null | ''): DevframeRpcClient;
22+
};
2023
}
2124
export interface DevframeRpcClientMode {
2225
readonly isTrusted: boolean;
@@ -43,6 +46,7 @@ export interface DevframeScopedClientContext<NS extends string = string, Setting
4346
readonly base: DevframeRpcClient;
4447
rpc: DevframeScopedClientRpc<NS>;
4548
settings: DevframeSettings<Settings>;
49+
scope: DevframeRpcClient['scope'];
4650
}
4751
export interface DevframeScopedClientRpc<NS extends string = string> {
4852
readonly namespace: NS;

0 commit comments

Comments
 (0)