Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions EWC/AddResource.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
alias AddResource folder;m;rc
⍝ Serve a folder over HTTP as a virtual folder, e.g.
⍝ 'kendo' AddResource '/path/to/plugin/assets'
⍝ makes that folder's contents available at /kendo/. Takes effect immediately -
⍝ wss reads its mappings per request - so this works before or after Init.
⍝ An alias is a single path segment: wss matches on the first '/' of the URL.
'Alias must not contain "/"'⎕SIGNAL('/'∊alias)/11
:If 2≠⎕NC'RESOURCES' ⋄ RESOURCES←0 2⍴⊂'' ⋄ :EndIf
:If 2≠⍴⍴RESOURCES ⋄ RESOURCES←1 2⍴RESOURCES ⋄ :EndIf

m←(0≠≢¨RESOURCES[;1])∧~RESOURCES[;1]∊⊂alias
RESOURCES←(m⌿RESOURCES)⍪alias folder

:If 9=⌊⎕NC'WSS'
WSS.Aliases←RESOURCES
rc←WSS.CheckAliases
:If 0≠⊃rc
'W'Log'AddResource ',alias,': ',2⊃rc
:EndIf
:EndIf
9 changes: 5 additions & 4 deletions EWC/GetClass.aplf
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
r←GetClass (ns name);t;c;m
r←GetClass (ns name);t;m
r←⎕NULL
:If 9=⎕NC 'ns'
:AndIf ∨/m←0≠ns.⎕NC t←name∘,¨'.type' '.Type'
:AndIf 9=⎕NC c←'classes.',⎕C ns⍎⊃m/t
r←⍎c
:Else
r←findClass ns⍎⊃m/t
:EndIf
:If ⎕NULL≡r
('Not a supported GUI object: ',name) ⎕SIGNAL 6
:EndIf
2 changes: 2 additions & 0 deletions EWC/Init.aplf
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
⎕←'Using client found at ',JSClientFolder

setDefaults
ensurePlugins ⍝ Plugins may already have registered; keep them detached from Link

:If DIRECT
'D' Log 'Direct mode enabled'
Expand Down Expand Up @@ -102,6 +103,7 @@
:EndIf
WSS.HTMLFolder←JSClientFolder
WSS.ShowHTMLRenderer←0
applyPluginResources ⍝ RESOURCES may have been reassigned since a plugin registered
WSS.Aliases←RESOURCES
WSS.OnConnect←''
WSS.OnWSUpgrade←(⍕⎕THIS),'.newSession'
Expand Down
30 changes: 30 additions & 0 deletions EWC/RegisterPlugin.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{r}←RegisterPlugin plugin;name;nm;target
⍝ Register a plugin with EWC. Safe before or after Init.
⍝ plugin is a namespace ref providing:
⍝ Name plugin id; also the key its classes are tagged with
⍝ Classes ref to a namespace whose sub-namespaces are EWC class definitions
⍝ Folder the plugin's root folder on disk
⍝ Inject (optional) niladic fn returning the JavaScript to evaluate in the
⍝ client, the first time one of the plugin's classes is created
⍝ Resources (optional) an (alias folder) pair, or a 2-column matrix of them
⍝ Class definitions are copied rather than referenced, so a plugin can be
⍝ re-registered after editing its source without stale state.
ensurePlugins
'Plugin must supply a Name'⎕SIGNAL(2≠plugin.⎕NC'Name')/11
'Plugin must supply Classes'⎕SIGNAL(9≠plugin.⎕NC'Classes')/11
r←name←,plugin.Name

⍎'PLUGINS.',name,'←plugin'

:For nm :In plugin.Classes.⎕NL ¯9
target←(⍕PLUGINS),'.classes.',nm
target ⎕NS plugin.Classes⍎nm
(⍎target).PluginName←name
:EndFor
ensurePlugins ⍝ the class namespaces are new, so detach those too

applyResources plugin

'D'Log'Registered plugin "',name,'", classes: ',⍕plugin.Classes.⎕NL ¯9
9 changes: 6 additions & 3 deletions EWC/WaitForWG.aplf
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
R←msg WaitForWG(Props ID WGID);j;⎕TRAP;p;seqok;nl;nlc;m;t;token;wx;z;ref;ok
R←msg WaitForWG arg;Props;ID;WGID;timeout;j;⎕TRAP;p;seqok;nl;nlc;m;t;token;wx;z;ref;ok
⍝ Right argument is (Props ID WGID), with an optional 4th element giving the
⍝ seconds to wait. Defaults to WG_TIMEOUT.
(Props ID WGID timeout)←4↑arg,⊂WG_TIMEOUT

wx←Props≡'WX' ⍝ A WX rather than a WG

token←WG_TOKENBASE+TOKENSTEP×1⌈ID×MODE=2
AGAIN:
j←WG_TIMEOUT ⎕TGET token
j←timeout ⎕TGET token
:If 0=≢j
'W' Log 'Client did not respond within ',(⍕WG_TIMEOUT),' seconds.'
'W' Log 'Client did not respond within ',(⍕timeout),' seconds.'
'W' Log 'Request was: ',msg
⎕SIGNAL 6
:EndIf
Expand Down
12 changes: 12 additions & 0 deletions EWC/applyPluginResources.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
applyPluginResources;nm
⍝ Re-mount every registered plugin's resources.
⍝ RESOURCES is an ordinary variable and applications assign it wholesale
⍝ (demo/Run.aplf does), which would drop mounts made by a plugin that registered
⍝ before Init. Init calls this once RESOURCES has settled.
:If 9≠⎕NC'PLUGINS' ⋄ →0 ⋄ :EndIf
:For nm :In PLUGINS.⎕NL ¯9
:If 'classes'≢nm
applyResources PLUGINS⍎nm
:EndIf
:EndFor
9 changes: 9 additions & 0 deletions EWC/applyResources.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
applyResources plugin;res;alias;folder
⍝ Mount whatever a plugin declared in Resources: an (alias folder) pair, or a
⍝ 2-column matrix of them.
:If 2≠plugin.⎕NC'Resources' ⋄ →0 ⋄ :EndIf
res←plugin.Resources
:If 2≠⍴⍴res ⋄ res←1 2⍴res ⋄ :EndIf
:For (alias folder) :In ↓res
alias AddResource folder
:EndFor
8 changes: 6 additions & 2 deletions EWC/dWC.aplf
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
→0⊣R←nsname caller.⎕WC Props
:EndIf

:If 9≠classes.⎕NC type
class←findClass type
:If ⎕NULL≡class
Warning 'WC ',Name,' - unsupported class "',type,'"'
⎕←⎕SE.UCMD 'disp ⍪Props'
nsname caller.⎕NS''
(caller⍎nsname).Type←type
→0
:EndIf

class←classes⍎type
:If 2=class.⎕NC'PluginName' ⍝ First use of a plugin class loads its JS
caller injectPlugin class.PluginName Name
:EndIf

proplist←⎕C PropList←class.PropList
valid←(lcp←⎕C propnames)∊allprops←⎕C AllProps←PropList,objProps,deprecatedprops
values←valid↓¨Props
Expand Down
7 changes: 5 additions & 2 deletions EWC/dWX.aplf
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{R}←dWX args;Name;Method;Info;caller;ID;conn;WX;wx;msg;z;⎕TRAP;txt
{R}←{timeout}dWX args;Name;Method;Info;caller;ID;conn;WX;wx;msg;z;⎕TRAP;txt
⍝ Optional left argument: seconds to wait for the client, defaulting to
⍝ WG_TIMEOUT. Loading a plugin's JavaScript can take longer than a property read.
⎕TRAP←0 'S'
:If 0=⎕NC'timeout' ⋄ timeout←WG_TIMEOUT ⋄ :EndIf
(Name Method)←2↑args ⋄ Info←2↓args

:If 'gettextsize'≡⎕C Method
Expand All @@ -25,7 +28,7 @@
:EndIf

:Trap 6 ⍝ If we get no response
R←msg WaitForWG 'WX' ID(⍕WGID)
R←msg WaitForWG 'WX' ID(⍕WGID)timeout
:Else
⎕DMX.EM ⎕SIGNAL ⎕DMX.EN
:EndTrap
21 changes: 21 additions & 0 deletions EWC/ensurePlugins.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{r}←ensurePlugins
⍝ The plugin registry: PLUGINS.classes holds class definitions contributed by
⍝ plugins, PLUGINS.<Name> the record for each registered plugin.
⍝ It is detached from Link. #.EWC is Link-managed, so without this a plugin's
⍝ classes would be serialised into the EWC source tree. This function is the
⍝ single place that decides where the registry lives; if breaking a child of a
⍝ live link ever proves unreliable, move it out of #.EWC here and nothing else
⍝ needs to change - everything reaches it through the PLUGINS ref.
⍝ Registration is allowed before Init, so the globals the plugin path touches -
⍝ RESOURCES to mount against, LOGMODES to log through - may not exist yet.
⍝ setDefaults only fills in names that are undefined, so this cannot clobber
⍝ anything the application has already set.
setDefaults

:If 9≠⎕NC'PLUGINS'
'PLUGINS'⎕NS''
'PLUGINS.classes'⎕NS''
:EndIf
{}{0::⍬ ⋄ ⎕SE.Link.Break ⍵}'#.EWC.PLUGINS' ⍝ reports "Not linked" when already detached
r←PLUGINS
19 changes: 19 additions & 0 deletions EWC/findClass.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
r←findClass type;names;i
⍝ Resolve an EWC class name to its definition namespace, or ⎕NULL.
⍝ Core classes live in #.EWC.classes under lowercase names. Plugin classes live
⍝ in PLUGINS.classes and keep the casing their author gave them, so they are
⍝ matched case-insensitively. See RegisterPlugin and ensurePlugins.
type←⎕C type
:If 9=classes.⎕NC type
→0⊣r←classes⍎type
:EndIf

r←⎕NULL
:If 9=⎕NC'PLUGINS'
:AndIf 9=PLUGINS.⎕NC'classes'
names←PLUGINS.classes.⎕NL ¯9
:If 0≠≢i←⍸(⎕C names)∊⊂type
r←PLUGINS.classes⍎(⊃i)⊃names
:EndIf
:EndIf
25 changes: 8 additions & 17 deletions EWC/getConnection.aplf
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
r←getConnection(caller names);name;top;n;p
name←,⊃⊆names
n←≢p←(⍕caller),'.'
name←(n×p≡n↑name)↓name ⍝ drop caller prefix
top←(¯1+name⍳'.')↑name
r←getConnection(caller names);ns
ns←caller sessionNS names

:If 9=caller.⎕NC n←top,'.HR._EWC'
r←(caller⍎n).(ID conn)

:ElseIf MODE=2 ⍝ Multi
:If 9=caller.⎕NC'_EWC'
r←caller._EWC.(ID conn)
:Else
∘∘∘ ⍝ Unable to locate app session
:EndIf

:Else
:If ⎕NULL≢ns
:Trap 0
r←CODELOCATION._EWC.(ID conn)
r←ns.(ID conn)
:Else
r←0 ¯1 ⍝ no connection: ¯1 is an invalid conn (0 is a VALID Conga connection)
:EndTrap
:ElseIf MODE=2 ⍝ Multi
∘∘∘ ⍝ Unable to locate app session
:Else
r←0 ¯1
:EndIf
6 changes: 2 additions & 4 deletions EWC/getObj.aplf
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
(type class objref)←getObj Name;t
(type class objref)←getObj Name

:Trap 0
objref←caller⍎Name
type←objref.Type
:If 9=classes.⎕NC t←⎕C type
class←classes⍎t
:Else
:If ⎕NULL≡class←findClass type
Warning'Unsupported class: ',type
⎕←Props
→0
Expand Down
35 changes: 35 additions & 0 deletions EWC/injectPlugin.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
caller injectPlugin(name objname);sns;plugin;js;r;top
⍝ Send a plugin's JavaScript to this session's client, once per session.
⍝ Called from ∆WC the first time one of the plugin's classes is created, before
⍝ that object's WC message goes out. dWX blocks until the client replies, and
⍝ the client holds every later frame until the injected code has resolved - so
⍝ the component is registered by the time the WC is handled.
⍝ Load state lives in the session's own _EWC namespace: per clone in Multi mode,
⍝ per HTMLRenderer in Desktop, one in CODELOCATION otherwise.
sns←caller sessionNS objname
:If ⎕NULL≡sns ⋄ →0 ⋄ :EndIf ⍝ no client yet
:If 0=sns.⎕NC'PLUGINS_LOADED' ⋄ sns.PLUGINS_LOADED←⍬ ⋄ :EndIf

⍝ Keyed by connection, not just name: outside Multi mode _EWC outlives any one
⍝ browser, and a reload gives the client a fresh, empty registry. Entries for
⍝ connections that have gone are dropped rather than accumulating.
sns.PLUGINS_LOADED←(sns.conn=⊃¨sns.PLUGINS_LOADED)/sns.PLUGINS_LOADED
:If (⊂sns.conn name)∊sns.PLUGINS_LOADED ⋄ →0 ⋄ :EndIf
:If 9≠PLUGINS.⎕NC name ⋄ →0 ⋄ :EndIf

plugin←PLUGINS⍎name
sns.PLUGINS_LOADED,←⊂sns.conn name ⍝ marked before injecting: a failure must not retry per object
:If ~(⌊plugin.⎕NC'Inject')∊2 3 ⋄ →0 ⋄ :EndIf

js←,⊆plugin.Inject ⍝ a niladic function, or the JavaScript itself
top←(¯1+objname⍳'.')↑objname
:Trap 6 85
r←PLUGIN_TIMEOUT dWX(top 'EvalJS'),js
:If ∨/¯1=⊃¨,⊆r
'E'Log'Plugin "',name,'" injection failed: ',⍕r
:EndIf
:Else
'E'Log'Plugin "',name,'" injection got no response from the client'
:EndTrap
28 changes: 28 additions & 0 deletions EWC/sessionNS.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
r←caller sessionNS names;name;top;n;p
⍝ The per-session _EWC namespace for an object name, or ⎕NULL if there is none.
⍝ Desktop gives each Form's HTMLRenderer its own copy; Multi clones the whole
⍝ application namespace per connection; otherwise there is a single one, in
⍝ CODELOCATION. getConnection reads (ID conn) out of whatever this returns.
name←,⊃⊆names
n←≢p←(⍕caller),'.'
name←(n×p≡n↑name)↓name ⍝ drop caller prefix
top←(¯1+name⍳'.')↑name

:If 9=caller.⎕NC n←top,'.HR._EWC'
r←caller⍎n

:ElseIf MODE=2 ⍝ Multi
:If 9=caller.⎕NC'_EWC'
r←caller._EWC
:Else
r←⎕NULL
:EndIf

:Else
:Trap 0
r←CODELOCATION._EWC
:Else
r←⎕NULL
:EndTrap
:EndIf
1 change: 1 addition & 0 deletions EWC/setDefaults.aplf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ setDefaults
'BREAK' default ⍬ ⍬ ⍝ IDs Events
'SHOWDEVTOOLS' default 0
'FONTMAP' default FontMap
'PLUGIN_TIMEOUT' default 30 ⍝ Seconds to wait while a plugin's JS loads
57 changes: 57 additions & 0 deletions demo/DemoPluginRegistry.aplf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
DemoPluginRegistry connected;plugin;cls;js;r
⍝ The plugin mechanism end to end, with no external dependency: an EWC class
⍝ registered at runtime, its React component injected as JavaScript the first
⍝ time the class is used, and an event travelling from that component to APL.
EWC.CONNECTED←connected

CBPluginBox←{'F1.STATUS'eWS'Caption'('Select from ',(⊃⍵),' at ',⍕3↑3↓⎕TS)}

'F1'eWC'Form' 'Plugin registry'(50 50)(220 460)('Coord' 'Pixel')
'F1.L1'eWC'Label' 'The box below is drawn by injected JavaScript.'('Posn' 10 10)('Size' 20 440)
'F1.STATUS'eWC'Label' 'no event yet'('Posn' 170 10)('Size' 20 440)

plugin←⎕NS''
plugin.Name←'DemoBox'
plugin.Folder←EWC.FOLDER

plugin.Classes←⎕NS''
'PluginBox'plugin.Classes.⎕NS''
cls←plugin.Classes.PluginBox
cls.ClassName←'PluginBox'
cls.PropList←'Type' 'Posn' 'Size' 'Caption' 'Event' 'MethodList' 'ChildList' 'EventList' 'PropList'
cls.Defaults←('PluginBox')(10 10)(50 100)('')(⍬)(⍬)(⍬)(⍬)(cls.PropList)
cls.Supported←cls.PropList
cls.Dynamic←⍬
cls.methodlist←⍬
cls.SupportedEvents←,⊂'Select'

⍝ The delay is deliberate: it proves the client waits for an asynchronous
⍝ injection before handling the WC that follows.
js←'(async () => {'
js,←' await new Promise(r => setTimeout(r, 250));'
js,←' const R = window.EWC.React;'
js,←' window.EWC.registerComponent("PluginBox", ({ data, ewc }) => {'
js,←' const P = data.Properties || {};'
js,←' const style = Object.assign({}, ewc.utils.setStyle(P), {'
js,←' background: "#cfe8ff", border: "2px solid #0066cc", borderRadius: "6px",'
js,←' display: "flex", alignItems: "center", justifyContent: "center",'
js,←' cursor: "pointer", userSelect: "none" });'
js,←' const fire = () => ewc.socket.send(JSON.stringify('
js,←' { Event: { EventName: "Select", ID: data.ID } }));'
js,←' return R.createElement("div", { id: data.ID, style, onClick: fire }, P.Caption || "");'
js,←' });'
js,←' return "PluginBox registered";'
js,←'})()'
plugin.Inject←js

EWC.RegisterPlugin plugin

'F1.BOX'eWC'PluginBox'('Posn' 60 10)('Size' 55 440)('Caption' 'Click me - I am a plugin')('Event' 'Select' 'CBPluginBox')

⍝ A second object of the same class must reuse the loaded plugin: the log
⍝ should show one EvalJS, not two.
'F1.BOX2'eWC'PluginBox'('Posn' 120 10)('Size' 40 440)('Caption' 'and so am I')('Event' 'Select' 'CBPluginBox')

⍝ An injection that rejects must come back as ¯1, not hang until the timeout.
r←2 eNQ'F1' 'EvalJS' 'import("/no-such-plugin-bundle.js")'
'F1.REJECT'eWC'Label'('Caption'('rejected import rc=',⍕⊃⊃,⊆r))('Posn' 195 10)('Size' 20 440)