Intellisense provides different suggestions on autocomplete when using indexed access or dotted access

🔎 Search Terms
"autocomplete suggestions intellisense differences between indexed access and dotted access"
"indexed access vs dotted access suggested properties"
🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about

I tested it in TS Playground with Nightly and TS 5.2.
⏯ Playground Link
https://www.typescriptlang.org/play?ts=5.3.0-dev.20231013#code/PQKhCgAIUhxB5AggGQFyQOYEsBuBTAO0gEMjCBXAW0gHsAzSABxoGcWsAjAGz0j3wIAXFgBpIAd17jSgyIJpQYxNlgxEWNSr36FZjYgE8uNYgBM5Bxr3mQNW2g0EALbQOEA6RZC8AVF5AAnPBZZFicaci5zDDxZYkxCPACsAGNIACIAUTd0pkNjMxIVNTxTT2hIAHU8MWdCMTCIqMgOXmJuaxpILQCYlppnOX8QgPIUwXIgli9Sc1aSDsgsIjpyAnGsGgIWIeJZchZeGIIk1J2uLABraycsHa3eVuNxLxsaDkFiZaHefSMTcwhYgBQTLDCQOgBTQ-PhuCxWcogYDgcCmPApLjA3irdagrYQrBcQRJADCB3klGyugACvkAQAxGgBKlCAA8Pj4AA9iQRTDsSdSWcIAHwACjoTKF6B8AEp0PAOIcAjh2jxWQKhSwALLERgAbR8AF1hSjQBAKlqsJRUpcwYFOQArHY2Yg4GhYcxoqy8wgpAxLSjMEGKZHgQSWXgKpUqjqsgAawsgAF5IABvcAASBY5EVKWSrVFcsgUaSMbVCfAAF8ANymsCQZCxADkO1a2CISJRFGoGrcO3TGZJyAAkiSANLJjIYm3pESZ6nIRAATUn6UYmIMs8zJMQADkF8vVylSOvDFuMwel8Pd7BV6eDGDz9TEABVADKmTvxAOeHPmQASv+8D-quSRQgEf67gAIpkUGgT6pjnsgSBQW+PiIP+HIpukBSmG+nwgue-6ID4mQkgAEnusCfthAR7OiTikDE56VIgw4+Net7YdIWCggQGDnh+mRjpxq6HHgtr8YJmTCbBYl4BJpTnhxWqZC+1JQSRNEZKCWjkIwpj0eeABq8DIC+qkUVR2npG6XBUAxTG-nOGZQS+xEcfAu5WTeNmmJMeybOsjH8c5maIFBUE+MR46rmYpiCHRKSXH+WpvlxGR4JQLACS5l4AEKIOOAD6+WZLA17FZRyCkXB2H3hwxDJfleDtuRxBEkpLloRhPjFSpmTFYgxlsYu+XIDZQIgj4Vp4IgKqEqqYWDl5pG7n1PWkcVPnUXVU5bDygj4fRJIhTEiEua+UHDvAxWXsV-7DrA5F9UVACKL7Dv+cnYd+pibNSG7-qoTiCIgKQAI7kFgQQXZmm1vsV6maaRYmfMIL4GUZc6VqawCQLS-xmDsjH4C0ClEEEp4pKUEJMjCVNQgAtMehxiBw5CyHU-qmF0BADN0ezEgEgQg7I-PiJ4yzC3QTW8AKOqMFYpiammmZ6r2ugsO4mRpbAhroGslwSwQ1beBmGuCn27huR5N3eZRvkG5ARsm7WFua0I2umeZlmO9RzuuzQ4im+rnseANyNaYHBDG8Hoce1bWs64BwEx3HIfu5bmruB5ZH+5k6du2HSde+4EVRTFY5F-HWfh9rJKrZk63FZtg07bBNeZyXOc9Zh-XDqpQ0jcOY0TV3CfZ9bBVFWOpXlZV1W1RPdelx4CNIxp0eG7HxeJznV03Xdi5Lg9T0vUNJIfV9nc7xnk-1+4QkiTeK899bz+3y7u+11WdbmjAPwdwliiAcJAFIEQAhsxhCwYg9h6BeC8AAAxQV4f86ImSmFZJkTkGJyBonVGvUB1wDD0EgArXUytNTCjEEKE0FQUFIJDCicMVhyHUhfAQSglDSiqxTAOPUE5vg4LwQQ+uYgSFkIoUrXhfZhTOyFLWGsXZORBi5hGdhmpFaTmkVQvskAABk7DOHcJkSrPstZwB4FUUydRbCBQlmVEtPhasLZCKIJIhg9dFbO0cWWPAhCtG6kEcaJRlizReEbIIFs4Cgj0VoJMWgHAHTolkOIXiTgEki1MYwMEXhWiCEkIQPgBAqCQBITsWYSToxLW1sw1E6JMRBHAVsEIsJdB+NqegBxipSzOIsf-Lwu5g60G9DCaWeAuAXEONsXg+NvzyAgYGHgeIiBaFKbQIgzhgHWMYFMdg+IOA83dPxfozhUCIIYagioOghCdI6HUq5TCKgzF5JcmAjCvC3MEPcngLA9TpHSIaZBKCAAF7zvAVD8NoVRqTaZTEiLIYBzg9hLC5i4f0jRIjmD2TQHAHprDiC6P9OgdAki6C8NmXpwgwEpHCKkYIkBRTEhCLTdJgxdxiy4P6fE8BxiQAAIwAGYZT1O+b84IgyKj5RfD4MQWAGDDkgLzEgKQaZsCGFCcgGBMm80EMScwTV1XOi6PEXFVgQT+hRUinY-NQjapiKy0wc4Ki8RAYEDBagsAAC9abKEgCqC4hreQSEmVweEpREShnFb0pxDz3ANSapcFqbUOoGvAMAfGkBs05tzXmyAAA9AA-FKwBtwdh3HldsYkZgxBBAgV631ZRIDQuaeJbKchTVKyxEIbl5NNl9oeMqhVZKgjrBuHsLw1odWyHmHUcpeAeWOH8LijolB0DmqSOGBdBhKnNPrTQRtfqdgktHboKNXY3ASu1voH8Gas35sfdm4tLCNFjkXSweAdAhTXsnJ4iNZCY01IebWTNuaX2luLAwCBkxoFGuCOwU5zgtU6pKWUip7qQiEnDYGj0F7wAQOrZATkk5RRAb6Q8qeycZ4lTKhVXcVUUDLxlKBh9haS0EdabIf0KYyNXtjf4-5j9nzvkLix+94Gi1AA
💻 Code
/**
 * Mimicking rxjs to avoid dependency import
 */

type Observable<X> = {
	subscribe(): Observable<X>
};

enum CPEvents {
	CLICK = "click",
	PLAY = "play",
	CANPLAY = "canplay",
	PLAYING = "playing",
	PAUSE = "pause",
	ERROR = "error",
	ENDED = "ended",
	LOADSTART = "loadStart",
	RATECHANGE = "ratechange",
	WAITING = "waiting",
	SEEKING = "seeking",
	SEEKED = "seeked",
	TIMEUPDATE = "timeupdate",
	VOLUMECHANGE = "volumechange",
	DURATIONCHANGE = "durationchange",
	ADDTRACK = "addtrack",
	EMSG = "emsg",
	PLAYBACK_BEGIN_HALTED = "playbackBeginHalted",
	START_TIME_AVAILABLE = "startTimeAvailable",
	CONTENT_STATE_CHANGED = "contentStateChanged",
	AUDIO_PLAY_RIGHT_ACQUIRED = "audioPlayRightAcquired",
	STATS_UPDATE = "statsUpdate",
}

// Payloads have been replaced for the repro-case, but they do not matter right now.
interface CPMappedEvents {
	[CPEvents.EMSG]: unknown;
	[CPEvents.DURATIONCHANGE]: unknown;
	[CPEvents.VOLUMECHANGE]: unknown;
	[CPEvents.TIMEUPDATE]: unknown;
	[CPEvents.ERROR]: unknown;
	[CPEvents.RATECHANGE]: unknown;
	[CPEvents.ADDTRACK]: unknown;
	[CPEvents.CONTENT_STATE_CHANGED]: unknown;
	[CPEvents.START_TIME_AVAILABLE]: unknown;
	[CPEvents.PLAYBACK_BEGIN_HALTED]: unknown;
	[CPEvents.STATS_UPDATE]: unknown;
	[CPEvents.AUDIO_PLAY_RIGHT_ACQUIRED]: unknown;
	[CPEvents.SEEKING]: unknown;
	[CPEvents.SEEKED]: unknown;
}

type CPUnmappedEvents = {
	[K in Exclude<CPEvents, keyof CPMappedEvents>]: Event;
};

export type CPEventsMap = CPMappedEvents & CPUnmappedEvents;

export type CPObservableEvents = {
	[K in keyof CPEventsMap]: Observable<CPEventsMap[K]>;
};

declare const eventObservables: CPObservableEvents;

🙁 Actual behavior
Intellisense (autocomplete) suggests two different subset of valid values when accessing a mapped object through dotted access and indexed access, but accessing with a dotted access to an unsuggested (available in indexed access and there fore still valid) property seems to be still valid and well-recognized.
🙂 Expected behavior
The suggested values should be the same if there is not a specific indexed signature.
Additional information about the issue
No response

A minimal repro would really be appreciated. Thanks!

@RyanCavanaugh So a git repo? I thought the playground was enough as the code is the same

@alexandercerutti I assume Ryan was only looking at what you put under "Code", which isn't a full repro.  That's why the issue template says the Playground and Code section should match.

@fatcerberus The template says:

Please post the relevant code sample here as well.
This code and the Playground code should be the same, do not use separate examples.
We can quickly address your report if:
The code sample is short. Nearly all TypeScript bugs can be demonstrated in 20-30 lines of code!
It doesn't use external libraries. These are often issues with the type definitions rather than TypeScript bugs.
The incorrectness of the behavior is readily apparent from reading the sample.

Reports are slower to investigate if:
We have to pare too much extraneous code.
We have to clone a large repo and validate that the problem isn't elsewhere.
The sample is confusing or doesn't clearly demonstrate what's wrong.

I literally added what I was thinking it was "relevant", by taking it from the playground. 😄
That's because in the playground I added a lot of comments.
But if "relevant" means "playground code"... what is playground link need for? eheh
Anyway, I'll change the main message to include the whole code (without comments, I guess).

This code and the Playground code should be the same, do not use separate examples.

is the relevant bit.

But if "relevant" means "playground code"... what is playground link need for? eheh

Playground is still useful so you can see hover tips, execute the code, etc.

thought the playground was enough as the code would be the same.

The Playground example is definitely a reproduction, but it usually saves us a lot of time if you can make a minimal reproduction that cuts away any extra cruft. This also makes it easier to find duplicates later, etc.

@RyanCavanaugh thank you. I hope the code I setup is enough. Let me know if I can add anything further.

I've tried to simplify this and it comes down to this code:
type Foo = {
	["FOO"]: Event;
}

type Bar = {
	[K in "BAR"]: Event;
};

type FooBar1 = {
	[k in keyof (Foo & Bar)]: number
}

type FooBarKeys = keyof (Foo & Bar);
type FooBar2 = {
	[k in FooBarKeys]: number
}

declare const foobar1: FooBar1;
declare const foobar2: FooBar2;

foobar1 // dot completion: BAR
foobar2 // dot completion: BAR, FOO

I think @alexandercerutti's concern is that foobar1 only has BAR as the completion (this is due to how property access completions don't show for index signatures while they do show for mapped type signatures). I think this seems non-intuitive since keyof is behaving differently when used in the mapped type directly vs another place (FooBarKeys for example), but this doesn't seem like a bug.

@PranavSenthilnathan thank you for giving a look into this.

I think this seems non-intuitive since keyof is behaving differently when used in the mapped type directly vs another place (FooBarKeys for example)

This is really really a subtle behavior. To me, there is really no difference with FooBar1 and FooBar2.
I honestly would have issues in explaining this to anyone reading my code. Also, that type (FooBarKeys) would be prone to be removed by anyone not knowing the difference between the two examples above.
Probably, as you say, there is no bug here if the keyof behavior is intended. It might, instead, be considered "only" a very bad user experience matter to me.
If keyof behavior is not intended, that's a bug.
When I create libraries, I put very much attention on autocompletion values to help developers, even on custom values with module augmentation. This just to tell you how much I care about autocompletion showing the correct values.
So, I guess that the behavior described by foobar1.FOO not being suggested but accepted when used is still related to this, isn't it?
Is this keyof behavior difference a documented behavior? I never read of it... do you perhaps have any link for future reference?
Thank you.

I agree with you, since the compiler knows the access of the properties is valid/invalid, we should offer them for completion. I was looking into this more and it seems like the behavior I mentioned above is actually only on the playground (monaco editor). In VSCode, both completions are shown. Going back to your original example, the dot completion will show the same completions as the index completion with one exception. For the enum, it shows the enum instead of the enum member. Let me just share the VSCode screenshot for your original example.
Dot access:

Index access (the scroll bar doesn't show up in the screenshot but it's there):

The monaco issue is something that you can report there. I believe tsserver will return the remaining completions if they pass the includeInsertTextCompletions flag (or includeCompletionsWithInsertText) here. Then their editor can decide whether or not to show it.
For VSCode (and more generally, tsserver), I think the behavior here should be changed to include the enum members because it's possible only a subset of the enum values are valid properties on the object.
Is this a fair summary of your issue @alexandercerutti ?

Monaco doesn't pass the flag that allows dot-completions to have different completion edits than just the entry text. This is why if you write
declare const obj: Record<"foo bar" | "baz", string>;
obj. <-

You'll see "foo bar" in VS Code but not in the Playground (since it completes to obj["foo bar"]). Seems like we're incorrectly filtering the enum-originating members as needing non-textual completions.

@PranavSenthilnathan oh, I see! I was really weirded to see that.
Yeah, that pretty sums up the problem.

the dot completion will show the same completions as the index completion with one exception. For the enum, it shows the enum instead of the enum member.

They are "kind of" the same. Not really, visually, but they are the same in terms of possible "ways to access to a property".
Everything seems to be caused by the Mapped Type, which completely removes the references to the enum (even I think I should be able to choose whether they should be kept or not), along with the intersection, which causes the mix of properties.
type CPUnmappedEvents = {
  [K in CPEvents]: Event;
};

I'm wondering if perhaps isn't there something wrong on these two, other (or instead) then the completion...

Maybe a bit unrelated but not that much. I think this problem impacts also when using an array of enum and, in general, when creating a variable (so not just the type):
type X = Array<CPEvents>;

Array completion should show all the enum keys.

Object completion should ALSO show the enum and the enum keys.

Should I open a report on monaco about this, for playground?


More efficient sparsity handling

Codecov Report
All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 94.64%. Comparing base (e788f03) to head (e509311).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #222      +/-   ##
==========================================
- Coverage   95.60%   94.64%   -0.96%
==========================================
  Files          73       15      -58
  Lines        3820     1775    -2045
==========================================
- Hits         3652     1680    -1972
+ Misses        168       95      -73

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.


Felix could be smarter with its RTNETLINK parsing

Felix detects interfaces coming up using the netlink interface. It doesn't parse every flag in the message, but instead just tries to program the interface and route every time anything changes.
We could optimise this by being smarter and only programming interfaces when they are in a state for that to be worthwhile. We have to be a little careful, since if an interface flaps (as we have seen on some misbehaving VMs) Felix is doing the right thing of applying the route again when it comes back; that behaviour must not be broken.
This is a performance optimisation because we seem to see quite a lot of CPU usage under load. It's not clear that we need to do this, but raising the issue so it can be considered.
@fasaxc Please be aware of this issue.

I'm increasing the priority of this. It appears that this takes Felix from "happily handles 150 creates per second at 50% CPU" to "cannot handle 150 creates per second". Hence I think we should definitely spend a day or so fixing this.

Observations so far...
For an individual VM being launched, I see 7 RTM_NEWLINK messages for its TAP interface, with the following IFF flags.

1002 - not UP, not RUNNING
1002 - same again
1003 - UP, not RUNNING
11043 - UP, RUNNING, + whatever 10000 means
11043 - again
11043 - again
11043 - again

For reference, the individual flag values, that are relevant here, are:

1 - UP
2 - has a broadcast address
40 - RUNNING
1000 - multicast-capable
10000 - unknown

So one partial win would be to only trigger Felix processing when UP and RUNNING are both set.  However, that still leaves us with 4 occurrences of the 11043 state.
Thoughts?

If it helps, note that the 4 occurrences of the 11043 state happen very quickly.  For the last VM that I launched, the first 3 occurrences happened so quickly that UpdateSplitter didn't get to run until there were 3 copies of the on_interface_update message on its queue.
It appears, though, that the UpdateSplitter or LocalEndpoint receivers don't usefully coalesce their processing of the on_interface_update message - so it probably doesn't help that those netlink occurrences happen so quickly after each other, as the code currently stands.

I've posted a fix for this at #792


Refactor Feature Agent API

The current API is a bit bloated. We should simplify the API with minimalistic approach.

Closed by #96


UI throwing insufficient gas even with suffice balance

Describe the bug
A clear and concise description of what the bug is.
To Reproduce
Steps to reproduce the behavior:

Go to '...'

https://cdn.discordapp.com/attachments/909827059373723648/1070246713455030322/Screenshot_2023-02-01_at_1.08.02_PM.png
Additional context
Add any other context about the problem here.

@pjenviri Thanks for quick fix 🙏


panicked at 'Could not figure out an ID in create

Bug description
This schema was produced by npx prisma db pull & npx prisma generate
This same type of schema works on another table that was generated by npx

thread 'tokio-runtime-worker' panicked at 'Could not figure out an ID in create', query-engine\connectors\sql-query-connector\src\database\operations\write.rs:73:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
PrismaClientUnknownRequestError:
Invalid `prisma.user_clock.create()` invocation:

How to reproduce

Expected behavior
Just insert row into table
Prisma information
model user_role {
  id      Int  @id @unique(map: "iduser_role_UNIQUE") @default(dbgenerated("(concat(`user_id`,`role_id`))"))
  user_id Int
  role_id Int
  role    role @relation(fields: [role_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "role_id")
  user    user @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "user_id")

  @@index([role_id], map: "role_idx")
  @@index([user_id], map: "user_idx")
}

model user_clock {
  id        Int       @id @unique(map: "id_UNIQUE") @default(dbgenerated("(concat(`user_id`,`event_id`))"))
  clock_in  DateTime  @db.DateTime(0)
  clock_out DateTime? @db.DateTime(0)
  user_id   Int
  event_id  Int
  event     event     @relation(fields: [event_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "event_id_clock")
  user      user      @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "user_id_clock")

  @@index([event_id], map: "event_id_clock_idx")
  @@index([user_id], map: "user_id_clock_idx")
}

Environment & setup

OS:
Database:
Node.js version:

Prisma Version
3.0.2

Can you please also share user, role and event model so that we can try to reproduce this?

Sure no problem

Here it is in a code ready format
`model event {
id                         Int                         @id @unique(map: "idevents_UNIQUE") @default(autoincrement())
name                       String                      @db.VarChar(255)
location_address           String                      @db.VarChar(255)
location_name              String                      @db.VarChar(255)
number_of_leads_needed     Int
number_of_non_leads_needed Int
start_time                 DateTime                    @db.DateTime(0)
end_time                   DateTime                    @db.DateTime(0)
notes                      String?                     @db.VarChar(255)
show_event                 Boolean                     @default(false)
user_clock                 user_clock[]
user_registered_for_event  user_registered_for_event[]
}
model role {
id        Int         @id @unique(map: "idroles_UNIQUE") @default(autoincrement())
name      String      @unique(map: "name_UNIQUE") @db.VarChar(255)
user_role user_role[]
}
model user {
id                        Int                         @id @unique(map: "id_UNIQUE") @default(autoincrement())
user_name                 String                      @db.VarChar(255)
user_email                String                      @unique(map: "user_email_UNIQUE") @db.VarChar(255)
user_clock                user_clock[]
user_registered_for_event user_registered_for_event[]
user_role                 user_role[]
}`

I edited your comment so the schema is formatted as code properly. Look at the edit of the comment to learn how I did it @ZacharyToney (triple backtick).

Ah got it like slack cool my bad

If anyone makes it back to this thread from a search or just has a table setup not compatible with a prisma function temporarily I just used their raw query function make sure to sanitize your inputs and currently as of the time of this writing
their documentation says it doesn't support mongodb connector for it
Documentation for Raw Query
Again always good to sanitize your inputs :) and avoid SQL injection risks
use cases include in their docs for this type of solution include:

Use cases for raw SQL include:

You are a strong SQL user and want to run a heavily optimized query
Prisma Client does not yet support the feature that you require (please consider raising an issue)

examples if you wanted to use this approach:
await prisma.$queryRawUnsafe(`your sql query here probably using string interpolation`)
or
await prisma.$queryRaw(`your sql query here probably using string interpolation`)
Hopefully this gets fixed/solution better then this for but if not just wanted to leave this here in case anyone was at a road block although I'm sure they came to the same temporary solution.

Did you figure out what the actual problem was? (Can you maybe also share your create() call as text here instead of as a screenshot? Thanks)

I am going to close this as its been 3 months now and there is no activity here.
Please reply if you want us to take another look.


Latest version?

The "latest" version according to github is still r23.02.14, but I see a tag for  r23.12.04 also exists. Is that a pre-release or is there some other reason why it is not marked as "latest" release?

Net setting it as latest was accidental. Fixed.


source config directly and don't mess with XDG

Pull Request Test Coverage Report for Build<PHONE_NUMBER>

0 of 0   changed or added relevant lines in 0 files are covered.
No unchanged relevant lines lost coverage.
Overall coverage remained the same at 0.0%

Totals

Change from base Build<PHONE_NUMBER>:
0.0%

Covered Lines:
0

Relevant Lines:
0

💛  - Coveralls


Transformation is applied even if all options are set to false

with following options with 0.11.1 -
compact: false,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
debugProtectionInterval: false,
disableConsoleOutput: false,
log: false,
mangle: false,
renameGlobals: false,
rotateStringArray: false,
selfDefending: false,
stringArray: false,
unicodeEscapeSequence: false

my game is still changed and gets stuck at asset loading. Game is made using cocos creator 1.6.1

Transformation is applied even if all options are set to false

This is correct behaviour. Many base transformations always will applied.

ok what are the base transformations? i am trying to figure out what is causing issue in the game

This transformers:
https://github.com/javascript-obfuscator/javascript-obfuscator/tree/master/src/node-transformers/obfuscating-transformers
and this:
https://github.com/javascript-obfuscator/javascript-obfuscator/tree/master/src/node-transformers/converting-transformers

What error you got?

thats the issue, there is no error in console, however game gets stuck while loading game scene. Seems to be due to some base transform.

This is very strange. Debug it.

sure, will keep you updated

now we do see an error
project.dev.js:2620 TypeError: this._super is not a function
at CCClass.initialize (project.dev.js:1062)
at CCClass._initTriggers (project.dev.js:8811)
at CCClass.initialize (project.dev.js:8806)
at CCClass.initializeEpisode (project.dev.js:8872)
at CCClass. (project.dev.js:2616)
at _0x3371e1 (project.dev.js:11098)
at project.dev.js:11178
at _0x31350f (project.dev.js:11269)
at _0x3b72da (project.dev.js:11303)

Can you reproduce this error with more smaller amount of code? Or upload your unobfuscated and obfuscated code and i'll try to inspect code.

sure i ll try to narrow down the code which causes this

Ping

sorry but i dint get time to do that, this will most likely happen in a few months.

Ok, i close this issue, please make another one when you will ready.


unsuccessful job

I'm using ruby on rails and integrated workerholic with activejob and ran workerholic in project root folder as suggested in doc. but in workerholic log it shown:

ERROR: Failed: your job from class NewsletterJob was unsuccessful because of the folloing error: undefined method constantize' for NewsletterJob:Class Did you mean?  constants => undefined method constantize' for NewsletterJob:Class
Did you mean?  constants. Retrying in 10 secs...

Thanks for the reply.


DriverIOError in centerlines_to_shape notebook

Has something to do with the output directory, but could not find a quick fix.

The folder needs to be created if not there. I have a fix will commit later


Implement FromStr for DeriveColumn

Adds an implementation for enums which derive DeriveColumn making them import std::str::FromStr.
The FromStr supports the column names as snake_case and mixedCase.
#[derive(DeriveColumn)]
pub enum Column {
    Id,
    Name,
    CakeId,
}

Column::from_str("id");       // Ok(Column::Id)
Column::from_str("name");     // Ok(Column::Name)
Column::from_str("cake_id");  // Ok(Column::CakeId)
Column::from_str("cakeId");   // Ok(Column::CakeId)
Column::from_str("non-existent column");  // Err(ParseColumnErr)

I've written a test for this in /src/entity/column.rs.

@billy1624 Thoughts?

Interesting! Can I know the use case of it? @Acidic9

In my case, it's useful with graphql for only selecting the columns specified by the users graphql query. This is the main use case, I'm not sure of many others.

So you parse GraphQL query and construct the SQL with SeaORM? Cool!

I think this could be useful to anyone wants to parse arbitrary string input into concrete column enum variant. Without the need to write a custom match or chained if statements.

Yeah exactly!
In my case I use it with:
let columns: Vec<_> = ctx
    .field()
    .selection_set()
    .filter_map(|selection| coupon::Column::from_str(selection.name()).ok())
    .collect();

columns
    .into_iter()
    .fold(Coupon::find().select_only(), |acc, col| acc.column(col))
    .all(db)
    .await?;

So it selects only columns from the graphql query.

This also makes the use case for #[derive(DeriveModel, DerivePartialModel)] very useful, so you can select a dynamic subset of columns, and use .into_model::<cake::PartialModel>() where PartialModel is every column wrapped in Option<T>.

This also makes the use case for #[derive(DeriveModel, DerivePartialModel)] very useful, so you can select a dynamic subset of columns, and use .into_model::<cake::PartialModel>() where PartialModel is every column wrapped in Option<T>.

For the column already wrapped in Option<T> will it be wrapped twice in PartialModel?

For the column already wrapped in Option<T> will it be wrapped twice in PartialModel?

I started implementing DerivePartialModel... and no I was thinking it will only be wrapped in one Option<T>, never Option<Option<T>>. I don't see any use in having the multi-layered option in this case.

For the column already wrapped in Option<T> will it be wrapped twice in PartialModel?

I started implementing DerivePartialModel... and no I was thinking it will only be wrapped in one Option<T>, never Option<Option<T>>. I don't see any use in having the multi-layered option in this case.

Yes, I was thinking the same thing.

I've made a PR for implementing PartialModel: https://github.com/SeaQL/sea-orm/pull/95

We might as well 'inherit' std::str::FromStr for ColumnTrait


Trigger the display before it is about to disappear, hideAfter is invalid, toast will always exist

Trigger the display before it is about to disappear, hideAfter is invalid, toast will always exist

@PandaPy Thanks for your feedback. Can you please be more specific how to reproduce the bug you describe and/or provide some code?

same problem, display again when disappear animating,toast will always exist

@brooksGetIt Thank you for the feedback. Will have a look into it.

@brooksGetIt @PandaPy The problem should be fixed with version 0.7.2


Polishing

Clean up, including structural changes to synthetic.py, docstrings, and pep8

Resolves #14


add new node and drag this new node will error:    Multiple widgets used the same GlobalKey

add new node and drag this new node will error:
Another exception was thrown: Multiple widgets used the same GlobalKey.
drag_and_drop.dart in example,
import 'dart:math' show Random;

import 'package:flutter/material.dart';
import 'package:flutter_fancy_tree_view/flutter_fancy_tree_view.dart';

import '../shared.dart' show watchAnimationDurationSetting;

//! Disclaimer: This example is very opinionated, it may not work for your usecase.

class Node {
  Node({
    required this.id,
    Iterable<Node>? children,
  }) : _children = <Node>[] {
    if (children == null) return;

    for (final Node child in children) {
      child._parent = this;
      _children.add(child);
    }
  }

  final int id;
  final List<Node> _children;

  Iterable<Node> get children => _children;
  bool get isLeaf => _children.isEmpty;

  Node? get parent => _parent;
  Node? _parent;

  int get index => _parent?._children.indexOf(this) ?? -1;

  void insertChild(int index, Node node) {
    // Adjust the index if necessary when dropping a node at the same parent
    if (node._parent == this && node.index < index) {
      index--;
    }

    // Ensure the node is removed from its previous parent and update it
    node
      .._parent?._children.remove(node)
      .._parent = this;

    _children.insert(index, node);
  }
}

extension on TreeDragAndDropDetails<Node> {
  /// Splits the target node's height in three and checks the vertical offset
  /// of the dragging node, applying the appropriate callback.
  T mapDropPosition<T>({
    required T Function() whenAbove,
    required T Function() whenInside,
    required T Function() whenBelow,
  }) {
    final double oneThirdOfTotalHeight = targetBounds.height * 0.3;
    final double pointerVerticalOffset = dropPosition.dy;

    if (pointerVerticalOffset < oneThirdOfTotalHeight) {
      return whenAbove();
    } else if (pointerVerticalOffset < oneThirdOfTotalHeight * 2) {
      return whenInside();
    } else {
      return whenBelow();
    }
  }
}

class DragAndDropTreeView extends StatefulWidget {
  const DragAndDropTreeView({super.key});

  @override
  State<DragAndDropTreeView> createState() => _DragAndDropTreeViewState();
}

class _DragAndDropTreeViewState extends State<DragAndDropTreeView> {
  late final Node root;
  late final TreeController<Node> treeController;

  @override
  void initState() {
    super.initState();
    root = Node(id: -1);
    populateExampleTree(root);

    treeController = TreeController<Node>(
      roots: root.children,
      childrenProvider: (Node node) => node.children,

      // The parentProvider is extremely important when automatically expanding
      // and collapsing tree nodes on hover, as the [TreeDragTarget] needs to
      // ensure that it doesn't collapse an ancestor of the dragging node as it
      // would be removed from the view stopping the drag updates and callbacks.
      //
      // When not provided, the [TreeController] would need to first locate the
      // target node in the tree and then check its ancestors, which could be
      // very expensive for deep trees.
      parentProvider: (Node node) => node.parent,
    );
  }

  @override
  void dispose() {
    treeController.dispose();
    super.dispose();
  }

  void onNodeAccepted(TreeDragAndDropDetails<Node> details) {
    Node? newParent;
    int newIndex = 0;

    details.mapDropPosition(
      whenAbove: () {
        // Insert the dragged node as the previous sibling of the target node.
        newParent = details.targetNode.parent;
        newIndex = details.targetNode.index;
      },
      whenInside: () {
        // Insert the dragged node as the last child of the target node.
        newParent = details.targetNode;
        newIndex = details.targetNode.children.length;

        // Ensure that the dragged node is visible after reordering.
        treeController.setExpansionState(details.targetNode, true);
      },
      whenBelow: () {
        // Insert the dragged node as the next sibling of the target node.
        newParent = details.targetNode.parent;
        newIndex = details.targetNode.index + 1;
      },
    );

    (newParent ?? root).insertChild(newIndex, details.draggedNode);

    // Rebuild the tree to show the reordered node in its new vicinity.
    treeController.rebuild();
  }

  Duration? longPressDelay;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    longPressDelay = switch (Theme.of(context).platform) {
      TargetPlatform.android || TargetPlatform.fuchsia || TargetPlatform.iOS => Durations.long2,
      TargetPlatform.linux || TargetPlatform.macOS || TargetPlatform.windows => null,
    };
  }

  @override
  Widget build(BuildContext context) {
    final IndentGuide indentGuide = DefaultIndentGuide.of(context);
    final BorderSide borderSide = BorderSide(
      color: Theme.of(context).colorScheme.outline,
      width: indentGuide is AbstractLineGuide ? indentGuide.thickness : 2.0,
    );

    return AnimatedTreeView<Node>(
      treeController: treeController,
      nodeBuilder: (BuildContext context, TreeEntry<Node> entry) {
        return DragAndDropTreeTile(
          entry: entry,
          treeController: treeController,
          borderSide: borderSide,
          longPressDelay: longPressDelay,
          onNodeAccepted: onNodeAccepted,
          onFolderPressed: () => treeController.toggleExpansion(entry.node),
        );
      },
      duration: watchAnimationDurationSetting(context),
    );
  }
}

class DragAndDropTreeTile extends StatelessWidget {
  const DragAndDropTreeTile({
    super.key,
    required this.entry,
    required this.onNodeAccepted,
    required this.treeController,
    this.borderSide = BorderSide.none,
    this.longPressDelay,
    this.onFolderPressed,
  });

  final TreeEntry<Node> entry;
  final TreeDragTargetNodeAccepted<Node> onNodeAccepted;
  final BorderSide borderSide;
  final Duration? longPressDelay;
  final VoidCallback? onFolderPressed;
  final TreeController<Node> treeController;

  @override
  Widget build(BuildContext context) {
    return TreeDragTarget<Node>(
      node: entry.node,
      onNodeAccepted: onNodeAccepted,
      builder: (BuildContext context, TreeDragAndDropDetails<Node>? details) {
        Decoration? decoration;

        if (details != null) {
          // Add a border to indicate in which portion of the target's height
          // the dragging node will be inserted.
          decoration = BoxDecoration(
            border: details.mapDropPosition(
              whenAbove: () => Border(top: borderSide),
              whenInside: () => Border.fromBorderSide(borderSide),
              whenBelow: () => Border(bottom: borderSide),
            ),
          );
        }

        return TreeDraggable<Node>(
          node: entry.node,
          longPressDelay: longPressDelay,
          childWhenDragging: Opacity(
            opacity: .5,
            child: IgnorePointer(
              child: TreeTile(treeController: treeController, entry: entry),
            ),
          ),
          feedback: IntrinsicWidth(
            child: Material(
              elevation: 4,
              child: TreeTile(
                entry: entry,
                treeController: treeController,
                showIndentation: false,
                onFolderPressed: () {},
              ),
            ),
          ),
          child: TreeTile(
            entry: entry,
            treeController: treeController,
            onFolderPressed: entry.node.isLeaf ? null : onFolderPressed,
            decoration: decoration,
          ),
        );
      },
    );
  }
}

class TreeTile extends StatelessWidget {
  const TreeTile({
    super.key,
    required this.entry,
    required this.treeController,
    this.onFolderPressed,
    this.decoration,
    this.showIndentation = true,
  });

  final TreeEntry<Node> entry;
  final VoidCallback? onFolderPressed;
  final Decoration? decoration;
  final bool showIndentation;
  final TreeController<Node> treeController;

  @override
  Widget build(BuildContext context) {
    Widget content = Padding(
      padding: const EdgeInsetsDirectional.only(end: 8),
      child: Row(
        children: [
          FolderButton(
            isOpen: entry.node.isLeaf ? null : entry.isExpanded,
            onPressed: onFolderPressed,
          ),
          Expanded(
            child: Text('Node ${entry.node.id}'),
          ),
          IconButton(
            onPressed: () {
              entry.node._children.add(Node(id: _uniqueId++));
              treeController.rebuild();
            },
            icon: const Icon(Icons.add, size: 20),
          ),
        ],
      ),
    );

    if (decoration != null) {
      content = DecoratedBox(
        decoration: decoration!,
        child: content,
      );
    }

    if (showIndentation) {
      return TreeIndentation(
        entry: entry,
        child: content,
      );
    }

    return content;
  }
}

int _uniqueId = 0;
void populateExampleTree(
  Node node, [
  int level = 0,
  Random? rng,
  int minChildCount = 3,
]) {
  if (level >= 3) return;

  rng ??= Random();

  for (int index = 0; index <= minChildCount + rng.nextInt(3); ++index) {
    final child = Node(id: _uniqueId++).._parent = node;
    node._children.add(child);
    populateExampleTree(child, level + 1, rng, 1);
  }
}

I'm having the same problem

Solved it.
It's not a problem with the package but with how the new is created. You are (and I was too) forgetting to set the parent of the newly created node. Threfore the onNodeAccepted method didn't know where to remove the dragged item from which resulted in 2 instances of the item being present in the tree.
Try changing your add icon button from:
IconButton(
	onPressed: () {
	  entry.node._children.add(Node(id: _uniqueId++));
	  treeController.rebuild();
	},
	icon: const Icon(Icons.add, size: 20),
)

to
IconButton(
	onPressed: () {
	  entry.node._children.add(Node(id: _uniqueId++).._parent = entry.node);` //Set the parent here
	  treeController.rebuild();
	},
	icon: const Icon(Icons.add, size: 20),
)

tankss.


Spelling

This PR corrects misspellings identified by the check-spelling action.
The misspellings have been reported at https://github.com/jsoref/hocon-parser/commit/c70e8f2981149127ed67a7f1c720829adaaa8475#commitcomment-51803678
The action reports that the changes in this PR would make it happy: https://github.com/jsoref/hocon-parser/commit/7bc98a8e2456bbf4b39d83c9a337f8e3035ff44c
Note: this PR does not include the action. If you're interested in running a spell check on every PR and push, that can be offered separately.

The typos have been corrected. Thanks for the patch.


Only enforce placement constraints for new or replaced pods

When placement constraints are updated, existing pods should not be moved to obey the constraints.
If an existing pod is restarted, it should not be moved to obey the constraint.
If a pod is added, it should obey the new constraint.
If a pod is replaced it should obey the new constraint.

retest this please


Laying Limit Orders, Persistence Type MARKET_ON_CLOSE

In Flumine backtesting, I had a problem with Lay Limit Orders, using the persistence type MARKET_ON_CLOSE. When those bets got matched at SP, the existing logic could give me more liability than I wanted if the actual SP was above my limit price, as it did not reduce my size.
For example, suppose I had a lay for<EMAIL_ADDRESS>Suppose the actual SP is 100.0, and the order ends up taking the SP. Then the existing logic would match my £100@100, which is too much liability. The bet size should be scaled such that my liability has the same £10 (£100 * (1.1-1.0)).
I believe that if my limit order ends up taking the SP, the size will be scaled such that the liability remains the same.
Here is my suggested code change:

diff --git a/flumine/backtest/simulated.py b/flumine/backtest/simulated.py
index 3d66803..54e83fd 100644
--- a/flumine/backtest/simulated.py
+++ b/flumine/backtest/simulated.py
@@ -43,7 +43,7 @@ class Simulated:
         ):
             self._process_sp(market_book.publish_time_epoch, runner)
-        if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT and self.size_remaining:
+        elif self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT and self.size_remaining:
             # todo piq cancellations
             self._process_traded(
                 market_book.publish_time_epoch, runner_analytics.traded
@@ -184,7 +184,13 @@ class Simulated:
             self._bsp_reconciled = True
             _order_type = self.order.order_type
             if _order_type.ORDER_TYPE == OrderTypes.LIMIT:
-                size = self.size_remaining
+                if self.side=='BACK':
+                    size = self.size_remaining
+                else:
+                    remaining_risk = (_order_type.price-1.0) * self.size_remaining
+                    size = round(remaining_risk / (actual_sp-1.0), 2)
+                    # Not sure if this is strictly accurate
+                    self.size_cancelled += self.size_remaining-size
             elif _order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
                 if self.side == "BACK":
                     if actual_sp < _order_type.price:[](url)

This is my proposed update to simulated.py.
import logging
import datetime
from typing import List
from betfairlightweight.resources.bettingresources import MarketBook, RunnerBook

from .utils import (
    SimulatedPlaceResponse,
    SimulatedCancelResponse,
    SimulatedUpdateResponse,
)
from ..utils import get_price, wap
from ..order.ordertype import OrderTypes
from .. import config

logger = logging.getLogger(__name__)

class Simulated:
    """
    Class to hold `simulated` order
    matching and status.
    """

    def __init__(self, order):
        self.order = order
        self.size_matched = 0
        self.average_price_matched = 0
        self.matched = []  # [[publishTime, price, size]..]
        self.size_cancelled = 0.0
        self.size_lapsed = 0.0
        self.size_voided = 0.0
        self._piq = 0.0
        self._bsp_reconciled = False
        # todo handle limit lapsing

    def __call__(self, market_book: MarketBook, runner_analytics):
        # simulates order matching
        runner = self._get_runner(market_book)
        if (
            self._bsp_reconciled is False
            and market_book.bsp_reconciled
            and self.take_sp
        ):
            self._process_sp(market_book.publish_time_epoch, runner)

        elif self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT and self.size_remaining:
            # todo piq cancellations
            self._process_traded(
                market_book.publish_time_epoch, runner_analytics.traded
            )

    def place(
        self, client, market_book: MarketBook, instruction: dict, bet_id: int
    ) -> SimulatedPlaceResponse:
        # simulates placeOrder request->matching->response
        # todo instruction/fillkill/timeInForce etc
        if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
            runner = self._get_runner(market_book)
            available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01
            available_to_lay = get_price(runner.ex.available_to_lay, 0) or 1000
            price = self.order.order_type.price
            size = self.order.order_type.size
            if self.order.side == "BACK":
                if not client.best_price_execution and available_to_back > price:
                    return self._create_place_response(
                        bet_id,
                        status="FAILURE",
                        error_code="BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE",
                    )
                elif available_to_back >= price:
                    self._process_price_matched(
                        market_book.publish_time_epoch,
                        price,
                        size,
                        runner.ex.available_to_back,
                    )
                    return self._create_place_response(bet_id)
                available = runner.ex.available_to_lay
            else:
                if not client.best_price_execution and available_to_lay < price:
                    return self._create_place_response(
                        bet_id,
                        status="FAILURE",
                        error_code="BET_LAPSED_PRICE_IMPROVEMENT_TOO_LARGE",
                    )
                elif available_to_lay <= price:
                    self._process_price_matched(
                        market_book.publish_time_epoch,
                        price,
                        size,
                        runner.ex.available_to_lay,
                    )
                    return self._create_place_response(bet_id)
                available = runner.ex.available_to_back

            # calculate position in queue
            for avail in available:
                if avail["price"] == price:
                    self._piq = avail["size"]
                    break

            logger.debug(
                "Simulated order {0} PIQ: {1}".format(self.order.id, self._piq)
            )
            return self._create_place_response(bet_id)
        else:
            return self._create_place_response(bet_id)

    def _create_place_response(
        self, bet_id: int, status: str = "SUCCESS", error_code: str = None
    ) -> SimulatedPlaceResponse:
        return SimulatedPlaceResponse(
            status=status,
            order_status="EXECUTABLE",  # todo?
            bet_id=str(bet_id),
            average_price_matched=self.average_price_matched,
            size_matched=self.size_matched,
            placed_date=datetime.datetime.utcnow(),
            error_code=error_code,
        )

    def cancel(self) -> SimulatedCancelResponse:
        # simulates cancelOrder request->cancel->response
        if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
            self.size_cancelled = self.size_remaining
            return SimulatedCancelResponse(
                status="SUCCESS",  # todo handle errors
                size_cancelled=self.size_cancelled,
                cancelled_date=datetime.datetime.utcnow(),
            )
        else:
            return SimulatedCancelResponse(
                status="FAILURE", error_code="BET_ACTION_ERROR",  # todo ?
            )

    def update(self, instruction: dict):
        # simulates updateOrder request->update->response
        if (
            self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT
            and self.size_remaining > 0
        ):
            self.order.order_type.persistence_type = instruction.get(
                "newPersistenceType"
            )
            return SimulatedUpdateResponse(status="SUCCESS")
        else:
            return SimulatedCancelResponse(
                status="FAILURE", error_code="BET_ACTION_ERROR",
            )

    def _get_runner(self, market_book: MarketBook) -> RunnerBook:
        runner_dict = {
            (runner.selection_id, runner.handicap): runner
            for runner in market_book.runners
        }
        return runner_dict.get((self.order.selection_id, self.order.handicap))

    def _process_price_matched(
        self, publish_time: int, price: float, size: float, available: list
    ) -> None:
        # calculate matched on execution
        size_remaining = size
        for avail in available:
            if size_remaining == 0:
                break
            elif (self.side == "BACK" and price <= avail["price"]) or (
                self.side == "LAY" and price >= avail["price"]
            ):
                _size_remaining = size_remaining
                size_remaining = max(size_remaining - avail["size"], 0)
                if size_remaining == 0:
                    _size_matched = _size_remaining
                else:
                    _size_matched = avail["size"]
                _matched = [publish_time, avail["price"], round(_size_matched, 2)]
                self._update_matched(_matched)
            else:
                break

    def _process_sp(self, publish_time: int, runner: RunnerBook) -> None:
        # calculate matched on BSP reconciliation
        actual_sp = runner.sp.actual_sp
        if actual_sp:
            self._bsp_reconciled = True
            _order_type = self.order.order_type
            if _order_type.ORDER_TYPE == OrderTypes.LIMIT:
                if self.side=='BACK':
                    size = self.size_remaining
                else:
                    remaining_risk = (_order_type.price-1.0) * self.size_remaining
                    size = round(remaining_risk / (actual_sp-1.0), 2)
                    # Not sure if this is strictly accurate
                    self.size_cancelled += self.size_remaining-size
            elif _order_type.ORDER_TYPE == OrderTypes.LIMIT_ON_CLOSE:
                if self.side == "BACK":
                    if actual_sp < _order_type.price:
                        return
                    size = _order_type.liability
                else:
                    if actual_sp > _order_type.price:
                        return
                    size = round(_order_type.liability / (actual_sp - 1), 2)
            elif _order_type.ORDER_TYPE == OrderTypes.MARKET_ON_CLOSE:
                if self.side == "BACK":
                    size = _order_type.liability
                else:
                    size = round(_order_type.liability / (actual_sp - 1), 2)
            else:
                raise NotImplementedError()
            self._update_matched([publish_time, actual_sp, size])

    def _process_traded(self, publish_time: int, traded: dict) -> None:
        # calculate matched on MarketBook update
        price = self.order.order_type.price
        for traded_price, traded_size in traded.items():
            logger.debug(
                "Simulated order {0} traded: {1} - {2}".format(
                    self.order.id, traded_price, traded_size
                )
            )
            if self.side == "BACK" and traded_price >= price:
                self._calculate_process_traded(publish_time, traded_size)
            elif self.side == "LAY" and traded_price <= price:
                self._calculate_process_traded(publish_time, traded_size)

    def _calculate_process_traded(self, publish_time: int, traded_size: float) -> None:
        traded_size = traded_size / 2
        if self._piq - traded_size < 0:
            size = traded_size - self._piq
            size = round(min(self.size_remaining, size), 2)
            if size:
                self._update_matched(
                    [
                        publish_time,
                        self.order.order_type.price,
                        size,
                    ]  # todo takes the worst price, i.e what was asked
                )
            self._piq = 0
        else:
            self._piq -= traded_size
            logger.debug(
                "Simulated order {0} PIQ: {1}".format(self.order.id, self._piq)
            )

    @property
    def take_sp(self) -> bool:
        if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
            if self.order.order_type.persistence_type == "MARKET_ON_CLOSE":
                return True
            return False
        else:
            return True

    @property
    def side(self) -> str:
        return self.order.side

    def _update_matched(self, data: List) -> None:
        logger.debug("Simulated order {0} matched: {1}".format(self.order.id, data))
        self.matched.append(data)
        self.size_matched, self.average_price_matched = wap(self.matched)

    @property
    def size_remaining(self) -> float:
        if self.order.order_type.ORDER_TYPE == OrderTypes.LIMIT:
            return round(
                self.order.order_type.size
                - self.size_matched
                - self.size_cancelled
                - self.size_lapsed
                - self.size_voided,
                2,
            )
        else:
            return 0.0

    @property
    def profit(self) -> float:
        if self.order.runner_status == "WINNER":
            if self.side == "BACK":
                return round((self.average_price_matched - 1) * self.size_matched, 2)
            else:
                return round((self.average_price_matched - 1) * -self.size_matched, 2)
        elif self.order.runner_status == "LOSER":
            if self.side == "BACK":
                return -self.size_matched
            else:
                return self.size_matched
        else:
            return 0.0

    def __bool__(self):
        return config.simulated or self.order.trade.client.paper_trade

Can you put this in a PR?

Looks like the change breaks some tests. I will submit the PR when everything is working.
Should I do it on top of dev branch?

Branch off master, dev is really old I think?

I created the PR here: https://github.com/liampauling/flumine/commit/2d6eb834a4a7fc2d58370df023627ec88608dd42
Please let me know if there are any problems with it.


Fixed #32002 -- Added headers parameter to HttpResponse

Ticket
There are now two overlapping ways to set the Content-Type header. I think it makes sense to take content_type over headers, but not sure if we should check this in __init__() and raise an exception, document it, or perhaps neither is necessary.

Looks good!
I don't want to complicate things more in this pull request BUT have you considered adding support for this argument to TemplateResponse and TemplateSimpleResponse too? Those two classes use explicit forwarding of arguments, not *a, **kw. I think Carlton definitely has a point regarding not adding a headers parameter to the render() shortcut, but the template responses are a different case.

I'm not against it but I'll hold off for a second opinion.
Is there some reason those classes don't just pass *args, **kwargs through like the other subclasses do?

Not passing *args makes sense since it's allowed to not specify the context at all.
Regarding not passing **kwargs: I do not see a good reason for this.

There are now two overlapping ways to set the Content-Type header. I think it makes sense to take content_type over headers, but not sure if we should check this in init() and raise an exception, document it, or perhaps neither is necessary.

I think we should raise a ValueError that they're mutually exclusive, and maybe deprecate content_type.

I think we should leave content_type. Just as before, it's a handy shortcut for (probably) the most common header. Plus much code will be using it.

I think we should raise a ValueError that they're mutually exclusive...

Yes, 👍. If content_type is not None we should raise if Content-Type (case-insensitive) in headers.

I'd love to deprecate it, but I agree with your Carlton, I think it would cause too many unnecessary complaints for too little gain.
I will add the check though, and update those docs.
@felixxm, @carltongibson, what do you both think about adding **kwargs to TemplateResponse and SimpleTemplateResponse? It seems like a good idea to me.

@felixxm, @carltongibson, what do you both think about adding **kwargs to TemplateResponse and SimpleTemplateResponse? It seems like a good idea to me.

I see the case for adding headers here. I'd think changing to **kwargs would be out of scope for this PR (and something we'd want to think about... — it's not always the case that the broader API is better...)

Thanks both, addressed those and fixed an issue where content type wouldn't be set from the headers dict. Also moved the code closer together to make it more understandable.

Thanks @felixxm, I've resolved those and also added a test for the case I found yesterday.

Merged in 2e7cc95499f758a1c4aa036cbf1dcddf82a89ea2 and dcb69043d0de45bb55998fc418d93c28bc7689ae.


More properties in JFXChipView

JFoenix is such a great library. Here is a small advice of JFXChipView.
I just want to add a chip with text in the text area while chipview is not focused.
At first, I tried focusedProperty() of view itself, but the event was not triggered.
Then, I tried whether I can get TextArea of view. I tried everything and got nothing but only getAccessibleText(), which is definitely not what I want.
After that, I went through the source code of JFXChipView and found JFXChipViewSkin which is what I could work around. But view.getSkin().getChildren() was null.
At last, I finished my target with code as flow which is weird and not elegant:
@FXML
private JFXChipView<String> view;
...
JFXChipViewSkin<String> skin = new JFXChipViewSkin<>(view);
    view.setSkin(skin);
    TextArea textArea = (TextArea) ((FlowPane)skin.getChildren().get(0)).getChildren().get(0);
    textArea.focusedProperty().addListener((obs, oldValue, newValue) -> {
        if (!newValue) {
            if (StringUtils.isNotBlank(textArea.getText())) {
                view.getChips().add(textArea.getText());
                textArea.clear();
            }
        }
    });

The problems I found was:

focusedProperty() is not working.
TextArea or text in TextArea is not reachable with property in JFXChipView. To get them requests some hack trick.

THX~~

Hello @tomriddly , can you provide a SSCCE demonstrating the issue?

JDK:1.8.0_191
jfoenix:8.0.7
I make a simple scene with JFXChipView and a button. I hope to get the event when focus changed.
But the console will only output a true and a false at very beginning and no longer answer my click operation.

Besides, there is no such a function which can get text area of chipview.
test/test.fxml:
<?import com.jfoenix.controls.JFXChipView?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane prefHeight="262.0" prefWidth="298.0" style="-fx-background-color: black;" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="test.testController">
   <children>
      <JFXChipView fx:id="view" style="-fx-background-color: white;" />
      <Button layoutX="166.0" layoutY="73.0" mnemonicParsing="false" text="Button" />
   </children>
</AnchorPane>

test/testController.java:
package test;

import java.net.URL;
import java.util.ResourceBundle;

import com.jfoenix.controls.JFXChipView;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;

public class testController implements Initializable {
    @FXML
    JFXChipView<String> view;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        view.focusedProperty().addListener((obs, oldValue, newValue) -> {
            System.out.println(newValue);
        });
    }

}

test/Main.java
package test;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {

        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        AnchorPane mainPanel = FXMLLoader.load(getClass().getResource("/test/test.fxml"));
        Scene scene = new Scene(mainPanel, 1000, 400);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.show();
    }

}

The purpose can be achieved by unpacking children of JFXChipViewSkin. But I thought a offical method should be provided.

package test;

import java.net.URL;
import java.util.ResourceBundle;

import com.jfoenix.controls.JFXChipView;
import com.jfoenix.skins.JFXChipViewSkin;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextArea;
import javafx.scene.layout.FlowPane;
import org.apache.commons.lang.StringUtils;

public class testController implements Initializable {
    @FXML
    JFXChipView<String> view;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        JFXChipViewSkin<String> skin = new JFXChipViewSkin<>(view);
        view.setSkin(skin);
        TextArea textArea = (TextArea) ((FlowPane)skin.getChildren().get(0)).getChildren().get(0);
        textArea.focusedProperty().addListener((obs, oldValue, newValue) -> {
            System.out.println(newValue);
            if (!newValue) {
                if (StringUtils.isNotBlank(textArea.getText())) {
                    view.getChips().add(textArea.getText());
                    textArea.clear();
                }
            }
        });
    }

}

Hello @tomriddly,
this issue has been fixed recently. However there is no build that includes this fix yet. it will be included in the next build, hopefully by the end of this month.
Regards,


Add answerphone :D

Many people use the main account and twinks for farming. I propose to make an autoresponder for private messages.
If user, by writing to the twink is not MasterId Account ,that corresponds to an answering machine.
It is desirable that the message would be inserted link and Cyrillic support.

That's not the purpose of ASF.


Open dialog filter does not except a file extension of tar.gz

VSCode Version: 1.39
OS Version: mac

Steps to Reproduce:

set the open dialog options filter property to include tar.gz
run the extension code and try and select a tar.gz file. It will be greyed out

Does this issue occur when all extensions are disabled?: Yes/No

/needsMoreInfo
What are you doing? Is this a problem when authoring an extension? Iff so, please attach a sample. In all cases, clarify this issue

This is when authoring an extension
 const openDialogOptions: vscode.OpenDialogOptions = {
        canSelectFiles: true,
        canSelectFolders: false,
        canSelectMany: false,
        openLabel: 'Select',
        filters: { Packages: ['tar.gz', 'tgz'] }
    };

 const fileBrowser: vscode.Uri[] = await vscode.window.showOpenDialog(openDialogOptions);

It seems that you need to use gz and not tar.gz. Since we only use an electron API for that, pretty please to file the this issue with them. Thanks


Update ONNX download links

Summary of Changes

Update links for ONNX models

Motivation
The ONNX repository is updating its models and layouts (see this announcement). This updates the links to a fixed older commit which maintains the files in the original locations.
Implementation
The chosen commit hash is the last hash before the ONNX repository moved files.
Notes
N/A

Build failed!

Build failed!

The MIGraphX tests continue to fail due to AMDMIGraphX#2208.


SAX doesn't fire the characters method when a node is empty

I'm trying to parse a large xml file using SAX parser. When the parser reaches a node which is empty, the characters method doesn't fire. Here is an example...
Here is the sample xml document.
<?xml version="1.0" encoding="UTF-8"?>
<root>
	<ISA type="array">
		<ISA>
			<I02>
				<name>Information1</name>
				<value>
					<raw>00</raw>
					<description></description>
				</value>
			</I02>
			<I02>
				<name>Information2</name>
				<value>
					<raw></raw>
					<description nil="true"/>
				</value>
			</I02>
		</ISA>
	</ISA>
</root>

Here is the sample script
require 'nokogiri'

class Parser < Nokogiri::XML::SAX::Document
  def initialize
    @count=1
  end
  def start_element(name, attrs = [])
    puts name
  end
  def characters(string)
    string.strip!
    puts "#{@count} #{string}"
    @count += 1
  end
  def end_element(name)
    puts name
  end
end

Nokogiri::XML::SAX::Parser.new(Parser.new).parse(File.open('sax_example3.xml'))

I had to use SAX because the file has 6.5 million lines.
What I'm trying to do is gather all the name values and then raw values into separate arrays and later I can zip both arrays to get key value pairs.
Am I approaching this the correct way? Is there any other way to do this?
Edit:
What I expected
array1 = ["Information1","Information2"]
array2 = ["00", ""]
All name values are assigned to array1 and raw values to array2 like shown above.
What I'm getting
array1 = ["Information1","Information2"]
array2 = ["00"]
array2 doesn't have the same number of elements as array1, which means there is no way to map names to array. The reason for this I think is that the characters method is not called if the node is empty.
Here is the output of the above program (Edited the above script and added line numbers)
root
1
ISA
2
ISA
3
I02
4
name
5 Information1
name
6
value
7
raw
8 00
raw
9
description
description
10
value
11
I02
12
I02
13
name
14 Information2
name
15
value
16
raw
raw
17
description
description
18
value
19
I02
20
ISA
21
ISA
22
root

As you can see between line 9 & 10, 16 & 17, 17 & 18 the start_element & end_element is executed but the characters method isn't.

@Vizkrig Thank you for submitting this! I'll try to take a look in the next day or so -- apologies for the slow response, it's been a nutty week.

+1

Sorry for the slow response. I'm working through the v1.11.0 milestone so I may not get to it soon, unfortunately. If anyone wants to investigate I'd be very grateful.

thanks for feedback. i see same issue using jruby.
maybe you can help me with starting point, what place should i check first for investigation?

@Vizkrig @YegorZdanovich got a few minutes to look into this this morning, and here's what I found.
TL;DR
The behavior you're describing, which is that characters callbacks aren't invoked for empty tags, is expected. Fortunately we can work around this using start_element and end_element.
Behavior of the underlying library
Nokogiri is a wrapper around a lower-level parser (libxml2 on CRuby, Xerces on JRuby). Let's step outside Nokogiri for a minute and check what libxml2's behavior is.
Helpfully, libxml2 provides a utility called testSAX that emits all the callbacks (here are some docs).
Here's the full output from testSAX when run against the XML provided by the OP:
SAX.setDocumentLocator()
SAX.startDocument()
SAX.startElement(root)
SAX.characters(
	, 2)
SAX.startElement(ISA, type='array')
SAX.characters(
		, 3)
SAX.startElement(ISA)
SAX.characters(
			, 4)
SAX.startElement(I02)
SAX.characters(
				, 5)
SAX.startElement(name)
SAX.characters(Information1, 12)
SAX.endElement(name)
SAX.characters(
				, 5)
SAX.startElement(value)
SAX.characters(
					, 6)
SAX.startElement(raw)
SAX.characters(00, 2)
SAX.endElement(raw)
SAX.characters(
					, 6)
SAX.startElement(description)
SAX.endElement(description)
SAX.characters(
				, 5)
SAX.endElement(value)
SAX.characters(
			, 4)
SAX.endElement(I02)
SAX.characters(
			, 4)
SAX.startElement(I02)
SAX.characters(
				, 5)
SAX.startElement(name)
SAX.characters(Information2, 12)
SAX.endElement(name)
SAX.characters(
				, 5)
SAX.startElement(value)
SAX.characters(
					, 6)
SAX.startElement(raw)
SAX.endElement(raw)
SAX.characters(
					, 6)
SAX.startElement(description, nil='true')
SAX.endElement(description)
SAX.characters(
				, 5)
SAX.endElement(value)
SAX.characters(
			, 4)
SAX.endElement(I02)
SAX.characters(
		, 3)
SAX.endElement(ISA)
SAX.characters(
	, 2)
SAX.endElement(ISA)
SAX.characters(
, 1)
SAX.endElement(root)
SAX.endDocument()

The important bits, which are the raw elements:
...
SAX.startElement(raw)
SAX.characters(00, 2)
SAX.endElement(raw)
...
SAX.startElement(raw)
SAX.endElement(raw)

You can hopefully see that libxml2's SAX parser itself doesn't invoke the characters callback for empty nodes. This intuitively makes sense to me, since there aren't any characters.
What does the API spec say?
I then went to the SAX API docs and, although it doesn't explicitly say that "this callback won't be invoked if there isn't any character data", it seems pretty straightforward to me that the callback is intended to only be called when there is character data: http://www.saxproject.org/apidoc/org/xml/sax/ContentHandler.html#characters(char[], int, int)
How can the OP solve the original problem?
Something like this, where we use start_element and end_element to indicate presence of the <raw> tag, and either collect characters data if it's there, or else default to the empty string.
#! /usr/bin/env ruby

require "nokogiri"

xml = <<~EOF
  <?xml version="1.0" encoding="UTF-8"?>
  <root>
  	<ISA type="array">
  		<ISA>
  			<I02>
  				<name>Information1</name>
  				<value>
  					<raw>00</raw>
  					<description></description>
  				</value>
  			</I02>
  			<I02>
  				<name>Information2</name>
  				<value>
  					<raw></raw>
  					<description nil="true"/>
  				</value>
  			</I02>
  		</ISA>
  	</ISA>
  </root>
EOF

# parser to collect tuples of `name` and `raw` contents
class Parser < Nokogiri::XML::SAX::Document
  attr :tuples
  COLLECT_NOTHING = 1, COLLECT_NAME = 2, COLLECT_RAW = 3

  def initialize
    @tuples = []
    reset_state
  end

  def reset_state
    @state = COLLECT_NOTHING
    @current_name = ""
    @current_raw = ""
  end

  def start_element(name, attrs = [])
    @state = case name
      when "name" then COLLECT_NAME
      when "raw" then COLLECT_RAW
      else COLLECT_NOTHING
      end
  end

  def characters(string)
    case @state
    when COLLECT_NAME
      @current_name = string
    when COLLECT_RAW
      @current_raw = string
    end
  end

  def end_element(name)
    case name
    when "name"
      @state = COLLECT_NOTHING
    when "raw"
      @tuples << [@current_name, @current_raw]
      reset_state
    end
  end
end

p = Parser.new
Nokogiri::XML::SAX::Parser.new(p).parse(xml)
p.tuples # => [["Information1", "00"], ["Information2", ""]]

I hope this helps?

@flavorjones thanks for quick response.


python38Packages.mautrix: 0.10.6 -> 0.10.9

Automatic update generated by nixpkgs-update tools. This update was made based on information from passthru.updateScript.
meta.description for python38Packages.mautrix is: "A Python 3 asyncio Matrix framework."
meta.homepage for python38Packages.mautrix is: "https://github.com/tulir/mautrix-python"
meta.changelog for python38Packages.mautrix is: ""
Updates performed

Ran passthru.UpdateScript

To inspect upstream changes
Impact

Checks done (click to expand)

built on NixOS

The tests defined in passthru.tests, if any, passed

0 of 0 passed binary check by having a zero exit code.

0 of 0 passed binary check by having the new version present in output.

found 0.10.9 with grep in /nix/store/5qmx2vkrxknn83sswvmmzk6xw1yaqpv9-python3.8-mautrix-0.10.9

found 0.10.9 in filename of file in /nix/store/5qmx2vkrxknn83sswvmmzk6xw1yaqpv9-python3.8-mautrix-0.10.9

directory tree listing: https://gist.github.com/f535f4383193545ea2635a6baab7fc16

du listing: https://gist.github.com/a6777da1615b25b6dbb80bfc90ba50e5

Rebuild report (if merged into master) (click to expand)

29 total rebuild path(s)

8 package rebuild(s)

8 x86_64-linux rebuild(s)
8 i686-linux rebuild(s)
5 x86_64-darwin rebuild(s)
8 aarch64-linux rebuild(s)

First fifty rebuilds by attrpath
heisenbridge
mautrix-facebook
mautrix-signal
mautrix-telegram
python38Packages.mautrix
python38Packages.mautrix-appservice
python39Packages.mautrix
python39Packages.mautrix-appservice

Instructions to test this update (click to expand)

Either download from Cachix:
nix-store -r /nix/store/5qmx2vkrxknn83sswvmmzk6xw1yaqpv9-python3.8-mautrix-0.10.9 \
  --option binary-caches 'https://cache.nixos.org/ https://nix-community.cachix.org/' \
  --option trusted-public-keys '
  nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
  cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
  '

(The Cachix cache is only trusted for this store-path realization.)
For the Cachix download to work, your user must be in the trusted-users list or you can use sudo since root is effectively trusted.
Or, build yourself:
nix-build -A python38Packages.mautrix https://github.com/r-ryantm/nixpkgs/archive/c19924ffb2aa4277fe52fdc55d04301ac93b765d.tar.gz

After you've downloaded or built it, look at the files and if there are any, run the binaries:
ls -la /nix/store/5qmx2vkrxknn83sswvmmzk6xw1yaqpv9-python3.8-mautrix-0.10.9
ls -la /nix/store/5qmx2vkrxknn83sswvmmzk6xw1yaqpv9-python3.8-mautrix-0.10.9/bin

Pre-merge build results
We have automatically built all packages that will get rebuilt due to
this change.
This gives evidence on whether the upgrade will break dependent packages.
Note sometimes packages show up as failed to build independent of the
change, simply because they are already broken on the target branch.
Result of nixpkgs-review run on x86_64-linux 1

6 packages built:

heisenbridge
mautrix-facebook
mautrix-signal
mautrix-telegram
python38Packages.mautrix
python39Packages.mautrix

Maintainer pings
cc @nyanloutre @ma27 for testing.

closing in favor of #139178 and #140234


Update Case_study.md

Tiny change in NAS wording in Server Config section

Merging this update, many thanks.


Added more docs for forcing. Closes #4581

Turns out I think the docs are all pretty good once I read them :)

Can one of the admins verify this patch?

Can one of the admins verify this patch?

Can one of the admins verify this patch?

@solarkennedy Thanks for reading the docs :)


AutoPr-Billing-asarkar84-REST Spec PrNumber 5727

@wilcobmsft could you take a look at the auto generated sdk


adding GOMEMLIMIT to helm chart for improvising OOM

What type of PR is this?
Improvement
What this PR does / why we need it:
Adding GOMEMLIMIT to helm chart for improvising OOM situations
It enhances reliability by making the garbage collector more aggressive when memory is scarce, preventing OOM situations.
For more info:
Reference #2337
Special notes for your reviewer:
┆Issue is synchronized with this Jira Task by Unito

hey @nitishchauhan0022!  We actually meant to close this PR as we are trying to migrate our deployments to operators so we put this functionality in the operator.
Thank you for your contribution but we will go ahead and close this PR and the issue.


Is it possible to exclude some columns from being considered by versioning?

I have a table containing meta-information about network devices. One such information is a timestamp when the device was last seen on the network. This value is updated regularly, and as such would trigger a new history entry every time the device is polled even if nothing else has changed.
At the moment I am working on implementing a history of changes to these devices and came across temporal tables which does exactly what I want.
Unfortunately the issue explained above will create plenty of useless revisions.
I could move the column from the DB into a separate table and reference it, but that would become quite cumbersome as the value is used as a filter in almost all application query.
It would help a lot if I could simply ignore that column from the versioning process.

I think you can mimic this behavior using WHEN clause. However, you'd need to check OLD.col vs NEW.col for those cols you would want the trigger to fire rather than excluding those you are not interested in.

Works for me. Perhaps it can be closed. One would need 2 separate triggers: for insert & delete and another one for updates.
On a side note, it would be better to place last seen in a separate table.

I agree. There are workarounds and this can be closed.


Integrate labeled storage into snapfaas and functions

Integration requires

implements RPC syscalls in lanuage entry points and protobuf data structures for the storage's APIs.
Have snapfaas correctly handles these syscalls
Have functions call these syscalls.

#25


Content Update: Clarify IRCC contact schedule/parameters on the confirmation page

This issue addresses 2 interventions from content testing:

Users think there is a possibility that IRCC will contact them for more information close to the final contact date.
Users worry that IRCC will contact them for more information 90 days from now and they will have to scramble or go through the whole process again.

Users worry that the final contact date is past one the dates that they select.
Users worry about how much notice they will receive, or if they will receive their notices after the dates of their test.

Before: By [DATE], your local IRCC office will send you a new appointment, or email you to ask for more information.
After: By [DATE], we'll send you a new appointment. You will always be contacted at least 3 weeks before your appointment.
Note: Decision to remove mention of the "we'll might ask you for more information" was based on the likelihood of that happening (in practice, it's uncommon, and increasingly so). It also contributes to the confusion in insight 1 - that IRCC may contact clients for more information very late in the waiting period, and require clients to choose dates again.
cc @anikbrazeau for French translation

@pcraig3 OK so we're ready to move ahead with this. Just for some context, we want to do some testing of 2 versions of this: With, and Without, the "By date" bit. Let's chat more about how that's going to work with Hillary.

This issue has been resolved since #309 and #317 went in.


deriving additional addresses violates the unused address gap limit

i got the same problem..

@prettyCoders the second param is 'gappolicy', set it to 'ignore' will solve the problem

please refer to hcomniEngine rpc code
https://github.com/HcashOrg/hcOmniEngine/blob/f1acc2ba3640a8e1c651ddc90a86d569d00704fe/rpcclient.py#L33


hide status bar item when no CODEOWNERS file

If:

you have a multi-root workspace
one workspace has CODEOWNERS, the other doesn't

Then when you switch to the one that doesn't, the status bar item will
remain and give incorrect information.
Fix this by hiding the status bar item when there is no CODEOWNERS file
nearby.
Before

After

Hey @jasonnutter, I'm thinking I might just fork this package - I'm excited about building out this package more, and I don't mind doing the extra maintenance work. Would you mind especially?

Whoa sorry I somehow totally missed these PRs, apologies about that! I'll take a look shortly.
You certainly don't need my permission to fork, but I'd also be happy to add you as a contributor.

Ah, no worries then! Sorry, I thought you had seen my commit but weren't interested in maintaining this extension.
I'm fine with still merging into this package - best to have only one package to choose from.


'drive list file' prints wrong name

How to reproduce:
$ echo foo | drive push -piped a/b.txt
$ drive list a/b.txt
/a/b.txt/b.txt

/a/b.txt is expected

Yap I noticed that too.

Addressed by https://github.com/odeke-em/drive/commit/a1255dc850b05e6e41eecb709aa689977d19bb25. Reopen if persists.

according to the drive_test.py
echo "foobar" | drive push -piped a/b/c.txt
drive list -no-prompt a/b/c.txt
[expected] ['/a/b/c.txt']
[actual] ['/c.txt']
failed

Reopening this.

Take a look at https://github.com/odeke-em/drive/commit/e667aa254efcbcd240c3341e558015ef5ffeb6ad


Feature Request: string shortening

It would be great if we could have some prop like shortenTextAfterLength to shorten long JSON strings after a desired length:
my not very long long string example

providing shortenTextAfterLength to 10 would render
my not very...

with the ability for users to expand/collapse the string. This would give a much better view for JSON objects that might have strings that are 1000+ characters long

@CamdenFoucht Upgrade v1.9.0
https://github.com/uiwjs/react-json-view/blob/9c03a58d97b070c6f0d315d0e3e4ca79e1074e23/core/src/index.tsx#L43-L44

Wow that was fast! Thank you, that is perfect!


feat: event doesn't return CleanListenerCallback

issue 132 done.

@VPringle97 you need to point to dev