01:04
<rkirsling>
woo-ow
01:04
<rkirsling>
that example Waldemar found is pretty epic
01:04
<littledan>
what example?
01:05
<rkirsling>
https://github.com/tc39/proposal-pattern-matching/issues/323, at the end of the link
01:06
<rkirsling>
contextual keywords are terrifying
02:04
<Justin Ridgewell>
@rbuckton Have you considered . for discard bindings before? It’s visually insigificant, isn’t an identifier, and can’t be used in any place that makes discard bindings useful.
02:05
<Justin Ridgewell>
It might be too visually small.
02:06
<rbuckton>
That, and too easy to confuse for a property access.
02:19
<Justin Ridgewell>
How would you confuse it for propery access?
02:19
<Justin Ridgewell>
There’s no preceding object
03:02
<rbuckton>
it could theoretically be mixed with and, or, or not in pattern matching, though that's not exactly practical.
10:24
<Jack Works>
for (using is of and [not/a+"/g]; b++; [/"/g, 5])
10:25
<Jack Works>
how Waldemar find this
10:26
<Jack Works>
😨 real insight
10:50
<Jack Works>
for (using is of and [not/a+"/g]; b++; [/"/g, 5])

First parse
for (using x of ...) {}
    keyword using
    Identifier is
    keyword of
    Expression [not/a+"/g]; b++; [/"/g, 5]
        ArrayExpression [
            Element not/a+"/g]; b++; [/"/g
                Division not/a
                Plus +
                Division "/g]; b++; [/"/g
                    Left "/g]; b++; [/"
                    Right g
            Element 5
        ]

Second parse
for (a; b; c)
FirstPart
    PatternMatching
        Identifier using
        Keyword is
        Pattern
            IdentifierPattern of
            Keyword and
            ArrayPatter []
                Keyword not
                RegExpPattern /a+"/g
SecondPart b++
ThirdPart [/"/g, 5]
10:53
<rbuckton>
Anything involving regexp literals is suspect
11:03
<Jack Works>
but it's a real ambiguous example
11:03
<Jack Works>
both interpretations are legal
13:09
<ryzokuken>
the authors' copy of the soon-to-be RFC 9557 🎉 https://www.rfc-editor.org/authors/rfc9557.html
13:29
<littledan>
the authors' copy of the soon-to-be RFC 9557 🎉

https://www.rfc-editor.org/authors/rfc9557.html
Wow! The extension if ISO 8601 with custom timezone and calendar syntax has an RFC number!
13:54
<keith_miller>
Does anyone else fill out the sign-in form every day because they didn't save the Webex link? lol
13:58
<ryzokuken>
starting real soon folks!
14:37
<Chris de Almeida>
we lost Nicolo as a notes helper. if someone else here could volunteer to help, even just for this topic, that would really help us out. chair group is trying to help w/ notes (as we often do), but also have to juggle other things. thank you 🙏
14:38
<Chris de Almeida>
it's easier during the presentation, and a little more work during discussion
14:46
<hax (HE Shi-Jun)>
for (using is of and [not/a+"/g]; b++; [/"/g, 5])
It's just like old for (async of () => {} issue, maybe worse?
14:49
<littledan>
saminahusain: It'd be great if Ecma could facilitate getting TC39 the relevant ISO/IEEE specs. This is a technical barrier for us in this proposal, as it was in the development of Temporal (with the relationship with ISO 8601)
14:54
<Chris de Almeida>
sent a file.
☝️
15:29
<waldemar>
What's TLS?
15:29
<rbuckton>
ljharb: no class extends sharedStruct {} won't work, for the reasons shu is discussing now, as well as that it has too many rough edges. You can't add new public or private fields in a subclass of a shared struct as it would violate the fixed layout. Plus we can't correlate a class prototype across realms.
15:29
<rbuckton>
TLS: thread-local storage
15:29
<rbuckton>
Though he really means "Realm-local storage"
15:29
<ljharb>
the instance would still be sealed ofc
15:30
<ljharb>
and it has the same problem of correlation, but it seems a better way to correlate to me than a "boxed primitive"-like hand wavy mechanism
15:30
<rbuckton>
the instance would still be sealed ofc
The prototype would not be correlated.
15:31
<ljharb>
this also reminds me of yaml class marshalling attack vectors in ruby, for some reason
15:31
<rbuckton>
You are misunderstanding correlation, I think. We're talking about ensuring the same struct type defined in the same file and loaded into two different realms/threads/agents will have same methods.
15:31
<ljharb>
right
15:32
<ljharb>
i'm suggesting that you make a class with methods, and extend the struct, and then the "automatic" part would be reloading that same class code in each place
15:32
<rbuckton>
We can't correlate a class prototype, because classes and class prototypes aren't correlated across threads. We can correlate a struct definition and prototype.
15:32
<ljharb>
the prototype is a mutable object tho
15:32
<ljharb>
if that can be correlated, why can't anything be?
15:33
<rbuckton>
No, correlation is based on syntax and source location.
15:33
<ljharb>
right but i mean, you have to re-evaluate code in each context in order to get the prototype object, right?
15:33
<rbuckton>
correlating every class is way too much overhead.
15:34
<littledan>
correlating every class is way too much overhead.
this is just the shared ones, right?
15:34
<ljharb>
well sure, not every class. only one canonical one that the shared struct creator indicates. that seems like it'd be precisely the same amount of overhead as the prototype object correlation
15:34
<rbuckton>
We would only correlate shared struct types between realms
15:35
<ljharb>
the prototype object isn't a shared struct
15:35
<rbuckton>
class extends sharedStruct cannot be canonical and cannot be determined syntactically.
15:35
<ljharb>
if a function is re-eval'd in multiple contexts to achieve the goal of "having the same methods", then surely any code could be
15:36
<rbuckton>
That isn't the problem.
15:36
<ljharb>
there's all sorts of ways that can be covered
15:36
<ljharb>
the wrapper class could be defined lexically inside the shared struct, or you could just define method syntax directly inside the shared struct definition and the semantics would be that shared struct methods are copied
15:37
<rbuckton>
Those sound categorically worse than what we are proposing. Class construction has the wrong semantics for shared structs.
15:38
<ljharb>

why would something like

shared struct {
  // existing stuff
  foo() {}
}

where the methods are auto-re-eval'd be worse?

15:38
<rbuckton>
the wrapper class could be defined lexically inside the shared struct, or you could just define method syntax directly inside the shared struct definition and the semantics would be that shared struct methods are copied
we are proposing methods defined in the struct.
15:38
<ljharb>
hm, maybe i misunderstood shu's slides, it looked like he was mutating .prototype
15:39
<rbuckton>
He was showing a build up to what we actually want.
15:46
<littledan>
We already reference source location in TemplateMap. This isn't a new concept
15:51
<Ashley Claymore>
Maybe tooling could extract out the minimal part of the struct declarations so they can have a more concrete source text location. And then re-write the code so it can reference that.
15:51
<Ashley Claymore>
(for the virtulization case)
15:52
<bakkot>
this is a sidebar but it would be nice if we could structuredClone symbols in a coherent way (i.e., such that if you send the same one over twice, you get the same value)
15:52
<rbuckton>
That's what bundlers would need to do if they have different entrypoints. IIRC, many bundlers can already do source splitting for those cases.
15:52
<littledan>
this is a sidebar but it would be nice if we could structuredClone symbols in a coherent way (i.e., such that if you send the same one over twice, you get the same value)
I've been told that this would be too troublesome to implement
15:53
<Ashley Claymore>
this is a sidebar but it would be nice if we could structuredClone symbols in a coherent way (i.e., such that if you send the same one over twice, you get the same value)
only RegisteredSymbols would be the easier form of this
15:53
<bakkot>
I've been told that this would be too troublesome to implement
huh. doesn't seem like it would be that hard.
15:54
<ljharb>
the usefulness for me would be unforgeable transferable symbols, ie, not registered ones
15:54
<bakkot>
you can do it in userland if you patch postMessage / the message event / structuredClone and stash symbols in a weakmap
15:56
<rbuckton>
littledan: Maybe doesn't matter, but I prefer the term ConcurrentMap to SharedMap. Just because a map is shareable does not mean it can be used concurrently by multiple threads. A true concurrent map uses lock-free CAS mechanisms to allow for concurrent reads and writes.
15:57
<Mathieu Hofman>
keith_miller: I have previously advocated for a manual correlation over postMessage, but apparently the DX of that is not acceptable, and prevents some use cases that I don't fully understand
15:58
<Ashley Claymore>
you can do it in userland if you patch postMessage / the message event / structuredClone and stash symbols in a weakmap
Like CK, if the sending realm is also trying to put these symbols in a weak{Map/set/ref}, now the local GC only holds them weakly. They might be GC'ed before the round trip comes back
15:58
<bakkot>
CK?
15:58
<ljharb>
put them in a weakref that's strongly held by a Map, and if the weakref is collected, you can just make a new symbol?
15:58
<Ashley Claymore>
CompositeKeys
15:59
<Ashley Claymore>
But then that's not round tripping
15:59
<bakkot>
ah, sure, ok
15:59
<ljharb>
ah true, hm
16:00
<Ashley Claymore>
In Java Valhala, they 'fix' that by letting WeakMaps have a 'soft' policy. Where there is no guarantee that the value will remain in the map, even if it's reachable
16:00
<littledan>
littledan: Maybe doesn't matter, but I prefer the term ConcurrentMap to SharedMap. Just because a map is shareable does not mean it can be used concurrently by multiple threads. A true concurrent map uses lock-free CAS mechanisms to allow for concurrent reads and writes.
I don't get it; shouldn't we use ConcurrentArray then?
16:01
<rbuckton>
Arrays only usually require a single operation to update. Maps require many more operations that are not thread safe.
16:02
<Mathieu Hofman>
I've been told that this would be too troublesome to implement
How is it so much more troublesome that sharing an instance of shared struct SharedSymbol {} ?
16:03
<Ashley Claymore>
SharedStructs are in someways finite (and eternal), due to source text location ?
16:03
<Mathieu Hofman>
you can do it in userland if you patch postMessage / the message event / structuredClone and stash symbols in a weakmap
You cannot if you want to make sure you don't leak memory. You need distributed GC for this to work
16:04
<bakkot>
presumably shared structs will in fact be more work than structuredCloning symbols would be, and engines just think it's worth it because it is plainly much more valuable
16:05
<Mathieu Hofman>
Right, if you do the work for shared structs you basically could share unique symbols almost for free
16:06
<Mathieu Hofman>
SharedStructs are in someways finite (and eternal), due to source text location ?
instances of shared structs are definitely not eternal. they require distributed gc. and fun part, they can be used as WeakMap keys
16:07
<bakkot>
it would certainly be a nice bonus if this feature gave us clonable symbols!
16:10
<hax (HE Shi-Jun)>
Will import {SharedStruct} from "shared.js" with { foo: "bar" } have same source location? I guess not?
16:12
<littledan>
How is it so much more troublesome that sharing an instance of shared struct SharedSymbol {} ?
I guess this was from back when we didn't have a shared heap, and it would've been about correlating signals in different agents (and then the question is, how do you GC them)
16:32
<Richard Gibson>
keith_miller: I have previously advocated for a manual correlation over postMessage, but apparently the DX of that is not acceptable, and prevents some use cases that I don't fully understand

I'd really like to see these use cases enumerated, because it seems to me like explicit handshaking in which e.g. a reference to the shared struct is exchanged constitutes a more comprehensible and robust pattern

let ready = false;
onmessage = evt => {
  if (!ready) {
    ready = true;
    correlate(localStructDef, evt.data);
    return;
  }

  assert(Object.getPrototypeOf(evt.data) === localStructDef.prototype);
  …use evt.data as an instance of the shared struct…
}
16:35
<keith_miller>
Yeah, I guess I still don't fully understand. It seems like the spawner of a worker could just forward their shared types to that worker before revealing the existence of that worker to the world. Or alternatively the spawned worker could fetch the shared structs lazily, if desired.
16:37
<littledan>
maybe the answer to this question would be in the V8 CL history, if Shu has already implemented it and found it to be not good enough?
16:39
<keith_miller>
I guess it could be hard to do it lazily because there's no sync postMessage for workers
16:41
<shu>

I'd really like to see these use cases enumerated, because it seems to me like explicit handshaking in which e.g. a reference to the shared struct is exchanged constitutes a more comprehensible and robust pattern

let ready = false;
onmessage = evt => {
  if (!ready) {
    ready = true;
    correlate(localStructDef, evt.data);
    return;
  }

  assert(Object.getPrototypeOf(evt.data) === localStructDef.prototype);
  …use evt.data as an instance of the shared struct…
}
that constitutes a barrier though, right?
16:42
<shu>
like, your workers can't just import and start doing stuff. it has to import, wait till some barrier is reached, then continue doing stuff. it's worse for loading performance
16:42
<shu>
it's definitely possible, rbuckton has built such a thing
16:43
<shu>
the "more robust" argument is interesting, i buy the explicitness of it for sure
16:43
<Richard Gibson>
an example of "more robust" is the fact that it isn't broken by naïve bundlers
16:44
<shu>
ah okay
16:44
<shu>
that depends on whether that's a goal worth optimizing for
16:44
<bakkot>
There's only a small handful of bundlers in common use, and they're all maintained by specific individuals who you can talk to, so I don't want to design a feature on the assumption that bundlers will not be writing careful handling
16:45
<shu>
i was also initially on the other side of comprehendability argument, but flipped after realizing that the "JS classes are distinct per evaluation" thing is pretty JS-specific and folks generally don't have that mental model
16:45
<rbuckton>
I did not build such a thing, but I do have a document describing that kind of interaction. There is a lot more user error involved though. I also have hopes to eventually support #name private state, but it isn't safe to do so unless I can be certain the struct declarations on both sides are identical. A handshaking mechanism makes that impossible.
16:45
<shu>
i see, i misremembered
16:45
<rbuckton>
Private state can only maintain its privacy if you can ensure the same encapsulation exists in both realms.
16:46
<Richard Gibson>
There's only a small handful of bundlers in common use, and they're all maintained by specific individuals who you can talk to, so I don't want to design a feature on the assumption that bundlers will not be writing careful handling
I'm not limiting "more robust" to bundlers, just using that as an easily understood example. The same applies to e.g. dynamic code evaluation/version skew/etc.
16:47
<keith_miller>
Do shared structs allow for private state in the current proposal?
16:47
<rbuckton>
No, it is not part of the MVP. They do allow fields, methods, getters, and setters though.
16:47
<keith_miller>
Is that strictly needed? Or just ergonomic?
16:47
<rbuckton>
I believe it is needed.
16:48
<rbuckton>
You cannot use a WeakMap as pseudo-privacy like you might have done for a class because a WeakMap is local to a specific thread/agent/realm.
16:48
<keith_miller>
Because it seems like you could wrap the private bits in a per-worker wrapper?
16:48
<rbuckton>
How, without exposing the private state as public state?
16:49
<rbuckton>
You have to share something between threads to communicate private state, and if it is not itself private then there is no private state.
16:49
<keith_miller>
I guess you can't have that in transitive members though?
16:50
<keith_miller>
I also don't fully understand how private state interacts with wasm GC
16:52
<rbuckton>
If private state was important for class, despite WeakMap as a possible solution, then it is more important for struct as WeakMap is not a possible solution. There are a lot of open questions on private state, which is why it's not part of the MVP, but I don't want to chose a direction that completely rules it out as a possibility.
16:52
<rbuckton>
Are we resuming at 1:00pm or 1:10pm?
16:53
<shu>
i heard 1
16:54
<rbuckton>
Ah, trying to finish eating and wrangling my dogs before we start back up. Thanks
16:54
<ljharb>
you're eating your dogs? (lol sorry, will take it to tdz)
16:54
<rbuckton>
lol.
16:55
<Richard Gibson>

I dispute the "makes that impossible" claim. As a strawperson, consider

shared struct SharedPoint {
  #id;
  x;
  y;
  …
}

describing a struct with three fields in which instance properties x and y can be accessed from anywhere but #id can be accessed only from functions within the definition (i.e., just like class)

16:56
<rbuckton>
Sure. And via handshaking I can replace what with my own SharedPoint that has a getter that exposes #id.
16:56
<rbuckton>
The correlation must be unforgeable.
16:56
<Richard Gibson>
correct
16:57
<Richard Gibson>
because...?
16:57
<rbuckton>
Otherwise a bad actor could spin up a worker with code that does this, pass it a shared struct instance, and extract the private state.
16:58
<ljharb>
or even observe that it exists.
16:58
<keith_miller>
How are they doing this?
16:58
<Richard Gibson>
it seems like you and I have wildly different threat models and conceptions of the purpose of private fields
16:58
<keith_miller>
Wouldn't they be different types? With different private names?
16:58
<rbuckton>
Sorry, must finish eating and prep for my presentation.
17:00
<Mathieu Hofman>
Private state can only maintain its privacy if you can ensure the same encapsulation exists in both realms.
Not entirely true. If the capability is derived from the shared struct declaration, it is likely fine for that capability to allow wiring private state access. The same way you can declare static methods on a class that can be plucked to give friends the ability to access class privates.
17:01
<Richard Gibson>
exactly. Access to private fields in the above example would require a reference to the struct definition; a mere instance would not suffice
17:02
<littledan>
Wouldn't they be different types? With different private names?
it'd be like, you'd have a different private name in each agent, but they would both be associated to the same underlying field in memory
17:02
<eemeli>
ryzokuken / Chris de Almeida : Will we have time for the extractor continuation?
17:02
<Chris de Almeida>
for anyone who is disappointed that they missed their opportunity to help with the notes during this plenary meeting, there is an opportunity opening up in about 30 minutes after this topic. please register your interest by contacting the chair group. first come, first served. thank you 🙏
17:03
<Chris de Almeida>
ryzokuken / Chris de Almeida : Will we have time for the extractor continuation?
virtually no chance, unfortunately 😞
17:03
<eemeli>
Alas.
17:04
<Aki>
yikes i'm having a really hard time cognitively keeping up, this is the first time i've tried doing notes since i went on leave for a head injury and maybe it was a touch overambitious of me
17:05
<Chris de Almeida>
oh. hopefully Daniel is able to keep up. chairs also help but aren't always able to. if you need to bow out, no worries, don't feel obliged if the vibes are off
17:10
<Michael Ficarra>
the getter and the FinalizationRegistry solutions don't seem so bad...
17:13
<ptomato>
particularly FinalizationRegistry seems pretty suboptimal compared to strict enforcement where you can just throw one exception saying "you forgot using" from the source code location where you forgot it
17:13
<bakkot>
FinalizationRegistry is very bad
17:13
<bakkot>
it is not a guarantee at all
17:13
<bakkot>
and it is at some point in the future
17:13
<bakkot>
it's really, really not a solution for anything except releasing memory
17:14
<bakkot>
which is the only thing it was ever for
17:15
<Mathieu Hofman>
FR is only ok here as a diagnostic mechanism
17:24
<Justin Ridgewell>
Is @shu's concern that you can just manually call foo[Symbol.enter]() and so there’s no real enforcement?
17:25
<shu>
  • it's still not local warning to the user doing something wrong
17:25
<bakkot>
yeah but you don't have the resource if you don't call Symbol.enter, so there's no leak
17:25
<shu>
right okay
17:25
<shu>
i think i understand it now, it's just not [[nodiscard]]
17:25
<shu>
it's something pretty different
17:25
<bakkot>
yeah
17:26
<ljharb>
it really does seem like you have to gate every public operation on "the dispose getter has been called, and the resource is not yet disposed" (in order to get the enforcement that motivates this proposal)
17:26
<ljharb>
iow, the entire thing is invalid before being usingd - queued for disposal - and again invalid after disposal
17:26
<bakkot>
"resource is not yet disposed" is not quite as important; that can be invalid but is not a leak
17:27
<bakkot>
it's still probably good practice but the hit to performance for public fields is not necessarily worth it
17:27
<shu>
can... TS add [[nodiscard]]?
17:28
<ljharb>
not yet disposed? or not yet queued for disposal? because the former true, and the latter false, is the only time you want people to interact with the resource, i'd think
17:28
<bakkot>
https://github.com/microsoft/TypeScript/issues/8240
17:28
<Ashley Claymore>
The userland solution might be a naming convention
17:28
<Ashley Claymore>
like how in react all hooks are use[A-Z...
17:28
<Ashley Claymore>
and linters enforce where they are allowed to be called from
17:28
<ljharb>
oof, that was such a poor choice by react tho
17:29
<bakkot>
unfortunately Ron wants to add Symbol.dispose to existing things, which do not follow any naming convention
17:29
<ljharb>
i think they're still planning to name their new mega-hook "use", so RIP googling for things
17:29
<Ashley Claymore>
unfortunately Ron wants to add Symbol.dispose to existing things, which do not follow any naming convention
they'll have to be grandparented in to the set of names ;)
17:29
<bakkot>
inconsistent naming I think makes that naming not viable as a solution for saving people from errors here
17:30
<littledan>
It'd be nice if we can really nail down the full scope of these protocols before Stage 3. It'll make rollout of this solution slower to have this diversity of interpretations for using.
17:31
<littledan>
It's too bad that this proposal doesn't give strong enough guarantees to let us use it for AsyncContext
17:32
<ptomato>
I'm not sure I understand why adding strict enforcement will bifurcate the ecosystem. resource management doesn't exist currently; code wanting to use resource management needs to port, so why would strict enforcement be a burden that hinders adoption?
17:33
<bakkot>
rbuckton: to be clear I am not going to block further advancement of using on this feature existing: it is too late in the process
17:33
<rbuckton>
Mandatory strict enforcement could cause bifurcation. That's why this is an opt-in mechanism.
17:33
<Andreu Botella>
I opened an issue yesterday to discuss this: https://github.com/rbuckton/proposal-using-enforcement/issues/1
17:33
<Mathieu Hofman>
It's too bad that this proposal doesn't give strong enough guarantees to let us use it for AsyncContext
why is it not sufficient?
17:33
<littledan>
I'm not sure I understand why adding strict enforcement will bifurcate the ecosystem. resource management doesn't exist currently; code wanting to use resource management needs to port, so why would strict enforcement be a burden that hinders adoption?
I think it means, if some transpilers/browsers ship the initial using semantics, and others ship the later semantics, then it's harder to start shipping APIs using strict enforcement (since it's not enough to check whether the syntax exists, you also have to check that users are updated)
17:34
<littledan>
why is it not sufficient?
you can just call enter and not call dispose, so you'd have unbalanced use of the stack
17:34
<ptomato>
I think it means, if some transpilers/browsers ship the initial using semantics, and others ship the later semantics, then it's harder to start shipping APIs using strict enforcement (since it's not enough to check whether the syntax exists, you also have to check that users are updated)
that's the breaks of stage 3 though?
17:34
<littledan>
that's the breaks of stage 3 though?
right, the part that makes this painful is that we knew about this part of the design space through the whole life of the using proposal.
17:34
<Michael Ficarra>
it is within our power to demote a proposal from stage 3 to 2
17:35
<rbuckton>
you can just call enter and not call dispose, so you'd have unbalanced use of the stack
Calling enter is an explicit action the user takes. They are opting out of strict enforcement, and thus must ensure they do the work themselves.
17:35
<rbuckton>
I've said elsewhere, I'd be fine with a Symbol.enter_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, or something less meme-y, to illustrate the unsafe nature of the operation.
17:35
<Andreu Botella>
We agree that if a user does this, their code is buggy. I don't think that's in discussion. But some use cases need something stricter
17:36
<keith_miller>
I'm going to guess that implementors are going to reject production time work for the equivelent of a [[nodiscard]] attribute. I know I currently would.
17:36
<littledan>
Calling enter is an explicit action the user takes. They are opting out of strict enforcement, and thus must ensure they do the work themselves.
right, so in the rollout, people may find themselves wanting to do that more than would be optimal, to deal with the risk that their code will be transpired with an older version of using.
17:36
<rbuckton>
We agree that if a user does this, their code is buggy. I don't think that's in discussion. But some use cases need something stricter
Without a compiler or type system, I'm not sure something stricter is entirely feasible in JS.
17:37
<littledan>
Without a compiler or type system, I'm not sure something stricter is entirely feasible in JS.
AsyncContext currently enforces stack discipline strictly by having a callback-based API.
17:37
<littledan>
but there's real demand for having something "flatter"
17:37
<ljharb>
you'd do it with a type-aware linter, like typescript-eslint. that's already how people catch unawaited promises with the no-floating-promises rule.
17:37
<Michael Ficarra>
unary plus is such a weird thing to have
17:37
<Michael Ficarra>
what was the intention behind its inclusion?
17:37
<Mathieu Hofman>
you can just call enter and not call dispose, so you'd have unbalanced use of the stack
sure, but just don't do that? disposables are not only used syntactically. If you call enter, you accept responsibility to call dispose
17:37
<ptomato>
I mean, I'm trying to understand how bad the burden of mandatory enforcement would be? I expect the vast majority of the potential future users of this feature have not ported their code yet (or written it, for that matter)
17:38
<Andreu Botella>
Do we want to allow that to effectively pollute the caller's scope, switching the current context at their function from under their feet? That's something the current AsyncContext proposal explicitly avoids doing
17:38
<Mathieu Hofman>
We agree that if a user does this, their code is buggy. I don't think that's in discussion. But some use cases need something stricter
I still do not understand the use case
17:38
<ptomato>
anyway is there a github issue where I can follow this? I'm splitting my attention between this discussion and Kevin's presentation and it's not working very well 😄
17:39
<littledan>
Yes, we can follow up in https://github.com/rbuckton/proposal-using-enforcement/issues/1
17:39
<rbuckton>
I mean, I'm trying to understand how bad the burden of mandatory enforcement would be? I expect the vast majority of the potential future users of this feature have not ported their code yet (or written it, for that matter)
It depends on the definition of mandatory enforcement. There are two approaches: some magic mechanism that indicates to the resource producer the user is initializing to a using, or that using and DisposableStack explicitly call [Symbol.enter] and throw if it doesn't exist.
17:40
<Michael Ficarra>
RequireObjectCoercible -> is an Object
17:41
<Mathieu Hofman>
Do we want to allow that to effectively pollute the caller's scope, switching the current context at their function from under their feet? That's something the current AsyncContext proposal explicitly avoids doing
Ok I think I now understand, and I have ideas on how to do this at the AsyncContext API layer
17:41
<rbuckton>
A magical mechanism doesn't work with DisposableStack and wouldn't allow user-defined resource management building blocks.
17:42
<ljharb>
lol, core-js used to make Number.prototype iterable
17:44
<rbuckton>
If [Symbol.enter] is mandatory, existing APIs would have to write [Symbol.enter]() { return this; }, or would need to duplicate the API to one that produces an object with a [Symbol.enter](). API Bifurcation is a documentation and maintenance headache and would break the ability for tools to introduce simple refactors from const res = ...; try { ... } finally { res.close(); } to using res = ...
17:45
<Andreu Botella>
are we conflating two separate things? I don't think "true strict enforcement" necessarily means a mandatory [Symbol.enter]
17:46
<rbuckton>
For examples of existing APIs that could be adapted to support [Symbol.dispose], see https://github.com/tc39/proposal-explicit-resource-management#relation-to-dom-apis and https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#relation-to-nodejs-apis
17:46
<rbuckton>
are we conflating two separate things? I don't think "true strict enforcement" necessarily means a mandatory [Symbol.enter]
What would you consider to be "true strict enforcement"?
17:47
<Andreu Botella>
A magical mechanism doesn't work with DisposableStack and wouldn't allow user-defined resource management building blocks.
^ this "magical mechanism"
17:48
<rbuckton>
Can you conceive of a magical mechanism that works with both using and DisposableStack and user-defined resource management wrapper classes?
17:48
<Andreu Botella>
I don't have an alternative, but I think that issue has been conflated with the mandatoryness of [Symbol.enter]
17:49
<rbuckton>
for example, if there were a function.using meta-property accessible within a function that is only true when being initialized to a using, how would that work with DisposableStack.prototype.use?
17:50
<rbuckton>
I wasn't trying to conflate strict enforcement with mandatory enforcement. They are mostly separate concerns.
17:50
<rbuckton>
I'm bouncing between two different discussions
17:51
<bakkot>
that was much faster than I was expecting!
17:52
<rbuckton>
Mandatory enforcement is a concern for API bifurcation. In general I don't believe mandatory enforcement is necessary, API producers should be able to opt-in or opt-out of enforcement, as should API consumers.
17:54
<rbuckton>
API producers need an opt-out mechanism for existing APIs or for APIs that don't require such a guarantee. API consumers need an opt-out mechanism so they can compose disposables in their own classes and extend resource management capabilities in userland.
17:55
<Justin Ridgewell>
Agree, mandatory use of enter is necessary.
17:55
<Justin Ridgewell>
But for APIs that choose to have an enter, some require much stricter requirements.
17:55
<Justin Ridgewell>
(I still need to read https://github.com/rbuckton/proposal-using-enforcement/issues/1)
17:55
<rbuckton>
Agree, mandatory use of enter is necessary.
What are you agreeing with?
17:55
<Justin Ridgewell>
But ideally we’d only allow syntatic using.
17:55
<Justin Ridgewell>
Mandatory enforcement is a concern for API bifurcation. In general I don't believe mandatory enforcement is necessary, API producers should be able to opt-in or opt-out of enforcement, as should API consumers.
I was agreeing with this.
17:55
<rbuckton>
I believe Symbol.enter is optional, for those APIs that need it.
17:56
<Mathieu Hofman>
Syntactic super powers make me feel suspicious
17:56
<rbuckton>
We can't only have syntactic using.
17:56
<rbuckton>
You absolutely need composition.
17:56
<Justin Ridgewell>
AsyncContext demands knowing whether you’re using using syntatically, using DisposableStack (bad), or manually calling (even worse).
17:57
<Mathieu Hofman>
I don't think this use case requires such a capability. I'll write down my idea after plenary
17:57
<rbuckton>
why is DisposableStack bad?
17:58
<Justin Ridgewell>
Only syntatic using guarantees returning the context stack to the appropriate state.
17:58
<Mathieu Hofman>
Nope there are alternatives
17:58
<Justin Ridgewell>
DisposableStack may not be disposed.
17:58
<Justin Ridgewell>
Or it’s disposed later.
17:58
<Justin Ridgewell>
In which case the caller has been polluted.
17:58
<Mathieu Hofman>
you can do a hybrid by introducing a single new stack entry
17:58
<Justin Ridgewell>
Either way, it doesn’t work.
17:59
<Andreu Botella>
My point with opening that issue is to point out that there are use cases that need something stricter. I would be fine with the proposal simply not supporting that, and using callbacks.
17:59
<rbuckton>
DisposableStack, or any other composition mechanism, is a necessity for building applications that work with resources.
17:59
<Justin Ridgewell>
I’m not arguing against DisposableStack in general.
18:00
<Justin Ridgewell>
But it’s not acceptable for AsyncContext.
18:01
<rbuckton>
I only have a very rough idea of the use case for AsyncContext you're concerned about.
18:01
<bakkot>
ljharb: should Array.zip zip iterables or arrays or array-likes?
18:01
<rbuckton>
I think I need more context.
18:01
<bakkot>
presumably array-likes?
18:01
<ljharb>
iirc all the array things, except Array.from/fromAsync, take "arraylikes" only (which includes arrays)
18:01
<bakkot>
and of course it will produce an array rather than an iterator
18:01
<ljharb>
right
18:02
<Mathieu Hofman>
The stack frame requirement comes from the use case (AsyncContext). It happens that using is a close concept: scope. I will provide an example of API that satisfies the use case without making using required
18:02
<ljharb>
i don't mind if it takes iterables, but at that point Array.zip(iterables) is just sugar for Iterator.zip(iterables).toArray() which isn't that valuable
18:03
<Justin Ridgewell>
I think I need more context.
I’ll provide details in the #1 issue later today
18:03
<bakkot>
(it's .zip(iterables) not a var-args, to be clear)
18:05
<bakkot>
this seems like it will be a useful place to Stop Coercing Things, so that { optionsbag } isn't treated as an array-like of length 0
18:09
<ljharb>

WH's example:

let x
(a) = b;
18:10
<bakkot>
let strikes again, I hate it
18:10
<ljharb>
an NLTH between the identifier and the ( seems fine to me, just for extractors
18:10
<bakkot>

the line break restriction is also bad, I want to write

let Foo
  (long extractor pattern) = RHS
18:10
<ljharb>

i don't have empathy for folks who want to write calls like

fn
(a, b)
18:10
<ljharb>
lol
18:11
<ljharb>

i'd expect there to see

let Foo(
  long extractor pattern
) = RHS
18:11
<littledan>
extractors are extremely limited in their syntax; this seems way more minor than the other parts
18:11
<littledan>

i'd expect there to see

let Foo(
  long extractor pattern
) = RHS
I don't think Waldemar's case runs into that
18:11
<littledan>
I think we're just talking about between Foo and )
18:11
<ljharb>
true, it's only if the extractor pattern looks like a valid parenthesized LHS?
18:12
<littledan>
(I don't understand the question)
18:12
<bakkot>
man ASI is just the worst
18:12
<ljharb>
like i think waldemar's ambiguity pops up only when the extractor pattern is a valid thing to be in a (reference) = usage
18:13
<ljharb>
eg, (x) = 3 and (x.y) = 3 are currently valid, (x, y) = 3 is not, so only the former have the conflict, afaict
18:13
<ljharb>
and we couldn't even agree to suggest people avoid it
18:14
<littledan>
we actually did agree to suggest people avoid it! and then took it back
18:14
<ljharb>
fax
18:14
<ljharb>
i guess our decision got a second-pass
18:14
<littledan>
I wish we required you to start your arrow functions with a ^ or something
18:15
<littledan>
but now I'm dreaming of other possible uses for prefix ^
18:15
<ljharb>
(parenthesized assignment LHS's are gross also)
18:16
<bakkot>
(x, y) = 3 technically doesn't but introducing a cover grammar which could distinguish those would be a nightmare
18:17
<ljharb>
but as long as we say that extractors are only an identifier followed by a no-newline (, then i think the problem's solved?
18:17
<rbuckton>

As I understand it, Waldemar's concern is this:

let a
(b) = {};
18:18
<dminor>
Did Extractors advance to Stage 2? I wasn't clear if all blocking concerns were withdrawn.
18:18
<rbuckton>
I don't believe it did, no.
18:18
<littledan>
Did Extractors advance to Stage 2? I wasn't clear if all blocking concerns were withdrawn.
Extractors did not advance to Stage 2. The sole blocking concern was the NLT issue.
18:18
<Michael Ficarra>
I wish we required you to start your arrow functions with a ^ or something
backslash surely
18:18
<dminor>
Ok, thank you.
18:18
<littledan>
backslash surely
,\
18:18
<rbuckton>
It sounded like the cover grammar complexity might not be blocking, but the NLT issue is still a concern.
18:19
<littledan>
It sounded like the cover grammar complexity might not be blocking, but the NLT issue is still a concern.
the cover grammar complexity will be investigated during stage 2, to resolve before stage 2.7
18:19
<bakkot>
but as long as we say that extractors are only an identifier followed by a no-newline (, then i think the problem's solved?
yeah, but waldemar didn't like that solution either
18:19
<shu>
It sounded like the cover grammar complexity might not be blocking, but the NLT issue is still a concern.
not blocking for stage 2. i need to do more work to figure out how blocking we consider it for 3
18:19
<bakkot>
(I would be fine with it)
18:21
<Richard Gibson>
(parenthesized assignment LHS's are gross also)
++((Ⅰ))
18:22
<rbuckton>
We would need an NLT in binding patterns, which seems fine to me. The question is how easy is it to make the NLT conditional on whether we're parsing a let/const/var or not.
18:26
<Michael Ficarra>
the first speaker (currently being referred to as JRA in the notes) is not assigned this abbreviation in https://github.com/tc39/notes/blob/main/delegates.txt
18:26
<Michael Ficarra>
could someone reach out to them to create a PR?
18:28
<shu>
i did already https://github.com/tc39/notes/pull/318
18:28
<waldemar>
I'm having trouble understanding the syntax of the slides
18:28
<Michael Ficarra>
oh okay thanks @shu
18:29
<waldemar>
What's ({{counter.parity}})?
18:29
<ljharb>
literal parens, with a rendered JS value inside them
18:29
<Michael Ficarra>
@shu oh, it hasn't been merged because CI is failing due to you not adding them in the correct order
18:29
<rbuckton>
It's vue's templating mechanism.
18:30
<shu>
@shu oh, it hasn't been merged because CI is failing due to you not adding them in the correct order
oh
18:31
<shu>
someone more inclined can fix
18:34
<ljharb>
i'm confused, isn't the parity supposed to autoupdate with the counter?
18:34
<littledan>
Preact does recreate the vdom, but at least we didn't bother recalculating parity
18:34
<ljharb>
but 2 isn't odd. the parity depends on the counter.
18:34
<littledan>
and Preact may be able to save more work when you have nested components--Preact only has to re-render at the component level
18:35
<littledan>
i'm confused, isn't the parity supposed to autoupdate with the counter?
in this case, because it incremented by 2, the parity bit stayed the same, so we didn't have to redo the expensive calculation to change 1/0 into odd/even
18:35
<rbuckton>
but 2 isn't odd
1 + 2 is
18:35
<keith_miller>
ljharb: It's was X+2 so parity shouldn't change
18:35
<ljharb>
ahhh gotcha
18:35
<ljharb>
thanks
18:35
<littledan>
so we memoize things in the data dependency graph so that we have to do minimal work to recompute based on changes
18:36
<keith_miller>
I'm confused is this some mechanism where at closure time we detect the captured variable is a Signal?
18:36
<littledan>
I'm confused is this some mechanism where at closure time we detect the captured variable is a Signal?
there's a hidden global variable that tracks all the signals read within the execution of the computed
18:37
<keith_miller>
I don't understand how we would know that parity doesn't need to be updated in general
18:37
<ljharb>
what about signals that aren't unconditionally read in the computation?
18:38
<rbuckton>
I expect the execution of Computed's callback could record any get for a nested signal
18:38
<rbuckton>
what about signals that aren't unconditionally read in the computation?
Their changes don't matter until the condition that made them unread changes?
18:38
<ljharb>
this sounds like why react hooks force you via linter to always unconditionally run all hooks, which is super annoying and unergonomic
18:38
<ljharb>
Their changes don't matter until the condition that made them unread changes?
right but you have to rerun the computation, then, to know if it's updated
18:39
<ljharb>
which means doing the expensive thing
18:39
<keith_miller>
What if the condition isn't tracked by a Signal?
18:39
<keith_miller>
Is that a bug?
18:39
<shu>
I don't understand how we would know that parity doesn't need to be updated in general
my understanding from reading the readme was that the dependencies are dynamically re-computed per computation (per "pull")
18:39
<rbuckton>
No, you either record what signals were used and query whether they've changed, or when they change they notify any computations that depend on them.
18:39
<shu>
oh dan is saying that right now
18:40
<bakkot>
if your signal depends on values which can change which are not themselves signals, it's not going to be able to correctly track updates
18:40
<bakkot>
but i mean... don't do that
18:40
<ljharb>
ok so it doesn't prevent an expensive computation, it prevents dependent expensive ones?
18:41
<Duncan MacGregor>
I'm trying to think how this would work if multiple realms are involved… Do they all need to share the hidden global?
18:42
<ljharb>
i'd assume it's realm-specific, since we probably wouldn't want another cross-realm registry
18:44
<Ashley Claymore>
I thought we didn't want per-realm state :D
18:44
<Mathieu Hofman>
Observable global mutable state is definitely a concern we want to discuss with the champions.
18:45
<Mathieu Hofman>
it's fine to have internal global state, but it's not to make it observable to user code. It's a fine line that AsyncContext took a lot of pain to prove and satisfy
18:46
<ljharb>
i mean, this would effectively just be closed-over variables
18:46
<ljharb>
it's not an object you can mutate
18:46
<Mathieu Hofman>
I'm not sufficiently familiar with signals usages, but I'm wondering if there would be ways to implement this auto tracking without making this internal contextual state observable
18:50
<Duncan MacGregor>
I'm not sufficiently familiar with signals usages, but I'm wondering if there would be ways to implement this auto tracking without making this internal contextual state observable
Yeah. It feels like the graph should have GC roots somewhere and shouldn't really need a separate global variable… but I think I'd need some time and paper to work that out.
18:50
<rbuckton>
Seems like this could be done with weakly held pub/sub events rather than a global variable.
18:51
<Anthony Bullard>
Seems like this could be done with weakly held pub/sub events rather than a global variable.
This is, if memory serves, closer to userland implementations I’ve seen
18:51
<Ashley Claymore>
the benefit is that the code doesn't need to know that it's being tracked
18:51
<Ashley Claymore>
everything adding event observers manually is a lot of work
18:51
<Ashley Claymore>
with signals, I can call functions and they don't need to know that the things they call to get data are getters that add to the tracking
18:52
<bakkot>
like Promises
18:52
<Ashley Claymore>
In FunctionalReactiveProgramming there are events and cells. Signals are much more like cells, than events
18:52
<bakkot>
the data does not need to know you're looking at it
18:52
<Mathieu Hofman>
I understand the motivation, but this is effectively dynamic scoping, and I'm trying to understand if we can put it back within the limits of fluid scoping that AsyncContext offers
18:56
<Anthony Bullard>
There needs to be a tracking context/scope, and a way to access the underlying value without tracking.
18:56
<Michael Ficarra>
this sounds like perfect incubation call material
18:57
<keith_miller>
Could there be a registry for each batch of signals that's not an hidden global value? Not sure if that would be possible or cause other problems?
18:57
<bakkot>
this sounds like perfect incubation call material
they already have other discussions going
18:57
<bakkot>
I don't think an incubation call would help
18:57
<shu>
signals sounds like it has way bigger scope than incubation calls
18:57
<ljharb>
if you create the computed by providing all the signals it cares about explicitly, then you wouldn't need any global state, i think?
18:57
<shu>
incubation calls are time set-aside for one-off smaller things that don't have regular side discussions
18:57
<Justin Ridgewell>
This is large enough they need a dedicated group call
18:58
<Duncan MacGregor>
Could there be a registry for each batch of signals that's not an hidden global value? Not sure if that would be possible or cause other problems?
Couldn't that be on the signal itself?
18:58
<ljharb>
like Signal.Computed(fn, signals)
18:58
<Michael Ficarra>
This is large enough they need a dedicated group call
sure, even better!
18:58
<shu>
that would be static dependencies, not dynamic
18:58
<shu>
that's a pretty big reduction in expressivity
18:58
<shu>
(not arguing for one way or another, but it's pretty different)
18:58
<bakkot>
yeah that API would not be worth having
18:58
<Michael Ficarra>
then I think we should just go for stage 1 and provide all of this feedback in such a call
18:59
<ljharb>
right, i agree with that too
18:59
<danielrosenwasser>
Yes, these topics seem fair to discuss after stage 1.
18:59
<Anthony Bullard>
if you create the computed by providing all the signals it cares about explicitly, then you wouldn't need any global state, i think?
This ruins a good amount of the ergonomics of this feature
18:59
<Duncan MacGregor>
Where did the term Signal itself come from? It feels like these are Signallers rather than Signals.
18:59
<ljharb>
This ruins a good amount of the ergonomics of this feature
it certainly does
18:59
<Justin Ridgewell>
I’m confused by the global state discussion, there shouldn’t be any.
19:00
<Justin Ridgewell>
It’s all on the Signal
19:00
<rbuckton>
While I don't consider it a Stage 1 blocking concern, my concern seemed wide reaching enough that it might be a concern for others who had not previously considered the implications.
19:00
<ljharb>
but if the ergonomics are only achievable with weird unenforceable caveats like "you must evaluate all signals unconditionally, in the callback" - like react has for hooks, i'm not sure it's worth striving for them (i dont think that's the case, to be clear)
19:00
<Mathieu Hofman>
It’s all on the Signal
From what I understand it's used for auto-tracking and preventing notify footguns
19:01
<Justin Ridgewell>
but if the ergonomics are only achievable with weird unenforceable caveats like "you must evaluate all signals unconditionally, in the callback" - like react has for hooks, i'm not sure it's worth striving for them (i dont think that's the case, to be clear)
You don’t need to
19:01
<Anthony Bullard>
I’m confused by the global state discussion, there shouldn’t be any.
Usually there is a tracking context that coordinates updates of a set of signals
19:01
<bakkot>
but if the ergonomics are only achievable with weird unenforceable caveats like "you must evaluate all signals unconditionally, in the callback" - like react has for hooks, i'm not sure it's worth striving for them (i dont think that's the case, to be clear)
you don't have to evaluate all signals unconditionally
19:01
<Justin Ridgewell>
From what I understand it's used for auto-tracking and preventing notify footguns
But the state is stored on the Signal itself
19:01
<ljharb>
i'd love to hear how a computed callback of one.get() ? two.get() : three.get() can deterministically track all three signals
19:01
<Ashley Claymore>
As Dan said, it's the last read that is tracked
19:01
<rbuckton>
i'd love to hear how a computed callback of one.get() ? two.get() : three.get() can deterministically track all three signals
Why would it need to?
19:01
<Ashley Claymore>
the last set of reads
19:02
<Anthony Bullard>
i'd love to hear how a computed callback of one.get() ? two.get() : three.get() can deterministically track all three signals
One of those would not be tracked until called
19:02
<Justin Ridgewell>
i'd love to hear how a computed callback of one.get() ? two.get() : three.get() can deterministically track all three signals
You only need to track 2 of the 3, the third is irrelevant.
19:02
<rbuckton>
It only needs to track one.get() and whichever branch was evaluated.
19:02
<ljharb>
hm
19:02
<rbuckton>
if three.get() changes it doesn't affect the computation.
19:02
<rbuckton>
If one.get() changes, it evaluates the other branch.
19:02
<Mathieu Hofman>
But the state is stored on the Signal itself
but it's ambient during signal computation, which is currently exposed to any user code running during that time
19:02
<ljharb>
ok, so the list of tracked signals potentially shifts over time?
19:02
<bakkot>
correct
19:03
<bakkot>
each time it's re-evaluated
19:03
<Anthony Bullard>
ljharb: yes
19:03
<rbuckton>
ok, so the list of tracked signals potentially shifts over time?
It would have to
19:03
<Anthony Bullard>
Which is why there is usually a hierarchy of tracking contexts
19:03
<Anthony Bullard>
That do the actually coordination and scheduling
19:03
<Ashley Claymore>
ok, so the list of tracked signals potentially shifts over time?

exactly

if (false) {
  readSignal();
}

tracks nothing

19:03
<Justin Ridgewell>
but it's ambient during signal computation, which is currently exposed to any user code running during that time
It’s the same as an AsyncContxt.Variable.run() internally in a computed() call
19:03
<Chris de Almeida>
it appears we lost the transcriptionist
19:04
<rbuckton>
My concern is how a Computed interacts with a weakly held Signal. Does it then strongly hold the Signal? that seems leaky.
19:04
<Mathieu Hofman>
It’s the same as an AsyncContxt.Variable.run() internally in a computed() call
Not quite, there are APIs that effectively expose the acVar.get() value
19:04
<bakkot>
wouldn't the Computed strongly hold the signal in its closure?
19:04
<rbuckton>
wouldn't the Computed strongly hold the signal in its closure?
Not if it's closure is () => weakRef.get()?.get()
19:04
<Anthony Bullard>
My concern is how a Computed interacts with a weakly held Signal. Does it then strongly hold the Signal? that seems leaky.
Isn’t putting a weakly held anything in a closure potentially leaky?
19:05
<bakkot>
oh, yeah, I would hope that this case wouldn't leak
19:05
<rbuckton>
Not if you're holding the weakRef.
19:05
<Justin Ridgewell>
Not quite, there are APIs that effectively expose the acVar.get() value
Internally the Signal can access the ambient Computed context’s var, but that’s not exposed?
19:05
<bakkot>
though, a weakRef is kind of a scary thing to use in a signal - it's a thing whose state can change which is not being read via a Signal
19:05
<Justin Ridgewell>
That’s still not global state any more than AsyncContext is.
19:06
<Xuan Huang (黄玄)>
ok, so the list of tracked signals potentially shifts over time?
Yes, the dependency graph collected and re-formed by Signals is dynamic, as opposed to the number and indexes of Hooks, which are fixed in React. Therefore you don't need it to be unconditional and don't need a lint rule
19:06
<rbuckton>
though, a weakRef is kind of a scary thing to use in a signal - it's a thing whose state can change which is not being read via a Signal
Sure, but its code someone could conceivably write. I'd just like to know what the behavior is.
19:06
<Mathieu Hofman>
Internally the Signal can access the ambient Computed context’s var, but that’s not exposed?
See things like currentComputed() and notify guards
19:07
<trueadm>
In various signal implementations, there's the notion of a computed signal being "connected" to the graph. In other words, they are actively being watched by a an effect (via a Watcher) when you get() the comptued. If a signal reads a state signal, the state signal strongly holds a relationship with the computed signal. However, if the comptued is read outside an effect, then no strong connection is made, and instead we use a form of counter/version instead.
19:07
<Ashley Claymore>
it appears we lost the transcriptionist
I'm guessing we only paid for up to the hour :)
19:07
<Chris de Almeida>
🙂 they bill us after the fact
19:07
<Duncan MacGregor>
So what can a Signal hold? How do we stop people putting something whose values can change unexpectedly?
19:07
<Justin Ridgewell>
See things like currentComputed() and notify guards
Oh, subtle exposes it.
19:08
<Ashley Claymore>
There is Signal.subtle.currentComputed
19:08
<trueadm>
The computed will always have a strong connection to the state signals and other comptueds it depends on, but they might not have a relationship back to it.
19:08
<Ashley Claymore>
It's like an async context that everyone has a reference to
19:08
<shu>
So what can a Signal hold? How do we stop people putting something whose values can change unexpectedly?
what does that mean? the developer has to update a Signal.State afaict
19:08
<trueadm>
So what can a Signal hold? How do we stop people putting something whose values can change unexpectedly?
I state signal holds a value. a computed also holds a value and is memoized.
19:08
<rbuckton>
I have a very mild concern over the name Signal. It could be conflated with AbortSignal. Many years ago it was indicated that WHATWG planned to eventually make more things called Signal that likely weren't this.
19:10
<Duncan MacGregor>
I have a very mild concern over the name Signal. It could be conflated with AbortSignal. Many years ago it was indicated that WHATWG planned to eventually make more things called Signal that likely weren't this.
I have many concerns with the name Signal, all of them mild but I think it overlaps existing terms that mean different things and doesn't quite express the concept they seem to be aiming for.
19:10
<Anthony Bullard>
I have a very mild concern over the name Signal. It could be conflated with AbortSignal. Many years ago it was indicated that WHATWG planned to eventually make more things called Signal that likely weren't this.
Yeah, but it is an omnipresent term in userland today, and always meaning something similar to this. A different term would be more confusing I think
19:10
<Yehuda Katz>
HI!
19:11
<Anthony Bullard>
Though this usage is only within the past few years
19:12
<Yehuda Katz>
This is valid. That said, Signal has a ton of momentum in the ecosystem right now to describe this effort, in part because the standardization effort was spurred by multiple shipped features called "Signal." I have been happy to go with the name :)
19:12
<bakkot>
it appears we lost the transcriptionist
whisper transcript (linked on reflector) is still going
19:12
<rbuckton>
I find it interesting that signals are gaining so much popularity right now, given that knockout existed
19:12
<trueadm>
Arguably, signals existed all the way back from Knockout.js and various other libraries, but the popularity of of the name "signal" has been in the last few years.
19:12
<Chris de Almeida>
whisper transcript (linked on reflector) is still going
saved again by the kevbot!
19:12
<ryzokuken>
bakkotbot*
19:13
<Yehuda Katz>
Starbeam calls the primitives Cell and Formula as an Excel pun, which I like but I'm not that attached to these details :)
19:13
<Anthony Bullard>
I find it interesting that signals are gaining so much popularity right now, given that knockout existed
Ryan Carniato was a knockout dev. Sometimes things come back around
19:13
<Yehuda Katz>
Convergence around pull-based signals is a pretty important detail imo.
19:13
<rbuckton>
In .NET there is a somewhat similar mechanism called dependency properties.
19:13
<rbuckton>
Though its tied to a property declaration, not an independent value.
19:14
<Yehuda Katz>
It's what makes them so abstractable and allows for the separation we were talking about.
19:14
<rbuckton>
(not suggesting that as a name, "dependency" has an overloaded meaning in JS already)
19:15
<danielrosenwasser>
whisper transcript (linked on reflector) is still going
let me know next time!
19:15
<Yehuda Katz>
Ultimately, for convergence/interop, it's pretty important that the shared primitive can support both arbitrarily interesting reactive objects built on the primitives and integrate deeply with the framework.
19:15
<danielrosenwasser>
:D
19:16
<Yehuda Katz>
If either of those are missing, there's no real motivation to collaborate on a standard rather than build something well tuned for your framework's rendering needs
19:16
<ljharb>
also "abortsignal" isn't a great name anyways. the DOM shouldn't build more "signal" things like it
19:17
<Yehuda Katz>
I am willing to go with this narrative, but I personally don't believe in the strong version that implies a lost decade simply because people couldn't see what was in front of their faces ;)
19:17
<Yehuda Katz>
👍️
19:18
<Duncan MacGregor>
(not suggesting that as a name, "dependency" has an overloaded meaning in JS already)
PropagatedValue and PropagatingCalculation?
19:18
<littledan>
Yeah we've definitely been making incremental progress the whole time
19:18
<ljharb>
"propagate" is too easy to misspell
19:20
<Yehuda Katz>
The signals proposal does something in between. There's always a counter that can be used for invalidation, and you can form a strong connection from state->effect (via a watcher). The invalidation itself works the same in both cases though (e.g. the cache is invalidated not via a watcher but via the counter)
19:21
<Yehuda Katz>
I think I had this in my slides: the watcher is narrowly focused on notifying something that is going to flush the signal out into the real world. But then the actual flushing is a regular pull that works in the the way you described the "no strong connection" scenario.
19:23
<rbuckton>
PropagatedValue and PropagatingCalculation?
That's decidedly worse.
19:24
<ptomato>
Yeah, but it is an omnipresent term in userland today, and always meaning something similar to this. A different term would be more confusing I think
a counterexample is the GNOME platform, where "Signal" means pub-sub, and "property notify" is used for this
19:24
<rbuckton>
Maybe something using Cell? i.e., ValueCell, DataCell, etc.
19:25
<Yehuda Katz>
I have a weak aesthetic dislike for Signal for a similar reason as these rub me the wrong way: the mental model (and design) is really focused on pull-based patterns (i.e. normal JS access patterns). Propagating implies that there's a strong, semantically meaningful arrow from the state through intermediate computeds that lands on effects, and that that's how you think about invalidation.
19:25
<shu>
CellLikeTheOneInExcel
19:26
<Yehuda Katz>
What systems that use signals are aiming for is to make the whole end-to-end feel like a one-shot function that somehow is up to date, and one reason signals end up working so well across many different paradigms (and evolved independently) is that the pull-based model facilitates that design.
19:27
<rbuckton>
If we had a native event mechanism, would we need Watcher?
19:27
<Yehuda Katz>
Starbeam is Cell and Formula for this reason 🤷
19:28
<Yehuda Katz>
I just want to respect the ecosystem and my fellow framework collaborators and not turn the conversation into a rathole on this. And frankly I would be ecstatic to get framework interop with a name I don't love.
19:30
<Yehuda Katz>
Watcher is very careful to facilitate lossy consumption patterns (i.e. the notification itself is fairly simple, you're not allowed to read from the reactive value inside of it, and you can trivially and efficiently coalesce notifications into a consumer-specified scheduling mechanism)
19:30
<littledan>
If we had a native event mechanism, would we need Watcher?
I don't understand the question; what would a native event mechanism solve to simplify Watcher?
19:30
<Yehuda Katz>
The scheduling mechanism can include things like "validation happens from the top-down in tree order"
19:31
<Yehuda Katz>
rbuckton: events and observables are really awesome for lossless streams of data, and have affordances and efficiency tradeoffs that are tuned for that.
19:31
<rbuckton>

For example, I'd sketched out an idea awhile back about using a symbol-protocol for pub/sub events that could sit on top of EventTarget/EventEmitter/etc., and just add a very lightweight event primitive to JS.

then you do something like cell::onchanged += () => {} to watch for changes on a single cell.

19:31
<Yehuda Katz>
stopImmediatePropagation 🤦
19:32
<Yehuda Katz>
Did I get it right?
19:33
<Yehuda Katz>
I feel like I'm brushing you off a little too much here. I'd love to sync up and give enough time to hear you out. You've thought about this a lot and I would love to mind-meld :)
19:33
<rbuckton>
If the event mechanism is hookable (which it would have to be to work with EventTarget/EventEmitter), then you can be flexible in the underlying event notifaction mechanism.
19:33
<trueadm>
I think the important part of Watcher, vs events, is that is that signals are designed to be lossy. If you were to write to a signal multiple times, we'd only really care that the signal is dirty and when we get around to scheduling an update from the watcher we get the latest value. The intermediate signal state values wouldn't be important as we only "pull" the latest value when we're interested in it, vs being notified everytime a signal changes.
19:34
<rbuckton>
I think the important part of Watcher, vs events, is that is that signals are designed to be lossy. If you were to write to a signal multiple times, we'd only really care that the signal is dirty and when we get around to scheduling an update from the watcher we get the latest value. The intermediate signal state values wouldn't be important as we only "pull" the latest value when we're interested in it, vs being notified everytime a signal changes.
That's perfectly fine. Events don't have to be lossless.
19:35
<Yehuda Katz>
I can definitely imagine a foundation for events that allows things like the watcher API to fit neatly into it.
19:36
<Yehuda Katz>
This use-case a good thing to keep in mind if people are going to try to nail down an events foundation
19:38
<rbuckton>
My main concern is adding Yet Another Subscription Mechanism to JS. I'll admit, I've done my fair share. DisposableStack can take callbacks and notify them, but that capability is a one shot notification and is primarily designed as an adapter for legacy code.
19:38
<Mathieu Hofman>
(I really wish Matrix had better threading support)
19:38
<rbuckton>
Same...
19:39
<Yehuda Katz>
rbuckton: that's a good framing for your feelings. It will help me remember your concern as we iterate the design :)
19:40
<Mathieu Hofman>
bakkot: is the log bot down?
19:40
<littledan>
But why can’t the unifying mechanism be “callbacks”?
19:41
<rbuckton>
Regardless as to its internal implementation, Watcher is yet another in a long line of pub/sub mechanisms. One would be enough, two is pushing it, but we already have 5+ and some are for very niche events. I'd like us to stop that trend if possible.
19:42
<littledan>
I mean, that underlies all these other ones
19:42
<rbuckton>
That's not a unified mechanism. If you ask any JS developer if JS has events, I expect they'd say yes. Except, it doesn't. DOM has events, NodeJS has events, JS itself does not.
19:43
<littledan>
One thing to understand about the watcher mechanism is that its use should really be buried within the implementation of effects or rendering, not for direct use by JS devs
19:43
<trueadm>
In an earlier design, we had the notion that you could pass a computed a callback function that would trigger when it was made dirty. The downside was that you had to pass this upon construction of the computed, which meant interoperability was impaired. However, it could be modelled differently. Is that better than having a watcher?
19:46
<rbuckton>
Maybe I didn't represent my position correctly when I asked if Watcher is needed. I agree it is needed, even with a native event mechanism, even if only as the event coordinator. I'm trying to ascertain which parts of Watcher are unique to Watcher as opposed to a generalized pub/sub mechanism.
19:46
<Yehuda Katz>
rbuckton: I mostly defer to Dan and other deep in the implementation constraint space on the fine details. What I primarily care about is not turning it into yet another pipe for data to flow through
19:46
<Yehuda Katz>
so I guess we're both worried about the same thing, it turns out? :)
19:47
<Yehuda Katz>
trueadm: doesn't that fly in the face of decoupling and abstraction goals?
19:47
<Yehuda Katz>
I guess not, since you're always making a computed at the edge
19:47
<rbuckton>
Using .NET as an example (as I am oft to do), events aren't only defined using Delegate or MulticastDelegate, they are hookable. WPF has RoutedEvents which act similar to DOM events and are primarily used to work with dependency properties.
19:47
<Yehuda Katz>
however... it implies that the watcher is forever, no?
19:49
<trueadm>
however... it implies that the watcher is forever, no?
Well back then the design allowed for effects to be detached from the graph. I was just seeing what the appetite was for an alternative approach.
19:50
<trueadm>
I think events might be fine. However, I do worry about the overhead in allocations from events and EventTarget. The signal graph is essential for performance so it needs to be as lean and optimal as possible to ensure it can scale well for complex UI cases.
19:51
<Yehuda Katz>
trueadm: I'm both pretty free-and-easy on the exact details of what's going on at the flushing edges and very committed to designing a feature that facilitates and supports "it looks like one-shot but is magically up to date" patterns.
19:51
<Yehuda Katz>
Preact and React get a lot of that "for free" (with some cost) from the JSX model.
19:51
<Yehuda Katz>
But the Watcher API should facilitate a way of thinking about Vue/Svelte/Ember/etc templates that are, more-or-less, like JSX.
19:51
<rbuckton>
It could be that Watcher ends up being a poor use case for events. I'm still concerned that we don't have events. Signals and events both belong to a similar class of observer-related mechanisms, and that we're catering to a niche use case without considering wider issues.
19:53
<Yehuda Katz>
I think I see where you're coming from. I'm especially interested in the possibility of stripping away the affordances that have accreted onto the high-level EventEmitter and get down to the fundamentals
19:54
<Yehuda Katz>
What bothers me about "why not use an event" is that it implies a bunch of noise around the usage pattern when a much tinier pattern actually works. This is probably also why we keep getting more of these things.
19:54
<rbuckton>
FYI, I think the general consensus is that Matrix threads are a terrible UI. Replies are easier to follow when there aren't 5 conversations going on at once.
19:56
<Yehuda Katz>
I guess I'm just used to it via work Slack. I have been keeping the threads pane open, but I'm probably swallowing a lot of cognitive overhead because I'm used to it.
19:56
<rbuckton>
What bothers me about "why not use an event" is that it implies a bunch of noise around the usage pattern when a much tinier pattern actually works. This is probably also why we keep getting more of these things.
Yes, but this becomes a death by a thousand papercuts. At some point we have to stop tacking on one-off "similar but not the same" mechanisms.
19:57
<Yehuda Katz>
I'm super into the design space of "what's the minimal EventEmitter that would be ergonomic enough so people would use it instead of ad-hoc callbacks, and also serve as a foundation for the ones that exist"
19:57
<Yehuda Katz>
Does that characterize your position correctly?
19:57
<rbuckton>
If Watcher is dissimilar enough, maybe I drop the concern, It just doesn't feel so dissimilar.
19:58
<Yehuda Katz>
ljharb was quibbling with this point, and he's not wrong about the fetch precedent. That said, I really liked your point (the "simple effects" people intuit they want are a DOM feature).
19:59
<rbuckton>
Does that characterize your position correctly?
That and "how do we make it compatible with the DOM and NodeJS". I'm pretty sure there would be opposition to a wholly new event mechanism.
20:01
<Ashley Claymore>
`Watcher` is in someways like `FinalizationRegistry`, where its core purpose is to be notified. So taking a single callback as a constructor argument maybe does fit this pattern well. As opposed to EventEmitters which are fan out to a changing N observers 
20:01
<Yehuda Katz>
`Watcher` is in someways like `FinalizationRegistry`, where its core purpose is to be notified. So taking a single callback as a constructor argument maybe does fit this pattern well. As opposed to EventEmitters which are fan out to a changing N
Great observation!
20:02
<rbuckton>
I would like to have proposed an events mechanism before, and have wanted one for years, but I'm already overcommitted to other work and I hadn't yet reached the proverbial "last straw".
20:04
<rbuckton>
`Watcher` is in someways like `FinalizationRegistry`, where its core purpose is to be notified. So taking a single callback as a constructor argument maybe does fit this pattern well. As opposed to EventEmitters which are fan out to a changing N observers 
Maybe it is, but Signals, Observables, Promises, and Events all belong to a similar category of observation mechanisms, even if they fill out different quadrants.
20:05
<Ashley Claymore>
True, but something grouping things hides how they should be approached differently 
20:07
<Ashley Claymore>
Observables can model Promises and also synchronous return values. But when I worked on projects that used Observables in perhaps too many places it was hard to reason about them. "Is this an observable of many values, or one. Is it async or sync"  
20:07
<Ashley Claymore>
The Watcher is more like a Sink than an Emitter. And has a different set of responsibilities 
20:07
<Ashley Claymore>
but I do see your point
20:08
<rbuckton>
Side question: How much of the Signals proposal is actually Signal/Computed vs Watcher? In other words, is Watcher the building block that both Signal and Computed are built on?
20:10
<Ashley Claymore>
Signals and computed on their own work, but to be reactive without watcher that would be a polling system. Like how some hardware works where the main loop just keeps checking what is dirty 
20:10
<Ashley Claymore>
on their own the caching still works 
20:10
<Yehuda Katz>
Side question: How much of the Signals proposal is actually Signal/Computed vs Watcher? In other words, is Watcher the building block that both Signal and Computed are built on?
No, it's not
20:10
<Ashley Claymore>
Reading a computed twice in a row is cached 
20:12
<Yehuda Katz>
No, it's not
Everything about reading computeds, having them cache, invalidation, etc can be described without reference to the Watcher
20:13
<Yehuda Katz>
a Watcher allows you to say "I am using this computed to populate a sink of some sort (usually a DOM node), and I need to know when any of its dependencies change so I can properly schedule it to update and read its value when I do that"
20:13
<rbuckton>
The Watcher is more like a Sink than an Emitter. And has a different set of responsibilities 
This is a fair point. I have to think more about my concerns and whether I might still consider them blocking for stage 2. Either way, I still think it is unfortunate that we might have this and yet still be missing events.
20:17
<Yehuda Katz>
This is a fair point. I have to think more about my concerns and whether I might still consider them blocking for stage 2. Either way, I still think it is unfortunate that we might have this and yet still be missing events.
I keep trying to nudge the term Sink into the explanations we give
20:17
<Yehuda Katz>
I think it's really good
20:17
<rbuckton>
Signals feels like we're giving everyone a chainsaw when they need a flathead screwdriver. Sure, that's great for the woodcutters, but that means the rest of us have to get by with a butter knife. Normally, I wouldn't conflate two proposals in this way were it not for the close similarities.
20:19
<Yehuda Katz>
Maybe it is, but Signals, Observables, Promises, and Events all belong to a similar category of observation mechanisms, even if they fill out different quadrants.
I'm pretty open to filling out the table. Some formal-ish observations: Signals facilitate discrete updates, are pull-based, and have a lot in common with the incremental computation academic space
20:19
<rbuckton>
And don't get me wrong, I like the idea of signals. I've used knockout and React and so many other systems with a similar concept, and I can see its utility in other places as well.
20:20
<Yehuda Katz>
I'm pretty open to filling out the table. Some formal-ish observations: Signals facilitate discrete updates, are pull-based, and have a lot in common with the incremental computation academic space
People want to tell me that they're push-pull, but signals do a pretty different thing than what people usually mean by push-pull, producing pretty different abstractability and interface outcomes
20:20
<Yehuda Katz>
I could believe you want to make "push-pull" more subtle
20:21
<rbuckton>
"Filling out the table" was supposed to include Observable as well. I'm not certain why the proposal switched venues.
20:21
<rbuckton>
Can setting a signal trigger a UI update? If so, it's also push based.
20:21
<Yehuda Katz>
rbuckton: What, imo, is new from the FRP wikipedia page (or even the various attempts to nail this down in terms of promises and observables) is that we have a lot of existence proofs now that the theoretical issues people have with those choices are managable and efficient at scale in the context of web UIs.
20:22
<Yehuda Katz>
Can setting a signal trigger a UI update? If so, it's also push based.
Setting a signal sends a notification, but there's an all-signals read-write barrier in the notification, which prevents you from simply updating the UI right there
20:22
<Yehuda Katz>
it also doesn't need to propagate through computeds
20:22
<rbuckton>
Computed is definitely pull only.
20:24
<Yehuda Katz>
At the extreme end, the original Ember model (which is spiritually equivalent) has a single microtask notification that coalesces any amount of signal updates
20:25
<Yehuda Katz>
and that causes a top-down revalidation uses pull-based revalidation to cut off subtrees, and which manages to be relatively efficient because of the natural logarithmic(ish) properties of output DOM trees
20:26
<Yehuda Katz>
You can call this a "push" but it's not really what people mean when they talk about "push-pull" models
20:29
<rbuckton>
I will withdraw my Stage 2 blocking concern. While I'd like to have a native event mechanism, Watcher does not seem like a fit for it and I don't think it's fair to block a proposal on the basis of the existence or non-existence of a separate proposal I do not have the capacity to champion myself. I am still concerned about the lack of events, but I'm willing to wait until the next push trigger to raise the concern, or for when I have less on my plate.
20:31
<rbuckton>
You can call this a "push" but it's not really what people mean when they talk about "push-pull" models
I suppose that's true in the broader sense
20:50
<rbuckton>

I have an interesting observation, though in the end it is perhaps not relevant to signals. For a number of years I have also been considering a proposal to add ref (though potentially spelled &) for both reference passing and reified references, i.e.:

function f(&x) { x = 1; } // reference passing
function g(xref) { xref.value = 2; } // reified reference

let y = 0;

f(&y); // pass reference to mutable binding
y; // 1

const yref = &y; 
g(yref); // pass reified reference to mutable binding

y; // 2

let &ym = yref; // dereference to mutable binding
ym = 3;

y; // 3

const &yc = yref; // dereference to immutable (but live) binding
y++;
yc; // 4

I wonder how interesting it would be if you could treat state/computed as refs:

let &counter = new Signal.State(0);
counter; // 0
counter++;
counter; // 1

const &parity = new Signal.Computed(() => counter % 2 ? "odd" : "even");
20:51
<rbuckton>
I've been collecting use cases and this seems like an interesting one.
20:57
<rbuckton>

Some of the other use cases include decorators (to handle circularity/TDZ)

@Entity
class Parent {
  @OneToMany(&Child) children;
}

@Entity
class Child {
  @ManyToOne(&Parent) parent;
}

and private state in shared structs

shared struct AtomicValue {
  #value; // do not allow unordered access to private field

  load() { return Atomics.load(&this.#value); }
  compareExchange(expected, replacement) { return Atomics.compareExchange(&this.#value, expected, replacement); }
  ...
}

Plus a few others.

21:02
<rbuckton>

To the runtime, & is more like a syntactic transformation, so the signals example ends up treated as something roughly like this:

const ref_mut_counter = new Signal.State(0);
ref_mut_counter.value; // 0
ref_mut_counter.value++;
ref_mut_counter.value; // 1

const ref_cnst_parity = new Signal.Computed(() => ref_mut_counter.value % 2 ? "odd" : "even");
21:03
<rbuckton>
One benefit to having Signals as a built-in mechanism in ECMA-262 is that we could get away with specifying it as a reified reference.
21:04
<rbuckton>
or rather, a kind of reified reference.
21:19
<Yehuda Katz>
I had to suddenly switch back to work-work -- sorry for disappearing mid-convo :)
21:21
<Yehuda Katz>

To the runtime, & is more like a syntactic transformation, so the signals example ends up treated as something roughly like this:

const ref_mut_counter = new Signal.State(0);
ref_mut_counter.value; // 0
ref_mut_counter.value++;
ref_mut_counter.value; // 1

const ref_cnst_parity = new Signal.Computed(() => ref_mut_counter.value % 2 ? "odd" : "even");
fwiw, I am personally interested in exploring trimming down the portion of the API that really has to be a reference, ideally separating it from the value portion. This is roughly the Starbeam API, but I would need to float it by other framework authors to see whether there are incidental simplifying assumptions in Ember that let us get away with that design, but which prevent it from being fully general
21:30
<Yehuda Katz>
I had to suddenly switch back to work-work -- sorry for disappearing mid-convo :)
And now I really do have to go! Please DM me with any follow-ups. The thoughtful feedback and questions were really invigorating :)
21:43
<trueadm>

That's pretty much how Svelte 5 works with signals. We don't actually expose Signal objects, instead they're hidden away by the compiler.

let counter = $state(0):

counter++;
console.log(counter); // 1
21:45
<Yehuda Katz>

That's pretty much how Svelte 5 works with signals. We don't actually expose Signal objects, instead they're hidden away by the compiler.

let counter = $state(0):

counter++;
console.log(counter); // 2
it's also how Ember works with our precursor to signals and how Starbeam (our next generation reactivity system, which is my primary technical project on Ember these days) interacts with them.
21:45
<Yehuda Katz>
There's mostly a pretty close mapping between the concepts we use and signals.
21:45
<Yehuda Katz>
Also, turns out if I want to avoid nerd-sniping myself, I need to turn off notifications :P