-
Notifications
You must be signed in to change notification settings - Fork 2
Home
Experimental branch: https://github.com/noyainrain/micro/tree/share-target
Considered alternatives:
-
Outweb
No submission status, no maintenance (see https://github.com/mariusclaret/outweb/issues/2) -
pwa.rocks
No maintenance (see https://github.com/pwarocks/pwa.rocks/issues/288)
Note that it would also be possible to publish on platform app stores, at least with considerable effort:
- Not all characters are available across keyboard layouts, e.g.
¥
with German layout - "Alt Graph" characters are produced with different modifiers depending on platform and thus
- combinations with "Alt Graph" characters are not availabe accross platform, e.g.
ß
on Windows (would beAlt+Control+ß
) - combinations with
Alt
are not available across platform, e.g.Alt+S
on macOS (would beAlt+ß
)
- combinations with "Alt Graph" characters are not availabe accross platform, e.g.
To ensure combatibility, do not define shortcuts with Alt
and only use characters available across keyboard layouts, without "Alt Graph" (e.g. A-Z
, 0-9
, .,
, ...).
Non-Latin keyboard layouts are typically used in combination with a secondary latin layout and users can switch layouts to activate shortcuts. For an improved experience, native software may match shortcuts against all installed layouts (e.g. Ψ
and C
with Greek layout), but unfortunately there is no adequate web API.
Using signals to interrupt blocking operations is not always possible in Python:
Python signal handlers are always executed in the main Python thread, even if the signal was received in another thread.
Mechanism inspired by AbortSignal:
class CancelSignal:
cancelled: bool
on_cancel: Optional[Callable[[], None]]
def cancel(self):
self.cancelled = True
if self.on_cancel:
self.on_cancel()
Example for bzpoptimed()
:
def bzpoptimed(..., signal: CancelSignal = None):
if signal:
signal.on_cancel = partial(r.publish, channel, 'int')
...
while True:
...
if signal and signal.cancelled:
raise CancelledError()
p.get_message(...)
bug.critical: Bug that disables a substantial feature, use case or the entire app. For non-deterministic / performance bugs, functionality is considered disabled if fail rate > success rate / run time >> expected time.
Data (here for client / JavaScript, but should be similar for server / Python):
- Time
- Error message
- Stack
- State:
- URL
- User
- Device
- No way to create memory / core dump for JavaScript (e.g. see
https://github.com/getsentry/sentry-javascript/issues/990), alternatives:
- window (globals)
- document.documentElement.innerHTML (DOM)
- ...
- (How to create a Python memory dump?)
- localStorage
- ...
- "Breadcrumbs", i.e. user interaction, navigation, ... (e.g. see https://docs.sentry.io/learn/breadcrumbs/?platform=javascript)
Store as file in home (too large for log / email and should also work in case of database error) and log error message along with report file name.
UI consent (if needed, depending on data):
- We'd kindly like to ask your permission to send error reports to help us fix 'em all.
- An error report contains information about the action(s) that lead to the error (e.g. which button(s) you clicked)
- Yes, Not now, Never
- Value object (struct):
{__type__}
,JSONifiable
- Temporary
- Immutable
- Equality
- Entity:
{__type__, id}
,Object(JSONifiable)
- Persistent
- Mutable
- Identity
- Collection:
{count, items, slice}
,Collection
Collection of entities; more precisely, a view to a slice of a collection
- Always return objects for REST API:
- Incompatible change:
"meow"
->["meow", 2]
- Compatible change:
{"a": "meow"}
->{"a": "meow", "b": 2}
- Incompatible change:
- Use options for functions:
- Incompatible change:
foo(a, x=None)
->foo(a, b, x=None)
- Compatible change:
foo(a, *, x=None)
->foo(a, b, *, x=None)
- JavaScript:
foo(a, {x: null} = {})
- Incompatible change:
- Input change: Overload
- Behaviour / output change: Overload with feature toggle
class OnType:
pass
On = OnType()
@overload
def foo(a: int, *, bar: OnType) -> str:
pass
@overload
def foo(a: int, *, bar: None = None) -> int:
pass
def foo(a: int, *, bar: OnType = None) -> Union[str, int]:
if bar is None:
return a + 1
return str(a + 1)
foo(2)
foo(2, bar=On)
foo(2, bar=On if args.get('bar', False) else None)
foo(a, {bar: false} = {}) {};
If type of attribute or behaviour of method changes (which we cannot handle otherwise, e.g. conforms to a protocol and cannot be called with an additional feature flag):
- Deprecate old name and add new functionality with alternative name
- Switch old name over to new functionality
- Optional: Remove alternative name (extra cost, requires another change by API user)
class Cat:
fur: int
coat: str
class Cat:
fur: str
coat: str
{"__type__": "Cat", "fur": 2, "coat": "tabby"}
{"__type__": "Cat", "fur": "tabby", "coat": "tabby"}
Major dependency updates are breaking changes.
If micro and a micro user share the same dependency A 1.*
and micro upgrades to A 2.*
, we break the user's deployment, because different incompatible versions cannot be installed next to each other. According to the SemVer FAQ, dependency updates should not be considered breaking unless the public API changes. We would however argue that the package is part of the public API.
It may be possible to make a major dependency update non-breaking by supporting both versions for a deprecation period, but this would likely result in a non-justifiable amount of workaround code.
class WithAuthors:
def __init__(self):
self.authors = Collection(RedisSortedSet('{}.authors'.format(self.ids.key), self.app.r.r),
app=self.app)
def on_add(self, item):
"""Subclass API."""
self.app.r.zadd(self.authors.ids.key, time.time(), item.author.id)
def on_remove(self, item):
"""Subclass API."""
if item.author.id not in set(item.author.id for item in self):
self.app.zrem(self.authors.ids.key, item.author.id)
def json(self, *, slc):
return {'authors': self.authors.json(slc=slc)}
class Comments(Collection, WithAuthors):
def __init__(self):
super().__init__()
WithAuthors.__init__(self)
def comment(self):
# ...
self.on_add(comment)
def json(self):
return {
**super().json(),
**WithAuthors.json(self, slc=slice(0:2))
}
# Maybe trashed count is not so relevant, but nice example of additional collection meta data
class WithTrashable:
def __init__(self):
self.__key = '{}.meta'.format(self.ids.key)
@property
def trashed_count(self):
return int(self.app.hget(self.__key, 'trashed_count'))
def on_trash(self, item):
self.app.r.hincrby(self.__key, 'trashed_count', 1)
def on_restore(self, item):
self.app.r.hincrby(self.__key, 'trashed_count', -1)
def json(self):
return {'trashed_count', self.trashed_count}
We step-by-step introduce type annotations for micro on a provisional bases to see how it works out in a dynamic language like Python.
Some stumbling blocks:
- Pylint does not understand @overload
- Generic class with default argument:
- Problem: User can annotate class with e.g.
Foo[A]
while default implementation usesB
- Default value for TypeVar is not implemented
- Could be solved (effortfully) with overloading
__new__
and two different subclasses
- Problem: User can annotate class with e.g.
Annotate micro as typed: http://mypy.readthedocs.io/en/latest/installed_packages.html
Cloned content of <use>
is placed and layed out
in the current container with the given width, height and preserveAspectRatio of the referenced
symbol. If width and height are set to 100%, the cloned content scales to the container (centered to
keep the aspect ratio). The container size is not influenced by the cloned content and must be
explicitly set, either for each icon (e.g. with .fa-w-X
) or for all icons to the same size.
<svg class="svg-inline--fa"><use href="{{ icons_url }}#angle-down"></use></svg>
Type hints may further improve code quality, but we should first investigate how they interact with the dynamic nature of Python. Experimental branch: https://github.com/noyainrain/micro/tree/type-hints
If the button is triggered, it will suspend (disable), then after the promise resume (enable). If the button disabled property is modified inbetween (e.g. by the promise itself), we lose this change.
// Suspend:
this.suspended = true;
this.disabledResume = false;
// Resume:
this.suspended = false;
this.disabled = this.disabledResume;
class Button {
set disabled(value) {
if (this.suspended) {
this.disabledResume = value;
} else {
super.disabled = value;
}
}
}
At the moment transforms are special functions that receive ctx as first argument. We could rather make ctx available as special variable, which can be passed to transforms that really need it.
let stack = [null].concat(data, micro.bind.transforms);
// ...
stack[0] = {ctx};
// data-content="list ctx posts 'post'"
Object.assign(this.eventTemplates, {
"purr": ".foo-purr-event-template"
});
this.eventData = {makeURL: foo.makeURL};
let elem = document.importNode(ui.querySelector(ui.eventTemplates[event.type]).content, true);
micro.bind.bind(elem, Object.assign({event}, this.eventData));
li.appendChild(elem);
- MVP / Beta 1:
- Target: App is usable in a minimal form. User mistakes are correctable.
- Audience: Groups the developers are themselves part of
- Feedback channel: Direct
- Beta 2:
- Target: App is usable on a daily basis. Core functionality is implemented. Purpose is clearly apparent to first time users. Feedback from Beta 1 is incorporated.
- Audience: Social networks of the developers
- Feedback channel: Social networks / App
- Beta 3:
- Target: App monitoring is available. Feedback from Beta 2 is incorporated.
- Audience: Public, announced on various platforms, e.g.:
- Feedback channel: App / Platforms
- Stable:
- Target: Features expected from a self-contained app are implemented. Feedback from Beta 3 is incorporated.
unwatch(prop, onUpdate) {
let i = (watchers[prop] || []).findIndex(f => f === onUpdate);
if (i === -1) {
throw new Error();
}
watchers.splice(i, 1);
}
Sophisticated user listing:
<p data-title="joinText list.authors ', ' map 'name'">
<span class="fa fa-pencil"></span>
<span data-content="join list.authors 'user' ', ' 3">
<template><micro-user></micro-user></template>
<template data-name="overflow">
+ <span data-content="sub list.authors.length 3"></span>
</template>
</span>
</p>
Format:
<span data-content="format 'Hello {user}!' 'user' user.name">
<span data-content="format 'Hello {user}!'">
<template name="user"><micro-user data-user="user"></micro-user></template>
</span>
Translate (calls format):
<span data-content="translate 'hello' 'user' user.name">
Advanced translation example:
<p>
<span data-content="translateEditedBy list.authors">
<template data-name="authors">
<span data-content="join list.authors 'user' ', ' 3">
<template><micro-user></micro-user></template>
</span>
</template>
</span>
</p>
<script>
this.data.translateEditedBy = (ctx, authors) => {
let n = authors.length;
return micro.bind.translate(ctx, n > 3 ? "edited-by-overflow" : "edited-by", "n", n);
}
{
"edited-by": "edited by {authors}",
"edited-by-overflow": ["edited by {authors} and {n} other", "edited by {authors} and {n} others"]
}
</script>
- Python / JavaScript API:
- Breaking changes okay
- User decides update time
- Sometimes non-breaking changes are not possible or only with disproportionate effort:
- (JavaScript) Convert
<custom-element>
from/to<div is="custom-element">
(can only be registered once)
- (JavaScript) Convert
- Web API:
- Breaking changes only after deprecation period
- Service provider decides update time, user needs time to make adjustments
Assume that micro introduces a new announcement feature with the following API:
- type
Announcement
- Attribute
Application.announcements
- DB key
announcements
- endpoint
/api/announcements
- page
/announcements
- ...
If an existing micro application already makes use of any of those names, this could lead to breaking behavior (e.g. micro code modifies Application.announcements
or posts to /api/announcements
and they have different, unexpected semantics).
How can we solve this? New features that involve the extendable API could be major releases. New features could be introduced behind a feature toggle with a minor release and enabled by default with a major release. DB keys could be prefixed with micro
.