Skip to content

Commit d3cb89d

Browse files
authored
fix: import in extensions (#3276)
1 parent c8625b3 commit d3cb89d

4 files changed

Lines changed: 41 additions & 37 deletions

File tree

package-lock.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/controllers/fs/LegacyFSController.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,6 @@ import {
4646
} from './legacyFsHelpers.js';
4747
import { RouteOptions } from '../../core/http/index.js';
4848

49-
/**
50-
* Legacy FS routes, implemented as thin shims over `FSService`.
51-
*
52-
* Each shim parses the request shape (FSNodeParam-style `{ path, uid, id }`
53-
* or `{ parent, name }`), invokes the service method, and returns the
54-
* snake_case response clients expect.
55-
*/
56-
5749
type RouterCache = Map<string, RequestHandler | null>;
5850

5951
const additionalRoutePaths: Record<string, string> = {};
@@ -1631,17 +1623,6 @@ export class LegacyFSController extends PuterController {
16311623
download.body.pipe(res);
16321624
};
16331625

1634-
// Helpers for writeFile
1635-
// -- GUI event emission -------------------------------------------
1636-
//
1637-
// Fire-and-forget `outer.gui.item.*` events so SocketService,
1638-
// BroadcastService, WorkerDriver (hot-reload), and cache-invalidation
1639-
// listeners pick up mutations made through the legacy (bare-path) routes.
1640-
// FSController (v2-native /fs/* routes) emits these from its own handlers;
1641-
// LegacyFSController delegates to the same FSService but needs its
1642-
// own emissions because the service layer deliberately doesn't emit GUI
1643-
// events (that's a controller concern).
1644-
16451626
async #emitGuiEvent(
16461627
eventName:
16471628
| 'outer.gui.item.added'

src/backend/extensions.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ describe('extension.import("client") optional client access', () => {
7878
return null;
7979
}
8080
};
81-
expect(probe()).toBe(fake);
81+
// The import proxy method-binds, so the result is a binding proxy over
82+
// `fake` rather than the raw reference (identity is intentionally not
83+
// preserved). What the probe pattern locks is that a registered client
84+
// surfaces a callable method.
85+
const result = probe();
86+
expect(result).not.toBeNull();
87+
expect(typeof (result as { query: unknown }).query).toBe('function');
8288
});
8389
});

src/backend/extensions.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,35 @@ const makeUseFn = (): ExtensionUseFn => {
201201
* `'store:baz'` / `'controller:qux'` / `'driver:fred'` — returns a lazy
202202
* proxy to the registered instance (thrown on use-before-init).
203203
*/
204+
/**
205+
* Wrap a resolved layer instance so that pulling a method off the import
206+
* comes out *bound* to the instance. Extensions routinely grab a method as a
207+
* bare reference — `const { write } = extension.import('service').fs` or
208+
* `const w = svc.fs.write` — then call it detached; without binding, `this`
209+
* is `undefined` and the method's private-field access throws on the first
210+
* line. Getters keep the real instance as their receiver, so private-field
211+
* reads inside accessors still resolve. Only `get` is trapped; writes, `in`,
212+
* and descriptor reads fall through to the instance unchanged.
213+
*
214+
* Trade-off: each method access returns a fresh bound function, so reference
215+
* identity is not stable (`svc.fs.write !== svc.fs.write`). That's acceptable
216+
* for the import surface, where instances are grabbed once and methods called.
217+
*/
218+
const bindLayerMethods = <T>(instance: T): T => {
219+
if (instance === null || typeof instance !== 'object') {
220+
return instance;
221+
}
222+
return new Proxy(instance as object, {
223+
get(target, prop) {
224+
const value = Reflect.get(target, prop, target);
225+
return typeof value === 'function'
226+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
227+
(value as (...a: any[]) => unknown).bind(target)
228+
: value;
229+
},
230+
}) as T;
231+
};
232+
204233
export const extension = {
205234
// -- Config access -----------------------------------------------
206235
//
@@ -324,7 +353,7 @@ export const extension = {
324353
};
325354
return new Proxy({}, proxyProxyHandler) as object;
326355
}
327-
return proxiedObj;
356+
return bindLayerMethods(proxiedObj);
328357
},
329358
};
330359
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -352,7 +381,7 @@ export const extension = {
352381
};
353382
return new Proxy({}, proxyProxyHandler) as object;
354383
}
355-
return proxiedObj;
384+
return bindLayerMethods(proxiedObj);
356385
},
357386
};
358387
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -380,7 +409,7 @@ export const extension = {
380409
};
381410
return new Proxy({}, proxyProxyHandler) as object;
382411
}
383-
return proxiedObj;
412+
return bindLayerMethods(proxiedObj);
384413
},
385414
};
386415
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -408,7 +437,7 @@ export const extension = {
408437
};
409438
return new Proxy({}, proxyProxyHandler) as object;
410439
}
411-
return proxiedObj;
440+
return bindLayerMethods(proxiedObj);
412441
},
413442
};
414443
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -436,7 +465,7 @@ export const extension = {
436465
};
437466
return new Proxy({}, proxyProxyHandler) as object;
438467
}
439-
return proxiedObj;
468+
return bindLayerMethods(proxiedObj);
440469
},
441470
};
442471
// eslint-disable-next-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)