Design question: how can a plug-in run one-time initialization when it's added to a wiki?

Question

I’ve been recently discussing a collection of simple wikis containing full bible text. I have a handful available now. The idea is that users would build upon these wikis with their own tools. But I would like to supply a few extensions to both act as demos of how to create them, and to supply certain widely useful functionality that shouldn’t make it into the core.

I was wondering if there is any mechanism that these extensions can use to do some one-time initialization when they’re loaded. I haven’t seen anything, and I suspect not. But I’d like to hear a more definitive answer.

Example

For an example, one such extension would allow you to navigate via sections of the bible (Old/New Testament, Major Prophets, Gospels, etc. You can load any of these English-language wikis:

then download this, and drag the resulting file onto the wiki:

sections.json (3.4 KB)

There will now be a new entry in the sidebar TOC called “Sections”. That will expand to show these groups. But the books are not yet tagged as such. There is a button in $:/_/bible/sections/scripts/update-books that will add tags to every book with this information. This needs only be run once.

(This won’t work for the one Spanish wiki, Reina Valera Gómez, as it still needs internationalization.)

Recap

My question is whether there’s any way to automate this? Can an extension (now a JSON bundle, probably eventually a plugin) do this when its installed?

I don’t think I have tried to tackle this for wikitext only plugins that dont need a wiki save and reload.

It highlights that while we usually think of plugins as either those that are wikitext only and don’t need a wiki reload, and those that include JavaScript and need a wiki reload, there is a third category of plugins that are wikitext only but need to execute intialization actions, which would currently be startup actions. @jeremyruston have you considered actions executed when a plugin is loaded, such as when importing a wikitext only plugin? Or alternatively, a means for such wikitext only plugins to flag themselves as needing a wiki save and reload?

For plugins that do need a reload, a common limitation in all possible approaches is that unless you save the state in a tiddler and save the wiki, the wiki has no way of knowing if the wiki is starting up for the first time after a plugin installation.

I haven’t come up with a satisfactory way to meet such needs yet. You can wrap startup actions in a conditional expression that checks for the presence of a tiddler, and create that tiddler as part of the actions. However, this is suboptimal since the actions are still invoked every time, albeit skipping the code wrapped in the conditional expression.

For editions where I have a wizard or configuration guide, I’ve bundled it as a plugin and offered the user a “dont show this again option” that disabled the plugin. The proposed nested plugin feature will make this more elegant, eliminating the need for an entirely separate plugin for the startup intialization: Introducing bundled sub plugins by Jermolene · Pull Request #8957 · TiddlyWiki/TiddlyWiki5 · GitHub

1 Like

I would be fine if this required a save and reload. Adding the plugin would mean a reload is required at some point, anyway. But what I’d love for is to have some actions happen on load of the plugin, or before the save, or some such. Right now, I think it will have to be left to documentation like, “import the plugin, open tiddler xyz, click the button, save the wiki.” It’s not horrible, but I would love something smoother.

I’m thinking mine may be a fairly unusual case, because I’m expecting to add these plugins to wikis with nearly identical structures. I can expect the book of Exodus to have forty chapters and its fifth chapter to have 23 verses. So adding functionality can be done by tweaking many known tiddlers, without overwriting them. My example above adds tiddlers like Law and Gospels, then tags every Book tiddler with one of these.

Maybe its more common than I realize, but I haven’t seen any plugins that initialize by tweaking existing tiddlers without overriding them. Are there public examples of this?

I do something vaguely akin to that in the sample above. Once the user invokes the actions to add the tags, a tiddler is set/updated to value yes and the button and explanation is replaced with:

This script has already been run. No need to run it again. See $:/_/bible/sections/config/converted to change this setting.

In retrospect this should probably be a field on the tiddler with that button.

Oh, that definitely looks useful!

1 Like

I think it is quite easy to design around this. First ask yourself if there is something the user is going to do regularly or even the first time that is a trigger, then insert some code to test if it has being run yet or not (setup tiddler contains yes), if not, do your initialization, if yes, don’t. However your initialization can replace this trigger all together, replacing even the test for first run.

  • You can use the startup actions to do this kind if thing and you could package something that forces the save and reload prompt eg a trivial javascript, then the startup trigger will test for first time else, delete the startup tiddler.

one thought is to customise the caption of the TOC so that the first time it is clicked, it runs the initialize procedure and updates a flag so that future clicks don’t try to run init again.

I would be concerned that it would be hard for the core to offer any meaningful guarantees as to whether and when the actions would be executed. For example, a technique I use often on the server is to stitch the tiddlers into an empty TiddlyWiki using simple string concatenation, giving no opportunity for these import actions to be performed.

I think that we should aim to architect things so that wikitext plugins are always able to work without a save and reload.

I think background actions (almost) gives us a viable approach. The idea would be that the filter triggering the background task would detect the absence of a configuration tiddler that indicates that the background task has been run. The background task actions would perform the needed initialisation and then set the configuration tiddler to indicate that the task has been run.

I think in practice that for this to work we’d need to extend the background actions mechanism to allow actions to optionally be triggered when the result of a filter is not blank, rather than just on changes to the result of the filter.

One issue is that infinite loops would be very easy to set up inadvertently. I suspect we will need throttling and also some kind of panic button that suspends all background actions.

This may well be what I end up doing. The thing I don’t like about it—and, mind you, it’s a minor thing— is that there is a double-save involved: Install the plugin, require a save/reload, trigger changes with a startup action, and now the wiki needs to be saved again. It’s not likely to be bad for an extension that is designed to help you add/edit content. But the particular example I’ve cooked up here is only to organize the content, and perhaps make it easier to navigate. So there’s a good chance that a user installing it would have no other reason for that second save. But, again, this is minor. I can certainly live with it. I was just hoping that there was some technique the community had to make it unnecessary.

Hmm, that’s a new one! The other suggestions here are variants of things I’ve considered, but this is entirely new to me. I’ve never before attached specific behaviors to the clicking of a TOC. Is that straightforward to do?

If I want to trigger a required save/reload, I can add a trivial JS tiddler, as @TW_Tones pointed out. That wouldn’t bother me too much. But anything I can come up with requires an additional save, even if I do this. If the startup action triggers updates to tiddlers, even on a first-time only basis, there will still be another save necessary when that has happened.

When I posted the OP, I was getting very tired. I skipped writing some of the supporting material describing approaches I had tried or investigated. But I swear that at that time, I was trying to chase down a spare reference in the main site to “background actions”. I can’t recall exactly where it was, but somewhere when I was looking for more information about StartupActions. I searched in vain for more information. This morning, fresh, I can’t find that reference. Now it turns out that background actions are part of an unmerged PR! It was late, and I was tired, but still… was I just hallucinating?

Wouldn’t not-blank serve as the change of the result of a filter that had previously been blank? So if our background action is listening for a change to the nonexistent $:/plugins/me/name/initialized!!text, wouldn’t the creation of that tiddler with text no serve as a such a trigger?

I don’t think there has ever been a reference to background actions on the main site.

Yes, in this case. I was thinking of more complex scenarios that did not use a state tiddler, but on further reflection I’m starting to think that all of those scenarios can be massaged to work with the current trigger-on-change architecture. For example, a background task that processes all tiddlers with a particular tag that have a specified field value could use a filter such as:

[tag[mytag]myfield[aa]]

If you mean a save after the start up actions, well there always is a save after changes, if you want to keep them, and you could trigger a save within your start up actions, even a reload. But there is no reason to insist on this save right away.

I do think it would be nice if we introduced a mechanism to the core to support these more advanced installation steps just to make it a standard practice and help if something goes wrong by knowing where to look.

I’ve not tested it, but one way that might work is to override the toc-body macro with something like:

\define toc-body(tag,sort:"",itemClassFilter,exclude,path)
\whitespace trim
<ol class="tc-toc">
  <$list filter="""[all[shadows+tiddlers]tag<__tag__>!has[draft.of]$sort$] -[<__tag__>] -[subfilter<__exclude__>]""">
    <$let item=<<currentTiddler>> path={{{ [<__path__>addsuffix[/]addsuffix<__tag__>] }}}>
      <$set name="excluded" filter="[subfilter<__exclude__>] [<__tag__>]">
        <$set name="toc-item-class" filter=<<__itemClassFilter__>> emptyValue="toc-item-selected" value="toc-item">
          <li class=<<toc-item-class>>>
            <%if [all[current]get[processed]is[blank]] $>
              <$button >
                <<action-init-process>>
                <<toc-caption>>
              </$button>
            <%elseif [all[current]toc-link[no]] $>
              <<toc-caption>>
            <%else%>
              <$link to={{{ [<currentTiddler>get[target]else<currentTiddler>] }}}><<toc-caption>></$link>
            <%endif%>
            <$macrocall $name="toc-body" tag=<<item>> sort=<<__sort__>> itemClassFilter=<<__itemClassFilter__>> exclude=<<excluded>> path=<<path>>/>
          </li>
        </$set>
      </$set>
    </$let>
  </$list>
</ol>
\end
  • replace <<action-init-process>> with a call to your initialisation process which updates the field processed to restore normal functionality.
  • modify all[current]get[processed]is[blank]] to only return a value the tiddler is a plugin tiddler and whatever criteria you want to use.

That is very interesting. It looks rather heavy-weight for my needs here, but I have other places where that might help me do some things that I could not figure a way to do. Thank you very much!