Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for setting/getting Date objects #89

Closed
wants to merge 19 commits into from

Conversation

sbencoding
Copy link

Implementing a new feature described in issue sindresorhus/electron-store#18.
Type information is stored under ${INTERNAL_KEY}.type as suggested, the get method checks if there's a type key present, if there's it converts the value to the type before returning it.
The set methods checks if the value passed in is an instance of Date, if it is it stores it as an object like:

key: {
    __internal__.type: 'Date',
    val: valuePassedIn
}

I've also added a test to test.js to test for the setting and getting Date objects
Let me know if there's anything I could change. For example I chose to include type information only for Date objects, but it might be better to store type information for every key-value pair to be more consistent, but it would also make the object and the file larger.

@sbencoding
Copy link
Author

The travis CI tests/builds are passing, but node v12 on Windows failed to download node.js v12, so the code should be fine I think.

@sindresorhus
Copy link
Owner

This needs to be documented in the readme and index.d.ts. Also make sure that $ npm test passes locally.

@sbencoding
Copy link
Author

Hi, sorry I've been busy yesterday, but here are the modifications.
I've updated the documentation in readme.md and in index.d.ts, I've added typescript tests for the Date serialization.
I've also refactored the code a bit, to handle Date objects within nested objects, moved serialization to the getter and setter directly instead of the user facing get and set method.
Adding new types/objects is also easier now, because the necessary information is stored in an array, instead of writing an if condition for every new type.
Let me know what you think

index.js Outdated
this.extraTypes = [
{
name: 'Date',
instance: Date,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a function too. We should not assume all future types can be detected through an instance check.

index.js Outdated
name: 'Date',
instance: Date,
convertFrom: val => val.getTime(),
convertTo: val => new Date(val)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val => value (everywhere)

index.js Outdated
stack.push(value);
while (stack.length > 0) {
const current = stack.pop();
Object.keys(current).forEach(currentItemKey => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Object.entries() and for-of loop.

readme.md Outdated
#### .set(key, value)

Set an item.

The `value` must be JSON serializable. Trying to set the type `undefined`, `function`, or `symbol` will result in a TypeError.
The `value` must be JSON serializable. Trying to set the type `undefined`, `function`, or `symbol` will result in a TypeError.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do unrelated changes.

t.context.config.set('dateobj', {str: 'this is a string', date: new Date()});
const myDate = t.context.config.get('dateobj').date;
t.true(myDate instanceof Date);
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a lot more tests for this feature, including ensuring it handles circular objects, objects inside arrays inside objects (and circular for that too), and other things you can think of.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added more tests, but I'm not sure how to go about circular objects, becuase Conf.serialize, which is JSON.stringify by default, throws an exception is there's a circulation. So all I did for now is to detect if there's a circulation and stop handling that branch of the object, because it would cause an infinite loop, and cause the application to crash, but when Conf gets to serializing the object with JSON it still throws an error. I've also tested this behavior with string so it's not a problem with how the new feature handles extra types.

index.d.ts Outdated
interface ExtraType<T> {
name: string,
instance: T,
convertFrom: (val: T) => string | number,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

index.d.ts Outdated
@@ -248,7 +256,7 @@ declare class Conf<T = any> implements Iterable<[keyof T, T[keyof T]]> {
Set an item.

@param key - You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties.
@param value - Must be JSON serializable. Trying to set the type `undefined`, `function`, or `symbol` will result in a `TypeError`.
@param value - Must be JSON serializable. Trying to set the type `undefined`, `function`, or `symbol` will result in a `TypeError`. Additionally Conf automatically seralizes and deserializes `Date` objects
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo

index.d.ts Outdated
@@ -211,6 +218,7 @@ declare class Conf<T = any> implements Iterable<[keyof T, T[keyof T]]> {
store: T;
readonly path: string;
readonly size: number;
readonly extraTypes: Conf.ExtraType<any>[];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be any.

// Sub-object handling of extra types
const stack = [];
stack.push(value);
while (stack.length > 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using a while loop?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in case of nested objects I want to reuse the same logic to serialize Date types, so there are two ways I can go about this. The first one is to use recursion, the other one is to use the iterative solution, which is the while loop.
I've decided to use the iterative solution over the recursive solution, because with really deeply nested objects, there's a possibility that the function could overflow the callstack.

readme.md Outdated
Currently supported objects are:
- `Date` - serializes the Date to milliseconds, then back to a `Date` object when the getter is called

If you want support for a new object/type please open a new issue for discussion.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs in the readme and index.d.ts should be in sync.

@delewis13
Copy link

Any updates on merging this functionality? Would be great to have a simple way to store and retrieve dates.

@sbencoding
Copy link
Author

@delewis13 I've implemented the requested changes, but I didn't want to update the PR, because there are still questions, namely:

  • Circular objects
    In the review there's a request to handle circular objects, but even if I handle them JSON.serialize() just throws and exception when it detects one, so I'm not sure what to do here
  • Usage of while loops
    I've commented on the review explaining my thought process, but there might be other, better ways instead of using the while loop I haven't thought about.

Other than that the rest of the requested changes are implemented

@papb
Copy link

papb commented Feb 14, 2020

Hi @sbencoding, there are other modules that can handle serialization and deserialization of circular objects automatically for you, such as flatted, maybe this is what Sindre had in mind...

@sbencoding
Copy link
Author

Hey, sorry for the late response.
I've implemented the serialization and deserialization of circular objects, as well as I've added some tests for it. Other requested changes are implemented as well.
I'm not sure why the travis build fails tho....

  • On all windows builds xo complains about import order in test.js, this doesn't happen for OSX or Linux or my local machine (Linux).
  • OSX with nodejs v12 fails the both of the watch option tests. This doesn't happen for v10, and on the linux tests.

@@ -1,9 +1,30 @@
/// <reference types="node"/>
import {JSONSchema} from 'json-schema-typed';
import { JSONSchema } from 'json-schema-typed';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do unrelated style changes. Applies in many places. If you upgrade the XO dependency and run npx xo --fix these things will be fixed for you.

index.d.ts Outdated
Comment on lines 7 to 9
/**
* Matches a JSON object
*/
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* Matches a JSON object
*/
/**
Matches a JSON object.
*/

Applies everywhere.

index.d.ts Outdated
@@ -225,6 +268,10 @@ declare class Conf<T = any> implements Iterable<[keyof T, T[keyof T]]> {
store: T;
readonly path: string;
readonly size: number;
/**
* Conf can automatically (de)serialize some objects/types, for more information please visit [Supported Types](https://https://github.com/sindresorhus/conf#Supported-Types)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readme and index.d.ts docs should be in sync. That means the linked to text should be included inline here.

index.js Outdated
let circularReferenceCounter = 1;
while (stack.length > 0) {
const {current, parent, key} = stack.pop();
if (objectSeenList.indexOf(current) === -1) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (objectSeenList.indexOf(current) === -1) {
if (!objectSeenList.includes(current)) {

}

return value;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is pretty large. I would look into splitting it into multiple methods and maybe something here is generic enough to be moved out into a generic utility function. Same with _deserializeExtraTypes.

index.js Outdated
// seenList.indexOf(current) is the object it's pointing to
const referenceTarget = objectSeenList[objectSeenList.indexOf(current)];
parent[key] = {
[TYPE_KEY]: 'circularObject',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see why you need the circularObject thing. Circular objects can simply be detected by comparing references. I think this logic could be simplified a lot.

See how it's done here: https://github.com/sindresorhus/serialize-error/blob/master/index.js and here: https://github.com/sindresorhus/map-obj/blob/master/index.js

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The circularObject type signals to the deserialize function that it was a circular object, so it can lookup the referenceNumber set in the value of the object, and set it to the object it was referencing to before serialization.

I might have misunderstood this problem. I made it so that when deserializing the circulation is reconstructed, so you're going to end up with the same circular object after getting it from the store.
So should I just completely remove circulation like the destroyCircular function does in the first example you've provided, or is there an easier way to reconstruct the circulation?

t.true(getCircular.c1.myDate instanceof Date);
t.true(getCircular.c2.myDate instanceof Date);
});

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There needs to be some tests for other types, just to ensure it works in general with custom types and other types than Date.

index.js Outdated
console.log(referenceTarget[CIRCULAR_REF_KEY]);
}

console.log('skipping pontentially circulation');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modules should not log to the console.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm very, very sorry about this, sometimes I just add some log statements for testing, and I always try to keep in mind to remove them, but it seems like I've forgotten this time.

@@ -356,6 +358,15 @@ conf.store = {

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to document how to use the custom deserializer/serializer.

@sindresorhus sindresorhus changed the title Store and get Date objects Add support for setting/getting Date objects Apr 7, 2020
@sbencoding
Copy link
Author

I've implemented most of the requested changes, I'm just going to re-work a bit the addition of extra types by the consumer program tomorrow, and then I'm going push the new changes.

I'm really confused about what I should do with circular objects now, but that's explained in a comment in the code review. I'd be happy to implement another approach, if I'm missing the point of this feature.

@sbencoding
Copy link
Author

I've implemented the requested changes, now the build only fails for node v12 on macOS during the 2 .watch() tests.

Instead of directly updating the extraTypes array of a Conf instance, I chose to implement the addition of extra types through the options argument that's passed to the constructor.

Let me know if there's anything else to change.

readme.md Outdated
- It must return the type that is defined.

#### Example
The following example demonstrates the configuration of the `Date` type, that is internally supported by default.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use a different type than the default.

@sindresorhus
Copy link
Owner

There are still some unresolved inline comments. Also make sure that the docs in the readme is in sync with index.d.ts and the other way.

@sbencoding
Copy link
Author

I've updated the example and double-checked to docs to reflect each other as much as possible, let me know if there's anything else to change/update.

Base automatically changed from master to main January 23, 2021 08:51
@sindresorhus
Copy link
Owner

I appreciate all the work done here, but I unfortunately don't think this is the optimal solution. Closing in favor of #154.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants