webguy
v5.2.1
Published
Guy for the web
Downloads
132
Readme
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 objectx
and all the objects encountered when walking downx
's' prototype chain (usingObject.getPrototypeOf()
repeatedly). Ifx
isnull
orundefined
, 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 asource
object, walk the property chain from the bottom to the top (usingwalk_depth_first_property_descriptors()
) and transfer all properties to a new or a giventarget
object. This is most useful when used with afilter
to select, agenerator
function to generate new, and / or adecorator
to 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 objectx
in the prototype chain will be looked at before properties onx
itself 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
cfg
object 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 thetarget
object 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 theyield
keyword). 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 havetarget
set (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 notfilter
ed out. Yielded keys and descriptors will be used to calldecorator
if 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
descriptor
that was passed in without copying it. Instead, always use syntax like yield{ key: 'foo', descriptor: { descriptor..., value: foo, } }
to prevent leakage of (most importantly) thevalue
from 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. Thedecorator
function may returnnull
orundefined
to 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 onlyvalue
is set will suffice. In case bothcfg.descriptor
and the return value of thedecorator
function mention the same descriptor settings, the ones returned by the latter (thedecorator
function) 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
f
return floats, - methods ending in
s
return strings; - methods ending in
1
return a single value, contrasted with - methods ending in
2
which 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 format
format
:milliseconds
: timestamps look like1693992062544.423:000
iso
: timestamps look like1970-01-01T00:00:00.456789Z:000
compact
: timestamps look like19700101000000456789:000
dense
: timestamps look like19700101@000000.456789:000
for 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 producescompact
format 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, ...
—returnstrue
on success, throws error otherwiseisa.t x, ...
—returnstrue
on success,false
otherwise
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.name
property or0
where missing - the value's Denicola Device Name (DDN), which is the
constructor
property'sname
or, if the value has no prototype, the digit zero0
. - the value's Carter Device Name (CDN), which is
class
for ES6class
es,fn
for functions, andother
for 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 descriptordsc
of the value's prototype is retrieved; if it is missing, the CDN isother
, too. Ifdsc.writable
istrue
, the CDN isfn
; otherwise, the CDN isclass
. N
ifNumber.isNaN x
istrue
, digit zero0
otherwise
- 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 whatiterable
andcontainer
are 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.iterator
still fails with an exception ('called on non-object').[–]
In the same vein, what exactly is anobject
in JS? Maybe indeed anything that is not a primitive value (i.e. notnull
,undefined
,true
,false
, number includingInfinity
andNaN
(but notBigInt
)). As such, maybeprimitive
,nonprimitive
would be OK?- Maybe any
d
for 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.name
into the mix which would then exclude instances ofclass O;
.
- Maybe any
[–]
implement inWEBGUY.errors
custom error classes with refs, use them inWEBGUY.types
[–]
disallow overrides by default whenextend
ing classIsa
to 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 ofTypes
in its methods ('dogfeeding')[–]
consider to instantiateTypes
fromPre_types
passing in an instance of itself (Types
), thus allowing the instance to use 'itself' / 'a clone of itself' without incurring infinite regress
Is Done
[+]
in theIsa
standard types, should e.g.integer
only refer to integer floats (4.0
) or to floats andBigInt
s (4.0
and4n
)? Could / should that be configurable? remove all mentions ofBigInt
s inisa
tests 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 x
is always satisfied unlessx
doesn't validate[+]
inprops.acquire_depth_first()
, do not silently overwrite earlier properties with later ones; instead, usecfg.overwrite
to determine what should happen (true
overwrites, function calls back,false
throw 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 bothgenerator
anddecorator
to produce a 'local' value fortarget
that 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.