From 226b50642af0895948fbaa623a9b7180399a63b6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 13 May 2026 19:15:48 +0000
Subject: [PATCH] =Queue fixes
---
assets/css/nav.min.css | 2
inc/managers/queue/Processor.php | 43 +--
activate.php | 1
inc/managers/ScriptLoader.php | 2
inc/meta/Meta.php | 8
inc/integrations/PostMark.php | 4
inc/managers/queue/Locker.php | 42 +--
inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php | 1
inc/admin/SEOAdmin.php | 6
inc/helpers/all.php | 2
inc/managers/queue/Storage.php | 442 +++++++++++++++++++--------------------
inc/managers/queue/Queue.php | 85 -------
inc/rest/routes/FormRoutes.php | 1
13 files changed, 267 insertions(+), 372 deletions(-)
diff --git a/activate.php b/activate.php
index 65ec6a9..c76a65d 100644
--- a/activate.php
+++ b/activate.php
@@ -30,7 +30,6 @@
do_action(BASE.'activation');
error_log('Action done!');
error_log('Checking custom tables...');
- Queue::defineTables();
CustomTable::ensureTables();
error_log('Dashboard is setup: '.print_r(JVB()->dashboard(), true));
diff --git a/assets/css/nav.min.css b/assets/css/nav.min.css
index de0c4f6..65848db 100644
--- a/assets/css/nav.min.css
+++ b/assets/css/nav.min.css
@@ -1 +1 @@
-nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>.toggle{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}
\ No newline at end of file
+nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;padding:0;margin:0}nav li{width:100%;--justify:center;max-inline-size:none;padding:0;list-style:none}nav a,nav button{--justify:center;width:100%;white-space:nowrap;text-transform:uppercase;border-radius:0;background-color:transparent;text-decoration:none}nav a{padding:var(--padding)}nav .toggle{aspect-ratio:1;border:1px solid var(--base);color:var(--contrast)}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.open>.row>.toggle .icon-caret-down,.open>.toggle .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:max-height var(--trans-base),transform var(--trans-base);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:1px solid var(--action-0);outline-offset:1px}nav.always{--dir:column;--justify:flex-end;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.always>ul{z-index:1;--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;max-height:100%;position:relative;right:-300vw;width:100vw;padding:var(--btn) 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{--wrap:wrap;--dir:row;height:max-content;--justify:flex-start;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;--justify:center;max-width:calc(100% - var(--btn))}nav.always .has-submenu>a{z-index:var(--z-3)}nav.always .has-submenu>button{width:var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;top:0;border:1px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>.toggle{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>.toggle{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>.toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always button .icon-x,nav.always.open button .icon-list{display:none}nav.always button .icon-list,nav.always.open button .icon-x{display:block;--w:32px}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-5)}nav#breadcrumbs ol{height:max-content;--wrap:wrap}nav#breadcrumbs li{width:max-content;height:var(--chip);--wrap:nowrap}nav#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}nav#breadcrumbs li:last-of-type::after{display:none}nav#breadcrumbs a{height:var(--chip)}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;color:var(--contrast);text-transform:none}nav#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed{position:fixed;left:0;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom{bottom:0;width:calc(100% - var(--btn))}nav.fixed ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.always.fixed>ul{padding-top:var(--btn)}nav.fixed li{flex:1}nav.fixed a{--gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:100vw;z-index:var(--z-5);background-color:rgba(var(--base-rgb),var(--op-45));max-width:none}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}nav.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset}nav.on-this-page.open button{order:0}nav.on-this-page ul{width:100%;--gap:0}nav.on-this-page a{padding:0}nav.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}nav.on-this-page #back-to-top span{display:none}nav.on-this-page .active a{background-color:var(--action-0);color:var(--action-contrast)}nav.letters,nav.letters a,nav.letters li,nav.letters ul{height:var(--chip)}nav.letters li{max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}nav.index ul{width:100%}nav.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}nav.index li.active,nav.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}nav.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}nav.index a{border-bottom:4px solid transparent}nav.index .active a{border-color:var(--action-0);color:var(--contrast)}nav.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;--align:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}nav.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}nav.index.open a{--justify:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed,nav.condensed a,nav.condensed li,nav.condensed ul{height:max-content;width:max-content;--wrap:wrap;min-height:var(--chip)}nav.condensed{--gap:0 .25rem;width:100%}nav.condensed li+li::before{content:'·';padding:0 .25em}nav.condensed a{font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:transparent;color:var(--contrast);border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{padding-bottom:2px;touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button.active{cursor:default}nav.tabs button{font-family:var(--heading);font-size:var(--txt-x-small);border-bottom:4px solid transparent}nav.tabs button.active,nav.tabs button.active:hover{background-color:var(--action-0);color:var(--action-contrast);border-color:var(--base)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}.wp-site-blocks>header,body>header{--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}.dashboard-nav{width:100%}nav.filters{--dir:row;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem}nav.term-navigation:has([hidden]){display:none}
\ No newline at end of file
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index 45e215f..007e453 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -145,7 +145,7 @@
public function addDashboardSection(string $content, string $page):string
{
- if ($page !== 'jvb-seo') {
+ if ($page !== 'SEO') {
return $content;
}
ob_start();
@@ -203,6 +203,10 @@
$this->renderStyles();
}
}
+ private function renderStyles(): void
+ {
+ jvbInlineStyles('forms');
+ }
public function renderProperty(string $property, ?string $value, mixed $class):void
{
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 9d59203..67d79fe 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -54,6 +54,8 @@
// delete_option(BASE.'do_these_once');
//Ensure we have the option starting with BASE
$option = jvbCheckBase($option);
+// delete_option($option);
+// delete_option(BASE.'do_these_once');
$options = get_option(BASE.'do_these_once', []);
// delete_option($option);
if (!array_key_exists($option, $options)) {// Prevent concurrent runs
diff --git a/inc/integrations/PostMark.php b/inc/integrations/PostMark.php
index 9882d6c..7ac31e6 100644
--- a/inc/integrations/PostMark.php
+++ b/inc/integrations/PostMark.php
@@ -166,10 +166,12 @@
$result = $this->sendEmail($payload);
if ($result === true) {
+ error_log('================================ Email sent! ================================');
// Prevent default wp_mail from sending
add_filter('pre_wp_mail', '__return_true');
do_action('postmark_email_sent', $args, $payload);
} else {
+ error_log('=-======================[POSTMARK]Something went wrong... ================================');
// Log failure but allow fallback to default mail
do_action('postmark_email_failed', $args, $result);
@@ -263,7 +265,7 @@
}
try {
$response = $this->postRequest('email', $payload);
-
+ error_log('================================ POSTMARK RESPONSE: ================================'.print_r($response, true));
if (is_wp_error($response)) {
return $response;
}
diff --git a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
index 6965d2e..f6db0b9 100644
--- a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
+++ b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
@@ -2,3 +2,4 @@
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php');
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/State.php');
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index 90de1d6..dc66aaa 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -2,7 +2,7 @@
add_action('init', 'jvbRegisterScripts', 5);
function jvbRegisterScripts() {
- $version = '1.1.5';
+ $version = '1.1.55';
$strategy = [
'strategy' => 'defer',
'in_footer' => true
diff --git a/inc/managers/queue/Locker.php b/inc/managers/queue/Locker.php
index a048942..4d4b5d6 100644
--- a/inc/managers/queue/Locker.php
+++ b/inc/managers/queue/Locker.php
@@ -1,43 +1,24 @@
<?php
namespace JVBase\managers\queue;
-use wpdb;
if (!defined('ABSPATH')) {
exit;
}
-
class Locker
{
private string $lockKey;
private int $timeout;
- protected wpdb $wpdb;
private ?string $token = null;
- public function __construct(string $key = 'queue', int $timeout = 0)
+ public function __construct(string $key = 'queue', int $timeout = 60)
{
$this->lockKey = BASE . $key . '_lock';
$this->timeout = $timeout;
- global $wpdb;
- $this->wpdb = $wpdb;
}
- /**
- * Execute callback with lock, auto-release after
- */
public function withLock(callable $callback): void
{
- $acquired = $this->wpdb->get_var(
- $this->wpdb->prepare(
- 'SELECT GET_LOCK(%s, %d)',
- $this->lockKey,
- $this->timeout
- )
- );
-
- if ((int) $acquired !== 1) {
- // Lock already held — just exit quietly
- return;
- }
+ if (!$this->acquire()) return;
try {
$callback();
@@ -46,13 +27,18 @@
}
}
- public function unlock():void
+ private function acquire(): bool
{
- $this->wpdb->get_var(
- $this->wpdb->prepare(
- 'SELECT RELEASE_LOCK(%s)',
- $this->lockKey
- )
- );
+ $this->token = bin2hex(random_bytes(8));
+ return (bool) wp_cache_add($this->lockKey, $this->token, 'locks', $this->timeout);
+ }
+
+ public function unlock(): void
+ {
+ $current = wp_cache_get($this->lockKey, 'locks');
+ if ($current === $this->token) {
+ wp_cache_delete($this->lockKey, 'locks');
+ }
+ $this->token = null;
}
}
diff --git a/inc/managers/queue/Processor.php b/inc/managers/queue/Processor.php
index ccc4ddb..f54fadd 100644
--- a/inc/managers/queue/Processor.php
+++ b/inc/managers/queue/Processor.php
@@ -14,48 +14,39 @@
public function run(): void
{
- if (get_transient(BASE.'queue_running')) {
- return;
- }
- set_transient(BASE.'queue_running', true, 60);
if (!$this->hasAdequateResources()) {
error_log('[Processor] Insufficient resources to start processing');
return;
}
- $ops = $this->storage->fetchRunnable();
- if (empty($ops)) {
- return;
- }
- foreach ($ops as $op) {
- if ($op->state === 'completed') {
- return;
+ $op = null;
+ $this->storage->withTransaction(function() use (&$op) {
+ $candidates = $this->storage->fetchRunnable();
+ foreach ($candidates as $candidate) {
+ if ($candidate->state === 'completed') continue;
+ if (!$this->dependenciesSatisfied($candidate)) continue;
+ if ($this->storage->markProcessing($candidate->id)) {
+ $op = $candidate;
+ break;
+ }
}
- if (!$this->dependenciesSatisfied($op)) {
- continue;
- }
- if (!$this->storage->markProcessing($op->id)) {
- continue;
- }
- $this->processOne($op);
- usleep(10000);
- }
+ });
+
+ if (!$op) return;
+
+ $this->processOne($op);
+ usleep(10000);
$this->storage->invalidateQueueCache();
}
+
private function processOne(Operation $op): void
{
- if (get_transient(BASE.$op->id)) {
- return;
- }
- set_transient(BASE.$op->id, true, 500);
$progress = new Progress($op);
-
$executor = $this->registry->getExecutor($op->type) ?? $this->defaultExecutor;
$op->startedAt = current_time('mysql');
$op->state = 'processing';
-
$this->storage->saveProgress($op);
try {
diff --git a/inc/managers/queue/Queue.php b/inc/managers/queue/Queue.php
index 95b7afb..89a8ceb 100644
--- a/inc/managers/queue/Queue.php
+++ b/inc/managers/queue/Queue.php
@@ -19,7 +19,6 @@
public function __construct()
{
- $this->defineTables();
$this->storage = new Storage();
$this->registry = new TypeRegistry();
$this->locker = new Locker();
@@ -29,6 +28,7 @@
add_action('jvb_process_queue', [$this, 'checkQueue']);
add_action('jvb_queue_maintenance', [$this, 'maintenance']);
+ add_action('jvb_daily_snapshot', [$this->storage, 'snapshotDaily']);
if (!wp_next_scheduled('jvb_process_queue')) {
wp_schedule_event(time(), 'every-minute', 'jvb_process_queue');
@@ -36,89 +36,16 @@
if (!wp_next_scheduled('jvb_queue_maintenance')) {
wp_schedule_event(time(), 'hourly', 'jvb_queue_maintenance');
}
+ if (!wp_next_scheduled('jvb_daily_snapshot')) {
+ // Schedule for next 3am
+ $next3am = strtotime('tomorrow 3am', current_time('timestamp'));
+ wp_schedule_event($next3am, 'daily', 'jvb_daily_snapshot');
+ }
jvb_register_do_once('queue_admin_action_registered', [$this, 'registerAdminAction']);
add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3);
}
- public static function defineTables():void
- {
- $queue = CustomTable::for('_operation_queue');
- $queue->setColumns([
- 'id' => 'VARCHAR(64) NOT NULL',
- 'type' => 'VARCHAR(50) NOT NULL',
- 'user_id' => $queue->getUserIDType().' NOT NULL',
-
- 'request_data' => 'JSON NOT NULL CHECK (JSON_VALID(request_data))',
-
- 'total_items' => 'INT(11) NOT NULL DEFAULT 1',
- 'processed_items' => 'INT(11) DEFAULT 0',
- 'failed_items' => 'JSON',
-
- 'priority' => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
- 'state' => 'ENUM(\'pending\', \'scheduled\', \'processing\', \'completed\') DEFAULT \'pending\'',
- 'outcome' => 'ENUM(\'pending\', \'success\',\'partial\',\'merged\',\'failed\',\'failed_permanent\') DEFAULT \'pending\'',
-
- 'retries' => 'INT(11) DEFAULT 0',
- 'last_error_hash'=> 'CHAR(32) DEFAULT NULL',
- 'error_message' => 'TEXT',
-
- 'scheduled_at' => 'DATETIME DEFAULT NULL',
- 'started_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
- 'completed_at' => 'DATETIME DEFAULT NULL',
-
- 'metadata' => 'JSON DEFAULT NULL',
- 'result' => 'JSON',
- 'dependencies' => 'JSON',
- 'merged_into' => 'VARCHAR(64) DEFAULT NULL',
-
- 'user_dismissed'=> 'tinyint(1) DEFAULT 0',
- 'created_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
- 'updated_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
- ]);
-
- $queue->setKeys([
- ['key' => 'PRIMARY', 'value' => '(`id`)'],
- '`idx_run_queue` (`state`, `priority`, `scheduled_at`)',
- '`idx_user_ops` (`user_id`, `state`)',
- '`idx_user_type_pending` (`user_id`, `type`, `state`)',
- '`idx_completed_at` (`completed_at`)',
- '`idx_processing_stuck` (`state`, `started_at`)'
- ]);
-
- $queue->defineTable();
-
- $stats = CustomTable::for('stats__operation_queue');
- $stats->setColumns([
- 'id' => 'BIGINT unsigned AUTO_INCREMENT',
- 'date' => 'DATE NOT NULL',
- 'type' => 'VARCHAR(50) NOT NULL',
-
- 'total_operations' => 'INT NOT NULL DEFAULT 0',
- 'successful_operations' => 'INT NOT NULL DEFAULT 0',
- 'partial_operations' => 'INT NOT NULL DEFAULT 0',
- 'failed_operations' => 'INT NOT NULL DEFAULT 0',
- 'failed_permanent_operations'=> 'INT NOT NULL DEFAULT 0',
- 'total_items_processed' => 'INT NOT NULL DEFAULT 0',
-
- 'average_duration' => 'FLOAT DEFAULT NULL',
- 'max_duration' => 'INT DEFAULT NULL',
- 'peak_queue_size' => 'INT NOT NULL DEFAULT 0',
- 'peak_memory_usage' => 'INT DEFAULT NULL',
- 'peak_cpu_usage' => 'FLOAT DEFAULT NULL',
-
- 'created_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
- ]);
-
- $stats->setKeys([
- ['key' => 'PRIMARY', 'value' => '(`id`)'],
- ['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
- '`date_idx` (`date`)',
- '`type_idx` (`type`)'
- ]);
- $stats->defineTable();
- }
-
/**
* Access type registry for registering operation configs
*/
diff --git a/inc/managers/queue/Storage.php b/inc/managers/queue/Storage.php
index 40426e6..2a5d540 100644
--- a/inc/managers/queue/Storage.php
+++ b/inc/managers/queue/Storage.php
@@ -5,48 +5,39 @@
}
use JVBase\managers\Cache;
+use JVBase\managers\CustomTable;
use LogicException;
class Storage
{
- private \wpdb $wpdb;
- private string $table;
+ private CustomTable $table;
+ private CustomTable $stats;
private Cache $cache;
private const CACHE_QUEUE_INFO = 'queue_info';
public function __construct()
{
- global $wpdb;
- $this->wpdb = $wpdb;
- $this->table = $wpdb->prefix . BASE . '_operation_queue';
+ $this->defineTables();
$this->cache = Cache::for('queue', DAY_IN_SECONDS);
}
public function hasProcessingOperations(): bool
{
- return (bool) $this->wpdb->get_var(
- "SELECT 1 FROM {$this->table} WHERE state = 'processing' LIMIT 1"
- );
+ return (bool) $this->table->queryVar("SELECT 1 FROM {table} WHERE state = 'processing' LIMIT 1");
}
public function fetchRunnable(int $offset = 0): array
{
$now = current_time('mysql');
-
- $rows = $this->wpdb->get_results(
- $this->wpdb->prepare("
- SELECT *
- FROM {$this->table}
- WHERE state IN ('pending', 'scheduled')
- AND scheduled_at <= %s
- ORDER BY
- FIELD(priority, 'high', 'normal', 'low'),
- scheduled_at
- LIMIT 10 OFFSET %d
- FOR UPDATE SKIP LOCKED
- ", $now, $offset)
- );
+ $rows = $this->table->queryResults("
+ SELECT * FROM {table}
+ WHERE state IN ('pending', 'scheduled')
+ AND scheduled_at <= %s
+ ORDER BY FIELD(priority, 'high', 'normal', 'low'), scheduled_at
+ LIMIT 10 OFFSET %d
+ FOR UPDATE SKIP LOCKED
+ ", [$now, $offset]);
$total = count($rows);
foreach ($rows as $row) {
@@ -88,26 +79,22 @@
public function markProcessing(string $id): bool
{
$now = current_time('mysql');
-
- $affected = $this->wpdb->query($this->wpdb->prepare("
- UPDATE {$this->table}
- SET state = 'processing', started_at = %s, updated_at = %s
- WHERE id = %s AND state IN ('pending', 'scheduled')
- ", $now, $now, $id));
+ $affected = $this->table->query("
+ UPDATE {table}
+ SET state = 'processing', started_at = %s, updated_at = %s
+ WHERE id = %s AND state IN ('pending', 'scheduled')
+ ", [$now, $now, $id]);
if ($affected > 0) {
$op = $this->find($id);
- if ($op) {
- $this->invalidateUser($op->userId);
- }
+ if ($op) $this->invalidateUser($op->userId);
}
-
return $affected > 0;
}
public function save(Operation $op): bool
{
- $data = [
+ $result = $this->table->update([
'request_data' => json_encode($op->requestData),
'total_items' => $op->totalItems,
'processed_items' => $op->processedItems,
@@ -118,23 +105,17 @@
'retries' => $op->retries,
'last_error_hash' => $op->lastErrorHash,
'error_message' => $op->errorMessage,
- 'scheduled_at' => $op->scheduledAt,
- 'started_at' => $op->startedAt,
'completed_at' => $op->completedAt,
'metadata' => json_encode($op->metadata),
'result' => $op->result ? json_encode($op->result) : null,
'dependencies' => json_encode($op->dependencies),
'merged_into' => $op->merged_into,
'user_dismissed' => $op->userDismissed ? 1 : 0,
- 'updated_at' => current_time('mysql'),
- ];
+ ],
+ ['id' => $op->id]
+ );
- $result = $this->wpdb->update($this->table, $data, ['id' => $op->id]);
-
- if ($result !== false) {
- $this->invalidateUser($op->userId);
- }
-
+ if ($result !== false) $this->invalidateUser($op->userId);
return $result !== false;
}
@@ -143,35 +124,17 @@
* @param Operation $op
* @return bool
*/
- public function saveProgress(Operation $op): bool {
- global $wpdb;
-
- $table = $this->table;
-
- $data = [
+ public function saveProgress(Operation $op): bool
+ {
+ $result = $this->table->update([
'processed_items' => $op->processedItems ?? 0,
'failed_items' => $op->failedItems ? json_encode($op->failedItems) : null,
- 'metadata' => ($op->metadata) ? json_encode($op->metadata) : null,
- 'result' => ($op->result) ? json_encode($op->result) :null,
- 'updated_at' => current_time('mysql'),
- ];
+ 'metadata' => $op->metadata ? json_encode($op->metadata) : null,
+ 'result' => $op->result ? json_encode($op->result) : null,
+ ], ['id' => $op->id, 'state' => 'processing']); // state guard preserved
- // IMPORTANT: never touch terminal state
- $where = [
- 'id' => $op->id,
- 'state' => 'processing',
- ];
-
- $updated = $wpdb->update($table, $data, $where);
-
- if ($updated === false) {
- error_log('[Storage::saveProgress] DB error: ' . $wpdb->last_error);
- return false;
- }
-
-
+ if ($result === false) return false;
$this->invalidateUser($op->userId);
-
return true;
}
@@ -180,50 +143,32 @@
* @param Operation $op
* @return bool
*/
- public function saveFinal(Operation $op): bool {
- global $wpdb;
-
- if (($op->state?? null) !== 'completed') {
+ public function saveFinal(Operation $op): bool
+ {
+ if ($op->state !== 'completed') {
throw new LogicException('saveFinal called without completed state');
}
- $table = $this->table;
+ $result = $this->table->update([
+ 'state' => 'completed',
+ 'outcome' => $op->outcome ?? 'success',
+ 'processed_items' => $op->processedItems ?? 0,
+ 'failed_items' => $op->failedItems ? json_encode($op->failedItems) : null,
+ 'result' => isset($op->result) ? wp_json_encode($op->result) : null,
+ 'completed_at' => $op->completedAt ?? current_time('mysql'),
+ ], ['id' => $op->id, 'state' => 'processing']); // hard guard preserved
- $data = [
- 'state' => 'completed',
- 'outcome' => $op->outcome?? 'success',
- 'processed_items'=> $op->processedItems ?? 0,
- 'failed_items' => $op->failedItems ? json_encode($op->failedItems) : null,
- 'result' => isset($op->result) ? wp_json_encode($op->result) : null,
- 'completed_at' => $op->completedAt ?? current_time('mysql'),
- 'updated_at' => current_time('mysql'),
- ];
+ if ($result === 0) return true; // already completed, not an error
+ if ($result === false) return false;
- // HARD GUARD: cannot overwrite completed
- $where = [
- 'id' => $op->id,
- 'state' => 'processing',
- ];
-
- $updated = $wpdb->update($table, $data, $where);
-
- if ($updated === 0) {
- return true;
- }
-
- if ($updated === false) {
- error_log('[Storage::saveFinal] DB error: ' . $wpdb->last_error);
- return false;
- }
$this->invalidateQueueCache();
$this->invalidateUser($op->userId);
-
return true;
}
public function insert(Operation $op): bool
{
- $result = $this->wpdb->insert($this->table, [
+ $result = $this->table->insert([
'id' => $op->id,
'type' => $op->type,
'user_id' => $op->userId,
@@ -235,18 +180,10 @@
'state' => $op->state,
'outcome' => $op->outcome,
'retries' => 0,
- 'last_error_hash' => null,
- 'error_message' => null,
'scheduled_at' => $op->scheduledAt ?? current_time('mysql'),
- 'started_at' => null,
- 'completed_at' => null,
'metadata' => json_encode($op->metadata),
- 'result' => null,
'dependencies' => json_encode($op->dependencies),
- 'user_dismissed' => 0,
'merged_into' => null,
- 'created_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql'),
]);
if ($result) {
@@ -258,36 +195,25 @@
public function find(string $id): ?Operation
{
- $row = $this->wpdb->get_row($this->wpdb->prepare(
- "SELECT * FROM {$this->table} WHERE id = %s",
- $id
- ));
-
+ $row = $this->table->get(['id' => $id]);
return $row ? $this->rowToOperation($row) : null;
}
public function findMergeable(string $type, int $userId, array $criteria = []): ?Operation
{
- $sql = "SELECT * FROM {$this->table}
- WHERE type = %s AND user_id = %d AND state IN ('pending', 'scheduled')";
+ $sql = "SELECT * FROM {table} WHERE type = %s AND user_id = %d AND state IN ('pending', 'scheduled')";
$params = [$type, $userId];
foreach ($criteria as $key => $value) {
- if ($value === null) {
- continue;
- }
+ if ($value === null) continue;
$sql .= " AND JSON_UNQUOTE(JSON_EXTRACT(request_data, %s)) = %s";
$params[] = '$.' . $key;
$params[] = (string) $value;
}
-
$sql .= " ORDER BY created_at DESC LIMIT 1";
- $row = $this->wpdb->get_row($this->wpdb->prepare($sql, ...$params));
-
- $this->invalidateUser($userId);
-
- return $row ? $this->rowToOperation($row) : null;
+ $rows = $this->table->queryResults($sql, $params);
+ return !empty($rows) ? $this->rowToOperation($rows[0]) : null;
}
public function getUserOperations(int $userId, array $filters = []): array
@@ -328,14 +254,12 @@
// Order by state priority, then created_at
$orderBy = $filters['order_by'] ?? "FIELD(state, 'processing', 'pending', 'scheduled', 'completed'), created_at DESC";
- $limit = $filters['limit'] ?? 50;
- $params[] = $limit;
+ $params[] = $filters['limit'] ?? 50;
- $rows = $this->wpdb->get_results($this->wpdb->prepare(
- "SELECT * FROM {$this->table} WHERE " . implode(' AND ', $where) .
- " ORDER BY {$orderBy} LIMIT %d",
- ...$params
- ));
+ $rows = $this->table->queryResults(
+ "SELECT * FROM {table} WHERE " . implode(' AND ', $where) . " ORDER BY {$orderBy} LIMIT %d",
+ $params
+ );
return array_map([$this, 'rowToOperation'], $rows ?: []);
}
@@ -343,19 +267,17 @@
public function getQueueInfo(): array
{
$cached = $this->cache->get(self::CACHE_QUEUE_INFO);
- if ($cached !== false) {
- return $cached;
- }
+ if ($cached !== false) return $cached;
$now = current_time('mysql');
- $row = $this->wpdb->get_row($this->wpdb->prepare("
- SELECT
- COUNT(*) as total,
- SUM(IF(state IN ('pending', 'processing'), 1, 0)) as active,
- SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
- FROM {$this->table}
- WHERE state IN ('pending', 'processing', 'scheduled')
- ", $now));
+ $row = $this->table->queryResults("
+ SELECT COUNT(*) as total,
+ SUM(IF(state IN ('pending', 'processing'), 1, 0)) as active,
+ SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready_scheduled
+ FROM {table}
+ WHERE state IN ('pending', 'processing', 'scheduled')
+ ", [$now]);
+ $row = $row[0] ?? null;
$info = [
'total' => (int) ($row->total ?? 0),
@@ -399,33 +321,28 @@
public function getQueueStatus(): array
{
$now = current_time('mysql');
+ $rows = $this->table->queryResults("
+ SELECT state, COUNT(*) as count,
+ SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready
+ FROM {table} GROUP BY state
+ ", [$now]);
- $rows = $this->wpdb->get_results($this->wpdb->prepare("
- SELECT
- state,
- COUNT(*) as count,
- SUM(IF(state = 'scheduled' AND scheduled_at <= %s, 1, 0)) as ready
- FROM {$this->table}
- GROUP BY state
- ", $now), OBJECT_K);
-
+ $indexed = array_column($rows, null, 'state');
return [
- 'pending' => (int) ($rows['pending']->count ?? 0),
- 'scheduled' => (int) ($rows['scheduled']->count ?? 0),
- 'scheduled_ready' => (int) ($rows['scheduled']->ready ?? 0),
- 'processing' => (int) ($rows['processing']->count ?? 0),
- 'completed' => (int) ($rows['completed']->count ?? 0),
+ 'pending' => (int) ($indexed['pending']->count ?? 0),
+ 'scheduled' => (int) ($indexed['scheduled']->count ?? 0),
+ 'scheduled_ready' => (int) ($indexed['scheduled']->ready ?? 0),
+ 'processing' => (int) ($indexed['processing']->count ?? 0),
+ 'completed' => (int) ($indexed['completed']->count ?? 0),
];
}
public function getUserStats(int $userId): array
{
- $rows = $this->wpdb->get_results($this->wpdb->prepare("
- SELECT state, outcome, COUNT(*) as count
- FROM {$this->table}
- WHERE user_id = %d
- GROUP BY state, outcome
- ", $userId));
+ $rows = $this->table->queryResults(
+ "SELECT state, outcome, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY state, outcome",
+ [$userId]
+ );
$stats = [
'pending' => 0,
@@ -458,13 +375,7 @@
public function dismiss(string $id): bool
{
- $result = $this->wpdb->update(
- $this->table,
- ['user_dismissed' => 1, 'updated_at' => current_time('mysql')],
- ['id' => $id]
- );
-
- return $result !== false;
+ return $this->table->update(['user_dismissed' => 1], ['id' => $id]) !== false;
}
/**
@@ -473,14 +384,8 @@
public function delete(string $id): bool
{
$op = $this->find($id);
- $userId = $op?->userId;
-
- $result = $this->wpdb->delete($this->table, ['id' => $id]);
-
- if ($result && $userId) {
- $this->invalidateUser($userId);
- }
-
+ $result = $this->table->delete(['id' => $id]);
+ if ($result && $op) $this->invalidateUser($op->userId);
return $result !== false;
}
@@ -493,21 +398,16 @@
{
Cache::for($userId.'_queue')->flush();
}
- public function getLastError(): string
- {
- return $this->wpdb->last_error;
- }
public function withTransaction(callable $callback): mixed
{
- $this->wpdb->query('START TRANSACTION');
-
+ $this->table->startTransaction();
try {
$result = $callback();
- $this->wpdb->query('COMMIT');
+ $this->table->commit();
return $result;
} catch (\Throwable $e) {
- $this->wpdb->query('ROLLBACK');
+ $this->table->rollback();
error_log('[Storage] Transaction rolled back: ' . $e->getMessage());
throw $e;
}
@@ -518,36 +418,19 @@
return $this->withTransaction(function () use ($stuckMinutes) {
$cutoff = date('Y-m-d H:i:s', strtotime("-{$stuckMinutes} minutes"));
- // Get IDs first for logging
- $stuckIds = $this->wpdb->get_col($this->wpdb->prepare("
- SELECT id FROM {$this->table}
- WHERE state = 'processing'
- AND started_at < %s
- FOR UPDATE
- ", $cutoff));
+ $stuckIds = $this->table->queryResults(
+ "SELECT id FROM {table} WHERE state = 'processing' AND started_at < %s FOR UPDATE",
+ [$cutoff]
+ );
+ $stuckIds = array_column($stuckIds, 'id');
- if (empty($stuckIds)) {
- return 0;
- }
+ if (empty($stuckIds)) return 0;
- // Reset them
- $affected = $this->wpdb->query($this->wpdb->prepare("
- UPDATE {$this->table}
- SET state = 'scheduled',
- scheduled_at = %s,
- retries = retries + 1,
- updated_at = %s
- WHERE id IN (" . implode(',', array_fill(0, count($stuckIds), '%s')) . ")
- ",
- date('Y-m-d H:i:s', time() + 60),
- current_time('mysql'),
- ...$stuckIds
- ));
-
- // Optional: Log to audit table
- // $this->logStuckReset($stuckIds);
-
- return (int) $affected;
+ $placeholders = implode(',', array_fill(0, count($stuckIds), '%s'));
+ return (int) $this->table->query(
+ "UPDATE {table} SET state = 'scheduled', scheduled_at = %s, retries = retries + 1, updated_at = %s WHERE id IN ({$placeholders})",
+ array_merge([date('Y-m-d H:i:s', time() + 60), current_time('mysql')], $stuckIds)
+ );
});
}
@@ -557,23 +440,124 @@
public function replaceDependency(string $fromId, string $toId): int
{
return $this->withTransaction(function () use ($fromId, $toId) {
-
- // Only affect pending/scheduled operations
- $affected = $this->wpdb->query($this->wpdb->prepare("
- UPDATE {$this->table}
- SET dependencies = REPLACE(dependencies, %s, %s),
- updated_at = %s
- WHERE state IN ('pending', 'scheduled')
- AND dependencies LIKE %s
- ",
- '"' . $fromId . '"',
- '"' . $toId . '"',
- current_time('mysql'),
- '%"' . $fromId . '"%'
- ));
-
- return (int) $affected;
+ return (int) $this->table->query(
+ "UPDATE {table} SET dependencies = REPLACE(dependencies, %s, %s), updated_at = %s WHERE state IN ('pending', 'scheduled') AND dependencies LIKE %s",
+ ['"'.$fromId.'"', '"'.$toId.'"', current_time('mysql'), '%"'.$fromId.'"%']
+ );
});
}
+ public function defineTables():void
+ {
+ $queue = CustomTable::for('_operation_queue');
+ $queue->setColumns([
+ 'id' => 'VARCHAR(64) NOT NULL',
+ 'type' => 'VARCHAR(50) NOT NULL',
+ 'user_id' => $queue->getUserIDType().' NOT NULL',
+
+ 'request_data' => 'JSON NOT NULL CHECK (JSON_VALID(request_data))',
+
+ 'total_items' => 'INT(11) NOT NULL DEFAULT 1',
+ 'processed_items' => 'INT(11) DEFAULT 0',
+ 'failed_items' => 'JSON',
+
+ 'priority' => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'',
+ 'state' => 'ENUM(\'pending\', \'scheduled\', \'processing\', \'completed\') DEFAULT \'pending\'',
+ 'outcome' => 'ENUM(\'pending\', \'success\',\'partial\',\'merged\',\'failed\',\'failed_permanent\') DEFAULT \'pending\'',
+
+ 'retries' => 'INT(11) DEFAULT 0',
+ 'last_error_hash'=> 'CHAR(32) DEFAULT NULL',
+ 'error_message' => 'TEXT',
+
+ 'scheduled_at' => 'DATETIME DEFAULT NULL',
+ 'started_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
+ 'completed_at' => 'DATETIME DEFAULT NULL',
+
+ 'metadata' => 'JSON DEFAULT NULL',
+ 'result' => 'JSON',
+ 'dependencies' => 'JSON',
+ 'merged_into' => 'VARCHAR(64) DEFAULT NULL',
+
+ 'user_dismissed'=> 'tinyint(1) DEFAULT 0',
+ 'created_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
+ 'updated_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
+ ]);
+
+ $queue->setKeys([
+ ['key' => 'PRIMARY', 'value' => '(`id`)'],
+ '`idx_run_queue` (`state`, `priority`, `scheduled_at`)',
+ '`idx_user_ops` (`user_id`, `state`)',
+ '`idx_user_type_pending` (`user_id`, `type`, `state`)',
+ '`idx_completed_at` (`completed_at`)',
+ '`idx_processing_stuck` (`state`, `started_at`)'
+ ]);
+
+ $queue->defineTable();
+ $this->table = $queue;
+
+ $stats = CustomTable::for('stats__operation_queue');
+ $stats->setColumns([
+ 'id' => 'BIGINT unsigned AUTO_INCREMENT',
+ 'date' => 'DATE NOT NULL',
+ 'type' => 'VARCHAR(50) NOT NULL',
+
+ // Only store what can't be queried from the main table later
+ 'peak_queue_size' => 'INT NOT NULL DEFAULT 0',
+ 'peak_memory_bytes' => 'BIGINT DEFAULT NULL',
+
+ // Snapshot totals for post-purge historical view
+ 'total_operations' => 'INT NOT NULL DEFAULT 0',
+ 'successful_operations' => 'INT NOT NULL DEFAULT 0',
+ 'failed_permanent_operations' => 'INT NOT NULL DEFAULT 0',
+ 'total_items_processed' => 'INT NOT NULL DEFAULT 0',
+
+ 'created_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
+ 'updated_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
+ ]);
+
+ $stats->setKeys([
+ ['key' => 'PRIMARY', 'value' => '(`id`)'],
+ ['key' => 'UNIQUE', 'value' => '(`date`, `type`)'],
+ '`date_idx` (`date`)',
+ '`type_idx` (`type`)'
+ ]);
+ $stats->defineTable();
+
+ $this->stats = $stats;
+ }
+
+ public function snapshotDaily(): void
+ {
+ $today = current_time('Y-m-d');
+ $types = $this->table->queryResults(
+ "SELECT DISTINCT type FROM {table} WHERE DATE(completed_at) = %s",
+ [$today]
+ );
+
+ foreach ($types as $row) {
+ $stats = $this->table->queryResults("
+ SELECT
+ COUNT(*) as total,
+ SUM(outcome = 'success') as successful,
+ SUM(outcome = 'failed_permanent') as failed_permanent,
+ SUM(processed_items) as items_processed
+ FROM {table}
+ WHERE type = %s AND DATE(completed_at) = %s
+ ", [$row->type, $today]);
+
+ $s = $stats[0] ?? null;
+ if (!$s) continue;
+
+ $this->stats->table->query("
+ INSERT INTO {table} (date, type, total_operations, successful_operations, failed_permanent_operations, total_items_processed)
+ VALUES (%s, %s, %d, %d, %d, %d)
+ ON DUPLICATE KEY UPDATE
+ total_operations = VALUES(total_operations),
+ successful_operations = VALUES(successful_operations),
+ failed_permanent_operations = VALUES(failed_permanent_operations),
+ total_items_processed = VALUES(total_items_processed),
+ updated_at = NOW()
+ ", [$today, $row->type, $s->total, $s->successful, $s->failed_permanent, $s->items_processed]);
+ }
+ }
}
diff --git a/inc/meta/Meta.php b/inc/meta/Meta.php
index a6594a7..fcf262a 100644
--- a/inc/meta/Meta.php
+++ b/inc/meta/Meta.php
@@ -17,9 +17,9 @@
*/
protected string $type;
/**
- * @var string the full slug, with BASE
+ * @var ?string the full slug, with BASE
*/
- protected string $slug;
+ protected ?string $slug;
protected string $contentType;
protected Item $item;
@@ -27,7 +27,7 @@
protected Validator $validator;
protected Sanitizer $sanitizer;
protected array $fields;
- protected WP_Post|WP_Term|WP_User|null $wpObject;
+ protected WP_Post|WP_Term|WP_User|false|null $wpObject;
protected int|string $ID;
protected MetaTypeManager $typeManager;
protected static array $instances = ['post' => [],'term' => [], 'user'=>[],'options'=>[]];
@@ -116,7 +116,7 @@
- $registrar = Registrar::getInstance($this->slug);
+ $registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false;
$fields = $registrar ? $registrar->getFields() : [];
$meta = match($type) {
'post' => get_post_meta($id),
diff --git a/inc/rest/routes/FormRoutes.php b/inc/rest/routes/FormRoutes.php
index e57163d..1cf8b23 100644
--- a/inc/rest/routes/FormRoutes.php
+++ b/inc/rest/routes/FormRoutes.php
@@ -165,7 +165,6 @@
} catch (\Exception $e) {
return new WP_Error('validation_failed', 'Data validation error: ' . $e->getMessage());
}
-
if (array_key_exists('success', $processed_data) && $processed_data['success'] === false) {
return $processed_data;
}
--
Gitblit v1.10.0