chore: mesurer la forme du tableau au lieu de la supposer, et retirer les any

La branche tableau se decide sur `typeof cible.length === "number"`, ce qui est
une question posee a un proxy : mesure contre le vrai
`@ng-org/alien-deepsignals`, `deepSignal(new Set(...)).length` vaut `undefined`
et ses `Object.keys` sont vides, tandis que `deepSignal([...]).length` est un
nombre. La forme ensemble n-entre donc jamais dans cette branche, et c-est aussi
celle qui ne porte aucune cle nommant un objet.

Les trois `as any` des tests ajoutes et le `Iterable<any>` du harness sont
types.
This commit is contained in:
Sylvain Duchesne
2026-09-24 18:39:33 +02:00
parent e86e249a5a
commit b1b099b6c7
3 changed files with 18 additions and 10 deletions
+5 -3
View File
@@ -1088,9 +1088,11 @@ const identity = new IdentityStore(
// `data` - the member its own example destructures first, and one of the three that // `data` - the member its own example destructures first, and one of the three that
// used to come back unfiltered. // used to come back unfiltered.
const read = (): string[] => const read = (): string[] =>
[...(libUseShape(wsShape("urn:e2e:caps:Item"), EVERYTHING).data as Iterable<any>)].map( [
(i) => i.v, ...(libUseShape(wsShape("urn:e2e:caps:Item"), EVERYTHING).data as Iterable<
); { v: string }
>),
].map((i) => i.v);
setCurrentUser("owner-O"); setCurrentUser("owner-O");
getCaps().open("did:ng:o:protdoc", "protected"); getCaps().open("did:ng:o:protdoc", "protected");
const link = getCaps().open("did:ng:o:pubdoc", "public"); const link = getCaps().open("did:ng:o:pubdoc", "public");
@@ -213,6 +213,12 @@ export function makeReadFilteredView<S extends object>(set: S, caps: CapRegistry
* `DeepSignal<ReadOnlyArray>` instead of a `DeepSignalSet`, and an array is addressed by * `DeepSignal<ReadOnlyArray>` instead of a `DeepSignalSet`, and an array is addressed by
* INDEX and counts itself with `length`. Decided from the target once, so the view never * INDEX and counts itself with `length`. Decided from the target once, so the view never
* grows a member the collection it wraps does not have. * grows a member the collection it wraps does not have.
*
* MEASURED against the real `@ng-org/alien-deepsignals` rather than assumed, because the
* collection is itself a proxy and what it answers for a key it does not carry is its
* business: `deepSignal(new Set(...)).length` is `undefined` and its `Object.keys` is
* empty, while `deepSignal([...]).length` is a number. So the set form never enters this
* branch, and the set form is also the one with no key naming an item to project.
*/ */
const arrayLike = typeof (set as { length?: unknown }).length === "number"; const arrayLike = typeof (set as { length?: unknown }).length === "number";
/** The index a key names, or `null` when it names no position in this collection. */ /** The index a key names, or `null` when it names no position in this collection. */
+7 -7
View File
@@ -149,10 +149,10 @@ test("forEach is filtered too", () => {
// hatch included, which `get` refuses by name. Everything built on that call goes the same // hatch included, which `get` refuses by name. Everything built on that call goes the same
// way: `Object.keys`, `Object.entries`, a spread, `JSON.stringify`, a console inspection. // way: `Object.keys`, `Object.entries`, a spread, `JSON.stringify`, a console inspection.
test("a descriptor cannot answer what reading the member refuses", () => { test("a descriptor cannot answer what reading the member refuses", () => {
const set = new Set<Item>([MINE]) as any; const set: Set<Item> & { __raw__?: unknown } = new Set<Item>([MINE]);
set.__raw__ = set; set.__raw__ = set;
const { caps, become } = setup("alice"); const { caps, become } = setup("alice");
const view = makeReadFilteredView(set, caps) as any; const view = makeReadFilteredView(set, caps);
become("bob"); become("bob");
expect(() => Object.getOwnPropertyDescriptor(view, "__raw__")).toThrow(/raw collection/i); expect(() => Object.getOwnPropertyDescriptor(view, "__raw__")).toThrow(/raw collection/i);
}); });
@@ -163,9 +163,9 @@ test("a descriptor cannot answer what reading the member refuses", () => {
// count of what a holder may NOT read came back out through the other three, and the // count of what a holder may NOT read came back out through the other three, and the
// descriptor handed back the items themselves. // descriptor handed back the items themselves.
test("the array form counts and addresses the readable items, by every route", () => { test("the array form counts and addresses the readable items, by every route", () => {
const items = [MINE, FOREIGN, LINKED]; const items: Item[] = [MINE, FOREIGN, LINKED];
const { caps, become } = setup("alice"); const { caps, become } = setup("alice");
const view = makeReadFilteredView(items, caps) as any; const view = makeReadFilteredView(items, caps);
become("bob"); // holds nothing of the three documents become("bob"); // holds nothing of the three documents
expect(view.length).toBe(0); expect(view.length).toBe(0);
@@ -178,9 +178,9 @@ test("the array form counts and addresses the readable items, by every route", (
become("alice"); // holds her own document and the one she published become("alice"); // holds her own document and the one she published
expect(view.length).toBe(2); expect(view.length).toBe(2);
expect(Object.keys(view)).toEqual(["0", "1"]); expect(Object.keys(view)).toEqual(["0", "1"]);
expect(view[0].id).toBe("a"); expect(view[0]).toBe(MINE);
expect(view[1].id).toBe("p"); expect(view[1]).toBe(LINKED);
expect(Object.getOwnPropertyDescriptor(view, "1")?.value.id).toBe("p"); expect(Object.getOwnPropertyDescriptor(view, "1")?.value).toBe(LINKED);
expect(2 in view).toBe(false); expect(2 in view).toBe(false);
// `length` is the array's own key and is never hidden - a proxy may not drop a key the // `length` is the array's own key and is never hidden - a proxy may not drop a key the
// target cannot configure away - but it is not a position and does not enumerate. // target cannot configure away - but it is not a position and does not enumerate.