The Array.isArray shenanigans

I'm covering all traps, and literally everything works as expected, except this brand check that doesn't pass through getPrototypeOf descriptor while syntax insteanceof does.

Once again, can anyone please show me a single code base where after an explicit, and quite semnatic, Array.isArray(ref) performs a ref instanceof Array check?

I am not questioning the status quo, it's broken by legacy specs, I am questioning if nobody out there should ever use Proxy(array) when such check is meant to return true even if the meant array is just a postMessage friendly tuple to forward details about the reference it holds, not necessarily an array one.

I don't understand what you're asking here.

Yes, isArray is kind of strange. But it's not that strange - all it means is that you need to know up front if you're going to be pretending to be an array, just like you need to know up front if you're going to be pretending to be a function. Yes, that means you can't just an array as the target for everything. I don't understand what problem this is causing you.

1 Like

the issue here is not what I pretend to do, is what every other 3rd party library assume after a proxy from my side reaches that Array.isArray(proxy) thing and the 3rd party library branches out assuming my proxy is an array, when it's not by any other Proxy traps mean ... including instanceof brand check ...

So make your target be something which is not an array, and then their isArray check will return false. I still do not understand the problem here.

Due to other realms, using instanceof with builtins isnā€™t reliable, so altho Iā€™m sure thereā€™s tons of code doing it, Array.isArray is the proper way to check if somethingā€™s an array - but as you know, its pretty much a guarantee that codebases will exist that do things an improper way.

the plot twist in here seems to be that I am (user of JS) in charge of wrapping a different proxy value when isArray brand check is meant by 3rd part libraries ... I am OK with this resolution, as it resulted in me patching the isArray method in 3 lines instead of changng everything else within my code, but I am warning you, Array.isArray is broken as it is and I'll hear from you later when this issue will arise more times, but like I've said, I do stuff nobody did before and that's fine, I am worried about others needing to do same stuff I do in the next future.

it's a solved issue in my code, I can drive 100% main thread from a worker, and even symbols can travel, not a concern ... again, I didn't want to abuse this space for my latest library, but you folks should have really a close look to it: Array.isArray needs a global patch or nothing from 3rd party world would work.

To that I say, nobody still answered my question: show me a single code base that after an isArray(ref) check also is kind and paranoid enough to trigger Proxy traps and do ref instanceof Array after ...

but you folks should have really a close look to it: Array.isArray needs a global patch or nothing from 3rd party world would work.

Wouldn't it also work if you changed your code so that you did not use an array as the target, in cases where you want isArray to return false?

You've chosen to use a global patch instead of refactoring your code, which is a decision you are free to make although personally I think it's a bad idea. But that doesn't mean there's a missing feature here.

1 Like

few issues here:

  • I am using a single typed ref to mimic the original reference ... the function case is the only exception, make it 3 and my code-base will grow a lot for no real-wold benefits compared to a 3 LOC to fix isArray
  • if I make the default {type, value} the isArray will always return false, if it's a postMessage friendly (less serialization) [type, value] it will always return true
  • as typeof is fast and returns object for both references and arrays, you are asking me to put a native (not patched, maybe) isArray check on the reading path for each value ... but I'd take perf over specs bugs any day

I could make my code more complex due this (quite stubborn) take from TC39 around isArray, but TC39 won't pay me to maintain more code to fix this issue in the long term ... so I had some hope here somebody would listen, but so far that didn't seem to be the case.

It's all good, and worth trying though, I'll read about this in the near future, as I'm sure about it, this will haunt more devs, yet meanwhile, you all have a lovely weekend!

Iā€™m not sure what you mean about ā€œlistenā€ - no change can possibly be made here; these semantics are more than 8 years shipped.

a new Proxy trap for that abomination that is at this point the fully unreliable Array.isArray brand check, where no brand is acutally ever checked when it's a Proxy, would be a resolution to me.

const notArray = new Proxy([], {
  isArray: () => false
});

// ECMAScript 2024 ... maybe
Array.isArray(notArray); // false
1 Like
// polyfill
comst {isArray} = Array;
Array.isArray = function (ref) {
  return isArray(ref) && ref instanceof this;
};

OK, this is simplified, but hopefully you got the gist of it!

Since instanceof is unreliable, that will break on const a = []; Object.setPrototypeOf(a, null);, for example - also, anyone can override Symbol.hasInstance on Array and break it.

This post seem to confirm that your usage of proxy is not the one intended by the language. As I mentioned before in the post you dismissed, the target object has special semantics regarding the proxy instance. It's not intended to be a generic structure to store information. If you you need to store information about the proxy or its real target, you should be using a WeakMap.

I am sorry if I appeared dismissive earlier, but please be yourself open minded of suggestions when people are trying to help you, and attempt to decipher where the misunderstanding might be, especially given the limited amount details you share. Proxies are weird and often misunderstood, but they are working as intended.

I can guarantee you I can find other inconsistent behavior expressed by your proxy instances if you don't carefully handle the target object. Please check the enforced invariants in the notes of each of the handler traps.

The language doesn't need a new trap for the target type, you just need to use the right target object when creating the proxy. And no one has answered your instanceof example request because no one is able to understand the request. The instanceof operator as you likely know compares the prototype chain of the first operand to the .prototype property of the second operand, and as such is already going through the proxy's getPrototypeOf trap. As such it's the proxy's responsibility to make the target type and the prototype return by the trap be consistent with each other.

1 Like

this the slightly sad part of these discussions, we often take no as answers when the problem is not even fully understood.

Beside me moving on with the isArray patch in workers, hopefully less obtrusive than a patch in global shared context, I'll try to explain.

This handler forward every Proxy operation to the main thread via Atomics.

What travels via postMessage, which cannot be a Proxy itself, is always the same structure that is recursively transformed into the "protocol" used to understand proxied values VS non proxied values.

This has to be always the same [type, value] tuple.

To preserve typeof function and/or object, the proxied tuple is bound to a function so that also apply and construct traps can be used.

The result? I can run a whole UI thread dedicated library (any library so far) via a Worker, without blocking the main thread but, on top of it, I can add listeners, reach localStorage or literally do anything I want or need to do from a thread in a sync-like way in the main.

The branching for function is still resolved as ["function", functionIDinWorker] or ["function", functionIDinMain] so there is honestly a single kind of branded tuple that specifies what should we do on the other thread with that tuple ... usually is nothing for primitives, including well known Symbols, but it cross reference for non primitives, being objects and functions / constructors.

Everything is orchestrated through FinalizationRegistry, WeakRef, WeakMap when needed, and everything works except some library out there had the issue that Array.isArray was branching logic elsewhere, even if the underlying / roxied data referred to something that was absoltely not meant to be used as Array.

It doesn't matter if I change the tuple with a {type, value} object, I'd have at that point the exact inverted behavior with Array.isArray where everything is false then because no Reflect.isArray trap exists, so I would still need to patch Array.isArray.

Should I instead branch my logic to proxy an array only when the initial intent is an array? Maybe, but that means that I need to deal with 2 different kind of proxied value and only because there's no trap for it, which is harder to document, explain, debug, and so on + it would add more code to maintain and more checks that would make everything slower for no benefit, compared to current patch.

Has anyone done this before? No, not the way I did in coincident or not that I am aware of.

Was Proxy meant to do this stuff? This is like asking if JS was meant to be what it is today ... we can answer ourselves that question ... and I've asked a new trap for this reason: yes, Proxy can do much more than its initial intent and that not only work, but it's absolutely fine.

Have a nice week.

Yes, when creating a proxy, you must use the appropriate object which you are trying to emulate. If you want your object to have callable behavior, the target must be a callable. If you want your object to pass Array.isArray checks, the target object must be an array.

You don't need to change your internal protocol about what information you keep about the "real target", that can be a tuple or a record. However that structure cannot be the target of the Proxy instance.

Yes people have done exactly this kind of things before. From what I gather, your library implements a synchronous membrane between agents. There are a quite a few membrane implementations out there, both synchronous and asynchronous, either with attenuations/transformations, or plain passthrough. Implementing a synchronous membrane between agents (Worker) may not have any open source implementation, but I do remember reading about some experiments in that area a couple years ago (some were abusing synchronous XHR and ServiceWorker).

Yes, Proxy was actually meant to implement membranes, which were also one of the motivations for adding WeakMap to the language.

Proxy are meant to intercept any object behavior, and in the case of membranes, reflect it onto a "real target". However objects in JavaScript have invariants. In order for exotic proxy objects to not break the language invariants, Proxy enforces these invariants on the target object. As such, the target of a Proxy has special constraints, and cannot be any value you want. However the target object of the proxy does not need to be the same as the "real target" the proxy ultimately intends in the case of membranes. In some cases like cross agent, or even cross network, you obviously cannot have a direct references to real target objects, only serializable identifiers.

Interestingly, the special casing of Array.isArray to check the target object of proxies was actually to make it possible for membranes to be more transparent, and have the ability to create objects that pass existing array checks. It is consistent and compatible with the Callable check when a target is a function or constructor.

As I mentioned before, the proxy target is not there just to determine the "type" of proxy objects, but also to enforce the object invariants of the language. One example is that if a property is non-configurable and non-writable, its descriptor cannot change, aka 2 calls to getOwnPropertyDescriptor must return the same descriptor and in particular the same value. This is enforced by the proxy trap with the implementation doing a [[GetOwnProperty]] on the target object, and compare it to the result returned by the proxy trap.

The observed behavior of proxy and its interaction with Array.isArray is not a language problem. Although it may be surprising, it is there for a reason, now hopefully fully explained.
The 2 things I did not understand and tried to resolve:

  • what your usage of proxy was, and how that deviated from the intended usage by the spec. I did suspect you were mis-using the target of the Proxy, but wasn't sure.
  • What your proposed new trap should do? And how that would solve the problem you perceived. Let's for example assume there was a isArray trap. You would then be able to create an object that has callable semantics and pretends to be an array as well. While not an explicit invariant, that's something the language doesn't allow today, and would wreak havoc on all kinds of code out there. You also cannot move both the callable and array type checks as traps, as too much of the language relies on callable checks not executing user code (including the proxy implementation itself).

I don't believe at any point anyone answered "no". With knowledge of the problem space of proxies and membranes we tried to figure out where the perceived problem may lie, and provide solutions to get the results you seek. In summary:

Array.isArray(new Proxy([])) === true;
Array.isArray(new Proxy({})) === false;
typeof (new Proxy(() => {})) === 'function';
const realTargetInfo = new WeakMap();

const makeProxyFromInfo = (info) => {
  const target = makeTargetFromInfo(info); // create an object, array or function
  realTargetInfo.set(target, info);
  return new Proxy(target, proxyHandler);
};
1 Like

that's what it does already (if you read anything I wrote already) ... the callback returns the same [type, value] tuple before postMessage as the function just binds that and it uses the same proxy.

function Bind() {
  return this;
}

new Proxy(Bind.bind([type, value]), handler);

The detail here is typeof which is fast and always reliable, so I can distinguish without any paranoia around polyfills and ugly global patches, at the syntax level, what is an object and what is a function, there's nothing cheaper in the language ... enter isArray that could be already patched and return weird stuff for my membranes and goodby certainty around what I am doing, which is the highest priority as my code enables access to the main thread DOM and the rest via WASM interpreters such as Pyodide, MicroPython, Wasmoon, Ruby wasm-wasi, and so on ... I can't afford for a project that big to have undesired quirks, I can't be sure the Array.isArray I am using is the native one as we still don't have any way to import native non-patched safe utilities, all I can do is use syntax nobody cna patch or fix, as typeof is, and move forwad.

Again, everything works, we can close this issue to me, as it's clear there's no interest in fixing Array.isArray which is clearly not broken to you, it's full of quirks to users though out there.

Not that I am aware of, and Comlink is not nearly close as well as Partytown to what I am offering ... neither does that, and the sync XHR is a hack, and you are just not fully understanding what coincident does if this is your answer, or you have access to closed gardens I don't and yet I also don't care, as I just move forwad without that but of course I'd love to share knowledge there ... yet, the common knowledge is about different projects that do different things and coincident has no equivalent out there ... but thanks for dismissing again my work, it feels like you really don't realize when you do so.

I know how Proxy work and what these do, I am saying there is a missing trap for Array.isArray when all syntax instead work seamlessly: instanceof works via Proxy, typeof works well too, and so does callable and stuff ... there's a bomb in the specs that is Array.isArray that fools every code base out there and has no way to be intercepted but it's not even syntax, it's a method that can be patched at runtime so since that's the status quo, I'll exploit that method for my needs any day until here somebody realizes how broken is that ... but I guess, after this thread, that won't happen until more issues will be raised in the future.

Regards.

P.S. I am not even asking to fix Array.isArray, I am asking for a trap in Proxy that no matter how patched is Array.isArray, if the native check is reached, it returns wathever ProxyHandler.isArray(target) returns instead of misleading everyone with a true when a brand instanceof ref right after could be false.

That's true also for function references, the instanceof might return something else, but the typeof function cannot possibly have polyfills and mokey-patches on the way, so it's the most reliable check we have for code where performance and reliability matters.

edit there are no other types beside typeof function or object in a Proxy

Does this not apply also to the use of Proxy, WeakMap etc? To be able to trust any of these APIs you need to be the code that runs first and capture references to them and then only use those references.

1 Like