webguy v5.2.1
WebGuy is a Guy for the Web
Table of Contents generated with DocToc
WebGuy is a Guy for the Web
props
public_keys = ( owner ) ->: return a list of property names, including inherited ones, but excluding non-enumerables, symbols, and non-userland ones likeconstructor.get_prototype_chain = ( x ) ->: return a list containing objectxand all the objects encountered when walking downx's' prototype chain (usingObject.getPrototypeOf()repeatedly). Ifxisnullorundefined, return an empty list because in that case there are zero objects where property lookup could happen. Thereverse()d prototype chain is used by the two*depth_first*()method, below.walk_depth_first_property_descriptors = ( x ) ->: Given a valuex, return an iteratoracquire_depth_first = ( source, cfg ) ->: given asourceobject, walk the property chain from the bottom to the top (usingwalk_depth_first_property_descriptors()) and transfer all properties to a new or a giventargetobject. This is most useful when used with afilterto select, ageneratorfunction to generate new, and / or adecoratorto modify accepted and generated properties.acquire_depth_first()will keep the relative ordering: (1) 'top-down' for each object (properties declared earlier will appear, on the target, before ones declared later); (2) w.r.t. inheritance in the sense that the prototype of a given objectxin the prototype chain will be looked at before properties onxitself is considered. Later properties may shadow (replace) earlier ones but it's also possible to forbid shadowing or ignore it altogether (seeoverwrite, below).When a
cfgobject is given as second arguments, it may have the below settings, all of which are optional:filter: An optional function that will be called with an object{ target, owner, key, descriptor, }for (1) each found property; it should return eithertrue(to keep the property) orfalse(to skip the property); non-Boolean return values will cause an error.descriptor: An optional object containing updates to each property's descriptor. Use e.g.descriptor: { enumerate: true, }in the call toacquire_depth_first()to ensure that all acquired properties on thetargetobject will be enumerable.target: the 'static' or 'default target', i.e. the object to which the properties are to be assigned to. If not given, a new empty object{}will be used. It is also possible to set a 'dynamic target' (that will override the static target) in the yielded values ofgenerator, for which see below.overwrite: controls how to deal with property keys that appear more than once in the prototype chain. Sinceacquire_depth_first()'s raison d'être is doing depth-first 'anti-inheritance', there are several ways to deal with repeated properties, as the case may be:false(default): Throw an error when an overriding key is detectedtrue: Later key / value pairs (that are closer to the source value) override earlier ones, resulting in a key resolution that is like inheritance (but without the possibility to access a shadowed value).'ignore': Silently ignore later keys that are already set; only the first mention of a key / value pair is retained.
generator: if given, must be a generator functiongf()(a function using theyieldkeyword). The generator function will be called with an object{ target, owner, key, descriptor, }for each property found and is expected to yield any number of values of the format{ key, descriptor, }. Optionally, this object may also havetargetset (the 'dynamic target'), which will be the object that the current property will be set on. This is useful e.g. to distribute multiple derived properties over a number of target objects.gf()will only be called if the property has not been notfiltered out. Yielded keys and descriptors will be used to calldecoratorif that is set.Points to keep in mind:
- The most trivial setting for
generator, a generator that doesn't yield anything—( d ) -> yield return null; JS:function*( d ) { return null; }—has the effect of preventing any property to be set on the target. This is because the original key / value pair is not treated specially in any way, so the user can (and must) freely decide whether and where they want the original property to appear in the target. - Take care not to re-use the
descriptorthat was passed in without copying it. Instead, always use syntax like yield{ key: 'foo', descriptor: { descriptor..., value: foo, } }to prevent leakage of (most importantly) thevaluefrom one property to another.
- The most trivial setting for
decorator: An optional function that will be called with an object{ target, owner, key, descriptor, }for (1) each found property and (2) each generated property, too. Thedecoratorfunction may returnnullorundefinedto indicate no change for the given property; otherwise, it should return an object that will be used (likecfg.descriptor) to update settings in the property's descriptor—in other words, the returned object needs only to mention those parts of the decorator that should be changed, and most commonly, an object like{ value: 'helo', }where onlyvalueis set will suffice. In case bothcfg.descriptorand the return value of thedecoratorfunction mention the same descriptor settings, the ones returned by the latter (thedecoratorfunction) will overwrite those of the former (i.e. the decorator always has the last word).
time
WEBGUY.time contains facilities to create timestamps for purposes like logging or to create dated DB
records.
Timestamps are one of those things that seem easy (like, +new Date() or Date.now()) but get quite a bit
harder when you bring in typical constraints. One wants one's timestamps to be:
precise: Computers are fast and a millisecond is sort of a long time for a CPU. Naïve JS timestamps only have millisecond precision, so one can easily end up with two consecutive timestamps that are equal. This leads to
monotonous: You don't want your timestamps to ever 'stand still' or, worse, decrement and repeat at any point in time. Because
new Date()is tied to civil time, they are not guaranteed to do that.relatable: Ideally, you want your timestamps to tell you when something happened. A 13-digit number can do that—in theory. In practice, only some nerds can reliably tell the difference between timestamps from today and those from last week or last year.
durable: Time-keeping is complicated: Timezones are introduced and abolished, daylight saving dates can vary within a single country and may get cancelled in some years or split into two separate durations within a year; some people count years ab urbe condita, some days since CE 1900, some seconds and others milliseconds from CE 1970; in some years, you get a leap second and so on. For these reasons, local civil time is not a good choice for timestamps.
Suffixes:
- methods ending in
freturn floats, - methods ending in
sreturn strings; - methods ending in
1return a single value, contrasted with - methods ending in
2which return a list of two values.
- methods ending in
stamp_f = ->utc_timestamp = performance.timeOrigin + performance.now(): return a float representing present time as milliseconds elapsed since the Unix epoch (1970-01-01T00:00:00Z), including microseconds as a fraction. This is the recommended way to measure time for performance measurements and so on, as it is reasonably precise and monotonic (i.e. it is unaffected by system time updates and will only ever increase). Here included as a convenience method.stamp_s = ( stamp_s = null ) ->( stamp_s ? @stamp_f() ).toFixed 3: return the numeric timestamp ortime.stamp_f()as a string with exactly 3 decimals; suitable for IDs, logs &c.monostamp_f2 = ->: return a list containing the result ofmonostamp_s2 = ( stamp_f = null, count = null ) ->: return a list containing the result oftime.stamp_s()and a monotonic zero-based, zero-padded counter which will be shared across all callers to this method. Sample return value:[ '1693992062544.423', '000' ]; shouldtime.stamp_and_count()get called within the same microsecond, it'd return[ '1693992062544.423', '001' ]&sf. Especially for testing purposes, one can pass in the fractional timestamp and a value for the counter.monostamp_s1 = ( stamp_f = null, count = null ) ->: return the same asmonostamp_s2(), but concatenated usingcfg.counter_joiner.stamp()is a convenience equivalent tomonostamp_s1().
Configuration
cfg =
count_digits: 3 # how many digits to use for counter
counter_joiner: ':' # comes between timestamp and counter
ms_digits: 13 # thirteen digits should be enough for anyone (until November 2286)
ms_padder: '0' # padding for short timestamps (before 2001)
format: 'iso' # should be 'iso', or 'milliseconds', or custom formatformat:milliseconds: timestamps look like1693992062544.423:000iso: timestamps look like1970-01-01T00:00:00.456789Z:000compact: timestamps look like19700101000000456789:000dense: timestamps look like19700101@000000.456789:000for readability- any other string will be interpreted by the
format()method ofdayjs, with the addition ofµU+00b5 Micro Sign, which symbolizes 6 digits for the microseconds part. A minimal template that doesn't leave out any vital data and still sorts correctly isYYYYMMDDHHmmssµ, which producescompactformat timestamps like20230913090909275140:000(the counter being implicitly added).
Performance Considerations
A quick test convinced me that I'm getting around 170 calls to time.monostamp_s1() into a single
millisecond; these timestamps then look like
1694515874596.967:000
1694515874596.976:000
1694515874596.981:000
1694515874596.990:000
1694515874596.995:000— that is, a repetition in the tens and hundredths of milliseconds is quite likely, but a repetition in the thhousandths of milliseconds (i.e. microseconds) is unlikely. It's a rare event (estimated to less than one in a million) that the counter ever goes up to even one. This tells me that on my (not up-market, not fast) laptop it should be more than safe to use three digits for the counter; however that may not be true for faster machines.
environment
( require 'webguy' ).environment is an object like { browser: false, node: true, webworker: false, jsdom:
false, deno: false, name: 'node', } with boolean and one text properties that tell you in what kind of
environment the code is running. Observe that there may be environments where no boolean property is true
and name is null.
trm
rpr = ( x ) ->: return a formatted textual representation of any valuex.
types
API
validate.t x, ...—returnstrueon success, throws error otherwiseisa.t x, ...—returnstrueon success,falseotherwise
Type Signatures
string of variable length reflecting the results of a minimal number of tests that never fail and give each type of values a unique name
Tests are:
- the result of
typeof x - the shortened Miller Device Name (MDN) obtained by
Object::toString.call x, but replacing the surrounding (and invariably constant)[object (.*)] - the value's
constructor.nameproperty or0where missing - the value's Denicola Device Name (DDN), which is the
constructorproperty'snameor, if the value has no prototype, the digit zero0. - the value's Carter Device Name (CDN), which is
classfor ES6classes,fnfor functions, andotherfor everything else. It works by first looking at a value's Miller Device Name; if that is not indicative of a function, the value's CDN isother. Else, the property descriptordscof the value's prototype is retrieved; if it is missing, the CDN isother, too. Ifdsc.writableistrue, the CDN isfn; otherwise, the CDN isclass. NifNumber.isNaN xistrue, digit zero0otherwise
- the result of
Results are joined with a slash /.
### TAINT test for class instances?
( typeof x )
( x?.constructor.name ? '-' )
( Number.isNaN x ) ].join '/'
( ( Object::toString.call x ).replace /^\[object (.+?)\]$/u, '$1' )
( x?.constructor.name ? '0' )
( if Number.isNaN x then 'N' else '-' )
###
xxx The [Carter Device (by one Ian Carter, 2021-09-24)](https://stackoverflow.com/a/69316645/7568091) for
those values whose Miller Device Name is `[object Function]`:
Also see [this detailed answer in the same discussion](https://stackoverflow.com/a/72326559/7568091).
[Link to specs](https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation)
###
get_carter_device_name = ( x, miller_device_name = null ) ->
miller_device_name ?= Object::toString.call x
return '-' unless miller_device_name is '[object Function]'
return 'fn' unless ( descriptor = Object.getOwnPropertyDescriptor x, 'prototype' )?
return 'fn' if descriptor.writable
return 'class'
console.log '^4234-1^', isa_class ( class D )
console.log '^4234-2^', isa_class ( -> )
f = -> new Promise ( resolve , reject ) ->
console.log '^4234-3^', isa_class resolve
console.log '^4234-4^', isa_class reject
console.log '^4234-5^', Object.getOwnPropertyDescriptor resolve, 'prototype'
resolve null
await f()
###
https://stackoverflow.com/a/69316645/7568091 (2021-09-24 Ian Carter)
https://stackoverflow.com/a/72326559/7568091
coffee> ( Object.getOwnPropertyDescriptor d, 'prototype' )?.writable ? false
{ value: {}, writable: false, enumerable: false, configurable: false }
coffee> Object.getOwnPropertyDescriptor (->), 'prototype'
{ value: {}, writable: true, enumerable: false, configurable: false }
###To Do
[–]types.isa.sized(),types.isa.iterable()test for 'existence' ofx(x?) but must test for non-objects as well or catch exception (better)[–]define whatiterableandcontainerare to mean precisely, as in, provide the defining characteristic. Somehow we can e.g. iterate over a string as inx for x in 'abc'andd = [ 'abc'..., ]butReflect.has 'abc', Symbol.iteratorstill fails with an exception ('called on non-object').[–]In the same vein, what exactly is anobjectin JS? Maybe indeed anything that is not a primitive value (i.e. notnull,undefined,true,false, number includingInfinityandNaN(but notBigInt)). As such, maybeprimitive,nonprimitivewould be OK?- Maybe any
dfor which[ ( typeof d ), ( Object::toString.call d ), ( d instanceof Object ), ]gives[ 'object', '[object Array]', true ]. This would include instances of a plainclass O;which are implicitly (but somehow different from explicitly?) derived fromObject. One could throw the Dominic Denicola Device i.e.d.constructor.nameinto the mix which would then exclude instances ofclass O;.
- Maybe any
[–]implement inWEBGUY.errorscustom error classes with refs, use them inWEBGUY.types[–]disallow overrides by default whenextending classIsato avoid surprising behavior (might want to implement with set of type names; every repetition is an error unless licensed)[–]might later want to allow overrides not for entire instance but per type by adding parameter to declaration object
[–]inprops.acquire_depth_first(), fix handling of descriptors[–]use an instance ofTypesin its methods ('dogfeeding')[–]consider to instantiateTypesfromPre_typespassing in an instance of itself (Types), thus allowing the instance to use 'itself' / 'a clone of itself' without incurring infinite regress
Is Done
[+]in theIsastandard types, should e.g.integeronly refer to integer floats (4.0) or to floats andBigInts (4.0and4n)? Could / should that be configurable? remove all mentions ofBigInts inisatests with a view to establish separate types for them in the future (bigint,zero_bigint&c)[+]intypes.validate, return input value such thatx is types.validate.whatever xis always satisfied unlessxdoesn't validate[+]inprops.acquire_depth_first(), do not silently overwrite earlier properties with later ones; instead, usecfg.overwriteto determine what should happen (trueoverwrites, function calls back,falsethrow an error).[+]inprops.acquire_depth_first(), addcfg.generator()(?) option to allow generation of any number of additional members in addition to seen ones. This should be called beforecfg.decorator()gets called. Should probably requirecfg.generator()to be a generator function.[+]inprops.acquire_depth_first(), allow bothgeneratoranddecoratorto produce a 'local' value fortargetthat will overridecfg.target; this will allow to distribute properties over a number of targets.[+]WEBGUY.types.declare: consider to prohibit adding, removing types from the default export instance as it may be considered too brittle: declaring a type can potentially change results oftype_of, too, so even consumers that do not make use of the new type could be affected. A dependent module may or may not see the same instance ofWEBGUY.types, depending on their precise dependency declarations and depending on the package manager used. Types are now always declared at instantiation time, later declarations are not (and likely will not be) implemented.
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago